1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.multidex;
18
19import com.android.dx.cf.attrib.AttRuntimeVisibleAnnotations;
20import com.android.dx.cf.direct.DirectClassFile;
21import com.android.dx.cf.iface.Attribute;
22import com.android.dx.cf.iface.FieldList;
23import com.android.dx.cf.iface.HasAttribute;
24import com.android.dx.cf.iface.MethodList;
25
26import java.io.FileNotFoundException;
27import java.io.IOException;
28import java.util.HashSet;
29import java.util.Set;
30import java.util.zip.ZipFile;
31
32/**
33 * This is a command line tool used by mainDexClasses script to build a main dex classes list. First
34 * argument of the command line is an archive, each class file contained in this archive is used to
35 * identify a class that can be used during secondary dex installation, those class files
36 * are not opened by this tool only their names matter. Other arguments must be zip files or
37 * directories, they constitute in a classpath in with the classes named by the first argument
38 * will be searched. Each searched class must be found. On each of this classes are searched for
39 * their dependencies to other classes. The tool also browses for classes annotated by runtime
40 * visible annotations and adds them to the list/ Finally the tools prints on standard output a list
41 * of class files names suitable as content of the file argument --main-dex-list of dx.
42 */
43public class MainDexListBuilder {
44    private static final String CLASS_EXTENSION = ".class";
45
46    private static final int STATUS_ERROR = 1;
47
48    private static final String EOL = System.getProperty("line.separator");
49
50    private static String USAGE_MESSAGE =
51            "Usage:" + EOL + EOL +
52            "Short version: Don't use this." + EOL + EOL +
53            "Slightly longer version: This tool is used by mainDexClasses script to build" + EOL +
54            "the main dex list." + EOL;
55
56    /**
57     * By default we force all classes annotated with runtime annotation to be kept in the
58     * main dex list. This option disable the workaround, limiting the index pressure in the main
59     * dex but exposing to the Dalvik resolution bug. The resolution bug occurs when accessing
60     * annotations of a class that is not in the main dex and one of the annotations as an enum
61     * parameter.
62     *
63     * @see <a href="https://code.google.com/p/android/issues/detail?id=78144">bug discussion</a>
64     *
65     */
66    private static final String DISABLE_ANNOTATION_RESOLUTION_WORKAROUND =
67            "--disable-annotation-resolution-workaround";
68
69    private Set<String> filesToKeep = new HashSet<String>();
70
71    public static void main(String[] args) {
72
73        int argIndex = 0;
74        boolean keepAnnotated = true;
75        while (argIndex < args.length -2) {
76            if (args[argIndex].equals(DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) {
77                keepAnnotated = false;
78            } else {
79                System.err.println("Invalid option " + args[argIndex]);
80                printUsage();
81                System.exit(STATUS_ERROR);
82            }
83            argIndex++;
84        }
85        if (args.length - argIndex != 2) {
86            printUsage();
87            System.exit(STATUS_ERROR);
88        }
89
90        try {
91            MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex],
92                    args[argIndex + 1]);
93            Set<String> toKeep = builder.getMainDexList();
94            printList(toKeep);
95        } catch (IOException e) {
96            System.err.println("A fatal error occured: " + e.getMessage());
97            System.exit(STATUS_ERROR);
98            return;
99        }
100    }
101
102    public MainDexListBuilder(boolean keepAnnotated, String rootJar, String pathString)
103            throws IOException {
104        ZipFile jarOfRoots = null;
105        Path path = null;
106        try {
107            try {
108                jarOfRoots = new ZipFile(rootJar);
109            } catch (IOException e) {
110                throw new IOException("\"" + rootJar + "\" can not be read as a zip archive. ("
111                        + e.getMessage() + ")", e);
112            }
113            path = new Path(pathString);
114
115            ClassReferenceListBuilder mainListBuilder = new ClassReferenceListBuilder(path);
116            mainListBuilder.addRoots(jarOfRoots);
117            for (String className : mainListBuilder.getClassNames()) {
118                filesToKeep.add(className + CLASS_EXTENSION);
119            }
120            if (keepAnnotated) {
121                keepAnnotated(path);
122            }
123        } finally {
124            try {
125                jarOfRoots.close();
126            } catch (IOException e) {
127                // ignore
128            }
129            if (path != null) {
130                for (ClassPathElement element : path.elements) {
131                    try {
132                        element.close();
133                    } catch (IOException e) {
134                        // keep going, lets do our best.
135                    }
136                }
137            }
138        }
139    }
140
141    /**
142     * Returns a list of classes to keep. This can be passed to dx as a file with --main-dex-list.
143     */
144    public Set<String> getMainDexList() {
145        return filesToKeep;
146    }
147
148    private static void printUsage() {
149        System.err.print(USAGE_MESSAGE);
150    }
151
152    private static void printList(Set<String> fileNames) {
153        for (String fileName : fileNames) {
154            System.out.println(fileName);
155        }
156    }
157
158    /**
159     * Keep classes annotated with runtime annotations.
160     */
161    private void keepAnnotated(Path path) throws FileNotFoundException {
162        for (ClassPathElement element : path.getElements()) {
163            forClazz:
164                for (String name : element.list()) {
165                    if (name.endsWith(CLASS_EXTENSION)) {
166                        DirectClassFile clazz = path.getClass(name);
167                        if (hasRuntimeVisibleAnnotation(clazz)) {
168                            filesToKeep.add(name);
169                        } else {
170                            MethodList methods = clazz.getMethods();
171                            for (int i = 0; i<methods.size(); i++) {
172                                if (hasRuntimeVisibleAnnotation(methods.get(i))) {
173                                    filesToKeep.add(name);
174                                    continue forClazz;
175                                }
176                            }
177                            FieldList fields = clazz.getFields();
178                            for (int i = 0; i<fields.size(); i++) {
179                                if (hasRuntimeVisibleAnnotation(fields.get(i))) {
180                                    filesToKeep.add(name);
181                                    continue forClazz;
182                                }
183                            }
184                        }
185                    }
186                }
187        }
188    }
189
190    private boolean hasRuntimeVisibleAnnotation(HasAttribute element) {
191        Attribute att = element.getAttributes().findFirst(
192                AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME);
193        return (att != null && ((AttRuntimeVisibleAnnotations)att).getAnnotations().size()>0);
194    }
195}
196