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