Main.java revision f870f2dce9300c8dec620613371f08e5c234245b
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.DexOptions; 27import com.android.dx.dex.cf.CfOptions; 28import com.android.dx.dex.cf.CfTranslator; 29import com.android.dx.dex.cf.CodeStatistics; 30import com.android.dx.dex.code.PositionList; 31import com.android.dx.dex.file.ClassDefItem; 32import com.android.dx.dex.file.DexFile; 33import com.android.dx.dex.file.EncodedMethod; 34import com.android.dx.io.DexBuffer; 35import com.android.dx.merge.CollisionPolicy; 36import com.android.dx.merge.DexMerger; 37import com.android.dx.rop.annotation.Annotation; 38import com.android.dx.rop.annotation.Annotations; 39import com.android.dx.rop.annotation.AnnotationsList; 40import com.android.dx.rop.cst.CstNat; 41import com.android.dx.rop.cst.CstString; 42import com.android.dx.util.FileUtils; 43import java.io.ByteArrayInputStream; 44import java.io.ByteArrayOutputStream; 45import java.io.File; 46import java.io.FileOutputStream; 47import java.io.IOException; 48import java.io.OutputStream; 49import java.io.OutputStreamWriter; 50import java.io.PrintWriter; 51import java.util.ArrayList; 52import java.util.Arrays; 53import java.util.List; 54import java.util.Map; 55import java.util.TreeMap; 56import java.util.concurrent.ExecutorService; 57import java.util.concurrent.Executors; 58import java.util.concurrent.TimeUnit; 59import java.util.jar.Attributes; 60import java.util.jar.JarEntry; 61import java.util.jar.JarOutputStream; 62import java.util.jar.Manifest; 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 /** Library .dex files to merge into the output .dex. */ 148 private static final List<byte[]> libraryDexBuffers = new ArrayList<byte[]>(); 149 150 /** thread pool object used for multi-threaded file processing */ 151 private static ExecutorService threadPool; 152 153 /** true if any files are successfully processed */ 154 private static boolean anyFilesProcessed; 155 156 /** class files older than this must be defined in the target dex file. */ 157 private static long minimumFileAge = 0; 158 159 /** 160 * This class is uninstantiable. 161 */ 162 private Main() { 163 // This space intentionally left blank. 164 } 165 166 /** 167 * Run and exit if something unexpected happened. 168 * @param argArray the command line arguments 169 */ 170 public static void main(String[] argArray) throws IOException { 171 Arguments arguments = new Arguments(); 172 arguments.parse(argArray); 173 174 int result = run(arguments); 175 if (result != 0) { 176 System.exit(result); 177 } 178 } 179 180 /** 181 * Run and return a result code. 182 * @param arguments the data + parameters for the conversion 183 * @return 0 if success > 0 otherwise. 184 */ 185 public static int run(Arguments arguments) throws IOException { 186 // Reset the error/warning count to start fresh. 187 warnings = 0; 188 errors = 0; 189 // empty the list, so that tools that load dx and keep it around 190 // for multiple runs don't reuse older buffers. 191 libraryDexBuffers.clear(); 192 193 args = arguments; 194 args.makeOptionsObjects(); 195 196 File incrementalOutFile = null; 197 if (args.incremental) { 198 if (args.outName == null) { 199 System.err.println( 200 "error: no incremental output name specified"); 201 return -1; 202 } 203 incrementalOutFile = new File(args.outName); 204 if (incrementalOutFile.exists()) { 205 minimumFileAge = incrementalOutFile.lastModified(); 206 } 207 } 208 209 if (!processAllFiles()) { 210 return 1; 211 } 212 213 if (args.incremental && !anyFilesProcessed) { 214 return 0; // this was a no-op incremental build 215 } 216 217 // this array is null if no classes were defined 218 byte[] outArray = null; 219 220 if (!outputDex.isEmpty()) { 221 outArray = writeDex(); 222 223 if (outArray == null) { 224 return 2; 225 } 226 } 227 228 if (args.incremental) { 229 outArray = mergeIncremental(outArray, incrementalOutFile); 230 } 231 232 outArray = mergeLibraryDexBuffers(outArray); 233 234 if (args.jarOutput) { 235 // Effectively free up the (often massive) DexFile memory. 236 outputDex = null; 237 238 if (!createJar(args.outName, outArray)) { 239 return 3; 240 } 241 } else if (outArray != null && args.outName != null) { 242 OutputStream out = openOutput(args.outName); 243 out.write(outArray); 244 closeOutput(out); 245 } 246 247 return 0; 248 } 249 250 /** 251 * Merges the dex files {@code update} and {@code base}, preferring 252 * {@code update}'s definition for types defined in both dex files. 253 * 254 * @param base a file to find the previous dex file. May be a .dex file, a 255 * jar file possibly containing a .dex file, or null. 256 * @return the bytes of the merged dex file, or null if both the update 257 * and the base dex do not exist. 258 */ 259 private static byte[] mergeIncremental(byte[] update, File base) throws IOException { 260 DexBuffer dexA = null; 261 DexBuffer dexB = null; 262 263 if (update != null) { 264 dexA = new DexBuffer(update); 265 } 266 267 if (base.exists()) { 268 dexB = new DexBuffer(base); 269 } 270 271 DexBuffer result; 272 if (dexA == null && dexB == null) { 273 return null; 274 } else if (dexA == null) { 275 result = dexB; 276 } else if (dexB == null) { 277 result = dexA; 278 } else { 279 result = new DexMerger(dexA, dexB, CollisionPolicy.KEEP_FIRST).merge(); 280 } 281 282 ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); 283 result.writeTo(bytesOut); 284 return bytesOut.toByteArray(); 285 } 286 287 /** 288 * Merges the dex files in library jars. If multiple dex files define the 289 * same type, this fails with an exception. 290 */ 291 private static byte[] mergeLibraryDexBuffers(byte[] outArray) throws IOException { 292 for (byte[] libraryDexBuffer : libraryDexBuffers) { 293 if (outArray == null) { 294 outArray = libraryDexBuffer; 295 continue; 296 } 297 298 DexBuffer a = new DexBuffer(outArray); 299 DexBuffer b = new DexBuffer(libraryDexBuffer); 300 DexBuffer ab = new DexMerger(a, b, CollisionPolicy.FAIL).merge(); 301 outArray = ab.getBytes(); 302 } 303 304 return outArray; 305 } 306 307 /** 308 * Constructs the output {@link DexFile}, fill it in with all the 309 * specified classes, and populate the resources map if required. 310 * 311 * @return whether processing was successful 312 */ 313 private static boolean processAllFiles() { 314 outputDex = new DexFile(args.dexOptions); 315 316 if (args.jarOutput) { 317 outputResources = new TreeMap<String, byte[]>(); 318 } 319 320 if (args.dumpWidth != 0) { 321 outputDex.setDumpWidth(args.dumpWidth); 322 } 323 324 anyFilesProcessed = false; 325 String[] fileNames = args.fileNames; 326 327 if (args.numThreads > 1) { 328 threadPool = Executors.newFixedThreadPool(args.numThreads); 329 } 330 331 try { 332 for (int i = 0; i < fileNames.length; i++) { 333 if (processOne(fileNames[i])) { 334 anyFilesProcessed = true; 335 } 336 } 337 } catch (StopProcessing ex) { 338 /* 339 * Ignore it and just let the warning/error reporting do 340 * their things. 341 */ 342 } 343 344 if (args.numThreads > 1) { 345 try { 346 threadPool.shutdown(); 347 threadPool.awaitTermination(600L, TimeUnit.SECONDS); 348 } catch (InterruptedException ex) { 349 throw new RuntimeException("Timed out waiting for threads."); 350 } 351 } 352 353 if (warnings != 0) { 354 DxConsole.err.println(warnings + " warning" + 355 ((warnings == 1) ? "" : "s")); 356 } 357 358 if (errors != 0) { 359 DxConsole.err.println(errors + " error" + 360 ((errors == 1) ? "" : "s") + "; aborting"); 361 return false; 362 } 363 364 if (args.incremental && !anyFilesProcessed) { 365 return true; 366 } 367 368 if (!(anyFilesProcessed || args.emptyOk)) { 369 DxConsole.err.println("no classfiles specified"); 370 return false; 371 } 372 373 if (args.optimize && args.statistics) { 374 CodeStatistics.dumpStatistics(DxConsole.out); 375 } 376 377 return true; 378 } 379 380 /** 381 * Processes one pathname element. 382 * 383 * @param pathname {@code non-null;} the pathname to process. May 384 * be the path of a class file, a jar file, or a directory 385 * containing class files. 386 * @return whether any processing actually happened 387 */ 388 private static boolean processOne(String pathname) { 389 ClassPathOpener opener; 390 391 opener = new ClassPathOpener(pathname, false, 392 new ClassPathOpener.Consumer() { 393 public boolean processFileBytes(String name, long lastModified, byte[] bytes) { 394 if (args.numThreads > 1) { 395 threadPool.execute(new ParallelProcessor(name, lastModified, bytes)); 396 return false; 397 } else { 398 return Main.processFileBytes(name, lastModified, bytes); 399 } 400 } 401 public void onException(Exception ex) { 402 if (ex instanceof StopProcessing) { 403 throw (StopProcessing) ex; 404 } else if (ex instanceof SimException) { 405 DxConsole.err.println("\nEXCEPTION FROM SIMULATION:"); 406 DxConsole.err.println(ex.getMessage() + "\n"); 407 DxConsole.err.println(((SimException) ex).getContext()); 408 } else { 409 DxConsole.err.println("\nUNEXPECTED TOP-LEVEL EXCEPTION:"); 410 ex.printStackTrace(DxConsole.err); 411 } 412 errors++; 413 } 414 public void onProcessArchiveStart(File file) { 415 if (args.verbose) { 416 DxConsole.out.println("processing archive " + file + 417 "..."); 418 } 419 } 420 }); 421 422 return opener.process(); 423 } 424 425 /** 426 * Processes one file, which may be either a class or a resource. 427 * 428 * @param name {@code non-null;} name of the file 429 * @param bytes {@code non-null;} contents of the file 430 * @return whether processing was successful 431 */ 432 private static boolean processFileBytes(String name, long lastModified, byte[] bytes) { 433 boolean isClass = name.endsWith(".class"); 434 boolean isClassesDex = name.equals(DexFormat.DEX_IN_JAR_NAME); 435 boolean keepResources = (outputResources != null); 436 437 if (!isClass && !isClassesDex && !keepResources) { 438 if (args.verbose) { 439 DxConsole.out.println("ignored resource " + name); 440 } 441 return false; 442 } 443 444 if (args.verbose) { 445 DxConsole.out.println("processing " + name + "..."); 446 } 447 448 String fixedName = fixPath(name); 449 450 if (isClass) { 451 if (keepResources && args.keepClassesInJar) { 452 synchronized (outputResources) { 453 outputResources.put(fixedName, bytes); 454 } 455 } 456 if (lastModified < minimumFileAge) { 457 return true; 458 } 459 return processClass(fixedName, bytes); 460 } else if (isClassesDex) { 461 synchronized (libraryDexBuffers) { 462 libraryDexBuffers.add(bytes); 463 } 464 return true; 465 } else { 466 synchronized (outputResources) { 467 outputResources.put(fixedName, bytes); 468 } 469 return true; 470 } 471 } 472 473 /** 474 * Processes one classfile. 475 * 476 * @param name {@code non-null;} name of the file, clipped such that it 477 * <i>should</i> correspond to the name of the class it contains 478 * @param bytes {@code non-null;} contents of the file 479 * @return whether processing was successful 480 */ 481 private static boolean processClass(String name, byte[] bytes) { 482 if (! args.coreLibrary) { 483 checkClassName(name); 484 } 485 486 try { 487 ClassDefItem clazz = 488 CfTranslator.translate(name, bytes, args.cfOptions, args.dexOptions); 489 synchronized (outputDex) { 490 outputDex.add(clazz); 491 } 492 return true; 493 } catch (ParseException ex) { 494 DxConsole.err.println("\ntrouble processing:"); 495 if (args.debug) { 496 ex.printStackTrace(DxConsole.err); 497 } else { 498 ex.printContext(DxConsole.err); 499 } 500 } 501 502 warnings++; 503 return false; 504 } 505 506 /** 507 * Check the class name to make sure it's not a "core library" 508 * class. If there is a problem, this updates the error count and 509 * throws an exception to stop processing. 510 * 511 * @param name {@code non-null;} the fully-qualified internal-form 512 * class name 513 */ 514 private static void checkClassName(String name) { 515 boolean bogus = false; 516 517 if (name.startsWith("java/")) { 518 bogus = true; 519 } else if (name.startsWith("javax/")) { 520 int slashAt = name.indexOf('/', 6); 521 if (slashAt == -1) { 522 // Top-level javax classes are verboten. 523 bogus = true; 524 } else { 525 String pkg = name.substring(6, slashAt); 526 bogus = (Arrays.binarySearch(JAVAX_CORE, pkg) >= 0); 527 } 528 } 529 530 if (! bogus) { 531 return; 532 } 533 534 /* 535 * The user is probably trying to include an entire desktop 536 * core library in a misguided attempt to get their application 537 * working. Try to help them understand what's happening. 538 */ 539 540 DxConsole.err.println("\ntrouble processing \"" + name + "\":\n\n" + 541 IN_RE_CORE_CLASSES); 542 errors++; 543 throw new StopProcessing(); 544 } 545 546 /** 547 * Converts {@link #outputDex} into a {@code byte[]} and do whatever 548 * human-oriented dumping is required. 549 * 550 * @return {@code null-ok;} the converted {@code byte[]} or {@code null} 551 * if there was a problem 552 */ 553 private static byte[] writeDex() { 554 byte[] outArray = null; 555 556 try { 557 OutputStream humanOutRaw = null; 558 OutputStreamWriter humanOut = null; 559 try { 560 if (args.humanOutName != null) { 561 humanOutRaw = openOutput(args.humanOutName); 562 humanOut = new OutputStreamWriter(humanOutRaw); 563 } 564 565 if (args.methodToDump != null) { 566 /* 567 * Simply dump the requested method. Note: The call 568 * to toDex() is required just to get the underlying 569 * structures ready. 570 */ 571 outputDex.toDex(null, false); 572 dumpMethod(outputDex, args.methodToDump, humanOut); 573 } else { 574 /* 575 * This is the usual case: Create an output .dex file, 576 * and write it, dump it, etc. 577 */ 578 outArray = outputDex.toDex(humanOut, args.verboseDump); 579 } 580 581 if (args.statistics) { 582 DxConsole.out.println(outputDex.getStatistics().toHuman()); 583 } 584 } finally { 585 if (humanOut != null) { 586 humanOut.flush(); 587 } 588 closeOutput(humanOutRaw); 589 } 590 } catch (Exception ex) { 591 if (args.debug) { 592 DxConsole.err.println("\ntrouble writing output:"); 593 ex.printStackTrace(DxConsole.err); 594 } else { 595 DxConsole.err.println("\ntrouble writing output: " + 596 ex.getMessage()); 597 } 598 return null; 599 } 600 601 return outArray; 602 } 603 604 /** 605 * Creates a jar file from the resources and given dex file array. 606 * 607 * @param fileName {@code non-null;} name of the file 608 * @param dexArray array containing the dex file to include, or null if the 609 * output contains no class defs. 610 * @return whether the creation was successful 611 */ 612 private static boolean createJar(String fileName, byte[] dexArray) { 613 /* 614 * Make or modify the manifest (as appropriate), put the dex 615 * array into the resources map, and then process the entire 616 * resources map in a uniform manner. 617 */ 618 619 try { 620 Manifest manifest = makeManifest(); 621 OutputStream out = openOutput(fileName); 622 JarOutputStream jarOut = new JarOutputStream(out, manifest); 623 624 if (dexArray != null) { 625 outputResources.put(DexFormat.DEX_IN_JAR_NAME, dexArray); 626 } 627 628 try { 629 for (Map.Entry<String, byte[]> e : 630 outputResources.entrySet()) { 631 String name = e.getKey(); 632 byte[] contents = e.getValue(); 633 JarEntry entry = new JarEntry(name); 634 635 if (args.verbose) { 636 DxConsole.out.println("writing " + name + "; size " + 637 contents.length + "..."); 638 } 639 640 entry.setSize(contents.length); 641 jarOut.putNextEntry(entry); 642 jarOut.write(contents); 643 jarOut.closeEntry(); 644 } 645 } finally { 646 jarOut.finish(); 647 jarOut.flush(); 648 closeOutput(out); 649 } 650 } catch (Exception ex) { 651 if (args.debug) { 652 DxConsole.err.println("\ntrouble writing output:"); 653 ex.printStackTrace(DxConsole.err); 654 } else { 655 DxConsole.err.println("\ntrouble writing output: " + 656 ex.getMessage()); 657 } 658 return false; 659 } 660 661 return true; 662 } 663 664 /** 665 * Creates and returns the manifest to use for the output. This may 666 * modify {@link #outputResources} (removing the pre-existing manifest). 667 * 668 * @return {@code non-null;} the manifest 669 */ 670 private static Manifest makeManifest() throws IOException { 671 byte[] manifestBytes = outputResources.get(MANIFEST_NAME); 672 Manifest manifest; 673 Attributes attribs; 674 675 if (manifestBytes == null) { 676 // We need to construct an entirely new manifest. 677 manifest = new Manifest(); 678 attribs = manifest.getMainAttributes(); 679 attribs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); 680 } else { 681 manifest = new Manifest(new ByteArrayInputStream(manifestBytes)); 682 attribs = manifest.getMainAttributes(); 683 outputResources.remove(MANIFEST_NAME); 684 } 685 686 String createdBy = attribs.getValue(CREATED_BY); 687 if (createdBy == null) { 688 createdBy = ""; 689 } else { 690 createdBy += " + "; 691 } 692 createdBy += "dx " + Version.VERSION; 693 694 attribs.put(CREATED_BY, createdBy); 695 attribs.putValue("Dex-Location", DexFormat.DEX_IN_JAR_NAME); 696 697 return manifest; 698 } 699 700 /** 701 * Opens and returns the named file for writing, treating "-" specially. 702 * 703 * @param name {@code non-null;} the file name 704 * @return {@code non-null;} the opened file 705 */ 706 private static OutputStream openOutput(String name) throws IOException { 707 if (name.equals("-") || 708 name.startsWith("-.")) { 709 return System.out; 710 } 711 712 return new FileOutputStream(name); 713 } 714 715 /** 716 * Flushes and closes the given output stream, except if it happens to be 717 * {@link System#out} in which case this method does the flush but not 718 * the close. This method will also silently do nothing if given a 719 * {@code null} argument. 720 * 721 * @param stream {@code null-ok;} what to close 722 */ 723 private static void closeOutput(OutputStream stream) throws IOException { 724 if (stream == null) { 725 return; 726 } 727 728 stream.flush(); 729 730 if (stream != System.out) { 731 stream.close(); 732 } 733 } 734 735 /** 736 * Returns the "fixed" version of a given file path, suitable for 737 * use as a path within a {@code .jar} file and for checking 738 * against a classfile-internal "this class" name. This looks for 739 * the last instance of the substring {@code "/./"} within 740 * the path, and if it finds it, it takes the portion after to be 741 * the fixed path. If that isn't found but the path starts with 742 * {@code "./"}, then that prefix is removed and the rest is 743 * return. If neither of these is the case, this method returns 744 * its argument. 745 * 746 * @param path {@code non-null;} the path to "fix" 747 * @return {@code non-null;} the fixed version (which might be the same as 748 * the given {@code path}) 749 */ 750 private static String fixPath(String path) { 751 /* 752 * If the path separator is \ (like on windows), we convert the 753 * path to a standard '/' separated path. 754 */ 755 if (File.separatorChar == '\\') { 756 path = path.replace('\\', '/'); 757 } 758 759 int index = path.lastIndexOf("/./"); 760 761 if (index != -1) { 762 return path.substring(index + 3); 763 } 764 765 if (path.startsWith("./")) { 766 return path.substring(2); 767 } 768 769 return path; 770 } 771 772 /** 773 * Dumps any method with the given name in the given file. 774 * 775 * @param dex {@code non-null;} the dex file 776 * @param fqName {@code non-null;} the fully-qualified name of the 777 * method(s) 778 * @param out {@code non-null;} where to dump to 779 */ 780 private static void dumpMethod(DexFile dex, String fqName, 781 OutputStreamWriter out) { 782 boolean wildcard = fqName.endsWith("*"); 783 int lastDot = fqName.lastIndexOf('.'); 784 785 if ((lastDot <= 0) || (lastDot == (fqName.length() - 1))) { 786 DxConsole.err.println("bogus fully-qualified method name: " + 787 fqName); 788 return; 789 } 790 791 String className = fqName.substring(0, lastDot).replace('.', '/'); 792 String methodName = fqName.substring(lastDot + 1); 793 ClassDefItem clazz = dex.getClassOrNull(className); 794 795 if (clazz == null) { 796 DxConsole.err.println("no such class: " + className); 797 return; 798 } 799 800 if (wildcard) { 801 methodName = methodName.substring(0, methodName.length() - 1); 802 } 803 804 ArrayList<EncodedMethod> allMeths = clazz.getMethods(); 805 TreeMap<CstNat, EncodedMethod> meths = 806 new TreeMap<CstNat, EncodedMethod>(); 807 808 /* 809 * Figure out which methods to include in the output, and get them 810 * all sorted, so that the printout code is robust with respect to 811 * changes in the underlying order. 812 */ 813 for (EncodedMethod meth : allMeths) { 814 String methName = meth.getName().getString(); 815 if ((wildcard && methName.startsWith(methodName)) || 816 (!wildcard && methName.equals(methodName))) { 817 meths.put(meth.getRef().getNat(), meth); 818 } 819 } 820 821 if (meths.size() == 0) { 822 DxConsole.err.println("no such method: " + fqName); 823 return; 824 } 825 826 PrintWriter pw = new PrintWriter(out); 827 828 for (EncodedMethod meth : meths.values()) { 829 // TODO: Better stuff goes here, perhaps. 830 meth.debugPrint(pw, args.verboseDump); 831 832 /* 833 * The (default) source file is an attribute of the class, but 834 * it's useful to see it in method dumps. 835 */ 836 CstString sourceFile = clazz.getSourceFile(); 837 if (sourceFile != null) { 838 pw.println(" source file: " + sourceFile.toQuoted()); 839 } 840 841 Annotations methodAnnotations = 842 clazz.getMethodAnnotations(meth.getRef()); 843 AnnotationsList parameterAnnotations = 844 clazz.getParameterAnnotations(meth.getRef()); 845 846 if (methodAnnotations != null) { 847 pw.println(" method annotations:"); 848 for (Annotation a : methodAnnotations.getAnnotations()) { 849 pw.println(" " + a); 850 } 851 } 852 853 if (parameterAnnotations != null) { 854 pw.println(" parameter annotations:"); 855 int sz = parameterAnnotations.size(); 856 for (int i = 0; i < sz; i++) { 857 pw.println(" parameter " + i); 858 Annotations annotations = parameterAnnotations.get(i); 859 for (Annotation a : annotations.getAnnotations()) { 860 pw.println(" " + a); 861 } 862 } 863 } 864 } 865 866 pw.flush(); 867 } 868 869 /** 870 * Exception class used to halt processing prematurely. 871 */ 872 private static class StopProcessing extends RuntimeException { 873 // This space intentionally left blank. 874 } 875 876 /** 877 * Command-line argument parser and access. 878 */ 879 public static class Arguments { 880 /** whether to run in debug mode */ 881 public boolean debug = false; 882 883 /** whether to emit high-level verbose human-oriented output */ 884 public boolean verbose = false; 885 886 /** whether to emit verbose human-oriented output in the dump file */ 887 public boolean verboseDump = false; 888 889 /** whether we are constructing a core library */ 890 public boolean coreLibrary = false; 891 892 /** {@code null-ok;} particular method to dump */ 893 public String methodToDump = null; 894 895 /** max width for columnar output */ 896 public int dumpWidth = 0; 897 898 /** {@code null-ok;} output file name for binary file */ 899 public String outName = null; 900 901 /** {@code null-ok;} output file name for human-oriented dump */ 902 public String humanOutName = null; 903 904 /** whether strict file-name-vs-class-name checking should be done */ 905 public boolean strictNameCheck = true; 906 907 /** 908 * whether it is okay for there to be no {@code .class} files 909 * to process 910 */ 911 public boolean emptyOk = false; 912 913 /** 914 * whether the binary output is to be a {@code .jar} file 915 * instead of a plain {@code .dex} 916 */ 917 public boolean jarOutput = false; 918 919 /** 920 * when writing a {@code .jar} file, whether to still 921 * keep the {@code .class} files 922 */ 923 public boolean keepClassesInJar = false; 924 925 /** what API level to target */ 926 public int targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES; 927 928 /** how much source position info to preserve */ 929 public int positionInfo = PositionList.LINES; 930 931 /** whether to keep local variable information */ 932 public boolean localInfo = true; 933 934 /** whether to merge with the output dex file if it exists. */ 935 public boolean incremental = false; 936 937 /** whether to force generation of const-string/jumbo for all indexes, 938 * to allow merges between dex files with many strings. */ 939 public boolean forceJumbo = false; 940 941 /** {@code non-null} after {@link #parse}; file name arguments */ 942 public String[] fileNames; 943 944 /** whether to do SSA/register optimization */ 945 public boolean optimize = true; 946 947 /** Filename containg list of methods to optimize */ 948 public String optimizeListFile = null; 949 950 /** Filename containing list of methods to NOT optimize */ 951 public String dontOptimizeListFile = null; 952 953 /** Whether to print statistics to stdout at end of compile cycle */ 954 public boolean statistics; 955 956 /** Options for class file transformation */ 957 public CfOptions cfOptions; 958 959 /** Options for dex file output */ 960 public DexOptions dexOptions; 961 962 /** number of threads to run with */ 963 public int numThreads = 1; 964 965 private static class ArgumentsParser { 966 967 /** The arguments to process. */ 968 private final String[] arguments; 969 /** The index of the next argument to process. */ 970 private int index; 971 /** The current argument being processed after a {@link #getNext()} call. */ 972 private String current; 973 /** The last value of an argument processed by {@link #isArg(String)}. */ 974 private String lastValue; 975 976 public ArgumentsParser(String[] arguments) { 977 this.arguments = arguments; 978 index = 0; 979 } 980 981 public String getCurrent() { 982 return current; 983 } 984 985 public String getLastValue() { 986 return lastValue; 987 } 988 989 /** 990 * Moves on to the next argument. 991 * Returns false when we ran out of arguments that start with --. 992 */ 993 public boolean getNext() { 994 if (index >= arguments.length) { 995 return false; 996 } 997 current = arguments[index]; 998 if (current.equals("--") || !current.startsWith("--")) { 999 return false; 1000 } 1001 index++; 1002 return true; 1003 } 1004 1005 /** 1006 * Similar to {@link #getNext()}, this moves on the to next argument. 1007 * It does not check however whether the argument starts with -- 1008 * and thus can be used to retrieve values. 1009 */ 1010 private boolean getNextValue() { 1011 if (index >= arguments.length) { 1012 return false; 1013 } 1014 current = arguments[index]; 1015 index++; 1016 return true; 1017 } 1018 1019 /** 1020 * Returns all the arguments that have not been processed yet. 1021 */ 1022 public String[] getRemaining() { 1023 int n = arguments.length - index; 1024 String[] remaining = new String[n]; 1025 if (n > 0) { 1026 System.arraycopy(arguments, index, remaining, 0, n); 1027 } 1028 return remaining; 1029 } 1030 1031 /** 1032 * Checks the current argument against the given prefix. 1033 * If prefix is in the form '--name=', an extra value is expected. 1034 * The argument can then be in the form '--name=value' or as a 2-argument 1035 * form '--name value'. 1036 */ 1037 public boolean isArg(String prefix) { 1038 int n = prefix.length(); 1039 if (n > 0 && prefix.charAt(n-1) == '=') { 1040 // Argument accepts a value. Capture it. 1041 if (current.startsWith(prefix)) { 1042 // Argument is in the form --name=value, split the value out 1043 lastValue = current.substring(n); 1044 return true; 1045 } else { 1046 // Check whether we have "--name value" as 2 arguments 1047 prefix = prefix.substring(0, n-1); 1048 if (current.equals(prefix)) { 1049 if (getNextValue()) { 1050 lastValue = current; 1051 return true; 1052 } else { 1053 System.err.println("Missing value after parameter " + prefix); 1054 throw new UsageException(); 1055 } 1056 } 1057 return false; 1058 } 1059 } else { 1060 // Argument does not accept a value. 1061 return current.equals(prefix); 1062 } 1063 } 1064 } 1065 1066 /** 1067 * Parses the given command-line arguments. 1068 * 1069 * @param args {@code non-null;} the arguments 1070 */ 1071 public void parse(String[] args) { 1072 ArgumentsParser parser = new ArgumentsParser(args); 1073 1074 while(parser.getNext()) { 1075 if (parser.isArg("--debug")) { 1076 debug = true; 1077 } else if (parser.isArg("--verbose")) { 1078 verbose = true; 1079 } else if (parser.isArg("--verbose-dump")) { 1080 verboseDump = true; 1081 } else if (parser.isArg("--no-files")) { 1082 emptyOk = true; 1083 } else if (parser.isArg("--no-optimize")) { 1084 optimize = false; 1085 } else if (parser.isArg("--no-strict")) { 1086 strictNameCheck = false; 1087 } else if (parser.isArg("--core-library")) { 1088 coreLibrary = true; 1089 } else if (parser.isArg("--statistics")) { 1090 statistics = true; 1091 } else if (parser.isArg("--optimize-list=")) { 1092 if (dontOptimizeListFile != null) { 1093 System.err.println("--optimize-list and " 1094 + "--no-optimize-list are incompatible."); 1095 throw new UsageException(); 1096 } 1097 optimize = true; 1098 optimizeListFile = parser.getLastValue(); 1099 } else if (parser.isArg("--no-optimize-list=")) { 1100 if (dontOptimizeListFile != null) { 1101 System.err.println("--optimize-list and " 1102 + "--no-optimize-list are incompatible."); 1103 throw new UsageException(); 1104 } 1105 optimize = true; 1106 dontOptimizeListFile = parser.getLastValue(); 1107 } else if (parser.isArg("--keep-classes")) { 1108 keepClassesInJar = true; 1109 } else if (parser.isArg("--output=")) { 1110 outName = parser.getLastValue(); 1111 if (FileUtils.hasArchiveSuffix(outName)) { 1112 jarOutput = true; 1113 } else if (outName.endsWith(".dex") || 1114 outName.equals("-")) { 1115 jarOutput = false; 1116 } else { 1117 System.err.println("unknown output extension: " + 1118 outName); 1119 throw new UsageException(); 1120 } 1121 } else if (parser.isArg("--dump-to=")) { 1122 humanOutName = parser.getLastValue(); 1123 } else if (parser.isArg("--dump-width=")) { 1124 dumpWidth = Integer.parseInt(parser.getLastValue()); 1125 } else if (parser.isArg("--dump-method=")) { 1126 methodToDump = parser.getLastValue(); 1127 jarOutput = false; 1128 } else if (parser.isArg("--positions=")) { 1129 String pstr = parser.getLastValue().intern(); 1130 if (pstr == "none") { 1131 positionInfo = PositionList.NONE; 1132 } else if (pstr == "important") { 1133 positionInfo = PositionList.IMPORTANT; 1134 } else if (pstr == "lines") { 1135 positionInfo = PositionList.LINES; 1136 } else { 1137 System.err.println("unknown positions option: " + 1138 pstr); 1139 throw new UsageException(); 1140 } 1141 } else if (parser.isArg("--no-locals")) { 1142 localInfo = false; 1143 } else if (parser.isArg("--num-threads=")) { 1144 numThreads = Integer.parseInt(parser.getLastValue()); 1145 } else if (parser.isArg("--incremental")) { 1146 incremental = true; 1147 } else if (parser.isArg("--force-jumbo")) { 1148 forceJumbo = true; 1149 } else { 1150 System.err.println("unknown option: " + parser.getCurrent()); 1151 throw new UsageException(); 1152 } 1153 } 1154 1155 fileNames = parser.getRemaining(); 1156 if (fileNames.length == 0) { 1157 if (!emptyOk) { 1158 System.err.println("no input files specified"); 1159 throw new UsageException(); 1160 } 1161 } else if (emptyOk) { 1162 System.out.println("ignoring input files"); 1163 } 1164 1165 if ((humanOutName == null) && (methodToDump != null)) { 1166 humanOutName = "-"; 1167 } 1168 1169 makeOptionsObjects(); 1170 } 1171 1172 /** 1173 * Copies relevent arguments over into CfOptions and 1174 * DexOptions instances. 1175 */ 1176 private void makeOptionsObjects() { 1177 cfOptions = new CfOptions(); 1178 cfOptions.positionInfo = positionInfo; 1179 cfOptions.localInfo = localInfo; 1180 cfOptions.strictNameCheck = strictNameCheck; 1181 cfOptions.optimize = optimize; 1182 cfOptions.optimizeListFile = optimizeListFile; 1183 cfOptions.dontOptimizeListFile = dontOptimizeListFile; 1184 cfOptions.statistics = statistics; 1185 cfOptions.warn = DxConsole.err; 1186 1187 dexOptions = new DexOptions(); 1188 dexOptions.targetApiLevel = targetApiLevel; 1189 dexOptions.forceJumbo = forceJumbo; 1190 } 1191 } 1192 1193 /** Runnable helper class to process files in multiple threads */ 1194 private static class ParallelProcessor implements Runnable { 1195 1196 String path; 1197 long lastModified; 1198 byte[] bytes; 1199 1200 /** 1201 * Constructs an instance. 1202 * 1203 * @param path {@code non-null;} filename of element. May not be a valid 1204 * filesystem path. 1205 * @param bytes {@code non-null;} file data 1206 */ 1207 private ParallelProcessor(String path, long lastModified, byte bytes[]) { 1208 this.path = path; 1209 this.lastModified = lastModified; 1210 this.bytes = bytes; 1211 } 1212 1213 /** 1214 * Task run by each thread in the thread pool. Runs processFileBytes 1215 * with the given path and bytes. 1216 */ 1217 public void run() { 1218 if (Main.processFileBytes(path, lastModified, bytes)) { 1219 anyFilesProcessed = true; 1220 } 1221 } 1222 } 1223} 1224