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