| // 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.utils.DexInspector; |
| import com.android.tools.r8.utils.DexInspector.ClassSubject; |
| import com.android.tools.r8.utils.FeatureClassMapping.FeatureMappingException; |
| 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.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 CLASS4_CLASS = CLASS_DIR + "/Class4.class"; |
| private static final String CLASS4_LAMBDA_INTERFACE = CLASS_DIR + "/Class4$LambdaInterface.class"; |
| |
| |
| @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(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}) { |
| Marker marker = ExtractMarker.extractMarkerFromDexFile(path); |
| assertEquals(addMarkerToInput, marker != null); |
| } |
| } |
| |
| @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, ResourceException, |
| ExecutionException { |
| 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, ResourceException, |
| ExecutionException { |
| splitFromJars(true, true); |
| splitFromJars(false, true); |
| splitFromJars(true, false); |
| splitFromJars(false, false); |
| } |
| |
| private void splitFromJars(boolean useOptions, boolean explicitBase) |
| 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/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.addFeatureJar(baseJar.toString()); |
| } else { |
| // 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("--feature-jar"); |
| args.add(baseJar.toString()); |
| } else { |
| args.add("--base-output-name"); |
| args.add("base_renamed"); |
| } |
| |
| DexSplitter.main(args.toArray(new String[0])); |
| } |
| String baseOutputName = explicitBase ? "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(CLASS4_CLASS)) |
| .addProgramFiles(Paths.get(CLASS4_LAMBDA_INTERFACE)) |
| .addLibraryFiles(ToolHelper.getDefaultAndroidJar()) |
| .setProguardMapOutputPath(proguardMap) |
| .addProguardConfiguration(getProguardConf(), null) |
| .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. |
| DexInspector inspector = new DexInspector(base, proguardMap.toString()); |
| ClassSubject subject = inspector.clazz("dexsplitsample.Class1"); |
| assertTrue(subject.isPresent()); |
| assertTrue(subject.isRenamed()); |
| } |
| } |