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;
25import java.io.FileNotFoundException;
26import java.io.IOException;
27import java.util.HashSet;
28import java.util.Set;
29import 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 */
42public 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
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
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     */
143    public Set<String> getMainDexList() {
144        return filesToKeep;
145    }
146
147    private static void printUsage() {
148        System.err.print(USAGE_MESSAGE);
149    }
150
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     */
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
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