Main.java revision dc520adfdcf0f0e9190cdb90605c42a7cc8fa98f
1/*
2 * Copyright (C) 2007 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.dx.command.dexer;
18
19import com.android.dx.Version;
20import com.android.dx.cf.code.SimException;
21import com.android.dx.cf.direct.ClassPathOpener;
22import com.android.dx.cf.iface.ParseException;
23import com.android.dx.command.DxConsole;
24import com.android.dx.command.UsageException;
25import com.android.dx.dex.DexFormat;
26import com.android.dx.dex.cf.CfOptions;
27import com.android.dx.dex.cf.CfTranslator;
28import com.android.dx.dex.cf.CodeStatistics;
29import com.android.dx.dex.code.PositionList;
30import com.android.dx.dex.file.ClassDefItem;
31import com.android.dx.dex.file.DexFile;
32import com.android.dx.dex.file.EncodedMethod;
33import com.android.dx.io.DexBuffer;
34import com.android.dx.merge.DexMerger;
35import com.android.dx.rop.annotation.Annotation;
36import com.android.dx.rop.annotation.Annotations;
37import com.android.dx.rop.annotation.AnnotationsList;
38import com.android.dx.rop.cst.CstNat;
39import com.android.dx.rop.cst.CstUtf8;
40import com.android.dx.util.FileUtils;
41import java.io.ByteArrayInputStream;
42import java.io.ByteArrayOutputStream;
43import java.io.File;
44import java.io.FileInputStream;
45import java.io.FileOutputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.io.OutputStream;
49import java.io.OutputStreamWriter;
50import java.io.PrintWriter;
51import java.util.ArrayList;
52import java.util.Arrays;
53import java.util.Map;
54import java.util.TreeMap;
55import java.util.concurrent.ExecutorService;
56import java.util.concurrent.Executors;
57import java.util.concurrent.TimeUnit;
58import java.util.jar.Attributes;
59import java.util.jar.JarEntry;
60import java.util.jar.JarOutputStream;
61import java.util.jar.Manifest;
62import java.util.zip.ZipFile;
63
64/**
65 * Main class for the class file translator.
66 */
67public class Main {
68    /**
69     * {@code non-null;} the lengthy message that tries to discourage
70     * people from defining core classes in applications
71     */
72    private static final String IN_RE_CORE_CLASSES =
73        "Ill-advised or mistaken usage of a core class (java.* or javax.*)\n" +
74        "when not building a core library.\n\n" +
75        "This is often due to inadvertently including a core library file\n" +
76        "in your application's project, when using an IDE (such as\n" +
77        "Eclipse). If you are sure you're not intentionally defining a\n" +
78        "core class, then this is the most likely explanation of what's\n" +
79        "going on.\n\n" +
80        "However, you might actually be trying to define a class in a core\n" +
81        "namespace, the source of which you may have taken, for example,\n" +
82        "from a non-Android virtual machine project. This will most\n" +
83        "assuredly not work. At a minimum, it jeopardizes the\n" +
84        "compatibility of your app with future versions of the platform.\n" +
85        "It is also often of questionable legality.\n\n" +
86        "If you really intend to build a core library -- which is only\n" +
87        "appropriate as part of creating a full virtual machine\n" +
88        "distribution, as opposed to compiling an application -- then use\n" +
89        "the \"--core-library\" option to suppress this error message.\n\n" +
90        "If you go ahead and use \"--core-library\" but are in fact\n" +
91        "building an application, then be forewarned that your application\n" +
92        "will still fail to build or run, at some point. Please be\n" +
93        "prepared for angry customers who find, for example, that your\n" +
94        "application ceases to function once they upgrade their operating\n" +
95        "system. You will be to blame for this problem.\n\n" +
96        "If you are legitimately using some code that happens to be in a\n" +
97        "core package, then the easiest safe alternative you have is to\n" +
98        "repackage that code. That is, move the classes in question into\n" +
99        "your own package namespace. This means that they will never be in\n" +
100        "conflict with core system classes. JarJar is a tool that may help\n" +
101        "you in this endeavor. If you find that you cannot do this, then\n" +
102        "that is an indication that the path you are on will ultimately\n" +
103        "lead to pain, suffering, grief, and lamentation.\n";
104
105    /**
106     * {@code non-null;} name of the standard manifest file in {@code .jar}
107     * files
108     */
109    private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
110
111    /**
112     * {@code non-null;} attribute name for the (quasi-standard?)
113     * {@code Created-By} attribute
114     */
115    private static final Attributes.Name CREATED_BY =
116        new Attributes.Name("Created-By");
117
118    /**
119     * {@code non-null;} list of {@code javax} subpackages that are considered
120     * to be "core". <b>Note:</b>: This list must be sorted, since it
121     * is binary-searched.
122     */
123    private static final String[] JAVAX_CORE = {
124        "accessibility", "crypto", "imageio", "management", "naming", "net",
125        "print", "rmi", "security", "sip", "sound", "sql", "swing",
126        "transaction", "xml"
127    };
128
129    /** number of warnings during processing */
130    private static int warnings = 0;
131
132    /** number of errors during processing */
133    private static int errors = 0;
134
135    /** {@code non-null;} parsed command-line arguments */
136    private static Arguments args;
137
138    /** {@code non-null;} output file in-progress */
139    private static DexFile outputDex;
140
141    /**
142     * {@code null-ok;} map of resources to include in the output, or
143     * {@code null} if resources are being ignored
144     */
145    private static TreeMap<String, byte[]> outputResources;
146
147    /** thread pool object used for multi-threaded file processing */
148    private static ExecutorService threadPool;
149
150    /** true if any files are successfully processed */
151    private static boolean anyFilesProcessed;
152
153    /** class files older than this must be defined in the target dex file. */
154    private static long minimumFileAge = 0;
155
156    /**
157     * This class is uninstantiable.
158     */
159    private Main() {
160        // This space intentionally left blank.
161    }
162
163    /**
164     * Run and exit if something unexpected happened.
165     * @param argArray the command line arguments
166     */
167    public static void main(String[] argArray) throws IOException {
168        Arguments arguments = new Arguments();
169        arguments.parse(argArray);
170
171        int result = run(arguments);
172        if (result != 0) {
173            System.exit(result);
174        }
175    }
176
177    /**
178     * Run and return a result code.
179     * @param arguments the data + parameters for the conversion
180     * @return 0 if success > 0 otherwise.
181     */
182    public static int run(Arguments arguments) throws IOException {
183        // Reset the error/warning count to start fresh.
184        warnings = 0;
185        errors = 0;
186
187        args = arguments;
188        args.makeCfOptions();
189
190        File incrementalOutFile = null;
191        if (args.incremental) {
192            if (args.outName == null) {
193                System.err.println(
194                        "error: no incremental output name specified");
195                return -1;
196            }
197            incrementalOutFile = new File(args.outName);
198            if (incrementalOutFile.exists()) {
199                minimumFileAge = incrementalOutFile.lastModified();
200            }
201        }
202
203        if (!processAllFiles()) {
204            return 1;
205        }
206
207        if (args.incremental && !anyFilesProcessed) {
208            return 0; // this was a no-op incremental build
209        }
210
211        byte[] outArray = writeDex();
212
213        if (outArray == null) {
214            return 2;
215        }
216
217        if (args.incremental && incrementalOutFile.exists()) {
218            outArray = merge(outArray, incrementalOutFile);
219        }
220
221        if (args.jarOutput) {
222            // Effectively free up the (often massive) DexFile memory.
223            outputDex = null;
224
225            if (!createJar(args.outName, outArray)) {
226                return 3;
227            }
228        } else if (args.outName != null) {
229            OutputStream out = openOutput(args.outName);
230            out.write(outArray);
231            closeOutput(out);
232        }
233
234        return 0;
235    }
236
237    /**
238     * Merges the dex files {@code update} and {@code base}, preferring
239     * {@code update}'s definition for types defined in both dex files. Returns
240     * the bytes of the merged dex file.
241     */
242    private static byte[] merge(byte[] update, File base) throws IOException {
243        DexBuffer dexA = new DexBuffer();
244        dexA.loadFrom(new ByteArrayInputStream(update));
245
246        DexBuffer dexB = new DexBuffer();
247        if (args.jarOutput) {
248            ZipFile zipFile = new ZipFile(base);
249            dexB.loadFrom(zipFile.getInputStream(zipFile.getEntry(DexFormat.DEX_IN_JAR_NAME)));
250            zipFile.close();
251        } else {
252            InputStream in = new FileInputStream(base);
253            dexB.loadFrom(in);
254            in.close();
255        }
256
257        DexBuffer merged = new DexMerger(dexA, dexB).merge();
258        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
259        merged.writeTo(bytesOut);
260        return bytesOut.toByteArray();
261    }
262
263    /**
264     * Constructs the output {@link DexFile}, fill it in with all the
265     * specified classes, and populate the resources map if required.
266     *
267     * @return whether processing was successful
268     */
269    private static boolean processAllFiles() {
270        outputDex = new DexFile();
271
272        if (args.jarOutput) {
273            outputResources = new TreeMap<String, byte[]>();
274        }
275
276        if (args.dumpWidth != 0) {
277            outputDex.setDumpWidth(args.dumpWidth);
278        }
279
280        anyFilesProcessed = false;
281        String[] fileNames = args.fileNames;
282
283        if (args.numThreads > 1) {
284            threadPool = Executors.newFixedThreadPool(args.numThreads);
285        }
286
287        try {
288            for (int i = 0; i < fileNames.length; i++) {
289                if (processOne(fileNames[i])) {
290                    anyFilesProcessed = true;
291                }
292            }
293        } catch (StopProcessing ex) {
294            /*
295             * Ignore it and just let the warning/error reporting do
296             * their things.
297             */
298        }
299
300        if (args.numThreads > 1) {
301            try {
302                threadPool.shutdown();
303                threadPool.awaitTermination(600L, TimeUnit.SECONDS);
304            } catch (InterruptedException ex) {
305                throw new RuntimeException("Timed out waiting for threads.");
306            }
307        }
308
309        if (warnings != 0) {
310            DxConsole.err.println(warnings + " warning" +
311                               ((warnings == 1) ? "" : "s"));
312        }
313
314        if (errors != 0) {
315            DxConsole.err.println(errors + " error" +
316                    ((errors == 1) ? "" : "s") + "; aborting");
317            return false;
318        }
319
320        if (args.incremental && !anyFilesProcessed) {
321            return true;
322        }
323
324        if (!(anyFilesProcessed || args.emptyOk)) {
325            DxConsole.err.println("no classfiles specified");
326            return false;
327        }
328
329        if (args.optimize && args.statistics) {
330            CodeStatistics.dumpStatistics(DxConsole.out);
331        }
332
333        return true;
334    }
335
336    /**
337     * Processes one pathname element.
338     *
339     * @param pathname {@code non-null;} the pathname to process. May
340     * be the path of a class file, a jar file, or a directory
341     * containing class files.
342     * @return whether any processing actually happened
343     */
344    private static boolean processOne(String pathname) {
345        ClassPathOpener opener;
346
347        opener = new ClassPathOpener(pathname, false,
348                new ClassPathOpener.Consumer() {
349            public boolean processFileBytes(String name, long lastModified, byte[] bytes) {
350                if (args.numThreads > 1) {
351                    threadPool.execute(new ParallelProcessor(name, lastModified, bytes));
352                    return false;
353                } else {
354                    return Main.processFileBytes(name, lastModified, bytes);
355                }
356            }
357            public void onException(Exception ex) {
358                if (ex instanceof StopProcessing) {
359                    throw (StopProcessing) ex;
360                } else if (ex instanceof SimException) {
361                    DxConsole.err.println("\nEXCEPTION FROM SIMULATION:");
362                    DxConsole.err.println(ex.getMessage() + "\n");
363                    DxConsole.err.println(((SimException) ex).getContext());
364                } else {
365                    DxConsole.err.println("\nUNEXPECTED TOP-LEVEL EXCEPTION:");
366                    ex.printStackTrace(DxConsole.err);
367                }
368                errors++;
369            }
370            public void onProcessArchiveStart(File file) {
371                if (args.verbose) {
372                    DxConsole.out.println("processing archive " + file +
373                            "...");
374                }
375            }
376        });
377
378        return opener.process();
379    }
380
381    /**
382     * Processes one file, which may be either a class or a resource.
383     *
384     * @param name {@code non-null;} name of the file
385     * @param bytes {@code non-null;} contents of the file
386     * @return whether processing was successful
387     */
388    private static boolean processFileBytes(String name, long lastModified, byte[] bytes) {
389        boolean isClass = name.endsWith(".class");
390        boolean keepResources = (outputResources != null);
391
392        if (!isClass && !keepResources) {
393            if (args.verbose) {
394                DxConsole.out.println("ignored resource " + name);
395            }
396            return false;
397        }
398
399        if (args.verbose) {
400            DxConsole.out.println("processing " + name + "...");
401        }
402
403        String fixedName = fixPath(name);
404
405        if (isClass) {
406            if (keepResources && args.keepClassesInJar) {
407                synchronized (outputResources) {
408                    outputResources.put(fixedName, bytes);
409                }
410            }
411            if (lastModified < minimumFileAge) {
412                return true;
413            }
414            return processClass(fixedName, bytes);
415        } else {
416            synchronized (outputResources) {
417                outputResources.put(fixedName, bytes);
418            }
419            return true;
420        }
421    }
422
423    /**
424     * Processes one classfile.
425     *
426     * @param name {@code non-null;} name of the file, clipped such that it
427     * <i>should</i> correspond to the name of the class it contains
428     * @param bytes {@code non-null;} contents of the file
429     * @return whether processing was successful
430     */
431    private static boolean processClass(String name, byte[] bytes) {
432        if (! args.coreLibrary) {
433            checkClassName(name);
434        }
435
436        try {
437            ClassDefItem clazz =
438                CfTranslator.translate(name, bytes, args.cfOptions);
439            synchronized (outputDex) {
440                outputDex.add(clazz);
441            }
442            return true;
443        } catch (ParseException ex) {
444            DxConsole.err.println("\ntrouble processing:");
445            if (args.debug) {
446                ex.printStackTrace(DxConsole.err);
447            } else {
448                ex.printContext(DxConsole.err);
449            }
450        }
451
452        warnings++;
453        return false;
454    }
455
456    /**
457     * Check the class name to make sure it's not a "core library"
458     * class. If there is a problem, this updates the error count and
459     * throws an exception to stop processing.
460     *
461     * @param name {@code non-null;} the fully-qualified internal-form
462     * class name
463     */
464    private static void checkClassName(String name) {
465        boolean bogus = false;
466
467        if (name.startsWith("java/")) {
468            bogus = true;
469        } else if (name.startsWith("javax/")) {
470            int slashAt = name.indexOf('/', 6);
471            if (slashAt == -1) {
472                // Top-level javax classes are verboten.
473                bogus = true;
474            } else {
475                String pkg = name.substring(6, slashAt);
476                bogus = (Arrays.binarySearch(JAVAX_CORE, pkg) >= 0);
477            }
478        }
479
480        if (! bogus) {
481            return;
482        }
483
484        /*
485         * The user is probably trying to include an entire desktop
486         * core library in a misguided attempt to get their application
487         * working. Try to help them understand what's happening.
488         */
489
490        DxConsole.err.println("\ntrouble processing \"" + name + "\":\n\n" +
491                IN_RE_CORE_CLASSES);
492        errors++;
493        throw new StopProcessing();
494    }
495
496    /**
497     * Converts {@link #outputDex} into a {@code byte[]} and do whatever
498     * human-oriented dumping is required.
499     *
500     * @return {@code null-ok;} the converted {@code byte[]} or {@code null}
501     * if there was a problem
502     */
503    private static byte[] writeDex() {
504        byte[] outArray = null;
505
506        try {
507            OutputStream humanOutRaw = null;
508            OutputStreamWriter humanOut = null;
509            try {
510                if (args.humanOutName != null) {
511                    humanOutRaw = openOutput(args.humanOutName);
512                    humanOut = new OutputStreamWriter(humanOutRaw);
513                }
514
515                if (args.methodToDump != null) {
516                    /*
517                     * Simply dump the requested method. Note: The call
518                     * to toDex() is required just to get the underlying
519                     * structures ready.
520                     */
521                    outputDex.toDex(null, false);
522                    dumpMethod(outputDex, args.methodToDump, humanOut);
523                } else {
524                    /*
525                     * This is the usual case: Create an output .dex file,
526                     * and write it, dump it, etc.
527                     */
528                    outArray = outputDex.toDex(humanOut, args.verboseDump);
529                }
530
531                if (args.statistics) {
532                    DxConsole.out.println(outputDex.getStatistics().toHuman());
533                }
534            } finally {
535                if (humanOut != null) {
536                    humanOut.flush();
537                }
538                closeOutput(humanOutRaw);
539            }
540        } catch (Exception ex) {
541            if (args.debug) {
542                DxConsole.err.println("\ntrouble writing output:");
543                ex.printStackTrace(DxConsole.err);
544            } else {
545                DxConsole.err.println("\ntrouble writing output: " +
546                                   ex.getMessage());
547            }
548            return null;
549        }
550
551        return outArray;
552    }
553
554    /**
555     * Creates a jar file from the resources and given dex file array.
556     *
557     * @param fileName {@code non-null;} name of the file
558     * @param dexArray {@code non-null;} array containing the dex file
559     * to include
560     * @return whether the creation was successful
561     */
562    private static boolean createJar(String fileName, byte[] dexArray) {
563        /*
564         * Make or modify the manifest (as appropriate), put the dex
565         * array into the resources map, and then process the entire
566         * resources map in a uniform manner.
567         */
568
569        try {
570            Manifest manifest = makeManifest();
571            OutputStream out = openOutput(fileName);
572            JarOutputStream jarOut = new JarOutputStream(out, manifest);
573
574            outputResources.put(DexFormat.DEX_IN_JAR_NAME, dexArray);
575
576            try {
577                for (Map.Entry<String, byte[]> e :
578                         outputResources.entrySet()) {
579                    String name = e.getKey();
580                    byte[] contents = e.getValue();
581                    JarEntry entry = new JarEntry(name);
582
583                    if (args.verbose) {
584                        DxConsole.out.println("writing " + name + "; size " +
585                                           contents.length + "...");
586                    }
587
588                    entry.setSize(contents.length);
589                    jarOut.putNextEntry(entry);
590                    jarOut.write(contents);
591                    jarOut.closeEntry();
592                }
593            } finally {
594                jarOut.finish();
595                jarOut.flush();
596                closeOutput(out);
597            }
598        } catch (Exception ex) {
599            if (args.debug) {
600                DxConsole.err.println("\ntrouble writing output:");
601                ex.printStackTrace(DxConsole.err);
602            } else {
603                DxConsole.err.println("\ntrouble writing output: " +
604                                   ex.getMessage());
605            }
606            return false;
607        }
608
609        return true;
610    }
611
612    /**
613     * Creates and returns the manifest to use for the output. This may
614     * modify {@link #outputResources} (removing the pre-existing manifest).
615     *
616     * @return {@code non-null;} the manifest
617     */
618    private static Manifest makeManifest() throws IOException {
619        byte[] manifestBytes = outputResources.get(MANIFEST_NAME);
620        Manifest manifest;
621        Attributes attribs;
622
623        if (manifestBytes == null) {
624            // We need to construct an entirely new manifest.
625            manifest = new Manifest();
626            attribs = manifest.getMainAttributes();
627            attribs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
628        } else {
629            manifest = new Manifest(new ByteArrayInputStream(manifestBytes));
630            attribs = manifest.getMainAttributes();
631            outputResources.remove(MANIFEST_NAME);
632        }
633
634        String createdBy = attribs.getValue(CREATED_BY);
635        if (createdBy == null) {
636            createdBy = "";
637        } else {
638            createdBy += " + ";
639        }
640        createdBy += "dx " + Version.VERSION;
641
642        attribs.put(CREATED_BY, createdBy);
643        attribs.putValue("Dex-Location", DexFormat.DEX_IN_JAR_NAME);
644
645        return manifest;
646    }
647
648    /**
649     * Opens and returns the named file for writing, treating "-" specially.
650     *
651     * @param name {@code non-null;} the file name
652     * @return {@code non-null;} the opened file
653     */
654    private static OutputStream openOutput(String name) throws IOException {
655        if (name.equals("-") ||
656                name.startsWith("-.")) {
657            return System.out;
658        }
659
660        return new FileOutputStream(name);
661    }
662
663    /**
664     * Flushes and closes the given output stream, except if it happens to be
665     * {@link System#out} in which case this method does the flush but not
666     * the close. This method will also silently do nothing if given a
667     * {@code null} argument.
668     *
669     * @param stream {@code null-ok;} what to close
670     */
671    private static void closeOutput(OutputStream stream) throws IOException {
672        if (stream == null) {
673            return;
674        }
675
676        stream.flush();
677
678        if (stream != System.out) {
679            stream.close();
680        }
681    }
682
683    /**
684     * Returns the "fixed" version of a given file path, suitable for
685     * use as a path within a {@code .jar} file and for checking
686     * against a classfile-internal "this class" name. This looks for
687     * the last instance of the substring {@code "/./"} within
688     * the path, and if it finds it, it takes the portion after to be
689     * the fixed path. If that isn't found but the path starts with
690     * {@code "./"}, then that prefix is removed and the rest is
691     * return. If neither of these is the case, this method returns
692     * its argument.
693     *
694     * @param path {@code non-null;} the path to "fix"
695     * @return {@code non-null;} the fixed version (which might be the same as
696     * the given {@code path})
697     */
698    private static String fixPath(String path) {
699        /*
700         * If the path separator is \ (like on windows), we convert the
701         * path to a standard '/' separated path.
702         */
703        if (File.separatorChar == '\\') {
704            path = path.replace('\\', '/');
705        }
706
707        int index = path.lastIndexOf("/./");
708
709        if (index != -1) {
710            return path.substring(index + 3);
711        }
712
713        if (path.startsWith("./")) {
714            return path.substring(2);
715        }
716
717        return path;
718    }
719
720    /**
721     * Dumps any method with the given name in the given file.
722     *
723     * @param dex {@code non-null;} the dex file
724     * @param fqName {@code non-null;} the fully-qualified name of the
725     * method(s)
726     * @param out {@code non-null;} where to dump to
727     */
728    private static void dumpMethod(DexFile dex, String fqName,
729            OutputStreamWriter out) {
730        boolean wildcard = fqName.endsWith("*");
731        int lastDot = fqName.lastIndexOf('.');
732
733        if ((lastDot <= 0) || (lastDot == (fqName.length() - 1))) {
734            DxConsole.err.println("bogus fully-qualified method name: " +
735                               fqName);
736            return;
737        }
738
739        String className = fqName.substring(0, lastDot).replace('.', '/');
740        String methodName = fqName.substring(lastDot + 1);
741        ClassDefItem clazz = dex.getClassOrNull(className);
742
743        if (clazz == null) {
744            DxConsole.err.println("no such class: " + className);
745            return;
746        }
747
748        if (wildcard) {
749            methodName = methodName.substring(0, methodName.length() - 1);
750        }
751
752        ArrayList<EncodedMethod> allMeths = clazz.getMethods();
753        TreeMap<CstNat, EncodedMethod> meths =
754            new TreeMap<CstNat, EncodedMethod>();
755
756        /*
757         * Figure out which methods to include in the output, and get them
758         * all sorted, so that the printout code is robust with respect to
759         * changes in the underlying order.
760         */
761        for (EncodedMethod meth : allMeths) {
762            String methName = meth.getName().getString();
763            if ((wildcard && methName.startsWith(methodName)) ||
764                (!wildcard && methName.equals(methodName))) {
765                meths.put(meth.getRef().getNat(), meth);
766            }
767        }
768
769        if (meths.size() == 0) {
770            DxConsole.err.println("no such method: " + fqName);
771            return;
772        }
773
774        PrintWriter pw = new PrintWriter(out);
775
776        for (EncodedMethod meth : meths.values()) {
777            // TODO: Better stuff goes here, perhaps.
778            meth.debugPrint(pw, args.verboseDump);
779
780            /*
781             * The (default) source file is an attribute of the class, but
782             * it's useful to see it in method dumps.
783             */
784            CstUtf8 sourceFile = clazz.getSourceFile();
785            if (sourceFile != null) {
786                pw.println("  source file: " + sourceFile.toQuoted());
787            }
788
789            Annotations methodAnnotations =
790                clazz.getMethodAnnotations(meth.getRef());
791            AnnotationsList parameterAnnotations =
792                clazz.getParameterAnnotations(meth.getRef());
793
794            if (methodAnnotations != null) {
795                pw.println("  method annotations:");
796                for (Annotation a : methodAnnotations.getAnnotations()) {
797                    pw.println("    " + a);
798                }
799            }
800
801            if (parameterAnnotations != null) {
802                pw.println("  parameter annotations:");
803                int sz = parameterAnnotations.size();
804                for (int i = 0; i < sz; i++) {
805                    pw.println("    parameter " + i);
806                    Annotations annotations = parameterAnnotations.get(i);
807                    for (Annotation a : annotations.getAnnotations()) {
808                        pw.println("      " + a);
809                    }
810                }
811            }
812        }
813
814        pw.flush();
815    }
816
817    /**
818     * Exception class used to halt processing prematurely.
819     */
820    private static class StopProcessing extends RuntimeException {
821        // This space intentionally left blank.
822    }
823
824    /**
825     * Command-line argument parser and access.
826     */
827    public static class Arguments {
828        /** whether to run in debug mode */
829        public boolean debug = false;
830
831        /** whether to emit high-level verbose human-oriented output */
832        public boolean verbose = false;
833
834        /** whether to emit verbose human-oriented output in the dump file */
835        public boolean verboseDump = false;
836
837        /** whether we are constructing a core library */
838        public boolean coreLibrary = false;
839
840        /** {@code null-ok;} particular method to dump */
841        public String methodToDump = null;
842
843        /** max width for columnar output */
844        public int dumpWidth = 0;
845
846        /** {@code null-ok;} output file name for binary file */
847        public String outName = null;
848
849        /** {@code null-ok;} output file name for human-oriented dump */
850        public String humanOutName = null;
851
852        /** whether strict file-name-vs-class-name checking should be done */
853        public boolean strictNameCheck = true;
854
855        /**
856         * whether it is okay for there to be no {@code .class} files
857         * to process
858         */
859        public boolean emptyOk = false;
860
861        /**
862         * whether the binary output is to be a {@code .jar} file
863         * instead of a plain {@code .dex}
864         */
865        public boolean jarOutput = false;
866
867        /**
868         * when writing a {@code .jar} file, whether to still
869         * keep the {@code .class} files
870         */
871        public boolean keepClassesInJar = false;
872
873        /** how much source position info to preserve */
874        public int positionInfo = PositionList.LINES;
875
876        /** whether to keep local variable information */
877        public boolean localInfo = true;
878
879        /** whether to merge with the output dex file if it exists. */
880        public boolean incremental = false;
881
882        /** {@code non-null after {@link #parse};} file name arguments */
883        public String[] fileNames;
884
885        /** whether to do SSA/register optimization */
886        public boolean optimize = true;
887
888        /** Filename containg list of methods to optimize */
889        public String optimizeListFile = null;
890
891        /** Filename containing list of methods to NOT optimize */
892        public String dontOptimizeListFile = null;
893
894        /** Whether to print statistics to stdout at end of compile cycle */
895        public boolean statistics;
896
897        /** Options for dex.cf.* */
898        public CfOptions cfOptions;
899
900        /** number of threads to run with */
901        public int numThreads = 1;
902
903        /**
904         * Parses the given command-line arguments.
905         *
906         * @param args {@code non-null;} the arguments
907         */
908        public void parse(String[] args) {
909            int at = 0;
910
911            for (/*at*/; at < args.length; at++) {
912                String arg = args[at];
913                if (arg.equals("--") || !arg.startsWith("--")) {
914                    break;
915                } else if (arg.equals("--debug")) {
916                    debug = true;
917                } else if (arg.equals("--verbose")) {
918                    verbose = true;
919                } else if (arg.equals("--verbose-dump")) {
920                    verboseDump = true;
921                } else if (arg.equals("--no-files")) {
922                    emptyOk = true;
923                } else if (arg.equals("--no-optimize")) {
924                    optimize = false;
925                } else if (arg.equals("--no-strict")) {
926                    strictNameCheck = false;
927                } else if (arg.equals("--core-library")) {
928                    coreLibrary = true;
929                } else if (arg.equals("--statistics")) {
930                    statistics = true;
931                } else if (arg.startsWith("--optimize-list=")) {
932                    if (dontOptimizeListFile != null) {
933                        System.err.println("--optimize-list and "
934                                + "--no-optimize-list are incompatible.");
935                        throw new UsageException();
936                    }
937                    optimize = true;
938                    optimizeListFile = arg.substring(arg.indexOf('=') + 1);
939                } else if (arg.startsWith("--no-optimize-list=")) {
940                    if (dontOptimizeListFile != null) {
941                        System.err.println("--optimize-list and "
942                                + "--no-optimize-list are incompatible.");
943                        throw new UsageException();
944                    }
945                    optimize = true;
946                    dontOptimizeListFile = arg.substring(arg.indexOf('=') + 1);
947                } else if (arg.equals("--keep-classes")) {
948                    keepClassesInJar = true;
949                } else if (arg.startsWith("--output=")) {
950                    outName = arg.substring(arg.indexOf('=') + 1);
951                    if (FileUtils.hasArchiveSuffix(outName)) {
952                        jarOutput = true;
953                    } else if (outName.endsWith(".dex") ||
954                               outName.equals("-")) {
955                        jarOutput = false;
956                    } else {
957                        System.err.println("unknown output extension: " +
958                                           outName);
959                        throw new UsageException();
960                    }
961                } else if (arg.startsWith("--dump-to=")) {
962                    humanOutName = arg.substring(arg.indexOf('=') + 1);
963                } else if (arg.startsWith("--dump-width=")) {
964                    arg = arg.substring(arg.indexOf('=') + 1);
965                    dumpWidth = Integer.parseInt(arg);
966                } else if (arg.startsWith("--dump-method=")) {
967                    methodToDump = arg.substring(arg.indexOf('=') + 1);
968                    jarOutput = false;
969                } else if (arg.startsWith("--positions=")) {
970                    String pstr = arg.substring(arg.indexOf('=') + 1).intern();
971                    if (pstr == "none") {
972                        positionInfo = PositionList.NONE;
973                    } else if (pstr == "important") {
974                        positionInfo = PositionList.IMPORTANT;
975                    } else if (pstr == "lines") {
976                        positionInfo = PositionList.LINES;
977                    } else {
978                        System.err.println("unknown positions option: " +
979                                           pstr);
980                        throw new UsageException();
981                    }
982                } else if (arg.equals("--no-locals")) {
983                    localInfo = false;
984                } else if (arg.startsWith("--num-threads=")) {
985                    arg = arg.substring(arg.indexOf('=') + 1);
986                    numThreads = Integer.parseInt(arg);
987                } else if (arg.equals("--incremental")) {
988                    incremental = true;
989                } else {
990                    System.err.println("unknown option: " + arg);
991                    throw new UsageException();
992                }
993            }
994
995            int fileCount = args.length - at;
996
997            if (fileCount == 0) {
998                if (!emptyOk) {
999                    System.err.println("no input files specified");
1000                    throw new UsageException();
1001                }
1002            } else if (emptyOk) {
1003                System.out.println("ignoring input files");
1004                at = 0;
1005                fileCount = 0;
1006            }
1007
1008            fileNames = new String[fileCount];
1009            System.arraycopy(args, at, fileNames, 0, fileCount);
1010
1011            if ((humanOutName == null) && (methodToDump != null)) {
1012                humanOutName = "-";
1013            }
1014
1015            makeCfOptions();
1016        }
1017
1018        /**
1019         * Copies relevent arguments over into a CfOptions instance.
1020         */
1021        private void makeCfOptions() {
1022            cfOptions = new CfOptions();
1023
1024            cfOptions.positionInfo = positionInfo;
1025            cfOptions.localInfo = localInfo;
1026            cfOptions.strictNameCheck = strictNameCheck;
1027            cfOptions.optimize = optimize;
1028            cfOptions.optimizeListFile = optimizeListFile;
1029            cfOptions.dontOptimizeListFile = dontOptimizeListFile;
1030            cfOptions.statistics = statistics;
1031            cfOptions.warn = DxConsole.err;
1032        }
1033    }
1034
1035    /** Runnable helper class to process files in multiple threads */
1036    private static class ParallelProcessor implements Runnable {
1037
1038        String path;
1039        long lastModified;
1040        byte[] bytes;
1041
1042        /**
1043         * Constructs an instance.
1044         *
1045         * @param path {@code non-null;} filename of element. May not be a valid
1046         * filesystem path.
1047         * @param bytes {@code non-null;} file data
1048         */
1049        private ParallelProcessor(String path, long lastModified, byte bytes[]) {
1050            this.path = path;
1051            this.lastModified = lastModified;
1052            this.bytes = bytes;
1053        }
1054
1055        /**
1056         * Task run by each thread in the thread pool. Runs processFileBytes
1057         * with the given path and bytes.
1058         */
1059        public void run() {
1060            if (Main.processFileBytes(path, lastModified, bytes)) {
1061                anyFilesProcessed = true;
1062            }
1063        }
1064    }
1065}
1066