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 
17 package com.android.multidex;
18 
19 import com.android.dx.cf.attrib.AttRuntimeVisibleAnnotations;
20 import com.android.dx.cf.direct.DirectClassFile;
21 import com.android.dx.cf.iface.Attribute;
22 import com.android.dx.cf.iface.FieldList;
23 import com.android.dx.cf.iface.HasAttribute;
24 import com.android.dx.cf.iface.MethodList;
25 
26 import java.io.FileNotFoundException;
27 import java.io.IOException;
28 import java.util.HashSet;
29 import java.util.Set;
30 import 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  */
43 public 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 
main(String[] args)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 
MainDexListBuilder(boolean keepAnnotated, String rootJar, String pathString)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      */
getMainDexList()144     public Set<String> getMainDexList() {
145         return filesToKeep;
146     }
147 
printUsage()148     private static void printUsage() {
149         System.err.print(USAGE_MESSAGE);
150     }
151 
printList(Set<String> fileNames)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      */
keepAnnotated(Path path)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 
hasRuntimeVisibleAnnotation(HasAttribute element)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