diff options
author | Ivan Gavrilovic <gavra@google.com> | 2017-09-08 11:40:54 +0100 |
---|---|---|
committer | Ivan Gavrilovic <gavra@google.com> | 2017-09-08 11:40:54 +0100 |
commit | 4de512e03895a6a0807f3995e4fa4bd7023ebaf9 (patch) | |
tree | 37c01c8ecfcf31769cfd00e963d83c7b9dd7c133 | |
parent | e2e57e78f1478d15d7aedb565f1f141907185918 (diff) | |
parent | 0524d7eaff7f277fa13c0568f75498642a21089e (diff) | |
download | r8-4de512e03895a6a0807f3995e4fa4bd7023ebaf9.tar.gz |
Merge remote-tracking branch 'goog/upstream-d8-0.1' into studio-master-canary
* goog/upstream-d8-0.1:
Version 0.1.11.
24 files changed, 968 insertions, 256 deletions
diff --git a/build.gradle b/build.gradle index 63513ce32..c6fdf8ded 100644 --- a/build.gradle +++ b/build.gradle @@ -435,6 +435,20 @@ task DexSegments(type: Jar) { } } +task maindex(type: Jar) { + from sourceSets.main.output + baseName 'maindex' + manifest { + attributes 'Main-Class': 'com.android.tools.r8.GenerateMainDexList' + } + // In order to build without dependencies, pass the exclude_deps property using: + // gradle -Pexclude_deps maindex + if (!project.hasProperty('exclude_deps')) { + // Also include dependencies + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + } +} + task ExtractMarker(type: Jar) { from sourceSets.main.output baseName 'extractmarker' diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java index debe8a127..cbd8dd6bd 100644 --- a/src/main/java/com/android/tools/r8/BaseCommand.java +++ b/src/main/java/com/android/tools/r8/BaseCommand.java @@ -3,51 +3,33 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8; -import com.android.tools.r8.dex.Constants; import com.android.tools.r8.utils.AndroidApp; -import com.android.tools.r8.utils.FileUtils; import com.android.tools.r8.utils.InternalOptions; -import com.android.tools.r8.utils.OutputMode; import java.io.IOException; import java.nio.file.Path; import java.util.Collection; +/** + * Base class for commands and command builders for applications/tools which take an Android + * application (and a main-dex list) as input. + */ abstract class BaseCommand { private final boolean printHelp; private final boolean printVersion; private final AndroidApp app; - private final Path outputPath; - private final OutputMode outputMode; - private final CompilationMode mode; - private final int minApiLevel; BaseCommand(boolean printHelp, boolean printVersion) { this.printHelp = printHelp; this.printVersion = printVersion; // All other fields are initialized with stub/invalid values. this.app = null; - this.outputPath = null; - this.outputMode = OutputMode.Indexed; - this.mode = null; - this.minApiLevel = 0; } - BaseCommand( - AndroidApp app, - Path outputPath, - OutputMode outputMode, - CompilationMode mode, - int minApiLevel) { + BaseCommand(AndroidApp app) { assert app != null; - assert mode != null; - assert minApiLevel > 0; this.app = app; - this.outputPath = outputPath; - this.outputMode = outputMode; - this.mode = mode; - this.minApiLevel = minApiLevel; // Print options are not set. printHelp = false; printVersion = false; @@ -69,48 +51,27 @@ abstract class BaseCommand { // Internal access to the internal options. abstract InternalOptions getInternalOptions(); - public Path getOutputPath() { - return outputPath; - } - - public CompilationMode getMode() { - return mode; - } - - public int getMinApiLevel() { - return minApiLevel; - } - - public OutputMode getOutputMode() { - return outputMode; - } - - abstract static class Builder<C extends BaseCommand, B extends Builder<C, B>> { + abstract public static class Builder<C extends BaseCommand, B extends Builder<C, B>> { private boolean printHelp = false; private boolean printVersion = false; private final AndroidApp.Builder app; - private Path outputPath = null; - private OutputMode outputMode = OutputMode.Indexed; - private CompilationMode mode; - private int minApiLevel = Constants.DEFAULT_ANDROID_API; - // Internal flag used by CompatDx to ignore dex files in archives. - protected boolean ignoreDexInArchive = false; + protected Builder() { + this(AndroidApp.builder(), false); + } - protected Builder(CompilationMode mode) { - this(AndroidApp.builder(), mode); + protected Builder(boolean ignoreDexInArchive) { + this(AndroidApp.builder(), ignoreDexInArchive); } // Internal constructor for testing. Builder(AndroidApp app, CompilationMode mode) { - this(AndroidApp.builder(app), mode); + this(AndroidApp.builder(app), false); } - private Builder(AndroidApp.Builder builder, CompilationMode mode) { - assert mode != null; + protected Builder(AndroidApp.Builder builder, boolean ignoreDexInArchive) { this.app = builder; - this.mode = mode; app.setIgnoreDexInArchive(ignoreDexInArchive); } @@ -177,52 +138,6 @@ abstract class BaseCommand { return self(); } - /** Get current compilation mode. */ - public CompilationMode getMode() { - return mode; - } - - /** Set compilation mode. */ - public B setMode(CompilationMode mode) { - assert mode != null; - this.mode = mode; - return self(); - } - - /** Get the output path. Null if not set. */ - public Path getOutputPath() { - return outputPath; - } - - /** Get the output mode. */ - public OutputMode getOutputMode() { - return outputMode; - } - - /** Set an output path. Must be an existing directory or a zip file. */ - public B setOutputPath(Path outputPath) { - this.outputPath = outputPath; - return self(); - } - - /** Set an output mode. */ - public B setOutputMode(OutputMode outputMode) { - this.outputMode = outputMode; - return self(); - } - - /** Get the minimum API level (aka SDK version). */ - public int getMinApiLevel() { - return minApiLevel; - } - - /** Set the minimum required API level (aka SDK version). */ - public B setMinApiLevel(int minApiLevel) { - assert minApiLevel > 0; - this.minApiLevel = minApiLevel; - return self(); - } - /** * Add main-dex list files. * @@ -296,11 +211,7 @@ abstract class BaseCommand { } protected void validate() throws CompilationException { - if (app.hasMainDexList() && outputMode == OutputMode.FilePerClass) { - throw new CompilationException( - "Option --main-dex-list cannot be used with --file-per-class"); - } - FileUtils.validateOutputFile(outputPath); + // Currently does nothing. } } } diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java new file mode 100644 index 000000000..5e8f97584 --- /dev/null +++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java @@ -0,0 +1,146 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +package com.android.tools.r8; + +import com.android.tools.r8.dex.Constants; +import com.android.tools.r8.utils.AndroidApp; +import com.android.tools.r8.utils.FileUtils; +import com.android.tools.r8.utils.OutputMode; +import java.nio.file.Path; + +/** + * Base class for commands and command builders for compiler applications/tools which besides an + * Android application (and a main-dex list) also takes compilation output, compilation mode and + * min API level as input. + */ +abstract class BaseCompilerCommand extends BaseCommand { + + private final Path outputPath; + private final OutputMode outputMode; + private final CompilationMode mode; + private final int minApiLevel; + + BaseCompilerCommand(boolean printHelp, boolean printVersion) { + super(printHelp, printVersion); + + this.outputPath = null; + this.outputMode = OutputMode.Indexed; + this.mode = null; + this.minApiLevel = 0; + } + + BaseCompilerCommand( + AndroidApp app, + Path outputPath, + OutputMode outputMode, + CompilationMode mode, + int minApiLevel) { + super(app); + assert mode != null; + assert minApiLevel > 0; + this.outputPath = outputPath; + this.outputMode = outputMode; + this.mode = mode; + this.minApiLevel = minApiLevel; + } + + public Path getOutputPath() { + return outputPath; + } + + public CompilationMode getMode() { + return mode; + } + + public int getMinApiLevel() { + return minApiLevel; + } + + public OutputMode getOutputMode() { + return outputMode; + } + + abstract public static class Builder<C extends BaseCompilerCommand, B extends Builder<C, B>> + extends BaseCommand.Builder<C, B> { + + private Path outputPath = null; + private OutputMode outputMode = OutputMode.Indexed; + private CompilationMode mode; + private int minApiLevel = Constants.DEFAULT_ANDROID_API; + + protected Builder(CompilationMode mode) { + this(AndroidApp.builder(), mode, false); + } + + protected Builder(CompilationMode mode, boolean ignoreDexInArchive) { + this(AndroidApp.builder(), mode, ignoreDexInArchive); + } + + // Internal constructor for testing. + Builder(AndroidApp app, CompilationMode mode) { + this(AndroidApp.builder(app), mode, false); + } + + private Builder(AndroidApp.Builder builder, CompilationMode mode, boolean ignoreDexInArchive) { + super(builder, ignoreDexInArchive); + assert mode != null; + this.mode = mode; + } + + /** Get current compilation mode. */ + public CompilationMode getMode() { + return mode; + } + + /** Set compilation mode. */ + public B setMode(CompilationMode mode) { + assert mode != null; + this.mode = mode; + return self(); + } + + /** Get the output path. Null if not set. */ + public Path getOutputPath() { + return outputPath; + } + + /** Get the output mode. */ + public OutputMode getOutputMode() { + return outputMode; + } + + /** Set an output path. Must be an existing directory or a zip file. */ + public B setOutputPath(Path outputPath) { + this.outputPath = outputPath; + return self(); + } + + /** Set an output mode. */ + public B setOutputMode(OutputMode outputMode) { + this.outputMode = outputMode; + return self(); + } + + /** Get the minimum API level (aka SDK version). */ + public int getMinApiLevel() { + return minApiLevel; + } + + /** Set the minimum required API level (aka SDK version). */ + public B setMinApiLevel(int minApiLevel) { + assert minApiLevel > 0; + this.minApiLevel = minApiLevel; + return self(); + } + + protected void validate() throws CompilationException { + super.validate(); + if (getAppBuilder().hasMainDexList() && outputMode == OutputMode.FilePerClass) { + throw new CompilationException( + "Option --main-dex-list cannot be used with --file-per-class"); + } + FileUtils.validateOutputFile(outputPath); + } + } +} diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java index f7e65d9a2..e7a1d6ea3 100644 --- a/src/main/java/com/android/tools/r8/D8.java +++ b/src/main/java/com/android/tools/r8/D8.java @@ -55,7 +55,7 @@ import java.util.concurrent.ExecutorService; */ public final class D8 { - private static final String VERSION = "v0.1.10"; + private static final String VERSION = "v0.1.11"; private static final int STATUS_ERROR = 1; private D8() {} diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java index cb8c17045..5b2e5cfb3 100644 --- a/src/main/java/com/android/tools/r8/D8Command.java +++ b/src/main/java/com/android/tools/r8/D8Command.java @@ -27,15 +27,19 @@ import java.util.Collection; * .build(); * </pre> */ -public class D8Command extends BaseCommand { +public class D8Command extends BaseCompilerCommand { /** * Builder for constructing a D8Command. */ - public static class Builder extends BaseCommand.Builder<D8Command, Builder> { + public static class Builder extends BaseCompilerCommand.Builder<D8Command, Builder> { private boolean intermediate = false; + protected Builder(boolean ignoreDexInArchive) { + super(CompilationMode.DEBUG, ignoreDexInArchive); + } + protected Builder() { super(CompilationMode.DEBUG); } diff --git a/src/main/java/com/android/tools/r8/DexSegments.java b/src/main/java/com/android/tools/r8/DexSegments.java index 3827cae0f..24c1789ef 100644 --- a/src/main/java/com/android/tools/r8/DexSegments.java +++ b/src/main/java/com/android/tools/r8/DexSegments.java @@ -8,11 +8,9 @@ import com.android.tools.r8.dex.Segment; import com.android.tools.r8.shaking.ProguardRuleParserException; import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.InternalOptions; -import com.android.tools.r8.utils.OutputMode; import com.google.common.collect.ImmutableList; import com.google.common.io.Closer; import java.io.IOException; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -24,10 +22,6 @@ public class DexSegments { public static class Builder extends BaseCommand.Builder<Command, Builder> { - private Builder() { - super(CompilationMode.RELEASE); - } - @Override Command.Builder self() { return this; @@ -40,8 +34,7 @@ public class DexSegments { return new Command(isPrintHelp()); } validate(); - return new Command( - getAppBuilder().build(), getOutputPath(), getOutputMode(), getMode(), getMinApiLevel()); + return new Command(getAppBuilder().build()); } } @@ -79,13 +72,8 @@ public class DexSegments { } } - private Command( - AndroidApp inputApp, - Path outputPath, - OutputMode outputMode, - CompilationMode mode, - int minApiLevel) { - super(inputApp, outputPath, outputMode, mode, minApiLevel); + private Command(AndroidApp inputApp) { + super(inputApp); } private Command(boolean printHelp) { diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java index 2f1d523b9..6b7df778f 100644 --- a/src/main/java/com/android/tools/r8/Disassemble.java +++ b/src/main/java/com/android/tools/r8/Disassemble.java @@ -6,7 +6,6 @@ package com.android.tools.r8; import com.android.tools.r8.shaking.ProguardRuleParserException; import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.InternalOptions; -import com.android.tools.r8.utils.OutputMode; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; @@ -16,13 +15,13 @@ import java.util.concurrent.ExecutionException; public class Disassemble { public static class DisassembleCommand extends BaseCommand { + private final Path outputPath; + public static class Builder extends BaseCommand.Builder<DisassembleCommand, DisassembleCommand.Builder> { - private boolean useSmali = false; - private Builder() { - super(CompilationMode.RELEASE); - } + private Path outputPath = null; + private boolean useSmali = false; @Override DisassembleCommand.Builder self() { @@ -34,6 +33,15 @@ public class Disassemble { return this; } + public Path getOutputPath() { + return outputPath; + } + + public DisassembleCommand.Builder setOutputPath(Path outputPath) { + this.outputPath = outputPath; + return this; + } + public DisassembleCommand.Builder setUseSmali(boolean useSmali) { this.useSmali = useSmali; return this; @@ -47,13 +55,7 @@ public class Disassemble { } validate(); - return new DisassembleCommand( - getAppBuilder().build(), - getOutputPath(), - getOutputMode(), - getMode(), - getMinApiLevel(), - useSmali); + return new DisassembleCommand(getAppBuilder().build(), getOutputPath(), useSmali); } } @@ -94,6 +96,9 @@ public class Disassemble { builder.setUseSmali(true); } else if (arg.equals("--pg-map")) { builder.setProguardMapFile(Paths.get(args[++i])); + } else if (arg.equals("--output")) { + String outputPath = args[++i]; + builder.setOutputPath(Paths.get(outputPath)); } else { if (arg.startsWith("--")) { throw new CompilationException("Unknown option: " + arg); @@ -103,23 +108,22 @@ public class Disassemble { } } - private DisassembleCommand( - AndroidApp inputApp, - Path outputPath, - OutputMode outputMode, - CompilationMode mode, - int minApiLevel, - boolean useSmali) { - super(inputApp, outputPath, outputMode, mode, minApiLevel); - assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8"; + private DisassembleCommand(AndroidApp inputApp, Path outputPath, boolean useSmali) { + super(inputApp); + this.outputPath = outputPath; this.useSmali = useSmali; } private DisassembleCommand(boolean printHelp, boolean printVersion) { super(printHelp, printVersion); + this.outputPath = null; this.useSmali = false; } + public Path getOutputPath() { + return outputPath; + } + public boolean useSmali() { return useSmali; } diff --git a/src/main/java/com/android/tools/r8/ExtractMarker.java b/src/main/java/com/android/tools/r8/ExtractMarker.java index c1234b84e..bf3b9196b 100644 --- a/src/main/java/com/android/tools/r8/ExtractMarker.java +++ b/src/main/java/com/android/tools/r8/ExtractMarker.java @@ -3,19 +3,15 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8; -import com.google.common.collect.ImmutableList; - import com.android.tools.r8.dex.ApplicationReader; import com.android.tools.r8.dex.Marker; import com.android.tools.r8.graph.DexApplication; import com.android.tools.r8.shaking.ProguardRuleParserException; import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.InternalOptions; -import com.android.tools.r8.utils.OutputMode; import com.android.tools.r8.utils.Timing; - +import com.google.common.collect.ImmutableList; import java.io.IOException; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.ExecutionException; @@ -25,10 +21,6 @@ public class ExtractMarker { public static class Builder extends BaseCommand.Builder<ExtractMarker.Command, ExtractMarker.Command.Builder> { - private Builder() { - super(CompilationMode.RELEASE); - } - @Override ExtractMarker.Command.Builder self() { return this; @@ -41,8 +33,7 @@ public class ExtractMarker { return new ExtractMarker.Command(isPrintHelp()); } validate(); - return new ExtractMarker.Command( - getAppBuilder().build(), getOutputPath(), getOutputMode(), getMode(), getMinApiLevel()); + return new ExtractMarker.Command(getAppBuilder().build()); } } @@ -80,13 +71,8 @@ public class ExtractMarker { } } - private Command( - AndroidApp inputApp, - Path outputPath, - OutputMode outputMode, - CompilationMode mode, - int minApiLevel) { - super(inputApp, outputPath, outputMode, mode, minApiLevel); + private Command(AndroidApp inputApp) { + super(inputApp); } private Command(boolean printHelp) { diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java new file mode 100644 index 000000000..44d523e6b --- /dev/null +++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java @@ -0,0 +1,124 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +package com.android.tools.r8; + +import com.android.tools.r8.dex.ApplicationReader; +import com.android.tools.r8.graph.AppInfoWithSubtyping; +import com.android.tools.r8.graph.DexApplication; +import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.shaking.Enqueuer; +import com.android.tools.r8.shaking.MainDexListBuilder; +import com.android.tools.r8.shaking.ProguardRuleParserException; +import com.android.tools.r8.shaking.RootSetBuilder; +import com.android.tools.r8.shaking.RootSetBuilder.RootSet; +import com.android.tools.r8.utils.AndroidApp; +import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.ThreadUtils; +import com.android.tools.r8.utils.Timing; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; + +public class GenerateMainDexList { + private static final String VERSION = "v0.2.0"; + private final Timing timing = new Timing("maindex"); + private final InternalOptions options; + + private GenerateMainDexList(InternalOptions options) { + this.options = options; + } + + private List<String> run(AndroidApp app) throws IOException, ExecutionException { + ExecutorService executor = ThreadUtils.getExecutorService(options); + DexApplication application = new ApplicationReader(app, options, timing).read(executor); + AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application); + RootSet mainDexRootSet = + new RootSetBuilder(application, appInfo, options.mainDexKeepRules).run(executor); + Set<DexType> mainDexBaseClasses = new Enqueuer(appInfo).traceMainDex(mainDexRootSet, timing); + Set<DexType> mainDexClasses = new MainDexListBuilder(mainDexBaseClasses, application).run(); + + List<String> result = mainDexClasses.stream() + .map(c -> c.toSourceString().replace('.', '/') + ".class") + .sorted() + .collect(Collectors.toList()); + + if (options.printMainDexListFile != null) { + try (OutputStream mainDexOut = Files.newOutputStream(options.printMainDexListFile, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + PrintWriter writer = new PrintWriter(mainDexOut); + result.forEach(writer::println); + writer.flush(); + } + } + + return result; + } + + /** + * Main API entry for computing the main-dex list. + * + * The main-dex list is represented as a list of strings, each string specifies one class to + * keep in the primary dex file (<code>classes.dex</code>). + * + * A class is specified using the following format: "com/example/MyClass.class". That is + * "/" as separator between package components, and a trailing ".class". + * + * @param command main dex-list generator command. + * @return classes to keep in the primary dex file. + */ + public static List<String> run(GenerateMainDexListCommand command) + throws IOException, ExecutionException { + ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions()); + try { + return run(command, executorService); + } finally { + executorService.shutdown(); + } + } + + /** + * Main API entry for computing the main-dex list. + * + * The main-dex list is represented as a list of strings, each string specifies one class to + * keep in the primary dex file (<code>classes.dex</code>). + * + * A class is specified using the following format: "com/example/MyClass.class". That is + * "/" as separator between package components, and a trailing ".class". + * + * @param command main dex-list generator command. + * @param executor executor service from which to get threads for multi-threaded processing. + * @return classes to keep in the primary dex file. + */ + public static List<String> run(GenerateMainDexListCommand command, ExecutorService executor) + throws IOException, ExecutionException { + AndroidApp app = command.getInputApp(); + InternalOptions options = command.getInternalOptions(); + return new GenerateMainDexList(options).run(app); + } + + public static void main(String[] args) + throws IOException, ProguardRuleParserException, CompilationException, ExecutionException { + GenerateMainDexListCommand.Builder builder = GenerateMainDexListCommand.parse(args); + GenerateMainDexListCommand command = builder.build(); + if (command.isPrintHelp()) { + System.out.println(GenerateMainDexListCommand.USAGE_MESSAGE); + return; + } + if (command.isPrintVersion()) { + System.out.println("MainDexListGenerator " + VERSION); + return; + } + List<String> result = run(command); + if (command.getMainDexListOutputPath() == null) { + result.forEach(System.out::println); + } + } +} diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java new file mode 100644 index 000000000..76d7d56f9 --- /dev/null +++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java @@ -0,0 +1,200 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +package com.android.tools.r8; + +import com.android.tools.r8.graph.DexItemFactory; +import com.android.tools.r8.shaking.ProguardConfigurationParser; +import com.android.tools.r8.shaking.ProguardConfigurationRule; +import com.android.tools.r8.shaking.ProguardConfigurationSource; +import com.android.tools.r8.shaking.ProguardConfigurationSourceFile; +import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings; +import com.android.tools.r8.shaking.ProguardRuleParserException; +import com.android.tools.r8.utils.AndroidApp; +import com.android.tools.r8.utils.InternalOptions; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class GenerateMainDexListCommand extends BaseCommand { + + private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules; + private final Path mainDexListOutput; + private final DexItemFactory factory; + + /** + * Get the output path for the main-dex list. Null if not set. + */ + public Path getMainDexListOutputPath() { + return mainDexListOutput; + } + + public static class Builder extends BaseCommand.Builder<GenerateMainDexListCommand, Builder> { + + private final DexItemFactory factory = new DexItemFactory(); + private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>(); + private Path mainDexListOutput = null; + + @Override + GenerateMainDexListCommand.Builder self() { + return this; + } + + /** + * Add proguard configuration file resources for automatic main dex list calculation. + */ + public GenerateMainDexListCommand.Builder addMainDexRulesFiles(Path... paths) { + for (Path path : paths) { + mainDexRules.add(new ProguardConfigurationSourceFile(path)); + } + return self(); + } + + /** + * Add proguard configuration file resources for automatic main dex list calculation. + */ + public GenerateMainDexListCommand.Builder addMainDexRulesFiles(List<Path> paths) { + for (Path path : paths) { + mainDexRules.add(new ProguardConfigurationSourceFile(path)); + } + return self(); + } + + /** + * Add proguard configuration for automatic main dex list calculation. + */ + public GenerateMainDexListCommand.Builder addMainDexRules(List<String> lines) { + mainDexRules.add(new ProguardConfigurationSourceStrings(lines)); + return self(); + } + + /** + * Get the output path for the main-dex list. Null if not set. + */ + public Path getMainDexListOutputPath() { + return mainDexListOutput; + } + + /** + * Set the output file for the main-dex list. + * + * If the file exists it will be overwritten. + */ + public GenerateMainDexListCommand.Builder setMainDexListOutputPath(Path mainDexListOutputPath) { + mainDexListOutput = mainDexListOutputPath; + return self(); + } + + + @Override + public GenerateMainDexListCommand build() throws CompilationException, IOException { + // If printing versions ignore everything else. + if (isPrintHelp() || isPrintVersion()) { + return new GenerateMainDexListCommand(isPrintHelp(), isPrintVersion()); + } + + validate(); + ImmutableList<ProguardConfigurationRule> mainDexKeepRules; + if (this.mainDexRules.isEmpty()) { + mainDexKeepRules = ImmutableList.of(); + } else { + ProguardConfigurationParser parser = new ProguardConfigurationParser(factory); + try { + parser.parse(mainDexRules); + } catch (ProguardRuleParserException e) { + throw new CompilationException(e.getMessage(), e.getCause()); + } + mainDexKeepRules = parser.getConfig().getRules(); + } + + return new GenerateMainDexListCommand( + factory, + getAppBuilder().build(), + mainDexKeepRules, + mainDexListOutput); + } + } + + static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of( + "Usage: maindex [options] <input-files>", + " where <input-files> are JAR files", + " and options are:", + " --main-dex-rules <file> # Proguard keep rules for classes to place in the", + " # primary dex file.", + " --main-dex-list <file> # List of classes to place in the primary dex file.", + " --main-dex-list-output <file> # Output the full main-dex list in <file>.", + " --version # Print the version.", + " --help # Print this message.")); + + + public static GenerateMainDexListCommand.Builder builder() { + return new GenerateMainDexListCommand.Builder(); + } + + public static GenerateMainDexListCommand.Builder parse(String[] args) + throws CompilationException, IOException { + GenerateMainDexListCommand.Builder builder = builder(); + parse(args, builder); + return builder; + } + + private static void parse(String[] args, GenerateMainDexListCommand.Builder builder) + throws CompilationException, IOException { + for (int i = 0; i < args.length; i++) { + String arg = args[i].trim(); + if (arg.length() == 0) { + continue; + } else if (arg.equals("--help")) { + builder.setPrintHelp(true); + } else if (arg.equals("--version")) { + builder.setPrintVersion(true); + } else if (arg.equals("--main-dex-rules")) { + builder.addMainDexRulesFiles(Paths.get(args[++i])); + } else if (arg.equals("--main-dex-list")) { + builder.addMainDexListFiles(Paths.get(args[++i])); + } else if (arg.equals("--main-dex-list-output")) { + builder.setMainDexListOutputPath(Paths.get(args[++i])); + } else { + if (arg.startsWith("--")) { + throw new CompilationException("Unknown option: " + arg); + } + builder.addProgramFiles(Paths.get(arg)); + } + } + } + + private GenerateMainDexListCommand( + DexItemFactory factory, + AndroidApp inputApp, + ImmutableList<ProguardConfigurationRule> mainDexKeepRules, + Path mainDexListOutput) { + super(inputApp); + this.factory = factory; + this.mainDexKeepRules = mainDexKeepRules; + this.mainDexListOutput = mainDexListOutput; + } + + private GenerateMainDexListCommand(boolean printHelp, boolean printVersion) { + super(printHelp, printVersion); + this.factory = new DexItemFactory(); + this.mainDexKeepRules = ImmutableList.of(); + this.mainDexListOutput = null; + } + + @Override + InternalOptions getInternalOptions() { + InternalOptions internal = new InternalOptions(factory); + internal.mainDexKeepRules = mainDexKeepRules; + if (mainDexListOutput != null) { + internal.printMainDexListFile = mainDexListOutput; + } + internal.minimalMainDex = internal.debug; + internal.removeSwitchMaps = false; + internal.inlineAccessors = false; + return internal; + } +} + diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java index edfe01e9d..6e7e36dc9 100644 --- a/src/main/java/com/android/tools/r8/R8.java +++ b/src/main/java/com/android/tools/r8/R8.java @@ -71,7 +71,7 @@ import java.util.concurrent.Executors; public class R8 { - private static final String VERSION = "v0.1.10"; + private static final String VERSION = "v0.1.11"; private final Timing timing = new Timing("R8"); private final InternalOptions options; diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java index ec040a597..14b53c2e4 100644 --- a/src/main/java/com/android/tools/r8/R8Command.java +++ b/src/main/java/com/android/tools/r8/R8Command.java @@ -8,6 +8,9 @@ import com.android.tools.r8.shaking.ProguardConfiguration; import com.android.tools.r8.shaking.ProguardConfiguration.Builder; import com.android.tools.r8.shaking.ProguardConfigurationParser; import com.android.tools.r8.shaking.ProguardConfigurationRule; +import com.android.tools.r8.shaking.ProguardConfigurationSource; +import com.android.tools.r8.shaking.ProguardConfigurationSourceFile; +import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings; import com.android.tools.r8.shaking.ProguardRuleParserException; import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.FileUtils; @@ -18,19 +21,18 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Consumer; -public class R8Command extends BaseCommand { +public class R8Command extends BaseCompilerCommand { - public static class Builder extends BaseCommand.Builder<R8Command, Builder> { + public static class Builder extends BaseCompilerCommand.Builder<R8Command, Builder> { - private final List<Path> mainDexRules = new ArrayList<>(); + private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>(); private Path mainDexListOutput = null; private Consumer<ProguardConfiguration.Builder> proguardConfigurationConsumer = null; - private final List<Path> proguardConfigFiles = new ArrayList<>(); + private final List<ProguardConfigurationSource> proguardConfigs = new ArrayList<>(); private Optional<Boolean> treeShaking = Optional.empty(); private Optional<Boolean> minification = Optional.empty(); private boolean ignoreMissingClasses = false; @@ -68,16 +70,28 @@ public class R8Command extends BaseCommand { /** * Add proguard configuration file resources for automatic main dex list calculation. */ - public Builder addMainDexRules(Path... paths) { - Collections.addAll(mainDexRules, paths); + public Builder addMainDexRulesFiles(Path... paths) { + for (Path path : paths) { + mainDexRules.add(new ProguardConfigurationSourceFile(path)); + } return self(); } /** * Add proguard configuration file resources for automatic main dex list calculation. */ - public Builder addMainDexRules(List<Path> paths) { - mainDexRules.addAll(paths); + public Builder addMainDexRulesFiles(List<Path> paths) { + for (Path path : paths) { + mainDexRules.add(new ProguardConfigurationSourceFile(path)); + } + return self(); + } + + /** + * Add proguard configuration for automatic main dex list calculation. + */ + public Builder addMainDexRules(List<String> lines) { + mainDexRules.add(new ProguardConfigurationSourceStrings(lines)); return self(); } @@ -90,7 +104,9 @@ public class R8Command extends BaseCommand { * Add proguard configuration file resources. */ public Builder addProguardConfigurationFiles(Path... paths) { - Collections.addAll(proguardConfigFiles, paths); + for (Path path : paths) { + proguardConfigs.add(new ProguardConfigurationSourceFile(path)); + } return self(); } @@ -98,7 +114,17 @@ public class R8Command extends BaseCommand { * Add proguard configuration file resources. */ public Builder addProguardConfigurationFiles(List<Path> paths) { - proguardConfigFiles.addAll(paths); + for (Path path : paths) { + proguardConfigs.add(new ProguardConfigurationSourceFile(path)); + } + return self(); + } + + /** + * Add proguard configuration. + */ + public Builder addProguardConfiguration(List<String> lines) { + proguardConfigs.add(new ProguardConfigurationSourceStrings(lines)); return self(); } @@ -176,12 +202,12 @@ public class R8Command extends BaseCommand { mainDexKeepRules = parser.getConfig().getRules(); } ProguardConfiguration configuration; - if (proguardConfigFiles.isEmpty()) { + if (proguardConfigs.isEmpty()) { configuration = ProguardConfiguration.defaultConfiguration(factory); } else { ProguardConfigurationParser parser = new ProguardConfigurationParser(factory); try { - parser.parse(proguardConfigFiles); + parser.parse(proguardConfigs); } catch (ProguardRuleParserException e) { throw new CompilationException(e.getMessage(), e.getCause()); } @@ -308,7 +334,7 @@ public class R8Command extends BaseCommand { } else if (arg.equals("--no-minification")) { builder.setMinification(false); } else if (arg.equals("--main-dex-rules")) { - builder.addMainDexRules(Paths.get(args[++i])); + builder.addMainDexRulesFiles(Paths.get(args[++i])); } else if (arg.equals("--main-dex-list")) { builder.addMainDexListFiles(Paths.get(args[++i])); } else if (arg.equals("--main-dex-list-output")) { diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDxCommandBuilder.java b/src/main/java/com/android/tools/r8/compatdx/CompatDxCommandBuilder.java index ba9daece5..55206a407 100644 --- a/src/main/java/com/android/tools/r8/compatdx/CompatDxCommandBuilder.java +++ b/src/main/java/com/android/tools/r8/compatdx/CompatDxCommandBuilder.java @@ -8,6 +8,6 @@ import com.android.tools.r8.D8Command; public class CompatDxCommandBuilder extends D8Command.Builder { CompatDxCommandBuilder() { - ignoreDexInArchive = true; + super(true); } } diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java index 4fb7357dd..2667f79ff 100644 --- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java +++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java @@ -22,11 +22,8 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -85,31 +82,32 @@ public class ProguardConfigurationParser { } public void parse(Path path) throws ProguardRuleParserException, IOException { - parse(Collections.singletonList(path)); + parse(ImmutableList.of(new ProguardConfigurationSourceFile(path))); } - public void parse(List<Path> pathList) throws ProguardRuleParserException, IOException { - for (Path path : pathList) { - new ProguardFileParser(path).parse(); + public void parse(ProguardConfigurationSource source) + throws ProguardRuleParserException, IOException { + parse(ImmutableList.of(source)); + } + + public void parse(List<ProguardConfigurationSource> sources) + throws ProguardRuleParserException, IOException { + for (ProguardConfigurationSource source : sources) { + new ProguardFileParser(source).parse(); } } private class ProguardFileParser { - private final Path path; + private final String name; private final String contents; private int position = 0; private Path baseDirectory; - ProguardFileParser(Path path) throws IOException { - this.path = path; - contents = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); - baseDirectory = path.getParent(); - if (baseDirectory == null) { - // path parent can be null only if it's root dir or if its a one element path relative to - // current directory. - baseDirectory = Paths.get("."); - } + ProguardFileParser(ProguardConfigurationSource source) throws IOException { + contents = source.get(); + baseDirectory = source.getBaseDirectory(); + name = source.getName(); } public void parse() throws ProguardRuleParserException { @@ -267,7 +265,7 @@ public class ProguardConfigurationParser { private void parseInclude() throws ProguardRuleParserException { Path included = parseFileName(); try { - new ProguardFileParser(included).parse(); + new ProguardFileParser(new ProguardConfigurationSourceFile(included)).parse(); } catch (FileNotFoundException | NoSuchFileException e) { throw parseError("Included file '" + included.toString() + "' not found", e); } catch (IOException e) { @@ -1033,12 +1031,12 @@ public class ProguardConfigurationParser { String line = lines[lineNumber]; if (remaining <= line.length() || lineNumber == lines.length - 1) { String arrow = CharBuffer.allocate(remaining).toString().replace( '\0', ' ' ) + '^'; - return path.toString() + ":" + (lineNumber + 1) + ":" + remaining + "\n" + line + return name + ":" + (lineNumber + 1) + ":" + remaining + "\n" + line + '\n' + arrow; } remaining -= (line.length() + 1); // Include newline. } - return path.toString(); + return name; } private ProguardRuleParserException parseError(String message) { diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSource.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSource.java new file mode 100644 index 000000000..b0375c0a8 --- /dev/null +++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSource.java @@ -0,0 +1,14 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +package com.android.tools.r8.shaking; + +import java.io.IOException; +import java.nio.file.Path; + +public interface ProguardConfigurationSource { + String get() throws IOException; + Path getBaseDirectory(); + String getName(); +} diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceFile.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceFile.java new file mode 100644 index 000000000..058c14f08 --- /dev/null +++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceFile.java @@ -0,0 +1,37 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +package com.android.tools.r8.shaking; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ProguardConfigurationSourceFile implements ProguardConfigurationSource { + private final Path path; + + public ProguardConfigurationSourceFile(Path path) { + this.path = path; + } + + public String get() throws IOException{ + return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + } + + public Path getBaseDirectory() { + Path baseDirectory = path.getParent(); + if (baseDirectory == null) { + // Path parent can be null only if it's root dir or if its a one element path relative to + // current directory. + baseDirectory = Paths.get("."); + } + return baseDirectory; + } + + public String getName() { + return path.toString(); + } +} diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java new file mode 100644 index 000000000..ac5f0abf3 --- /dev/null +++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java @@ -0,0 +1,31 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +package com.android.tools.r8.shaking; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import joptsimple.internal.Strings; + +public class ProguardConfigurationSourceStrings implements ProguardConfigurationSource { + private final List<String> config; + + public ProguardConfigurationSourceStrings(List<String> config) { + this.config = config; + } + + public String get() throws IOException{ + return Strings.join(config, "\n"); + } + + public Path getBaseDirectory() { + return Paths.get("."); + } + + public String getName() { + return "<no file>"; + } +} diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java index 0fa5051d7..d40da6131 100644 --- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java +++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; @@ -582,5 +583,29 @@ public class RootSetBuilder { return Collections .unmodifiableMap(dependentNoShrinking.getOrDefault(item, Collections.emptyMap())); } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RootSet"); + + builder.append("\nnoShrinking: " + noShrinking.size()); + builder.append("\nnoOptimization: " + noOptimization.size()); + builder.append("\nnoObfuscation: " + noObfuscation.size()); + builder.append("\nreasonAsked: " + reasonAsked.size()); + builder.append("\nkeepPackageName: " + keepPackageName.size()); + builder.append("\ncheckDiscarded: " + checkDiscarded.size()); + builder.append("\nnoSideEffects: " + noSideEffects.size()); + builder.append("\nassumedValues: " + assumedValues.size()); + builder.append("\ndependentNoShrinking: " + dependentNoShrinking.size()); + + builder.append("\n\nNo Shrinking:"); + noShrinking.keySet().stream() + .sorted(Comparator.comparing(DexItem::toSourceString)) + .forEach(a -> builder + .append("\n").append(a.toSourceString()).append(" ").append(noShrinking.get(a))); + builder.append("\n"); + return builder.toString(); + } } } diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java index 8b8305826..e91870dd8 100644 --- a/src/test/java/com/android/tools/r8/ToolHelper.java +++ b/src/test/java/com/android/tools/r8/ToolHelper.java @@ -448,7 +448,9 @@ public class ToolHelper { return ProguardConfiguration.defaultConfiguration(factory); } ProguardConfigurationParser parser = new ProguardConfigurationParser(factory); - parser.parse(configPaths); + for (Path configPath : configPaths) { + parser.parse(configPath); + } return parser.getConfig(); } @@ -597,13 +599,31 @@ public class ToolHelper { .toArray(new String[0])); } + public static ProcessResult forkGenerateMainDexList(Path dir, List<String> args1, String... args2) + throws IOException, InterruptedException { + List<String> args = new ArrayList<>(); + args.addAll(args1); + args.addAll(Arrays.asList(args2)); + return forkJava(dir, GenerateMainDexList.class, args); + } + + public static ProcessResult forkGenerateMainDexList(Path dir, String... args) + throws IOException, InterruptedException { + return forkJava(dir, GenerateMainDexList.class, args); + } + private static ProcessResult forkJava(Path dir, Class clazz, String... args) throws IOException, InterruptedException { + return forkJava(dir, clazz, Arrays.asList(args)); + } + + private static ProcessResult forkJava(Path dir, Class clazz, List<String> args) + throws IOException, InterruptedException { List<String> command = new ImmutableList.Builder<String>() .add(getJavaExecutable()) .add("-cp").add(System.getProperty("java.class.path")) .add(clazz.getCanonicalName()) - .addAll(Arrays.asList(args)) + .addAll(args) .build(); return runProcess(new ProcessBuilder(command).directory(dir.toFile())); } diff --git a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java index dbe776de2..32227340e 100644 --- a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java +++ b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java @@ -6,33 +6,33 @@ package com.android.tools.r8.compatdx; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import com.android.tools.r8.ToolHelper; +import com.android.tools.r8.ToolHelper.ProcessResult; import com.android.tools.r8.dex.Constants; -import com.android.tools.r8.errors.CompilationError; -import com.android.tools.r8.maindexlist.MainDexListTests; -import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.FileUtils; -import com.android.tools.r8.utils.OutputMode; import com.android.tools.r8.utils.StringUtils; import com.android.tools.r8.utils.StringUtils.BraceType; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; public class CompatDxTests { @@ -100,38 +100,26 @@ public class CompatDxTests { } @Test - public void singleDexProgramFull() throws IOException, ExecutionException { - // Generate an application that fills the whole dex file. - AndroidApp generated = - MainDexListTests.generateApplication( - ImmutableList.of("A"), Constants.ANDROID_L_API, MAX_METHOD_COUNT + 1); - Path applicationJar = temp.newFile("application.jar").toPath(); - generated.write(applicationJar, OutputMode.Indexed); - runDexer(applicationJar.toString()); - } - - @Test - public void singleDexProgramIsTooLarge() throws IOException, ExecutionException { - // Generate an application that will not fit into a single dex file. - AndroidApp generated = MainDexListTests.generateApplication( - ImmutableList.of("A", "B"), Constants.ANDROID_L_API, MAX_METHOD_COUNT / 2 + 2); - Path applicationJar = temp.newFile("application.jar").toPath(); - generated.write(applicationJar, OutputMode.Indexed); - try { - runDexer(applicationJar.toString()); - fail("Expect to fail, for there are many classes while multidex is not enabled."); - } catch (CompilationError e) { - // Make sure {@link MonoDexDistributor} was used. - assertTrue(e.getMessage().contains("single dex file")); - // Make sure what exceeds the limit is the number of methods. - assertTrue(e.getMessage().contains("# methods: " - + String.valueOf((MAX_METHOD_COUNT / 2 + 2) * 2))); - } + public void keepClassesTest() throws IOException { + runDexerWithOutput("out.zip", "--keep-classes", EXAMPLE_JAR_FILE1); } @Test - public void keepClassesTest() throws IOException { - runDexerWithOutput("out.zip", "--keep-classes", EXAMPLE_JAR_FILE1); + public void ignoreDexInArchiveTest() throws IOException { + // Create a JAR with both a .class and a .dex file (the .dex file is just empty). + Path jarWithClassesAndDex = temp.newFile("test.jar").toPath(); + Files.copy(Paths.get(EXAMPLE_JAR_FILE1), jarWithClassesAndDex, + StandardCopyOption.REPLACE_EXISTING); + URI uri = URI.create("jar:" + jarWithClassesAndDex.toUri()); + FileSystem fileSystem = FileSystems.newFileSystem(uri, ImmutableMap.of("create", "true")); + Path dexFile = fileSystem.getPath("classes.dex"); + Files.newOutputStream(dexFile, StandardOpenOption.CREATE).close(); + fileSystem.close(); + + // Only test this with CompatDx, as dx does not like the empty .dex file. + List<String> d8Args =ImmutableList.of( + "--output=" + temp.newFolder("out").toString(), jarWithClassesAndDex.toString()); + CompatDx.main(d8Args.toArray(new String[d8Args.size()])); } private void runDexer(String... args) throws IOException { @@ -178,7 +166,8 @@ public class CompatDxTests { } Collections.addAll(dxArgs, args); System.out.println("running: dx " + StringUtils.join(dxArgs, " ")); - ToolHelper.runDX(dxArgs.toArray(new String[dxArgs.size()])); + ProcessResult result = ToolHelper.runDX(dxArgs.toArray(new String[dxArgs.size()])); + assertEquals(result.stderr, 0, result.exitCode); if (out == null) { // Can't check output if explicitly not writing any. diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java index dfc20f9a8..1039475cf 100644 --- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java +++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java @@ -38,7 +38,7 @@ public class MainDexListOutputTest extends TestBase { Path mainDexListOutput = temp.getRoot().toPath().resolve("main-dex-output.txt"); R8Command command = ToolHelper.prepareR8CommandBuilder(readClasses(HelloWorldMain.class)) - .addMainDexRules(mainDexRules) + .addMainDexRulesFiles(mainDexRules) .setMainDexListOutputPath(mainDexListOutput) .build(); ToolHelper.runR8(command); diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java index 57c8cf67c..ba5092fe7 100644 --- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java +++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java @@ -9,16 +9,18 @@ import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION; import static com.android.tools.r8.utils.FileUtils.ZIP_EXTENSION; import com.android.tools.r8.CompilationResult; +import com.android.tools.r8.GenerateMainDexList; +import com.android.tools.r8.GenerateMainDexListCommand; import com.android.tools.r8.R8Command; import com.android.tools.r8.ToolHelper; import com.android.tools.r8.graph.DexClass; import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.utils.DescriptorUtils; import com.android.tools.r8.utils.InternalOptions; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; @@ -107,7 +109,6 @@ public class MainDexTracingTest { mainDexRules, expectedMainDexList, minSdk, - R8Command.builder(), (options) -> { options.inlineAccessors = false; }); @@ -120,48 +121,86 @@ public class MainDexTracingTest { Path mainDexRules, Path expectedMainDexList, int minSdk, - R8Command.Builder builder, Consumer<InternalOptions> optionsConsumer) throws Throwable { Path out = temp.getRoot().toPath().resolve(testName + ZIP_EXTENSION); Path inputJar = Paths.get(buildDir, packageName + JAR_EXTENSION); - builder.setMinApiLevel(minSdk); try { - R8Command command = builder + // Build main-dex list using GenerateMainDexList. + GenerateMainDexListCommand.Builder mdlCommandBuilder = GenerateMainDexListCommand.builder(); + GenerateMainDexListCommand command2 = mdlCommandBuilder .addProgramFiles(inputJar) - .addLibraryFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION), - Paths.get(ToolHelper.getAndroidJar(minSdk))) + .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION)) + .addMainDexRulesFiles(mainDexRules) + .build(); + List<String> mainDexGeneratorMainDexList = + GenerateMainDexList.run(command2).stream() + .map(this::mainDexStringToDescriptor) + .sorted() + .collect(Collectors.toList()); + + // Build main-dex list using R8. + R8Command.Builder r8CommandBuilder = R8Command.builder(); + R8Command command = r8CommandBuilder + .setMinApiLevel(minSdk) + .addProgramFiles(inputJar) + .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION)) + .addLibraryFiles(Paths.get(ToolHelper.getAndroidJar(minSdk))) .setOutputPath(out) - .addMainDexRules(mainDexRules) + .addMainDexRulesFiles(mainDexRules) .build(); CompilationResult result = ToolHelper.runR8WithFullResult(command, optionsConsumer); - List<String> resultMainDexList = + List<String> r8MainDexList = result.dexApplication.mainDexList.stream() .filter(dexType -> isApplicationClass(dexType, result)) .map(dexType -> dexType.descriptor.toString()) + .sorted() .collect(Collectors.toList()); - Collections.sort(resultMainDexList); + + // Check that both generated lists are the same as the reference list, except for lambda + // classes which are only produced when running R8. String[] refList = new String(Files.readAllBytes( expectedMainDexList), StandardCharsets.UTF_8).split("\n"); + int nonLambdaOffset = 0; for (int i = 0; i < refList.length; i++) { String reference = refList[i].trim(); - String computed = resultMainDexList.get(i); - if (reference.contains("-$$Lambda$")) { - // For lambda classes we check that there is a lambda class for the right containing - // class. However, we do not check the hash for the generated lambda class. The hash - // changes for different compiler versions because different compiler versions generate - // different lambda implementation method names. - reference = reference.substring(0, reference.lastIndexOf('$')); - computed = computed.substring(0, computed.lastIndexOf('$')); + checkSameMainDexEntry(reference, r8MainDexList.get(i)); + // The main dex list generator does not do any lambda desugaring. + if (!isLambda(reference)) { + checkSameMainDexEntry(reference, mainDexGeneratorMainDexList.get(i - nonLambdaOffset)); + } else { + nonLambdaOffset++; } - Assert.assertEquals(reference, computed); } } catch (ExecutionException e) { throw e.getCause(); } } + private boolean isLambda(String mainDexEntry) { + return mainDexEntry.contains("-$$Lambda$"); + } + + private String mainDexStringToDescriptor(String mainDexString) { + final String CLASS_EXTENSION = ".class"; + Assert.assertTrue(mainDexString.endsWith(CLASS_EXTENSION)); + return DescriptorUtils.getDescriptorFromClassBinaryName( + mainDexString.substring(0, mainDexString.length() - CLASS_EXTENSION.length())); + } + + private void checkSameMainDexEntry(String reference, String computed) { + if (isLambda(reference)) { + // For lambda classes we check that there is a lambda class for the right containing + // class. However, we do not check the hash for the generated lambda class. The hash + // changes for different compiler versions because different compiler versions generate + // different lambda implementation method names. + reference = reference.substring(0, reference.lastIndexOf('$')); + computed = computed.substring(0, computed.lastIndexOf('$')); + } + Assert.assertEquals(reference, computed); + } + private boolean isApplicationClass(DexType dexType, CompilationResult result) { DexClass clazz = result.appInfo.definitionFor(dexType); return clazz != null && clazz.isProgramClass(); diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java index ea282149c..3d6ab6900 100644 --- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java +++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java @@ -14,6 +14,7 @@ import com.android.tools.r8.TestBase; import com.android.tools.r8.ToolHelper; import com.android.tools.r8.graph.DexAccessFlags; import com.android.tools.r8.graph.DexItemFactory; +import com.android.tools.r8.utils.FileUtils; import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode; import java.io.IOException; import java.nio.file.Path; @@ -98,11 +99,22 @@ public class ProguardConfigurationParserTest extends TestBase { @Test public void parse() throws IOException, ProguardRuleParserException { - ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory()); + ProguardConfigurationParser parser; + + // Parse from file. + parser = new ProguardConfigurationParser(new DexItemFactory()); parser.parse(Paths.get(PROGUARD_SPEC_FILE)); List<ProguardConfigurationRule> rules = parser.getConfig().getRules(); assertEquals(24, rules.size()); assertEquals(1, rules.get(0).getMemberRules().size()); + + // Parse from strings. + parser = new ProguardConfigurationParser(new DexItemFactory()); + List<String> lines = FileUtils.readTextFile(Paths.get(PROGUARD_SPEC_FILE)); + parser.parse(new ProguardConfigurationSourceStrings(lines)); + rules = parser.getConfig().getRules(); + assertEquals(24, rules.size()); + assertEquals(1, rules.get(0).getMemberRules().size()); } @Test diff --git a/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java b/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java new file mode 100644 index 000000000..64e7a5bd2 --- /dev/null +++ b/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java @@ -0,0 +1,144 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +package com.android.tools.r8.utils; + +import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.GenerateMainDexListCommand; +import com.android.tools.r8.ToolHelper; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +public class GenerateMainDexListCommandTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest(); + + @Test + public void emptyCommand() throws Throwable { + verifyEmptyCommand(GenerateMainDexListCommand.builder().build()); + verifyEmptyCommand(parse()); + verifyEmptyCommand(parse("")); + verifyEmptyCommand(parse("", "")); + verifyEmptyCommand(parse(" ")); + verifyEmptyCommand(parse(" ", " ")); + verifyEmptyCommand(parse("\t")); + verifyEmptyCommand(parse("\t", "\t")); + } + + private void verifyEmptyCommand(GenerateMainDexListCommand command) throws IOException { + assertEquals(0, ToolHelper.getApp(command).getDexProgramResources().size()); + assertEquals(0, ToolHelper.getApp(command).getClassProgramResources().size()); + assertFalse(ToolHelper.getApp(command).hasMainDexListResources()); + } + + // Add the jars used in the com.android.tools.r8.maindexlist.MainDexTracingTest test. + private void addInputJarsToCommandLine(List<String> args) { + args.add(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "multidex001" + JAR_EXTENSION) + .toAbsolutePath().toString()); + args.add(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION) + .toAbsolutePath().toString()); + } + + // Add main-dex rules used in the com.android.tools.r8.maindexlist.MainDexTracingTest test. + private void addMainDexRuleToCommandLine(List<String> args) { + args.add("--main-dex-rules"); + args.add(Paths.get(ToolHelper.EXAMPLES_DIR, "multidex", "main-dex-rules.txt") + .toAbsolutePath().toString()); + } + + @Test + public void defaultOutIsCwd() throws Throwable { + Path working = temp.getRoot().toPath(); + String mainDexListOutput = "main-dex-list.txt"; + Path output = working.resolve(mainDexListOutput); + assertFalse(Files.exists(output)); + List<String> args = new ArrayList<>(); + addInputJarsToCommandLine(args); + addMainDexRuleToCommandLine(args); + assertEquals(0, ToolHelper.forkGenerateMainDexList( + working, args, "--main-dex-list-output", mainDexListOutput).exitCode); + assertTrue(Files.exists(output)); + assertTrue(Files.size(output) > 0); + } + + @Test + public void validOutputPath() throws Throwable { + Path existingFile = temp.getRoot().toPath().resolve("existing_output"); + try (OutputStream existingFileOut = Files.newOutputStream(existingFile, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + PrintWriter writer = new PrintWriter(existingFileOut); + writer.println("Hello, world!"); + writer.flush(); + } + Path nonExistingFile = temp.getRoot().toPath().resolve("non_existing_output"); + assertEquals( + existingFile, + GenerateMainDexListCommand.builder().setMainDexListOutputPath(existingFile).build() + .getMainDexListOutputPath()); + assertEquals( + nonExistingFile, + GenerateMainDexListCommand.builder().setMainDexListOutputPath(nonExistingFile).build() + .getMainDexListOutputPath()); + assertEquals( + existingFile, + parse("--main-dex-list-output", existingFile.toString()).getMainDexListOutputPath()); + assertEquals( + nonExistingFile, + parse("--main-dex-list-output", nonExistingFile.toString()).getMainDexListOutputPath()); + } + + @Test + public void nonExistingOutputFileInNonExistingDir() throws Throwable { + Path nonExistingFileInNonExistingDir = + temp.getRoot().toPath().resolve("a/path/that/does/not/exist"); + assertEquals( + nonExistingFileInNonExistingDir, + GenerateMainDexListCommand.builder() + .setMainDexListOutputPath(nonExistingFileInNonExistingDir).build() + .getMainDexListOutputPath()); + assertEquals( + nonExistingFileInNonExistingDir, + parse("--main-dex-list-output", + nonExistingFileInNonExistingDir.toString()).getMainDexListOutputPath()); + } + + @Test + public void mainDexRules() throws Throwable { + Path mainDexRules1 = temp.newFile("main-dex-1.rules").toPath(); + Path mainDexRules2 = temp.newFile("main-dex-2.rules").toPath(); + parse("--main-dex-rules", mainDexRules1.toString()); + parse( + "--main-dex-rules", mainDexRules1.toString(), "--main-dex-rules", mainDexRules2.toString()); + } + + @Test + public void mainDexList() throws Throwable { + Path mainDexList1 = temp.newFile("main-dex-list-1.txt").toPath(); + Path mainDexList2 = temp.newFile("main-dex-list-2.txt").toPath(); + parse("--main-dex-list", mainDexList1.toString()); + parse("--main-dex-list", mainDexList1.toString(), "--main-dex-list", mainDexList2.toString()); + } + + private GenerateMainDexListCommand parse(String... args) throws Throwable { + return GenerateMainDexListCommand.parse(args).build(); + } +} |