Revert "Remove DexSplitter from codebase"
This reverts commit 908f1f58db9b9aa30e228cbd2816e655774d62f4.
Reason for revert: old code in google3 is relying on this, filed b/230692867 to get that removed
Bug: 230692867
Bug: 155577610
Change-Id: I9c21e43fc8c7648681f92f9e68236f09afea9b01
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
new file mode 100644
index 0000000..a7ac9e3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -0,0 +1,155 @@
+// Copyright (c) 2018, 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 static com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException;
+
+import com.android.tools.r8.DexIndexedConsumer.DirectoryConsumer;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.dex.ApplicationWriter;
+import com.android.tools.r8.dex.Marker;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.LazyLoadedDexApplication;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.FeatureClassMapping;
+import com.android.tools.r8.utils.FeatureClassMapping.FeatureMappingException;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.InternalOptions.DesugarState;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+@Keep
+public final class DexSplitterHelper {
+
+ public static void run(
+ D8Command command, FeatureClassMapping featureClassMapping, String output, String proguardMap)
+ throws CompilationFailedException {
+ ExecutorService executor = ThreadUtils.getExecutorService(ThreadUtils.NOT_SPECIFIED);
+ try {
+ ExceptionUtils.withCompilationHandler(
+ command.getReporter(),
+ () -> run(command, featureClassMapping, output, proguardMap, executor));
+ } finally {
+ executor.shutdown();
+ }
+ }
+
+ public static void run(
+ D8Command command,
+ FeatureClassMapping featureClassMapping,
+ String output,
+ String proguardMap,
+ ExecutorService executor)
+ throws IOException {
+ InternalOptions options = command.getInternalOptions();
+ options.desugarState = DesugarState.OFF;
+ options.enableMainDexListCheck = false;
+ options.ignoreMainDexMissingClasses = true;
+ options.minimalMainDex = false;
+ assert !options.isMinifying();
+ options.inlinerOptions().enableInlining = false;
+ options.outline.enabled = false;
+
+ try {
+ Timing timing = new Timing("DexSplitter");
+ ApplicationReader applicationReader =
+ new ApplicationReader(command.getInputApp(), options, timing);
+ DexApplication app = applicationReader.read(executor);
+ MainDexInfo mainDexInfo = applicationReader.readMainDexClasses(app);
+
+ List<Marker> markers = app.dexItemFactory.extractMarkers();
+
+ ClassNameMapper mapper = null;
+ if (proguardMap != null) {
+ mapper = ClassNameMapper.mapperFromFile(Paths.get(proguardMap));
+ }
+ Map<String, LazyLoadedDexApplication.Builder> applications =
+ getDistribution(app, featureClassMapping, mapper);
+ for (Entry<String, LazyLoadedDexApplication.Builder> entry : applications.entrySet()) {
+ String feature = entry.getKey();
+ timing.begin("Feature " + feature);
+ DexApplication featureApp = entry.getValue().build();
+ assert !options.hasMethodsFilter();
+
+ // If this is the base, we add the main dex list.
+ AppInfo appInfo =
+ feature.equals(featureClassMapping.getBaseName())
+ ? AppInfo.createInitialAppInfo(featureApp, mainDexInfo)
+ : AppInfo.createInitialAppInfo(featureApp);
+ AppView<AppInfo> appView = AppView.createForD8(appInfo);
+
+ // Run d8 optimize to ensure jumbo strings are handled.
+ D8.optimize(appView, options, timing, executor);
+
+ // We create a specific consumer for each split.
+ Path outputDir = Paths.get(output).resolve(entry.getKey());
+ if (!Files.exists(outputDir)) {
+ Files.createDirectory(outputDir);
+ }
+ DexIndexedConsumer consumer = new DirectoryConsumer(outputDir);
+
+ try {
+ new ApplicationWriter(
+ appView,
+ markers,
+ NamingLens.getIdentityLens(),
+ consumer)
+ .write(executor);
+ options.printWarnings();
+ } finally {
+ consumer.finished(options.reporter);
+ }
+ timing.end();
+ }
+ } catch (ExecutionException e) {
+ throw unwrapExecutionException(e);
+ } catch (FeatureMappingException e) {
+ options.reporter.error(e.getMessage());
+ } finally {
+ options.signalFinishedToConsumers();
+ }
+ }
+
+ private static Map<String, LazyLoadedDexApplication.Builder> getDistribution(
+ DexApplication app, FeatureClassMapping featureClassMapping, ClassNameMapper mapper)
+ throws FeatureMappingException {
+ Map<String, LazyLoadedDexApplication.Builder> applications = new HashMap<>();
+ for (DexProgramClass clazz : app.classes()) {
+ String clazzName =
+ mapper != null ? mapper.deobfuscateClassName(clazz.toString()) : clazz.toString();
+ String feature = featureClassMapping.featureForClass(clazzName);
+ LazyLoadedDexApplication.Builder featureApplication = applications.get(feature);
+ if (featureApplication == null) {
+ featureApplication = DexApplication.builder(app.options, app.timing);
+ applications.put(feature, featureApplication);
+ }
+ featureApplication.addProgramClass(clazz);
+ }
+ return applications;
+ }
+
+ public static void runD8ForTesting(D8Command command, boolean dontCreateMarkerInD8)
+ throws CompilationFailedException {
+ InternalOptions options = command.getInternalOptions();
+ options.testing.dontCreateMarkerInD8 = dontCreateMarkerInD8;
+ D8.runForTesting(command.getInputApp(), options);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/SwissArmyKnife.java b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
index 2e44e14..2d836ae 100644
--- a/src/main/java/com/android/tools/r8/SwissArmyKnife.java
+++ b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.bisect.Bisect;
import com.android.tools.r8.cf.CfVerifierTool;
import com.android.tools.r8.compatproguard.CompatProguard;
+import com.android.tools.r8.dexsplitter.DexSplitter;
import com.android.tools.r8.relocator.RelocatorCommandLine;
import com.android.tools.r8.tracereferences.TraceReferences;
import java.util.Arrays;
@@ -40,6 +41,9 @@
case "dexsegments":
DexSegments.main(shift(args));
break;
+ case "dexsplitter":
+ DexSplitter.main(shift(args));
+ break;
case "disasm":
Disassemble.main(shift(args));
break;
diff --git a/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java b/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
new file mode 100644
index 0000000..4ee416c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
@@ -0,0 +1,377 @@
+// Copyright (c) 2018, 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.dexsplitter;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DexSplitterHelper;
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.AbortException;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.FeatureClassMapping;
+import com.android.tools.r8.utils.FeatureClassMapping.FeatureMappingException;
+import com.android.tools.r8.utils.OptionsParsing;
+import com.android.tools.r8.utils.OptionsParsing.ParseContext;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+@Keep
+public final class DexSplitter {
+
+ private static final String DEFAULT_OUTPUT_DIR = "output";
+ private static final String DEFAULT_BASE_NAME = "base";
+
+ private static final boolean PRINT_ARGS = false;
+
+ public static class FeatureJar {
+ private String jar;
+ private String outputName;
+
+ public FeatureJar(String jar, String outputName) {
+ this.jar = jar;
+ this.outputName = outputName;
+ }
+
+ public FeatureJar(String jar) {
+ this(jar, featureNameFromJar(jar));
+ }
+
+ public String getJar() {
+ return jar;
+ }
+
+ public String getOutputName() {
+ return outputName;
+ }
+
+ private static String featureNameFromJar(String jar) {
+ Path jarPath = Paths.get(jar);
+ String featureName = jarPath.getFileName().toString();
+ if (featureName.endsWith(".jar") || featureName.endsWith(".zip")) {
+ featureName = featureName.substring(0, featureName.length() - 4);
+ }
+ return featureName;
+ }
+ }
+
+ private static class ZipFileOrigin extends PathOrigin {
+
+ public ZipFileOrigin(Path path) {
+ super(path);
+ }
+
+ @Override
+ public String part() {
+ return "splitting of file '" + super.part() + "'";
+ }
+ }
+
+ @Keep
+ public static final class Options {
+ private final DiagnosticsHandler diagnosticsHandler;
+ private List<String> inputArchives = new ArrayList<>();
+ private List<FeatureJar> featureJars = new ArrayList<>();
+ private List<String> baseJars = new ArrayList<>();
+ private String baseOutputName = DEFAULT_BASE_NAME;
+ private String output = DEFAULT_OUTPUT_DIR;
+ private String featureSplitMapping;
+ private String proguardMap;
+ private String mainDexList;
+ private boolean splitNonClassResources = false;
+
+ public Options() {
+ this(new DiagnosticsHandler() {});
+ }
+
+ public Options(DiagnosticsHandler diagnosticsHandler) {
+ this.diagnosticsHandler = diagnosticsHandler;
+ }
+
+ public DiagnosticsHandler getDiagnosticsHandler() {
+ return diagnosticsHandler;
+ }
+
+ public String getMainDexList() {
+ return mainDexList;
+ }
+
+ public void setMainDexList(String mainDexList) {
+ this.mainDexList = mainDexList;
+ }
+
+ public String getOutput() {
+ return output;
+ }
+
+ public void setOutput(String output) {
+ this.output = output;
+ }
+
+ public String getFeatureSplitMapping() {
+ return featureSplitMapping;
+ }
+
+ public void setFeatureSplitMapping(String featureSplitMapping) {
+ this.featureSplitMapping = featureSplitMapping;
+ }
+
+ public String getProguardMap() {
+ return proguardMap;
+ }
+
+ public void setProguardMap(String proguardMap) {
+ this.proguardMap = proguardMap;
+ }
+
+ public String getBaseOutputName() {
+ return baseOutputName;
+ }
+
+ public void setBaseOutputName(String baseOutputName) {
+ this.baseOutputName = baseOutputName;
+ }
+
+ public void addInputArchive(String inputArchive) {
+ inputArchives.add(inputArchive);
+ }
+
+ public void addBaseJar(String baseJar) {
+ baseJars.add(baseJar);
+ }
+
+ private void addFeatureJar(FeatureJar featureJar) {
+ featureJars.add(featureJar);
+ }
+
+ public void addFeatureJar(String jar) {
+ featureJars.add(new FeatureJar(jar));
+ }
+
+ public void addFeatureJar(String jar, String outputName) {
+ featureJars.add(new FeatureJar(jar, outputName));
+ }
+
+ public void setSplitNonClassResources(boolean value) {
+ splitNonClassResources = value;
+ }
+
+ public ImmutableList<String> getInputArchives() {
+ return ImmutableList.copyOf(inputArchives);
+ }
+
+ ImmutableList<FeatureJar> getFeatureJars() {
+ return ImmutableList.copyOf(featureJars);
+ }
+
+ ImmutableList<String> getBaseJars() {
+ return ImmutableList.copyOf(baseJars);
+ }
+
+ // Shorthand error messages.
+ public Diagnostic error(String msg) {
+ StringDiagnostic error = new StringDiagnostic(msg);
+ diagnosticsHandler.error(error);
+ return error;
+ }
+ }
+
+ /**
+ * Parse a feature jar argument and return the corresponding FeatureJar representation.
+ * Default to use the name of the jar file if the argument contains no ':', if the argument
+ * contains ':', then use the value after the ':' as the name.
+ * @param argument
+ */
+ private static FeatureJar parseFeatureJarArgument(String argument) {
+ if (argument.contains(":")) {
+ String[] parts = argument.split(":");
+ if (parts.length > 2) {
+ throw new RuntimeException("--feature-jar argument contains more than one :");
+ }
+ return new FeatureJar(parts[0], parts[1]);
+ }
+ return new FeatureJar(argument);
+ }
+
+ private static Options parseArguments(String[] args) {
+ Options options = new Options();
+ ParseContext context = new ParseContext(args);
+ while (context.head() != null) {
+ List<String> inputs = OptionsParsing.tryParseMulti(context, "--input");
+ if (inputs != null) {
+ inputs.forEach(options::addInputArchive);
+ continue;
+ }
+ List<String> featureJars = OptionsParsing.tryParseMulti(context, "--feature-jar");
+ if (featureJars != null) {
+ featureJars.forEach((feature) -> options.addFeatureJar(parseFeatureJarArgument(feature)));
+ continue;
+ }
+ List<String> baseJars = OptionsParsing.tryParseMulti(context, "--base-jar");
+ if (baseJars != null) {
+ baseJars.forEach(options::addBaseJar);
+ continue;
+ }
+ String output = OptionsParsing.tryParseSingle(context, "--output", "-o");
+ if (output != null) {
+ options.setOutput(output);
+ continue;
+ }
+
+ String mainDexList= OptionsParsing.tryParseSingle(context, "--main-dex-list", null);
+ if (mainDexList!= null) {
+ options.setMainDexList(mainDexList);
+ continue;
+ }
+
+ String proguardMap = OptionsParsing.tryParseSingle(context, "--proguard-map", null);
+ if (proguardMap != null) {
+ options.setProguardMap(proguardMap);
+ continue;
+ }
+ String baseOutputName = OptionsParsing.tryParseSingle(context, "--base-output-name", null);
+ if (baseOutputName != null) {
+ options.setBaseOutputName(baseOutputName);
+ continue;
+ }
+ String featureSplit = OptionsParsing.tryParseSingle(context, "--feature-splits", null);
+ if (featureSplit != null) {
+ options.setFeatureSplitMapping(featureSplit);
+ continue;
+ }
+ Boolean b = OptionsParsing.tryParseBoolean(context, "--split-non-class-resources");
+ if (b != null) {
+ options.setSplitNonClassResources(b);
+ continue;
+ }
+ throw new RuntimeException(String.format("Unknown options: '%s'.", context.head()));
+ }
+ return options;
+ }
+
+ private static FeatureClassMapping createFeatureClassMapping(Options options)
+ throws FeatureMappingException {
+ if (options.getFeatureSplitMapping() != null) {
+ return FeatureClassMapping.fromSpecification(
+ Paths.get(options.getFeatureSplitMapping()), options.getDiagnosticsHandler());
+ }
+ assert !options.getFeatureJars().isEmpty();
+ return FeatureClassMapping.Internal.fromJarFiles(options.getFeatureJars(),
+ options.getBaseJars(), options.getBaseOutputName(), options.getDiagnosticsHandler());
+ }
+
+ private static void run(String[] args)
+ throws CompilationFailedException, FeatureMappingException {
+ Options options = parseArguments(args);
+ run(options);
+ }
+
+ public static void run(Options options)
+ throws FeatureMappingException, CompilationFailedException {
+ Diagnostic error = null;
+ if (options.getInputArchives().isEmpty()) {
+ error = options.error("Need at least one --input");
+ }
+ if (options.getFeatureSplitMapping() == null && options.getFeatureJars().isEmpty()) {
+ error = options.error("You must supply a feature split mapping or feature jars");
+ }
+ if (options.getFeatureSplitMapping() != null && !options.getFeatureJars().isEmpty()) {
+ error = options.error("You can't supply both a feature split mapping and feature jars");
+ }
+ if (error != null) {
+ throw new AbortException(error);
+ }
+
+ D8Command.Builder builder = D8Command.builder(options.diagnosticsHandler);
+
+
+ for (String s : options.inputArchives) {
+ builder.addProgramFiles(Paths.get(s));
+ }
+ // We set the actual consumer on the ApplicationWriter when we have calculated the distribution
+ // since we don't yet know the distribution.
+ builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+ if (options.getMainDexList() != null) {
+ builder.addMainDexListFiles(Paths.get(options.getMainDexList()));
+ }
+
+ FeatureClassMapping featureClassMapping = createFeatureClassMapping(options);
+
+ DexSplitterHelper.run(
+ builder.build(), featureClassMapping, options.getOutput(), options.getProguardMap());
+
+ if (options.splitNonClassResources) {
+ splitNonClassResources(options, featureClassMapping);
+ }
+ }
+
+ private static void splitNonClassResources(Options options,
+ FeatureClassMapping featureClassMapping) {
+ for (String s : options.inputArchives) {
+ try (ZipFile zipFile = new ZipFile(s, StandardCharsets.UTF_8)) {
+ Enumeration<? extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (!ZipUtils.isDexFile(name) && !ZipUtils.isClassFile(name)) {
+ String feature = featureClassMapping.featureForNonClass(name);
+ Path outputDir = Paths.get(options.getOutput()).resolve(feature);
+ try (InputStream stream = zipFile.getInputStream(entry)) {
+ Path outputFile = outputDir.resolve(name);
+ Path parent = outputFile.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+ Files.copy(stream, outputFile);
+ }
+ }
+ }
+ } catch (IOException e) {
+ ExceptionDiagnostic error = new ExceptionDiagnostic(e, new ZipFileOrigin(Paths.get(s)));
+ options.getDiagnosticsHandler().error(error);
+ throw new AbortException(error);
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ if (PRINT_ARGS) {
+ printArgs(args);
+ }
+ ExceptionUtils.withMainProgramHandler(
+ () -> {
+ try {
+ run(args);
+ } catch (FeatureMappingException e) {
+ // TODO(ricow): Report feature mapping errors via the reporter.
+ throw new RuntimeException("Splitting failed: " + e.getMessage());
+ }
+ });
+ }
+
+ private static void printArgs(String[] args) {
+ System.err.printf("r8.DexSplitter");
+ for (String s : args) {
+ System.err.printf(" %s", s);
+ }
+ System.err.println("");
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
new file mode 100644
index 0000000..00412d9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
@@ -0,0 +1,313 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.ArchiveClassFileProvider;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.dexsplitter.DexSplitter.FeatureJar;
+import com.android.tools.r8.origin.PathOrigin;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Provides a mappings of classes to modules. The structure of the input file is as follows:
+ * packageOrClass:module
+ *
+ * <p>Lines with a # prefix are ignored.
+ *
+ * <p>We will do most specific matching, i.e.,
+ * <pre>
+ * com.google.foobar.*:feature2
+ * com.google.*:base
+ * </pre>
+ * will put everything in the com.google namespace into base, except classes in com.google.foobar
+ * that will go to feature2. Class based mappings takes precedence over packages (since they are
+ * more specific):
+ * <pre>
+ * com.google.A:feature2
+ * com.google.*:base
+ * </pre>
+ * Puts A into feature2, and all other classes from com.google into base.
+ *
+ * <p>Note that this format does not allow specifying inter-module dependencies, this is simply a
+ * placement tool.
+ */
+@Keep
+public final class FeatureClassMapping {
+
+ Map<String, String> parsedRules = new HashMap<>(); // Already parsed rules.
+ Map<String, String> parseNonClassRules = new HashMap<>();
+ boolean usesOnlyExactMappings = true;
+
+ Set<FeaturePredicate> mappings = new HashSet<>();
+
+ Path mappingFile;
+ String baseName = DEFAULT_BASE_NAME;
+
+ static final String DEFAULT_BASE_NAME = "base";
+
+ static final String COMMENT = "#";
+ static final String SEPARATOR = ":";
+
+ public String getBaseName() {
+ return baseName;
+ }
+
+ private static class SpecificationOrigin extends PathOrigin {
+
+ public SpecificationOrigin(Path path) {
+ super(path);
+ }
+
+ @Override
+ public String part() {
+ return "specification file '" + super.part() + "'";
+ }
+ }
+
+ private static class JarFileOrigin extends PathOrigin {
+
+ public JarFileOrigin(Path path) {
+ super(path);
+ }
+
+ @Override
+ public String part() {
+ return "jar file '" + super.part() + "'";
+ }
+ }
+
+ public static FeatureClassMapping fromSpecification(Path file) throws FeatureMappingException {
+ return fromSpecification(file, new DiagnosticsHandler() {});
+ }
+
+ public static FeatureClassMapping fromSpecification(Path file, DiagnosticsHandler reporter)
+ throws FeatureMappingException {
+ FeatureClassMapping mapping = new FeatureClassMapping();
+ List<String> lines = null;
+ try {
+ lines = FileUtils.readAllLines(file);
+ } catch (IOException e) {
+ ExceptionDiagnostic error = new ExceptionDiagnostic(e, new SpecificationOrigin(file));
+ reporter.error(error);
+ throw new AbortException(error);
+ }
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ mapping.parseAndAdd(line, i);
+ }
+ return mapping;
+ }
+
+ public static class Internal {
+ private static List<String> getClassFileDescriptors(String jar, DiagnosticsHandler reporter) {
+ Path jarPath = Paths.get(jar);
+ try {
+ return new ArchiveClassFileProvider(jarPath).getClassDescriptors()
+ .stream()
+ .map(DescriptorUtils::descriptorToJavaType)
+ .collect(Collectors.toList());
+ } catch (IOException e) {
+ ExceptionDiagnostic error = new ExceptionDiagnostic(e, new JarFileOrigin(jarPath));
+ reporter.error(error);
+ throw new AbortException(error);
+ }
+ }
+
+ private static List<String> getNonClassFiles(String jar, DiagnosticsHandler reporter) {
+ try (ZipFile zipfile = new ZipFile(jar, StandardCharsets.UTF_8)) {
+ return zipfile.stream()
+ .filter(entry -> !ZipUtils.isClassFile(entry.getName()))
+ .map(ZipEntry::getName)
+ .collect(Collectors.toList());
+ } catch (IOException e) {
+ ExceptionDiagnostic error = new ExceptionDiagnostic(e, new JarFileOrigin(Paths.get(jar)));
+ reporter.error(error);
+ throw new AbortException(error);
+ }
+ }
+
+ public static FeatureClassMapping fromJarFiles(
+ List<FeatureJar> featureJars, List<String> baseJars, String baseName,
+ DiagnosticsHandler reporter)
+ throws FeatureMappingException {
+ FeatureClassMapping mapping = new FeatureClassMapping();
+ if (baseName != null) {
+ mapping.baseName = baseName;
+ }
+ for (FeatureJar featureJar : featureJars) {
+ for (String javaType : getClassFileDescriptors(featureJar.getJar(), reporter)) {
+ mapping.addMapping(javaType, featureJar.getOutputName());
+ }
+ for (String nonClass : getNonClassFiles(featureJar.getJar(), reporter)) {
+ mapping.addNonClassMapping(nonClass, featureJar.getOutputName());
+ }
+ }
+ for (String baseJar : baseJars) {
+ for (String javaType : getClassFileDescriptors(baseJar, reporter)) {
+ mapping.addBaseMapping(javaType);
+ }
+ for (String nonClass : getNonClassFiles(baseJar, reporter)) {
+ mapping.addBaseNonClassMapping(nonClass);
+ }
+ }
+ assert mapping.usesOnlyExactMappings;
+ return mapping;
+ }
+
+ }
+
+ private FeatureClassMapping() {}
+
+ public void addBaseMapping(String clazz) throws FeatureMappingException {
+ addMapping(clazz, baseName);
+ }
+
+ public void addBaseNonClassMapping(String name) {
+ addNonClassMapping(name, baseName);
+ }
+
+ public void addMapping(String clazz, String feature) throws FeatureMappingException {
+ addRule(clazz, feature, 0);
+ }
+
+ public void addNonClassMapping(String name, String feature) {
+ // If a non-class file is present in multiple features put the resource in the base.
+ parseNonClassRules.put(name, parseNonClassRules.containsKey(name) ? baseName : feature);
+ }
+
+ FeatureClassMapping(List<String> lines) throws FeatureMappingException {
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ parseAndAdd(line, i);
+ }
+ }
+
+ public String featureForClass(String clazz) {
+ if (usesOnlyExactMappings) {
+ return parsedRules.getOrDefault(clazz, baseName);
+ } else {
+ FeaturePredicate bestMatch = null;
+ for (FeaturePredicate mapping : mappings) {
+ if (mapping.match(clazz)) {
+ if (bestMatch == null || bestMatch.predicate.length() < mapping.predicate.length()) {
+ bestMatch = mapping;
+ }
+ }
+ }
+ if (bestMatch == null) {
+ return baseName;
+ }
+ return bestMatch.feature;
+ }
+ }
+
+ public String featureForNonClass(String nonClass) {
+ return parseNonClassRules.getOrDefault(nonClass, baseName);
+ }
+
+ private void parseAndAdd(String line, int lineNumber) throws FeatureMappingException {
+ if (line.startsWith(COMMENT)) {
+ return; // Ignore comments
+ }
+ if (line.isEmpty()) {
+ return; // Ignore blank lines
+ }
+
+ if (!line.contains(SEPARATOR)) {
+ error("Mapping lines must contain a " + SEPARATOR, lineNumber);
+ }
+ String[] values = line.split(SEPARATOR);
+ if (values.length != 2) {
+ error("Mapping lines can only contain one " + SEPARATOR, lineNumber);
+ }
+
+ String predicate = values[0];
+ String feature = values[1];
+ addRule(predicate, feature, lineNumber);
+ }
+
+ private void addRule(String predicate, String feature, int lineNumber)
+ throws FeatureMappingException {
+ if (parsedRules.containsKey(predicate)) {
+ if (!parsedRules.get(predicate).equals(feature)) {
+ error("Redefinition of predicate " + predicate + "not allowed", lineNumber);
+ }
+ return; // Already have this rule.
+ }
+ parsedRules.put(predicate, feature);
+ FeaturePredicate featurePredicate = new FeaturePredicate(predicate, feature);
+ mappings.add(featurePredicate);
+ usesOnlyExactMappings &= featurePredicate.isExactmapping();
+ }
+
+ private void error(String error, int line) throws FeatureMappingException {
+ throw new FeatureMappingException(
+ "Invalid mappings specification: " + error + "\n in file " + mappingFile + ":" + line);
+ }
+
+ @Keep
+ public static class FeatureMappingException extends Exception {
+ FeatureMappingException(String message) {
+ super(message);
+ }
+ }
+
+ /** A feature predicate can either be a wildcard or class predicate. */
+ private static class FeaturePredicate {
+ private static Pattern identifier = Pattern.compile("[A-Za-z_\\-][A-Za-z0-9_$\\-]*");
+ final String predicate;
+ final String feature;
+ final boolean isCatchAll;
+ // False implies class predicate.
+ final boolean isWildcard;
+
+ FeaturePredicate(String predicate, String feature) throws FeatureMappingException {
+ isWildcard = predicate.endsWith(".*");
+ isCatchAll = predicate.equals("*");
+ if (isCatchAll) {
+ this.predicate = "";
+ } else if (isWildcard) {
+ String packageName = predicate.substring(0, predicate.length() - 2);
+ if (!DescriptorUtils.isValidJavaType(packageName)) {
+ throw new FeatureMappingException(packageName + " is not a valid identifier");
+ }
+ // Prefix of a fully-qualified class name, including a terminating dot.
+ this.predicate = predicate.substring(0, predicate.length() - 1);
+ } else {
+ if (!DescriptorUtils.isValidJavaType(predicate)) {
+ throw new FeatureMappingException(predicate + " is not a valid identifier");
+ }
+ this.predicate = predicate;
+ }
+ this.feature = feature;
+ }
+
+ boolean match(String className) {
+ if (isCatchAll) {
+ return true;
+ } else if (isWildcard) {
+ return className.startsWith(predicate);
+ } else {
+ return className.equals(predicate);
+ }
+ }
+
+ boolean isExactmapping() {
+ return !isWildcard && !isCatchAll;
+ }
+ }
+}
diff --git a/src/main/keep.txt b/src/main/keep.txt
index deb8e6d..e376924 100644
--- a/src/main/keep.txt
+++ b/src/main/keep.txt
@@ -12,6 +12,7 @@
-keep public class com.android.tools.r8.D8 { public static void main(java.lang.String[]); }
-keep public class com.android.tools.r8.R8 { public static void main(java.lang.String[]); }
-keep public class com.android.tools.r8.ExtractMarker { public static void main(java.lang.String[]); }
+-keep public class com.android.tools.r8.dexsplitter.DexSplitter { public static void main(java.lang.String[]); }
-keep public class com.android.tools.r8.Version { public static final java.lang.String LABEL; }
-keep public class com.android.tools.r8.Version { public static java.lang.String getVersionString(); }
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterFieldTypeStrengtheningTest.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterFieldTypeStrengtheningTest.java
index 71e7df5..45a3be4 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterFieldTypeStrengtheningTest.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterFieldTypeStrengtheningTest.java
@@ -47,6 +47,28 @@
}
@Test
+ public void testPropagationFromFeature() throws Exception {
+ ThrowingConsumer<R8TestCompileResult, Exception> ensureGetFromFeatureGone =
+ r8TestCompileResult -> {
+ // Ensure that getFromFeature from FeatureClass is inlined into the run method.
+ ClassSubject clazz = r8TestCompileResult.inspector().clazz(FeatureClass.class);
+ assertThat(clazz.uniqueMethodWithName("getFromFeature"), not(isPresent()));
+ };
+ ProcessResult processResult =
+ testDexSplitter(
+ parameters,
+ ImmutableSet.of(BaseSuperClass.class),
+ ImmutableSet.of(FeatureClass.class),
+ FeatureClass.class,
+ EXPECTED,
+ ensureGetFromFeatureGone,
+ TestShrinkerBuilder::noMinification);
+ // We expect art to fail on this with the dex splitter, see b/122902374
+ assertNotEquals(processResult.exitCode, 0);
+ assertTrue(processResult.stderr.contains("NoClassDefFoundError"));
+ }
+
+ @Test
public void testOnR8Splitter() throws IOException, CompilationFailedException {
assumeTrue(parameters.isDexRuntime());
ProcessResult processResult =
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java
index 53af2b6..96220f8 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterInlineRegression.java
@@ -48,6 +48,31 @@
}
@Test
+ public void testInliningFromFeature() throws Exception {
+ ThrowingConsumer<R8TestCompileResult, Exception> ensureGetFromFeatureGone =
+ r8TestCompileResult -> {
+ // Ensure that getFromFeature from FeatureClass is inlined into the run method.
+ ClassSubject clazz = r8TestCompileResult.inspector().clazz(FeatureClass.class);
+ assertThat(clazz.uniqueMethodWithName("getFromFeature"), not(isPresent()));
+ };
+ Consumer<R8FullTestBuilder> configurator =
+ r8FullTestBuilder ->
+ r8FullTestBuilder.enableNoVerticalClassMergingAnnotations().noMinification();
+ ProcessResult processResult =
+ testDexSplitter(
+ parameters,
+ ImmutableSet.of(BaseSuperClass.class),
+ ImmutableSet.of(FeatureClass.class),
+ FeatureClass.class,
+ EXPECTED,
+ ensureGetFromFeatureGone,
+ configurator);
+ // We expect art to fail on this with the dex splitter, see b/122902374
+ assertNotEquals(processResult.exitCode, 0);
+ assertTrue(processResult.stderr.contains("NoClassDefFoundError"));
+ }
+
+ @Test
public void testOnR8Splitter() throws IOException, CompilationFailedException {
assumeTrue(parameters.isDexRuntime());
ThrowableConsumer<R8FullTestBuilder> configurator =
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMemberValuePropagationRegression.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMemberValuePropagationRegression.java
index a2e1b1d..38d19da 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMemberValuePropagationRegression.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMemberValuePropagationRegression.java
@@ -46,6 +46,28 @@
}
@Test
+ public void testPropagationFromFeature() throws Exception {
+ ThrowingConsumer<R8TestCompileResult, Exception> ensureGetFromFeatureGone =
+ r8TestCompileResult -> {
+ // Ensure that getFromFeature from FeatureClass is inlined into the run method.
+ ClassSubject clazz = r8TestCompileResult.inspector().clazz(FeatureClass.class);
+ assertThat(clazz.uniqueMethodWithName("getFromFeature"), not(isPresent()));
+ };
+ ProcessResult processResult =
+ testDexSplitter(
+ parameters,
+ ImmutableSet.of(BaseSuperClass.class),
+ ImmutableSet.of(FeatureClass.class, FeatureEnum.class),
+ FeatureClass.class,
+ EXPECTED,
+ ensureGetFromFeatureGone,
+ builder -> builder.enableInliningAnnotations().noMinification());
+ // We expect art to fail on this with the dex splitter, see b/122902374
+ assertNotEquals(processResult.exitCode, 0);
+ assertTrue(processResult.stderr.contains("NoClassDefFoundError"));
+ }
+
+ @Test
public void testOnR8Splitter() throws IOException, CompilationFailedException {
assumeTrue(parameters.isDexRuntime());
ProcessResult processResult =
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
index a7d137f..71ddab8 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
@@ -48,6 +48,43 @@
}
@Test
+ public void testInliningFromFeature() throws Exception {
+ // Static merging is based on sorting order, we assert that we merged to the feature.
+ ThrowingConsumer<R8TestCompileResult, Exception> ensureMergingToFeature =
+ r8TestCompileResult -> {
+ ClassSubject clazz = r8TestCompileResult.inspector().clazz(AFeatureWithStatic.class);
+ assertEquals(2, clazz.allMethods().size());
+ assertThat(clazz.uniqueMethodWithName("getBase42"), isPresent());
+ assertThat(clazz.uniqueMethodWithName("getFoobar"), isPresent());
+ };
+ Consumer<R8FullTestBuilder> configurator =
+ r8FullTestBuilder ->
+ r8FullTestBuilder
+ .addOptionsModification(
+ options ->
+ options.testing.horizontalClassMergingTarget =
+ (appView, candidates, target) -> candidates.iterator().next())
+ .addHorizontallyMergedClassesInspector(
+ inspector ->
+ inspector.assertMergedInto(BaseWithStatic.class, AFeatureWithStatic.class))
+ .enableNoVerticalClassMergingAnnotations()
+ .enableInliningAnnotations()
+ .noMinification();
+ ProcessResult processResult =
+ testDexSplitter(
+ parameters,
+ ImmutableSet.of(BaseClass.class, BaseWithStatic.class),
+ ImmutableSet.of(FeatureClass.class, AFeatureWithStatic.class),
+ FeatureClass.class,
+ EXPECTED,
+ ensureMergingToFeature,
+ configurator);
+ // We expect art to fail on this with the dex splitter.
+ assertNotEquals(processResult.exitCode, 0);
+ assertTrue(processResult.stderr.contains("NoClassDefFoundError"));
+ }
+
+ @Test
public void testOnR8Splitter() throws IOException, CompilationFailedException {
assumeTrue(parameters.isDexRuntime());
ThrowableConsumer<R8FullTestBuilder> configurator =
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
new file mode 100644
index 0000000..e90e0ab
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
@@ -0,0 +1,484 @@
+// 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.dexsplitter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexSplitterHelper;
+import com.android.tools.r8.ExtractMarker;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
+import com.android.tools.r8.dex.Marker;
+import com.android.tools.r8.dexsplitter.DexSplitter.Options;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.FeatureClassMapping.FeatureMappingException;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class DexSplitterTests {
+
+ private static final String CLASS_DIR =
+ ToolHelper.EXAMPLES_ANDROID_N_BUILD_DIR + "classes/dexsplitsample";
+ private static final String CLASS1_CLASS = CLASS_DIR + "/Class1.class";
+ private static final String CLASS2_CLASS = CLASS_DIR + "/Class2.class";
+ private static final String CLASS3_CLASS = CLASS_DIR + "/Class3.class";
+ private static final String CLASS3_INNER_CLASS = CLASS_DIR + "/Class3$InnerClass.class";
+ private static final String CLASS3_SYNTHETIC_CLASS = CLASS_DIR + "/Class3$1.class";
+ private static final String CLASS4_CLASS = CLASS_DIR + "/Class4.class";
+ private static final String CLASS4_LAMBDA_INTERFACE = CLASS_DIR + "/Class4$LambdaInterface.class";
+ private static final String TEXT_FILE =
+ ToolHelper.EXAMPLES_ANDROID_N_DIR + "dexsplitsample/TextFile.txt";
+
+
+ @Rule public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+ private Path createInput(boolean dontCreateMarkerInD8)
+ throws IOException, CompilationFailedException {
+ // Initial normal compile to create dex files.
+ Path inputZip = temp.newFolder().toPath().resolve("input.zip");
+ D8Command command =
+ D8Command.builder()
+ .setOutput(inputZip, OutputMode.DexIndexed)
+ .addProgramFiles(Paths.get(CLASS1_CLASS))
+ .addProgramFiles(Paths.get(CLASS2_CLASS))
+ .addProgramFiles(Paths.get(CLASS3_CLASS))
+ .addProgramFiles(Paths.get(CLASS3_INNER_CLASS))
+ .addProgramFiles(Paths.get(CLASS3_SYNTHETIC_CLASS))
+ .addProgramFiles(Paths.get(CLASS4_CLASS))
+ .addProgramFiles(Paths.get(CLASS4_LAMBDA_INTERFACE))
+ .build();
+
+ DexSplitterHelper.runD8ForTesting(command, dontCreateMarkerInD8);
+
+ return inputZip;
+ }
+
+ private void testMarker(boolean addMarkerToInput)
+ throws CompilationFailedException, IOException, ResourceException, ExecutionException {
+ Path inputZip = createInput(!addMarkerToInput);
+
+ Path output = temp.newFolder().toPath().resolve("output");
+ Files.createDirectory(output);
+ Path splitSpec = createSplitSpec();
+
+ DexSplitter.main(
+ new String[] {
+ "--input", inputZip.toString(),
+ "--output", output.toString(),
+ "--feature-splits", splitSpec.toString()
+ });
+
+ Path base = output.resolve("base").resolve("classes.dex");
+ Path feature = output.resolve("feature1").resolve("classes.dex");
+
+ for (Path path : new Path[] {inputZip, base, feature}) {
+ Collection<Marker> markers = ExtractMarker.extractMarkerFromDexFile(path);
+ assertEquals(addMarkerToInput ? 1 : 0, markers.size());
+ }
+ }
+
+ @Test
+ public void testMarkerPreserved()
+ throws CompilationFailedException, IOException, ResourceException, ExecutionException {
+ testMarker(true);
+ }
+
+ @Test
+ public void testMarkerNotAdded()
+ throws CompilationFailedException, IOException, ResourceException, ExecutionException {
+ testMarker(false);
+ }
+
+ /**
+ * To test the file splitting we have 3 classes that we distribute like this: Class1 -> base
+ * Class2 -> feature1 Class3 -> feature1
+ *
+ * <p>Class1 and Class2 works independently of each other, but Class3 extends Class1, and
+ * therefore can't run without the base being loaded.
+ */
+ @Test
+ public void splitFilesNoObfuscation()
+ throws CompilationFailedException, IOException, FeatureMappingException {
+ noObfuscation(false);
+ noObfuscation(true);
+ }
+
+ private void noObfuscation(boolean useOptions)
+ throws IOException, CompilationFailedException, FeatureMappingException {
+ Path inputZip = createInput(false);
+ Path output = temp.newFolder().toPath().resolve("output");
+ Files.createDirectory(output);
+ Path splitSpec = createSplitSpec();
+
+ if (useOptions) {
+ Options options = new Options();
+ options.addInputArchive(inputZip.toString());
+ options.setFeatureSplitMapping(splitSpec.toString());
+ options.setOutput(output.toString());
+ DexSplitter.run(options);
+ } else {
+ DexSplitter.main(
+ new String[] {
+ "--input", inputZip.toString(),
+ "--output", output.toString(),
+ "--feature-splits", splitSpec.toString()
+ });
+ }
+
+ Path base = output.resolve("base").resolve("classes.dex");
+ Path feature = output.resolve("feature1").resolve("classes.dex");
+ validateUnobfuscatedOutput(base, feature);
+ }
+
+ private void validateUnobfuscatedOutput(Path base, Path feature) throws IOException {
+ // Both classes should still work if we give all dex files to the system.
+ for (String className : new String[] {"Class1", "Class2", "Class3"}) {
+ ArtCommandBuilder builder = new ArtCommandBuilder();
+ builder.appendClasspath(base.toString());
+ builder.appendClasspath(feature.toString());
+ builder.setMainClass("dexsplitsample." + className);
+ String out = ToolHelper.runArt(builder);
+ assertEquals(out, className + "\n");
+ }
+ // Individual classes should also work from the individual files.
+ String className = "Class1";
+ ArtCommandBuilder builder = new ArtCommandBuilder();
+ builder.appendClasspath(base.toString());
+ builder.setMainClass("dexsplitsample." + className);
+ String out = ToolHelper.runArt(builder);
+ assertEquals(out, className + "\n");
+
+ className = "Class2";
+ builder = new ArtCommandBuilder();
+ builder.appendClasspath(feature.toString());
+ builder.setMainClass("dexsplitsample." + className);
+ out = ToolHelper.runArt(builder);
+ assertEquals(out, className + "\n");
+
+ className = "Class3";
+ builder = new ArtCommandBuilder();
+ builder.appendClasspath(feature.toString());
+ builder.setMainClass("dexsplitsample." + className);
+ try {
+ ToolHelper.runArt(builder);
+ assertFalse(true);
+ } catch (AssertionError assertionError) {
+ // We expect this to throw since base is not in the path and Class3 depends on Class1
+ }
+
+ className = "Class4";
+ builder = new ArtCommandBuilder();
+ builder.appendClasspath(feature.toString());
+ builder.setMainClass("dexsplitsample." + className);
+ try {
+ ToolHelper.runArt(builder);
+ assertFalse(true);
+ } catch (AssertionError assertionError) {
+ // We expect this to throw since base is not in the path and Class4 includes a lambda that
+ // would have been pushed to base.
+ }
+ }
+
+ private Path createSplitSpec() throws FileNotFoundException, UnsupportedEncodingException {
+ Path splitSpec = temp.getRoot().toPath().resolve("split_spec");
+ try (PrintWriter out = new PrintWriter(splitSpec.toFile(), "UTF-8")) {
+ out.write(
+ "dexsplitsample.Class1:base\n"
+ + "dexsplitsample.Class2:feature1\n"
+ + "dexsplitsample.Class3:feature1\n"
+ + "dexsplitsample.Class4:feature1");
+ }
+ return splitSpec;
+ }
+
+ private List<String> getProguardConf() {
+ return ImmutableList.of(
+ "-keep class dexsplitsample.Class3 {",
+ " public static void main(java.lang.String[]);",
+ "}");
+ }
+
+ @Test
+ public void splitFilesFromJar()
+ throws IOException, CompilationFailedException, FeatureMappingException {
+ for (boolean useOptions : new boolean[]{false, true}) {
+ for (boolean explicitBase: new boolean[]{false, true}) {
+ for (boolean renameBase: new boolean[]{false, true}) {
+ splitFromJars(useOptions, explicitBase, renameBase);
+ }
+ }
+ }
+ }
+
+ private void splitFromJars(boolean useOptions, boolean explicitBase, boolean renameBase)
+ throws IOException, CompilationFailedException, FeatureMappingException {
+ Path inputZip = createInput(false);
+ Path output = temp.newFolder().toPath().resolve("output");
+ Files.createDirectory(output);
+ Path baseJar = temp.getRoot().toPath().resolve("base.jar");
+ Path featureJar = temp.getRoot().toPath().resolve("feature1.jar");
+ ZipOutputStream baseStream = new ZipOutputStream(Files.newOutputStream(baseJar));
+ String name = "dexsplitsample/Class1.class";
+ baseStream.putNextEntry(new ZipEntry(name));
+ baseStream.write(Files.readAllBytes(Paths.get(CLASS1_CLASS)));
+ baseStream.closeEntry();
+ baseStream.close();
+
+ ZipOutputStream featureStream = new ZipOutputStream(Files.newOutputStream(featureJar));
+ name = "dexsplitsample/Class2.class";
+ featureStream.putNextEntry(new ZipEntry(name));
+ featureStream.write(Files.readAllBytes(Paths.get(CLASS2_CLASS)));
+ featureStream.closeEntry();
+ name = "dexsplitsample/Class3.class";
+ featureStream.putNextEntry(new ZipEntry(name));
+ featureStream.write(Files.readAllBytes(Paths.get(CLASS3_CLASS)));
+ featureStream.closeEntry();
+ name = "dexsplitsample/Class3$InnerClass.class";
+ featureStream.putNextEntry(new ZipEntry(name));
+ featureStream.write(Files.readAllBytes(Paths.get(CLASS3_INNER_CLASS)));
+ featureStream.closeEntry();
+ name = "dexsplitsample/Class3$1.class";
+ featureStream.putNextEntry(new ZipEntry(name));
+ featureStream.write(Files.readAllBytes(Paths.get(CLASS3_SYNTHETIC_CLASS)));
+ featureStream.closeEntry();
+ name = "dexsplitsample/Class4";
+ featureStream.putNextEntry(new ZipEntry(name));
+ featureStream.write(Files.readAllBytes(Paths.get(CLASS4_CLASS)));
+ featureStream.closeEntry();
+ name = "dexsplitsample/Class4$LambdaInterface";
+ featureStream.putNextEntry(new ZipEntry(name));
+ featureStream.write(Files.readAllBytes(Paths.get(CLASS4_LAMBDA_INTERFACE)));
+ featureStream.closeEntry();
+ featureStream.close();
+ // Make sure that we can pass in a name for the output.
+ String specificOutputName = "renamed";
+ if (useOptions) {
+ Options options = new Options();
+ options.addInputArchive(inputZip.toString());
+ options.setOutput(output.toString());
+ if (explicitBase) {
+ options.addBaseJar(baseJar.toString());
+ } else if (renameBase){
+ // Ensure that we can rename base (if people called a feature base)
+ options.setBaseOutputName("base_renamed");
+ }
+ options.addFeatureJar(featureJar.toString(), specificOutputName);
+ DexSplitter.run(options);
+ } else {
+ List<String> args = Lists.newArrayList(
+ "--input",
+ inputZip.toString(),
+ "--output",
+ output.toString(),
+ "--feature-jar",
+ featureJar.toString().concat(":").concat(specificOutputName));
+ if (explicitBase) {
+ args.add("--base-jar");
+ args.add(baseJar.toString());
+ } else if (renameBase) {
+ args.add("--base-output-name");
+ args.add("base_renamed");
+ }
+
+ DexSplitter.main(args.toArray(StringUtils.EMPTY_ARRAY));
+ }
+ String baseOutputName = explicitBase || !renameBase ? "base" : "base_renamed";
+ Path base = output.resolve(baseOutputName).resolve("classes.dex");
+ Path feature = output.resolve(specificOutputName).resolve("classes.dex");;
+ validateUnobfuscatedOutput(base, feature);
+ }
+
+ @Test
+ public void splitFilesObfuscation()
+ throws CompilationFailedException, IOException, ExecutionException {
+ // Initial normal compile to create dex files.
+ Path inputDex = temp.newFolder().toPath().resolve("input.zip");
+ Path proguardMap = temp.getRoot().toPath().resolve("proguard.map");
+
+ R8.run(
+ R8Command.builder()
+ .setOutput(inputDex, OutputMode.DexIndexed)
+ .addProgramFiles(Paths.get(CLASS1_CLASS))
+ .addProgramFiles(Paths.get(CLASS2_CLASS))
+ .addProgramFiles(Paths.get(CLASS3_CLASS))
+ .addProgramFiles(Paths.get(CLASS3_INNER_CLASS))
+ .addProgramFiles(Paths.get(CLASS3_SYNTHETIC_CLASS))
+ .addProgramFiles(Paths.get(CLASS4_CLASS))
+ .addProgramFiles(Paths.get(CLASS4_LAMBDA_INTERFACE))
+ .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
+ .setProguardMapOutputPath(proguardMap)
+ .addProguardConfiguration(getProguardConf(), Origin.unknown())
+ .build());
+
+ Path outputDex = temp.newFolder().toPath().resolve("output");
+ Files.createDirectory(outputDex);
+ Path splitSpec = createSplitSpec();
+
+ DexSplitter.main(
+ new String[] {
+ "--input", inputDex.toString(),
+ "--output", outputDex.toString(),
+ "--feature-splits", splitSpec.toString(),
+ "--proguard-map", proguardMap.toString()
+ });
+
+ Path base = outputDex.resolve("base").resolve("classes.dex");
+ Path feature = outputDex.resolve("feature1").resolve("classes.dex");
+ String class3 = "dexsplitsample.Class3";
+ // We should still be able to run the Class3 which we kept, it has a call to the obfuscated
+ // class1 which is in base.
+ ArtCommandBuilder builder = new ArtCommandBuilder();
+ builder.appendClasspath(base.toString());
+ builder.appendClasspath(feature.toString());
+ builder.setMainClass(class3);
+ String out = ToolHelper.runArt(builder);
+ assertEquals(out, "Class3\n");
+
+ // Class1 should not be in the feature, it should still be in base.
+ builder = new ArtCommandBuilder();
+ builder.appendClasspath(feature.toString());
+ builder.setMainClass(class3);
+ try {
+ ToolHelper.runArt(builder);
+ assertFalse(true);
+ } catch (AssertionError assertionError) {
+ // We expect this to throw since base is not in the path and Class3 depends on Class1.
+ }
+
+ // Ensure that the Class1 is actually in the correct split. Note that Class2 would have been
+ // shaken away.
+ CodeInspector inspector = new CodeInspector(base, proguardMap);
+ ClassSubject subject = inspector.clazz("dexsplitsample.Class1");
+ assertTrue(subject.isPresent());
+ assertTrue(subject.isRenamed());
+ }
+
+ @Test
+ public void splitNonClassFiles()
+ throws CompilationFailedException, IOException, FeatureMappingException {
+ Path inputZip = temp.getRoot().toPath().resolve("input-zip-with-non-class-files.jar");
+ ZipOutputStream inputZipStream = new ZipOutputStream(Files.newOutputStream(inputZip));
+ String name = "dexsplitsample/TextFile.txt";
+ inputZipStream.putNextEntry(new ZipEntry(name));
+ byte[] fileBytes = Files.readAllBytes(Paths.get(TEXT_FILE));
+ inputZipStream.write(fileBytes);
+ inputZipStream.closeEntry();
+ name = "dexsplitsample/TextFile2.txt";
+ inputZipStream.putNextEntry(new ZipEntry(name));
+ inputZipStream.write(fileBytes);
+ inputZipStream.write(fileBytes);
+ inputZipStream.closeEntry();
+ inputZipStream.close();
+ Path output = temp.newFolder().toPath().resolve("output");
+ Files.createDirectory(output);
+ Path baseJar = temp.getRoot().toPath().resolve("base.jar");
+ Path featureJar = temp.getRoot().toPath().resolve("feature1.jar");
+ ZipOutputStream baseStream = new ZipOutputStream(Files.newOutputStream(baseJar));
+ name = "dexsplitsample/TextFile.txt";
+ baseStream.putNextEntry(new ZipEntry(name));
+ baseStream.write(fileBytes);
+ baseStream.closeEntry();
+ baseStream.close();
+ ZipOutputStream featureStream = new ZipOutputStream(Files.newOutputStream(featureJar));
+ name = "dexsplitsample/TextFile2.txt";
+ featureStream.putNextEntry(new ZipEntry(name));
+ featureStream.write(fileBytes);
+ featureStream.write(fileBytes);
+ featureStream.closeEntry();
+ featureStream.close();
+ Options options = new Options();
+ options.addInputArchive(inputZip.toString());
+ options.setOutput(output.toString());
+ options.addFeatureJar(baseJar.toString());
+ options.addFeatureJar(featureJar.toString());
+ options.setSplitNonClassResources(true);
+ DexSplitter.run(options);
+ Path baseDir = output.resolve("base");
+ Path featureDir = output.resolve("feature1");
+ byte[] contents = fileBytes;
+ byte[] contents2 = new byte[contents.length * 2];
+ System.arraycopy(contents, 0, contents2, 0, contents.length);
+ System.arraycopy(contents, 0, contents2, contents.length, contents.length);
+ Path baseTextFile = baseDir.resolve("dexsplitsample/TextFile.txt");
+ Path featureTextFile = featureDir.resolve("dexsplitsample/TextFile2.txt");
+ assert Files.exists(baseTextFile);
+ assert Files.exists(featureTextFile);
+ assert Arrays.equals(Files.readAllBytes(baseTextFile), contents);
+ assert Arrays.equals(Files.readAllBytes(featureTextFile), contents2);
+ }
+
+ @Test
+ public void splitDuplicateNonClassFiles()
+ throws IOException, CompilationFailedException, FeatureMappingException {
+ Path inputZip = temp.getRoot().toPath().resolve("input-zip-with-non-class-files.jar");
+ ZipOutputStream inputZipStream = new ZipOutputStream(Files.newOutputStream(inputZip));
+ String name = "dexsplitsample/TextFile.txt";
+ inputZipStream.putNextEntry(new ZipEntry(name));
+ byte[] fileBytes = Files.readAllBytes(Paths.get(TEXT_FILE));
+ inputZipStream.write(fileBytes);
+ inputZipStream.closeEntry();
+ inputZipStream.close();
+ Path output = temp.newFolder().toPath().resolve("output");
+ Files.createDirectory(output);
+ Path featureJar = temp.getRoot().toPath().resolve("feature1.jar");
+ Path feature2Jar = temp.getRoot().toPath().resolve("feature2.jar");
+ ZipOutputStream featureStream = new ZipOutputStream(Files.newOutputStream(featureJar));
+ name = "dexsplitsample/TextFile.txt";
+ featureStream.putNextEntry(new ZipEntry(name));
+ featureStream.write(fileBytes);
+ featureStream.closeEntry();
+ featureStream.close();
+ ZipOutputStream feature2Stream = new ZipOutputStream(Files.newOutputStream(feature2Jar));
+ name = "dexsplitsample/TextFile.txt";
+ feature2Stream.putNextEntry(new ZipEntry(name));
+ feature2Stream.write(fileBytes);
+ feature2Stream.closeEntry();
+ feature2Stream.close();
+ Options options = new Options();
+ options.addInputArchive(inputZip.toString());
+ options.setOutput(output.toString());
+ options.addFeatureJar(feature2Jar.toString());
+ options.addFeatureJar(featureJar.toString());
+ options.setSplitNonClassResources(true);
+ DexSplitter.run(options);
+ Path baseDir = output.resolve("base");
+ Path feature1Dir = output.resolve("feature1");
+ Path feature2Dir = output.resolve("feature2");
+ Path baseTextFile = baseDir.resolve("dexsplitsample/TextFile.txt");
+ Path feature1TextFile = feature1Dir.resolve("dexsplitsample/TextFile2.txt");
+ Path feature2TextFile = feature2Dir.resolve("dexsplitsample/TextFile2.txt");
+ assert !Files.exists(feature1TextFile);
+ assert !Files.exists(feature2TextFile);
+ assert Files.exists(baseTextFile);
+ assert Arrays.equals(Files.readAllBytes(baseTextFile), fileBytes);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
index bd7929d..05d6d3d 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
@@ -17,9 +17,12 @@
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dexsplitter.DexSplitter.Options;
import com.android.tools.r8.utils.ArchiveResourceProvider;
import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ThrowingConsumer;
import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import dalvik.system.PathClassLoader;
import java.io.IOException;
@@ -28,7 +31,9 @@
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@@ -150,6 +155,96 @@
toRun, baseOutput, r8TestCompileResult.getFeature(0), parameters.getRuntime());
}
+ // Compile the passed in classes plus RunInterface and SplitRunner using R8, then split
+ // based on the base/feature sets. toRun must implement the BaseRunInterface
+ <E extends Throwable> ProcessResult testDexSplitter(
+ TestParameters parameters,
+ Set<Class<?>> baseClasses,
+ Set<Class<?>> featureClasses,
+ Class<?> toRun,
+ String expectedOutput,
+ ThrowingConsumer<R8TestCompileResult, E> compileResultConsumer,
+ Consumer<R8FullTestBuilder> r8TestConfigurator)
+ throws Exception, E {
+ List<Class<?>> baseClassesWithRunner =
+ ImmutableList.<Class<?>>builder()
+ .add(RunInterface.class, SplitRunner.class)
+ .addAll(baseClasses)
+ .build();
+
+ Path baseJar = jarTestClasses(baseClassesWithRunner);
+ Path featureJar = jarTestClasses(featureClasses);
+
+ Path featureOnly =
+ testForR8(parameters.getBackend())
+ .addProgramClasses(featureClasses)
+ .addClasspathClasses(baseClasses)
+ .addClasspathClasses(RunInterface.class)
+ .addKeepAllClassesRule()
+ .addInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .writeToZip();
+ if (parameters.isDexRuntime()) {
+ // With D8 this should just work. We compile all of the base classes, then run with the
+ // feature loaded at runtime. Since there is no inlining/class merging we don't
+ // have any issues.
+ testForD8()
+ .addProgramClasses(SplitRunner.class, RunInterface.class)
+ .addProgramClasses(baseClasses)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .run(
+ parameters.getRuntime(),
+ SplitRunner.class,
+ toRun.getName(),
+ featureOnly.toAbsolutePath().toString())
+ .assertSuccessWithOutput(expectedOutput);
+ }
+
+ R8FullTestBuilder builder = testForR8(parameters.getBackend());
+ if (parameters.isCfRuntime()) {
+ // Compiling to jar we need to support the same way of loading code at runtime as
+ // android supports.
+ builder
+ .addProgramClasses(PathClassLoader.class)
+ .addKeepClassAndMembersRules(PathClassLoader.class);
+ }
+
+ R8FullTestBuilder r8FullTestBuilder =
+ builder
+ .setMinApi(parameters.getApiLevel())
+ .addProgramClasses(SplitRunner.class, RunInterface.class)
+ .addProgramClasses(baseClasses)
+ .addProgramClasses(featureClasses)
+ .addKeepMainRule(SplitRunner.class)
+ .addKeepClassRules(toRun);
+ r8TestConfigurator.accept(r8FullTestBuilder);
+ R8TestCompileResult r8TestCompileResult = r8FullTestBuilder.compile();
+ compileResultConsumer.accept(r8TestCompileResult);
+ Path fullFiles = r8TestCompileResult.writeToZip();
+
+ // Ensure that we can run the program as a unit (i.e., without splitting)
+ r8TestCompileResult
+ .run(parameters.getRuntime(), SplitRunner.class, toRun.getName())
+ .assertSuccessWithOutput(expectedOutput);
+
+ Path splitterOutput = temp.newFolder().toPath();
+ Path splitterBaseDexFile = splitterOutput.resolve("base").resolve("classes.dex");
+ Path splitterFeatureDexFile = splitterOutput.resolve("feature").resolve("classes.dex");
+
+ Options options = new Options();
+ options.setOutput(splitterOutput.toString());
+ options.addBaseJar(baseJar.toString());
+ options.addFeatureJar(featureJar.toString(), "feature");
+
+ options.addInputArchive(fullFiles.toString());
+ DexSplitter.run(options);
+
+ return runFeatureOnArt(
+ toRun, splitterBaseDexFile, splitterFeatureDexFile, parameters.getRuntime());
+ }
+
ProcessResult runFeatureOnArt(
Class toRun, Path splitterBaseDexFile, Path splitterFeatureDexFile, TestRuntime runtime)
throws IOException {
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index 75b7dda..dd40ecf 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -25,6 +25,8 @@
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.dex.ApplicationWriter;
import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.dexsplitter.DexSplitter;
+import com.android.tools.r8.dexsplitter.DexSplitter.Options;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
import com.android.tools.r8.errors.Unreachable;
@@ -253,6 +255,49 @@
}
@Test
+ public void everyThirdClassInMainWithDexSplitter() throws Throwable {
+ List<String> featureMappings = new ArrayList<>();
+ List<String> inFeatureMapping = new ArrayList<>();
+
+ ImmutableList.Builder<String> mainDexBuilder = ImmutableList.builder();
+ for (int i = 0; i < MANY_CLASSES.size(); i++) {
+ String clazz = MANY_CLASSES.get(i);
+ // Write the first 2 classes into the split.
+ if (i < 10) {
+ featureMappings.add(clazz + ":feature1");
+ inFeatureMapping.add(clazz);
+ }
+ if (i % 3 == 0) {
+ mainDexBuilder.add(clazz);
+ }
+ }
+ Path featureSplitMapping = temp.getRoot().toPath().resolve("splitmapping");
+ Path mainDexFile = temp.getRoot().toPath().resolve("maindex");
+ FileUtils.writeTextFile(featureSplitMapping, featureMappings);
+ List<String> mainDexList = mainDexBuilder.build();
+ FileUtils.writeTextFile(mainDexFile, ListUtils.map(mainDexList, MainDexListTests::typeToEntry));
+ Path output = temp.getRoot().toPath().resolve("split_output");
+ Files.createDirectories(output);
+ TestDiagnosticsHandler diagnosticsHandler = new TestDiagnosticsHandler();
+ Options options = new Options(diagnosticsHandler);
+ options.addInputArchive(getManyClassesMultiDexAppPath().toString());
+ options.setFeatureSplitMapping(featureSplitMapping.toString());
+ options.setOutput(output.toString());
+ options.setMainDexList(mainDexFile.toString());
+ DexSplitter.run(options);
+ assertEquals(0, diagnosticsHandler.numberOfErrorsAndWarnings());
+ Path baseDir = output.resolve("base");
+ CodeInspector inspector =
+ new CodeInspector(
+ AndroidApp.builder().addProgramFiles(baseDir.resolve("classes.dex")).build());
+ for (String clazz : mainDexList) {
+ if (!inspector.clazz(clazz).isPresent() && !inFeatureMapping.contains(clazz)) {
+ failedToFindClassInExpectedFile(baseDir, clazz);
+ }
+ }
+ }
+
+ @Test
public void singleClassInMainDex() throws Throwable {
ImmutableList<String> mainDex = ImmutableList.of(MANY_CLASSES.get(0));
verifyMainDexContains(mainDex, getManyClassesSingleDexAppPath(), true);
diff --git a/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
new file mode 100644
index 0000000..f41be5c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
@@ -0,0 +1,132 @@
+// Copyright (c) 2016, 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 junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+
+import com.android.tools.r8.utils.FeatureClassMapping.FeatureMappingException;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+
+public class FeatureClassMappingTest {
+
+ @Test
+ public void testSimpleParse() throws Exception {
+
+ List<String> lines =
+ ImmutableList.of(
+ "# Comment, don't care about contents: even more ::::",
+ "com.google.base:base",
+ "", // Empty lines allowed
+ "com.google.feature1:feature1",
+ "com.google.feature1:feature1", // Multiple definitions of the same predicate allowed.
+ "com.google$:feature1",
+ "_com.google:feature21",
+ "com.google.*:feature32");
+ FeatureClassMapping mapping = new FeatureClassMapping(lines);
+ }
+
+ private void ensureThrowsMappingException(List<String> lines) {
+ try {
+ new FeatureClassMapping(lines);
+ assertFalse(true);
+ } catch (FeatureMappingException e) {
+ // Expected
+ }
+ }
+
+ private void ensureThrowsMappingException(String string) {
+ ensureThrowsMappingException(ImmutableList.of(string));
+ }
+
+ @Test
+ public void testLookup() throws Exception {
+ List<String> lines =
+ ImmutableList.of(
+ "com.google.Base:base",
+ "",
+ "com.google.Feature1:feature1",
+ "com.google.Feature1:feature1", // Multiple definitions of the same predicate allowed.
+ "com.google.different.*:feature1",
+ "_com.Google:feature21",
+ "com.google.bas42.*:feature42");
+ FeatureClassMapping mapping = new FeatureClassMapping(lines);
+ assertEquals(mapping.featureForClass("com.google.Feature1"), "feature1");
+ assertEquals(mapping.featureForClass("com.google.different.Feature1"), "feature1");
+ assertEquals(mapping.featureForClass("com.google.different.Foobar"), "feature1");
+ assertEquals(mapping.featureForClass("com.google.Base"), "base");
+ assertEquals(mapping.featureForClass("com.google.bas42.foo.bar.bar.Foo"), "feature42");
+ assertEquals(mapping.featureForClass("com.google.bas42.f$o$o$.bar43.bar.Foo"), "feature42");
+ assertEquals(mapping.featureForClass("_com.Google"), "feature21");
+ }
+
+ @Test
+ public void testCatchAllWildcards() throws Exception {
+ testBaseWildcard(true);
+ testBaseWildcard(false);
+ testNonBaseCatchAll();
+ }
+
+ private void testNonBaseCatchAll() throws FeatureMappingException {
+ List<String> lines =
+ ImmutableList.of(
+ "com.google.Feature1:feature1",
+ "*:nonbase",
+ "com.strange.*:feature2");
+ FeatureClassMapping mapping = new FeatureClassMapping(lines);
+ assertEquals(mapping.featureForClass("com.google.Feature1"), "feature1");
+ assertEquals(mapping.featureForClass("com.google.different.Feature1"), "nonbase");
+ assertEquals(mapping.featureForClass("com.strange.different.Feature1"), "feature2");
+ assertEquals(mapping.featureForClass("Feature1"), "nonbase");
+ assertEquals(mapping.featureForClass("a.b.z.A"), "nonbase");
+ }
+
+ private void testBaseWildcard(boolean explicitBase) throws FeatureMappingException {
+ List<String> lines =
+ ImmutableList.of(
+ "com.google.Feature1:feature1",
+ explicitBase ? "*:base" : "",
+ "com.strange.*:feature2");
+ FeatureClassMapping mapping = new FeatureClassMapping(lines);
+ assertEquals(mapping.featureForClass("com.google.Feature1"), "feature1");
+ assertEquals(mapping.featureForClass("com.google.different.Feature1"), "base");
+ assertEquals(mapping.featureForClass("com.strange.different.Feature1"), "feature2");
+ assertEquals(mapping.featureForClass("com.stranger.Clazz"), "base");
+ assertEquals(mapping.featureForClass("Feature1"), "base");
+ assertEquals(mapping.featureForClass("a.b.z.A"), "base");
+ }
+
+ @Test
+ public void testWrongLines() throws Exception {
+ // No colon.
+ ensureThrowsMappingException("foo");
+ ensureThrowsMappingException("com.google.base");
+ // Two colons.
+ ensureThrowsMappingException(ImmutableList.of("a:b:c"));
+
+ // Empty identifier.
+ ensureThrowsMappingException("com..google:feature1");
+
+ // Ambiguous redefinition
+ ensureThrowsMappingException(
+ ImmutableList.of("com.google.foo:feature1", "com.google.foo:feature2"));
+ ensureThrowsMappingException(
+ ImmutableList.of("com.google.foo.*:feature1", "com.google.foo.*:feature2"));
+ }
+
+ @Test
+ public void testUsesOnlyExactMappings() throws Exception {
+ List<String> lines =
+ ImmutableList.of(
+ "com.pkg1.Clazz:feature1",
+ "com.pkg2.Clazz:feature2");
+ FeatureClassMapping mapping = new FeatureClassMapping(lines);
+
+ assertEquals(mapping.featureForClass("com.pkg1.Clazz"), "feature1");
+ assertEquals(mapping.featureForClass("com.pkg2.Clazz"), "feature2");
+ assertEquals(mapping.featureForClass("com.pkg1.Other"), mapping.baseName);
+ }
+}
diff --git a/tools/dexsplitter.py b/tools/dexsplitter.py
new file mode 100755
index 0000000..a97d089
--- /dev/null
+++ b/tools/dexsplitter.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python3
+# Copyright (c) 2018, 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.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+ sys.exit(toolhelper.run('dexsplitter', sys.argv[1:]))