Reapply "Add CLI support for resource input/output"
This reverts commit 5e457b4665bcfdc8389fa0d8a030feb68c8cce15.
Change-Id: I0c0e3862857c175d8979bd7fdc2aedb07148d780
diff --git a/src/main/java/com/android/tools/r8/R8CommandParser.java b/src/main/java/com/android/tools/r8/R8CommandParser.java
index 2ef79c8..fa0a094 100644
--- a/src/main/java/com/android/tools/r8/R8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/R8CommandParser.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.StringConsumer.FileConsumer;
import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.profile.art.ArtProfileConsumerUtils;
import com.android.tools.r8.profile.art.ArtProfileProviderUtils;
import com.android.tools.r8.profile.startup.StartupProfileProviderUtils;
@@ -19,12 +20,15 @@
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
public class R8CommandParser extends BaseCompilerCommandParser<R8Command, R8Command.Builder> {
@@ -41,6 +45,7 @@
"--main-dex-rules",
"--main-dex-list",
"--feature",
+ "--android-resources",
"--main-dex-list-output",
"--pg-conf",
"--pg-conf-output",
@@ -57,7 +62,7 @@
// Note: this must be a subset of OPTIONS_WITH_ONE_PARAMETER.
private static final Set<String> OPTIONS_WITH_TWO_PARAMETERS =
- ImmutableSet.of(ART_PROFILE_FLAG, "--feature");
+ ImmutableSet.of(ART_PROFILE_FLAG, "--feature", "--android-resources");
// Due to the family of flags (for assertions and diagnostics) we can't base the one/two args
// on this setup of flags. Thus, the flag collection just encodes the descriptive content.
@@ -96,11 +101,21 @@
.add(ParseFlagInfoImpl.getMainDexList())
.add(
flag2(
- "--feature",
+ "--android-resources",
"<input>",
"<output>",
+ "Add android resource input and output to be used in resource shrinking. Both ",
+ "input and output must be specified."))
+ .add(
+ flag2(
+ "--feature",
+ "<input>[:|;<res-input>]",
+ "<output>[:|;<res-output>]",
"Add feature <input> file to <output> file. Several ",
- "occurrences can map to the same output."))
+ "occurrences can map to the same output. If <res-input> and <res-output> are ",
+ "specified use these as resource shrinker input and output. Separator is : on ",
+ "linux/mac, ; on windows. It is possible to supply resource only features by ",
+ " using an empty string for <input> and <output>, e.g. --feature :in.ap_ :out.ap_"))
.add(ParseFlagInfoImpl.getIsolatedSplits())
.add(flag1("--main-dex-list-output", "<file>", "Output the full main-dex list in <file>."))
.addAll(ParseFlagInfoImpl.getAssertionsFlags())
@@ -193,7 +208,7 @@
private void parse(
String[] args, Origin argsOrigin, R8Command.Builder builder, ParseState state) {
String[] expandedArgs = FlagFile.expandFlagFiles(args, builder::error);
- Map<Path, List<Path>> featureSplitJars = new HashMap<>();
+ FeatureSplitConfigCollector featureSplitConfigCollector = new FeatureSplitConfigCollector();
for (int i = 0; i < expandedArgs.length; i++) {
String arg = expandedArgs[i].trim();
String nextArg = null;
@@ -290,10 +305,15 @@
builder.setDisableDesugaring(true);
} else if (arg.equals("--main-dex-rules")) {
builder.addMainDexRulesFiles(Paths.get(nextArg));
+ } else if (arg.equals("--android-resources")) {
+ Path inputPath = Paths.get(nextArg);
+ Path outputPath = Paths.get(nextNextArg);
+ builder.setAndroidResourceProvider(
+ new ArchiveProtoAndroidResourceProvider(inputPath, new PathOrigin(inputPath)));
+ builder.setAndroidResourceConsumer(
+ new ArchiveProtoAndroidResourceConsumer(outputPath, inputPath));
} else if (arg.equals("--feature")) {
- featureSplitJars
- .computeIfAbsent(Paths.get(nextNextArg), k -> new ArrayList<>())
- .add(Paths.get(nextArg));
+ featureSplitConfigCollector.addInputOutput(nextArg, nextNextArg);
} else if (arg.equals(ISOLATED_SPLITS_FLAG)) {
builder.setEnableIsolatedSplits(true);
} else if (arg.equals("--main-dex-list")) {
@@ -358,20 +378,117 @@
builder.addProgramFiles(Paths.get(arg));
}
}
- featureSplitJars.forEach(
- (outputPath, inputJars) -> addFeatureJar(builder, outputPath, inputJars));
+ addFeatureSplitConfigs(builder, featureSplitConfigCollector.getConfigs());
}
- public void addFeatureJar(R8Command.Builder builder, Path outputPath, List<Path> inputJarPaths) {
- builder.addFeatureSplit(
- featureSplitGenerator -> {
- featureSplitGenerator.setProgramConsumer(
- builder.createProgramOutputConsumer(outputPath, OutputMode.DexIndexed, true));
- for (Path inputPath : inputJarPaths) {
- featureSplitGenerator.addProgramResourceProvider(
- ArchiveProgramResourceProvider.fromArchive(inputPath));
- }
- return featureSplitGenerator.build();
- });
+ private void addFeatureSplitConfigs(
+ R8Command.Builder builder, Collection<FeatureSplitConfig> featureSplitConfigs) {
+ for (FeatureSplitConfig featureSplitConfig : featureSplitConfigs) {
+ builder.addFeatureSplit(
+ featureSplitGenerator -> {
+ if (featureSplitConfig.outputJar != null) {
+ featureSplitGenerator.setProgramConsumer(
+ builder.createProgramOutputConsumer(
+ featureSplitConfig.outputJar, OutputMode.DexIndexed, true));
+ }
+ for (Path inputPath : featureSplitConfig.inputJars) {
+ featureSplitGenerator.addProgramResourceProvider(
+ ArchiveProgramResourceProvider.fromArchive(inputPath));
+ }
+ if (featureSplitConfig.inputResources != null) {
+ featureSplitGenerator.setAndroidResourceProvider(
+ new ArchiveProtoAndroidResourceProvider(
+ featureSplitConfig.inputResources,
+ new PathOrigin(featureSplitConfig.inputResources)));
+ }
+ if (featureSplitConfig.outputResources != null) {
+ featureSplitGenerator.setAndroidResourceConsumer(
+ new ArchiveProtoAndroidResourceConsumer(
+ featureSplitConfig.outputResources, featureSplitConfig.inputResources));
+ }
+ return featureSplitGenerator.build();
+ });
+ }
+ }
+
+ // Represents a set of paths parsed from a string that may contain a ":" (";" on windows).
+ // Supported examples are:
+ // pathA -> first = pathA, second = null
+ // pathA:pathB -> first = pathA, second = pathB
+ // :pathB -> first = null, second = pathB
+ // pathA: -> first = pathA, second = null
+ private static class PossibleDoublePath {
+
+ public final Path first;
+ public final Path second;
+
+ private PossibleDoublePath(Path first, Path second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public static PossibleDoublePath parse(String input) {
+ Path first = null, second = null;
+ List<String> inputSplit = StringUtils.split(input, File.pathSeparatorChar);
+ if (inputSplit.size() == 0 || inputSplit.size() > 2) {
+ throw new IllegalArgumentException("Feature input/output takes one or two paths.");
+ }
+ String firstString = inputSplit.get(0);
+ if (!firstString.isEmpty()) {
+ first = Paths.get(firstString);
+ }
+ if (inputSplit.size() == 2) {
+ // "a:".split() gives just ["a"], so we should never get here if we don't have
+ // a second string. ":b".split gives ["", "b"] which is handled for first above.
+ assert inputSplit.get(1).length() > 0;
+ second = Paths.get(inputSplit.get(1));
+ }
+ return new PossibleDoublePath(first, second);
+ }
+ }
+
+ private static class FeatureSplitConfig {
+ private List<Path> inputJars = new ArrayList<>();
+ private Path inputResources;
+ private Path outputResources;
+ private Path outputJar;
+ }
+
+ private static class FeatureSplitConfigCollector {
+
+ private List<FeatureSplitConfig> resourceOnlySplits = new ArrayList<>();
+ private Map<Path, FeatureSplitConfig> withCodeSplits = new HashMap<>();
+
+ public void addInputOutput(String input, String output) {
+ PossibleDoublePath inputPaths = PossibleDoublePath.parse(input);
+ PossibleDoublePath outputPaths = PossibleDoublePath.parse(output);
+ FeatureSplitConfig featureSplitConfig;
+ if (outputPaths.first != null) {
+ featureSplitConfig =
+ withCodeSplits.computeIfAbsent(outputPaths.first, k -> new FeatureSplitConfig());
+ featureSplitConfig.outputJar = outputPaths.first;
+ // We support adding resources independently of the input jars, which later --feature
+ // can add, so we might have no input jars here, example:
+ // ... --feature :input_feature.ap_ out.jar:out_feature.ap_ --feature in.jar out.jar
+ if (inputPaths.first != null) {
+ featureSplitConfig.inputJars.add(inputPaths.first);
+ }
+ } else {
+ featureSplitConfig = new FeatureSplitConfig();
+ resourceOnlySplits.add(featureSplitConfig);
+ }
+ if (Objects.isNull(inputPaths.second) != Objects.isNull(outputPaths.second)) {
+ throw new IllegalArgumentException(
+ "Both input and output for feature resources must be provided");
+ }
+ featureSplitConfig.inputResources = inputPaths.second;
+ featureSplitConfig.outputResources = outputPaths.second;
+ }
+
+ public Collection<FeatureSplitConfig> getConfigs() {
+ ArrayList<FeatureSplitConfig> featureSplitConfigs = new ArrayList<>(resourceOnlySplits);
+ featureSplitConfigs.addAll(withCodeSplits.values());
+ return featureSplitConfigs;
+ }
}
}
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index d22f197..208d106 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -17,6 +17,7 @@
import com.android.tools.r8.AssertionsConfiguration.AssertionTransformationScope;
import com.android.tools.r8.ProgramResource.Kind;
import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils;
import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
import com.android.tools.r8.dex.Marker;
import com.android.tools.r8.dex.Marker.Tool;
@@ -31,6 +32,7 @@
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
+import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
@@ -46,6 +48,7 @@
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@@ -78,6 +81,18 @@
return jar;
}
+ private Path getTestResources() throws Exception {
+ Path resourceOutput = temp.newFile("base_resources.ap_").toPath();
+ AndroidResourceTestingUtils.writePrecompiledManifestAndResourcePB(resourceOutput);
+ return resourceOutput;
+ }
+
+ private Path getFeatureTestResources(TemporaryFolder temp) throws Exception {
+ Path resourceOutput = temp.newFile("feature_resources.ap_").toPath();
+ AndroidResourceTestingUtils.writePrecompiledManifestAndResourcePB(resourceOutput);
+ return resourceOutput;
+ }
+
@Test(expected = CompilationFailedException.class)
public void emptyBuilder() throws Throwable {
// The builder must have a program consumer.
@@ -178,6 +193,68 @@
}
@Test
+ public void passAndroidResources() throws Throwable {
+ Path working = temp.getRoot().toPath();
+ Path input = getJarWithA();
+ Path library = ToolHelper.getDefaultAndroidJar();
+ Path output = working.resolve("classes.dex");
+ Path resourceInput = getTestResources();
+ Path resourceOutput = working.resolve("resources_out.ap_");
+ assertFalse(Files.exists(output));
+ assertFalse(Files.exists(resourceOutput));
+ ProcessResult result =
+ ToolHelper.forkR8(
+ working,
+ input.toAbsolutePath().toString(),
+ "--lib",
+ library.toAbsolutePath().toString(),
+ "--android-resources",
+ resourceInput.toAbsolutePath().toString(),
+ resourceOutput.toAbsolutePath().toString(),
+ "--no-tree-shaking");
+ assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
+ assertTrue(Files.exists(output));
+ System.out.println(result.stdout);
+ assertTrue(Files.exists(resourceOutput));
+ }
+
+ @Test
+ public void passFeatureResources() throws Throwable {
+ Path working = temp.getRoot().toPath();
+ Path input = getJarWithA();
+ Path inputFeature = getJarWithB();
+ Path library = ToolHelper.getDefaultAndroidJar();
+ Path output = working.resolve("classes.dex");
+ Path featureOutput = working.resolve("feature.zip");
+ Path resourceInput = getTestResources();
+ Path resourceOutput = working.resolve("resources_out.ap_");
+ TemporaryFolder featureSplitTemp = ToolHelper.getTemporaryFolderForTest();
+ featureSplitTemp.create();
+ Path featureReasourceInput = getFeatureTestResources(featureSplitTemp);
+ Path featureResourceOutput = working.resolve("feature_resources_out.ap_");
+ assertFalse(Files.exists(output));
+ assertFalse(Files.exists(featureOutput));
+ String pathSeparator = File.pathSeparator;
+ ProcessResult result =
+ ToolHelper.forkR8(
+ working,
+ input.toAbsolutePath().toString(),
+ "--lib",
+ library.toAbsolutePath().toString(),
+ "--android-resources",
+ resourceInput.toAbsolutePath().toString(),
+ resourceOutput.toAbsolutePath().toString(),
+ "--feature",
+ inputFeature.toAbsolutePath() + pathSeparator + featureReasourceInput.toAbsolutePath(),
+ featureOutput.toAbsolutePath() + pathSeparator + featureResourceOutput.toAbsolutePath(),
+ "--no-tree-shaking");
+ assertEquals("R8 run failed: " + result.stderr, 0, result.exitCode);
+ assertTrue(Files.exists(output));
+ assertTrue(Files.exists(featureOutput));
+ assertTrue(Files.exists(resourceOutput));
+ }
+
+ @Test
public void featureOnlyOneArgument() throws Throwable {
Path working = temp.getRoot().toPath();
Path input = getJarWithA();
diff --git a/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 10486c8..eb5b8e2 100644
--- a/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -385,7 +385,7 @@
return this;
}
- AndroidTestResourceBuilder setPackageId(int packageId) {
+ public AndroidTestResourceBuilder setPackageId(int packageId) {
this.packageId = packageId;
return this;
}