Add command-line flags for global synthetics
Fixes: b/306120364
Bug: b/324270842
Change-Id: Iaf9b4a0f717f21d84666e6475ff408cea704bbc8
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 85d86a6..d3b820e 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -16,7 +16,9 @@
import com.android.tools.r8.keepanno.annotations.KeepForApi;
import com.android.tools.r8.naming.MapConsumer;
import com.android.tools.r8.naming.ProguardMapStringConsumer;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.profile.art.ArtProfileForRewriting;
import com.android.tools.r8.shaking.ProguardConfigurationParser;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
@@ -24,10 +26,14 @@
import com.android.tools.r8.shaking.ProguardConfigurationSourceFile;
import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
import com.android.tools.r8.startup.StartupProfileProvider;
+import com.android.tools.r8.synthesis.GlobalSyntheticsResourceBytes;
+import com.android.tools.r8.synthesis.GlobalSyntheticsResourceFile;
+import com.android.tools.r8.synthesis.GlobalSyntheticsUtils;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.AssertionConfigurationWithDefault;
import com.android.tools.r8.utils.DumpInputFlags;
+import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalGlobalSyntheticsProgramProvider;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.InternalOptions.DesugarState;
@@ -38,7 +44,10 @@
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -90,6 +99,7 @@
public static class Builder extends BaseCompilerCommand.Builder<D8Command, Builder> {
private boolean intermediate = false;
+ private Path globalSyntheticsOutput = null;
private GlobalSyntheticsConsumer globalSyntheticsConsumer = null;
private final List<GlobalSyntheticsResourceProvider> globalSyntheticsResourceProviders =
new ArrayList<>();
@@ -257,12 +267,28 @@
}
/**
+ * Set an output path for receiving the global synthetic content for the given compilation.
+ *
+ * <p>Note: this output is ignored if the compilation is not an "intermediate mode" compilation.
+ *
+ * <p>Note: setting this will clear out any consumer set by setGlobalSyntheticsConsumer.
+ */
+ public Builder setGlobalSyntheticsOutput(Path globalSyntheticsOutput) {
+ this.globalSyntheticsConsumer = null;
+ this.globalSyntheticsOutput = globalSyntheticsOutput;
+ return self();
+ }
+
+ /**
* Set a consumer for receiving the global synthetic content for the given compilation.
*
* <p>Note: this consumer is ignored if the compilation is not an "intermediate mode"
* compilation.
+ *
+ * <p>Note: setting this will clear out any output path set by setGlobalSyntheticsOutput.
*/
public Builder setGlobalSyntheticsConsumer(GlobalSyntheticsConsumer globalSyntheticsConsumer) {
+ this.globalSyntheticsOutput = null;
this.globalSyntheticsConsumer = globalSyntheticsConsumer;
return self();
}
@@ -288,11 +314,30 @@
/** Add global synthetics resource files. */
public Builder addGlobalSyntheticsFiles(Collection<Path> files) {
for (Path file : files) {
- addGlobalSyntheticsResourceProviders(new GlobalSyntheticsResourceFile(file));
+ addGlobalSyntheticsFileOrArchiveOfGlobalSynthetics(file);
}
return self();
}
+ private void addGlobalSyntheticsFileOrArchiveOfGlobalSynthetics(Path file) {
+ if (!FileUtils.isZipFile(file)) {
+ addGlobalSyntheticsResourceProviders(new GlobalSyntheticsResourceFile(file));
+ return;
+ }
+ PathOrigin origin = new PathOrigin(file);
+ try {
+ ZipUtils.iter(
+ file,
+ (entry, input) ->
+ addGlobalSyntheticsResourceProviders(
+ new GlobalSyntheticsResourceBytes(
+ new ArchiveEntryOrigin(entry.getName(), origin),
+ ByteStreams.toByteArray(input))));
+ } catch (IOException e) {
+ error(origin, e);
+ }
+ }
+
/**
* Set a consumer for receiving the keep rules to use when compiling the desugared library for
* the program being compiled in this compilation.
@@ -495,7 +540,8 @@
return new D8Command(isPrintHelp(), isPrintVersion());
}
- intermediate |= getProgramConsumer() instanceof DexFilePerClassFileConsumer;
+ final ProgramConsumer programConsumer = getProgramConsumer();
+ intermediate |= programConsumer instanceof DexFilePerClassFileConsumer;
DexItemFactory factory = new DexItemFactory();
DesugaredLibrarySpecification desugaredLibrarySpecification =
@@ -511,20 +557,24 @@
// If compiling to CF with --no-desugaring then the target API is B for consistency with R8.
int minApiLevel =
- getProgramConsumer() instanceof ClassFileConsumer && getDisableDesugaring()
+ programConsumer instanceof ClassFileConsumer && getDisableDesugaring()
? AndroidApiLevel.B.getLevel()
: getMinApiLevel();
+ GlobalSyntheticsConsumer globalConsumer =
+ GlobalSyntheticsUtils.determineGlobalSyntheticsConsumer(
+ intermediate, globalSyntheticsOutput, globalSyntheticsConsumer, programConsumer);
+
return new D8Command(
getAppBuilder().build(),
getMode(),
- getProgramConsumer(),
+ programConsumer,
getMainDexListConsumer(),
minApiLevel,
getReporter(),
getDesugaringState(),
intermediate,
- intermediate ? globalSyntheticsConsumer : null,
+ globalConsumer,
isOptimizeMultidexForLinearAlloc(),
getIncludeClassesChecksum(),
getDexClassChecksumFilter(),
@@ -841,4 +891,5 @@
.setEnableMissingLibraryApiModeling(enableMissingLibraryApiModeling)
.build();
}
+
}
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
index 49360fd..a16189b 100644
--- a/src/main/java/com/android/tools/r8/D8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -30,6 +30,8 @@
private static final Set<String> OPTIONS_WITH_ONE_PARAMETER =
ImmutableSet.of(
"--output",
+ "--globals",
+ "--globals-output",
"--lib",
"--classpath",
"--pg-map",
@@ -53,6 +55,8 @@
.add(ParseFlagInfoImpl.getDebug(true))
.add(ParseFlagInfoImpl.getRelease(false))
.add(ParseFlagInfoImpl.getOutput())
+ .add(ParseFlagInfoImpl.getGlobals())
+ .add(ParseFlagInfoImpl.getGlobalsOutput())
.add(ParseFlagInfoImpl.getLib())
.add(ParseFlagInfoImpl.getClasspath())
.add(ParseFlagInfoImpl.getMinApi())
@@ -211,6 +215,7 @@
private D8Command.Builder parse(String[] args, Origin origin, D8Command.Builder builder) {
CompilationMode compilationMode = null;
Path outputPath = null;
+ Path globalsOutputPath = null;
OutputMode outputMode = null;
boolean hasDefinedApiLevel = false;
OrderedClassFileResourceProvider.Builder classpathBuilder =
@@ -279,6 +284,21 @@
continue;
}
outputPath = Paths.get(nextArg);
+ } else if (arg.equals("--globals")) {
+ builder.addGlobalSyntheticsFiles(Paths.get(nextArg));
+ } else if (arg.equals("--globals-output")) {
+ if (globalsOutputPath != null) {
+ builder.error(
+ new StringDiagnostic(
+ "Cannot output globals both to '"
+ + globalsOutputPath.toString()
+ + "' and '"
+ + nextArg
+ + "'",
+ origin));
+ continue;
+ }
+ globalsOutputPath = Paths.get(nextArg);
} else if (arg.equals("--lib")) {
addLibraryArgument(builder, origin, nextArg);
} else if (arg.equals("--classpath")) {
@@ -373,6 +393,9 @@
if (outputPath == null) {
outputPath = Paths.get(".");
}
+ if (globalsOutputPath != null) {
+ builder.setGlobalSyntheticsOutput(globalsOutputPath);
+ }
return builder.setOutput(outputPath, outputMode);
}
}
diff --git a/src/main/java/com/android/tools/r8/ParseFlagInfoImpl.java b/src/main/java/com/android/tools/r8/ParseFlagInfoImpl.java
index 3273271..eb1fed3 100644
--- a/src/main/java/com/android/tools/r8/ParseFlagInfoImpl.java
+++ b/src/main/java/com/android/tools/r8/ParseFlagInfoImpl.java
@@ -47,6 +47,23 @@
"<file> must be an existing directory or a zip file.");
}
+ public static ParseFlagInfoImpl getGlobals() {
+ return flag1(
+ "--globals",
+ "<file>",
+ "Global synthetics <file> from a previous intermediate compilation.",
+ "The <file> may be either a zip-archive of global synthetics or the",
+ "global-synthetic files directly.");
+ }
+
+ public static ParseFlagInfoImpl getGlobalsOutput() {
+ return flag1(
+ "--globals-output",
+ "<file>",
+ "Output global synthetics in <file>.",
+ "<file> must be an existing directory or a non-existent zip archive.");
+ }
+
public static ParseFlagInfoImpl getLib() {
return flag1("--lib", "<file|jdk-home>", "Add <file|jdk-home> as a library resource.");
}
diff --git a/src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsResourceBytes.java b/src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsResourceBytes.java
new file mode 100644
index 0000000..08ace42
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsResourceBytes.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2024, 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.synthesis;
+
+import com.android.tools.r8.GlobalSyntheticsResourceProvider;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.origin.Origin;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class GlobalSyntheticsResourceBytes implements GlobalSyntheticsResourceProvider {
+
+ private final Origin origin;
+ private final byte[] bytes;
+
+ public GlobalSyntheticsResourceBytes(Origin origin, byte[] bytes) {
+ this.origin = origin;
+ this.bytes = bytes;
+ }
+
+ @Override
+ public Origin getOrigin() {
+ return origin;
+ }
+
+ @Override
+ public InputStream getByteStream() throws ResourceException {
+ return new ByteArrayInputStream(bytes);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsResourceFile.java b/src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsResourceFile.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/GlobalSyntheticsResourceFile.java
rename to src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsResourceFile.java
index 598a074..cf5b767 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsResourceFile.java
+++ b/src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsResourceFile.java
@@ -1,8 +1,10 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2024, 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;
+package com.android.tools.r8.synthesis;
+import com.android.tools.r8.GlobalSyntheticsResourceProvider;
+import com.android.tools.r8.ResourceException;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
import java.io.IOException;
diff --git a/src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsUtils.java b/src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsUtils.java
new file mode 100644
index 0000000..e132952
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/synthesis/GlobalSyntheticsUtils.java
@@ -0,0 +1,107 @@
+// Copyright (c) 2024, 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.synthesis;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.GlobalSyntheticsConsumer;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.utils.ArchiveBuilder;
+import com.android.tools.r8.utils.DirectoryBuilder;
+import com.android.tools.r8.utils.OutputBuilder;
+import com.android.tools.r8.utils.StringDiagnostic;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public final class GlobalSyntheticsUtils {
+
+ private GlobalSyntheticsUtils() {}
+
+ public static GlobalSyntheticsConsumer determineGlobalSyntheticsConsumer(
+ boolean intermediate,
+ Path output,
+ GlobalSyntheticsConsumer consumer,
+ ProgramConsumer programConsumer) {
+ assert output == null || consumer == null;
+ if (!intermediate) {
+ return null;
+ }
+ if (consumer != null) {
+ return consumer;
+ }
+ if (output == null) {
+ return null;
+ }
+
+ // Output is non-null, and we must create a consumer compatible with the program consumer.
+ OutputBuilder builder =
+ Files.isDirectory(output) ? new DirectoryBuilder(output) : new ArchiveBuilder(output);
+ builder.open();
+
+ if (programConsumer instanceof DexIndexedConsumer) {
+ return new GlobalSyntheticsConsumer() {
+ boolean written = false;
+
+ @Override
+ public synchronized void accept(
+ ByteDataView data, ClassReference context, DiagnosticsHandler handler) {
+ assert context == null;
+ if (written) {
+ String msg = "Attempt to write multiple global-synthetics files in dex-indexed mode.";
+ handler.error(new StringDiagnostic(msg));
+ throw new RuntimeException(msg);
+ }
+ builder.addFile("classes.globals", data, handler);
+ builder.close(handler);
+ written = true;
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {
+ // If not global info was written, close the builder with empty content.
+ if (!written) {
+ builder.close(handler);
+ }
+ }
+ };
+ }
+
+ if (programConsumer instanceof DexFilePerClassFileConsumer) {
+ return new GlobalSyntheticsConsumer() {
+
+ @Override
+ public void accept(ByteDataView data, ClassReference context, DiagnosticsHandler handler) {
+ builder.addFile(context.getBinaryName() + ".globals", data, handler);
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {
+ builder.close(handler);
+ }
+ };
+ }
+
+ if (programConsumer instanceof ClassFileConsumer) {
+ return new GlobalSyntheticsConsumer() {
+ @Override
+ public void accept(ByteDataView data, ClassReference context, DiagnosticsHandler handler) {
+ builder.addFile(context.getBinaryName() + ".globals", data, handler);
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {
+ builder.close(handler);
+ }
+ };
+ }
+
+ throw new Unreachable("Unexpected program consumer type");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticOutputCliTest.java b/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticOutputCliTest.java
new file mode 100644
index 0000000..a07ca84
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/globals/GlobalSyntheticOutputCliTest.java
@@ -0,0 +1,320 @@
+// Copyright (c) 2024, 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.synthesis.globals;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.D8;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class GlobalSyntheticOutputCliTest extends TestBase {
+
+ static final String EXPECTED =
+ StringUtils.lines("Hello", "all good...", "Hello again", "still good...");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDefaultDexRuntime().withApiLevel(AndroidApiLevel.N_MR1).build();
+ }
+
+ public GlobalSyntheticOutputCliTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ private static String getAndroidJar() {
+ return ToolHelper.getAndroidJar(AndroidApiLevel.LATEST).toString();
+ }
+
+ private String getApiLevelString() {
+ return "" + parameters.getApiLevel().getLevel();
+ }
+
+ private ProcessResult forkD8(String... args) throws IOException {
+ return forkD8(Arrays.asList(args));
+ }
+
+ private ProcessResult forkD8(List<String> args) throws IOException {
+ ImmutableList.Builder<String> command =
+ new ImmutableList.Builder<String>()
+ .add(CfRuntime.getSystemRuntime().getJavaExecutable().toString())
+ .add("-Dcom.android.tools.r8.enableApiOutliningAndStubbing=1")
+ .add("-cp")
+ .add(System.getProperty("java.class.path"))
+ .add(D8.class.getName())
+ .add("--min-api", getApiLevelString())
+ .add("--lib", getAndroidJar())
+ .addAll(args);
+ ProcessBuilder processBuilder = new ProcessBuilder(command.build());
+ ProcessResult result = ToolHelper.runProcess(processBuilder);
+ assertEquals(result.toString(), 0, result.exitCode);
+ return result;
+ }
+
+ @Test
+ public void testDexIndexedZip() throws Exception {
+ Path input1 = transformClass(TestClass1.class);
+ Path input2 = transformClass(TestClass2.class);
+ Path dexOut = temp.newFolder().toPath().resolve("out.jar");
+ Path globalsOut = temp.newFolder().toPath().resolve("out.zip");
+ forkD8(
+ input1.toString(),
+ input2.toString(),
+ "--intermediate",
+ "--output",
+ dexOut.toString(),
+ "--globals-output",
+ globalsOut.toString());
+
+ assertTrue(Files.exists(dexOut));
+ assertTrue(Files.exists(globalsOut));
+
+ Path finalOut = temp.newFolder().toPath().resolve("out.jar");
+ forkD8(dexOut.toString(), "--globals", globalsOut.toString(), "--output", finalOut.toString());
+
+ testForD8()
+ .addProgramFiles(finalOut)
+ .run(parameters.getRuntime(), TestClass1.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testDexIndexedDir() throws Exception {
+ Path input1 = transformClass(TestClass1.class);
+ Path input2 = transformClass(TestClass2.class);
+ Path dexOut = temp.newFolder().toPath().resolve("out.jar");
+ Path globalsOut = temp.newFolder("out").toPath();
+ forkD8(
+ input1.toString(),
+ input2.toString(),
+ "--intermediate",
+ "--output",
+ dexOut.toString(),
+ "--globals-output",
+ globalsOut.toString());
+
+ Path expectedGlobalsFile = globalsOut.resolve("classes.globals");
+ assertTrue(Files.exists(dexOut));
+ assertTrue(Files.isDirectory(globalsOut));
+ assertTrue(Files.exists(expectedGlobalsFile));
+
+ Path finalOut = temp.newFolder().toPath().resolve("out.jar");
+ forkD8(
+ dexOut.toString(),
+ "--globals",
+ expectedGlobalsFile.toString(),
+ "--output",
+ finalOut.toString());
+
+ testForD8()
+ .addProgramFiles(finalOut)
+ .run(parameters.getRuntime(), TestClass1.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testDexPerClassZip() throws Exception {
+ Path input1 = transformClass(TestClass1.class);
+ Path input2 = transformClass(TestClass2.class);
+ Path dexOut = temp.newFolder().toPath().resolve("out.jar");
+ Path globalsOut = temp.newFolder().toPath().resolve("out.zip");
+ forkD8(
+ input1.toString(),
+ input2.toString(),
+ "--file-per-class",
+ "--output",
+ dexOut.toString(),
+ "--globals-output",
+ globalsOut.toString());
+
+ assertTrue(Files.exists(dexOut));
+ assertTrue(Files.exists(globalsOut));
+
+ Path finalOut = temp.newFolder().toPath().resolve("out.jar");
+ forkD8(dexOut.toString(), "--globals", globalsOut.toString(), "--output", finalOut.toString());
+
+ testForD8()
+ .addProgramFiles(finalOut)
+ .run(parameters.getRuntime(), TestClass1.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testDexPerClassDir() throws Exception {
+ Path input1 = transformClass(TestClass1.class);
+ Path input2 = transformClass(TestClass2.class);
+ Path dexOut = temp.newFolder().toPath().resolve("out.jar");
+ Path globalsOut = temp.newFolder("out").toPath();
+ forkD8(
+ input1.toString(),
+ input2.toString(),
+ "--file-per-class",
+ "--output",
+ dexOut.toString(),
+ "--globals-output",
+ globalsOut.toString());
+
+ assertTrue(Files.exists(dexOut));
+ assertTrue(Files.isDirectory(globalsOut));
+
+ List<Path> globalFiles =
+ ImmutableList.of(
+ globalsOut.resolve(binaryName(TestClass1.class) + ".globals"),
+ globalsOut.resolve(binaryName(TestClass2.class) + ".globals"));
+ globalFiles.forEach(f -> assertTrue(Files.exists(f)));
+
+ Path finalOut = temp.newFolder().toPath().resolve("out.jar");
+ forkD8(
+ ImmutableList.<String>builder()
+ .add(dexOut.toString())
+ .addAll(
+ ListUtils.flatMap(globalFiles, f -> ImmutableList.of("--globals", f.toString())))
+ .add("--output")
+ .add(finalOut.toString())
+ .build());
+
+ testForD8()
+ .addProgramFiles(finalOut)
+ .run(parameters.getRuntime(), TestClass1.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testDexPerClassFileZip() throws Exception {
+ Path input1 = transformClass(TestClass1.class);
+ Path input2 = transformClass(TestClass2.class);
+ Path dexOut = temp.newFolder().toPath().resolve("out.jar");
+ Path globalsOut = temp.newFolder().toPath().resolve("out.zip");
+ forkD8(
+ input1.toString(),
+ input2.toString(),
+ "--file-per-class-file",
+ "--output",
+ dexOut.toString(),
+ "--globals-output",
+ globalsOut.toString());
+
+ assertTrue(Files.exists(dexOut));
+ assertTrue(Files.exists(globalsOut));
+
+ Path finalOut = temp.newFolder().toPath().resolve("out.jar");
+ forkD8(dexOut.toString(), "--globals", globalsOut.toString(), "--output", finalOut.toString());
+
+ testForD8()
+ .addProgramFiles(finalOut)
+ .run(parameters.getRuntime(), TestClass1.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testDexPerClassFileDir() throws Exception {
+ Path input1 = transformClass(TestClass1.class);
+ Path input2 = transformClass(TestClass2.class);
+ Path dexOut = temp.newFolder().toPath().resolve("out.jar");
+ Path globalsOut = temp.newFolder("out").toPath();
+ forkD8(
+ input1.toString(),
+ input2.toString(),
+ "--file-per-class-file",
+ "--output",
+ dexOut.toString(),
+ "--globals-output",
+ globalsOut.toString());
+
+ assertTrue(Files.exists(dexOut));
+ assertTrue(Files.isDirectory(globalsOut));
+
+ List<Path> globalFiles =
+ ImmutableList.of(
+ globalsOut.resolve(binaryName(TestClass1.class) + ".globals"),
+ globalsOut.resolve(binaryName(TestClass2.class) + ".globals"));
+ globalFiles.forEach(f -> assertTrue(Files.exists(f)));
+
+ Path finalOut = temp.newFolder().toPath().resolve("out.jar");
+ forkD8(
+ ImmutableList.<String>builder()
+ .add(dexOut.toString())
+ .addAll(
+ ListUtils.flatMap(globalFiles, f -> ImmutableList.of("--globals", f.toString())))
+ .add("--output")
+ .add(finalOut.toString())
+ .build());
+
+ testForD8()
+ .addProgramFiles(finalOut)
+ .run(parameters.getRuntime(), TestClass1.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ // Transform the class such that its handlers use the AuthenticationRequiredException which
+ // is introduced with API level 25. This triggers the need for a class stub for the exception.
+ private Path transformClass(Class<?> clazz) throws IOException {
+ byte[] bytes =
+ transformer(clazz)
+ .transformTryCatchBlock(
+ MethodPredicate.all(),
+ (start, end, handler, type, visitor) -> {
+ assertEquals("java/lang/Exception", type);
+ visitor.visitTryCatchBlock(
+ start, end, handler, "android/app/AuthenticationRequiredException");
+ })
+ .transform();
+ Path file = temp.newFolder().toPath().resolve("input.class");
+ Files.write(file, bytes);
+ return file;
+ }
+
+ static class TestClass1 {
+
+ public static void main(String[] args) {
+ Runnable r =
+ () -> {
+ try {
+ System.out.println("Hello");
+ } catch (Exception /* will be AuthenticationRequiredException */ e) {
+ System.out.println("fail...");
+ throw e;
+ }
+ System.out.println("all good...");
+ };
+ r.run();
+ TestClass2.main(args);
+ }
+ }
+
+ static class TestClass2 {
+
+ public static void main(String[] args) {
+ try {
+ System.out.println("Hello again");
+ } catch (Exception /* will be AuthenticationRequiredException */ e) {
+ System.out.println("fail...");
+ throw e;
+ }
+ System.out.println("still good...");
+ }
+ }
+}