Add support for calculating the feature splits from jars
Bug: 71526824
Change-Id: I2b8cf6af5a8445c888028ecaa25f22d072c5371c
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index 0c9fbe8..c086c7e 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -29,7 +29,10 @@
public class DexSplitterHelper {
public static void run(
- D8Command command, String featureSplitMapping, String outputArchive, String proguardMap)
+ D8Command command,
+ FeatureClassMapping featureClassMapping,
+ String outputArchive,
+ String proguardMap)
throws IOException, CompilationException, ExecutionException {
InternalOptions options = command.getInternalOptions();
options.enableDesugaring = false;
@@ -45,8 +48,7 @@
Timing timing = new Timing("DexSplitter");
DexApplication app =
new ApplicationReader(command.getInputApp(), options, timing).read(null, executor);
- FeatureClassMapping featureClassMapping =
- new FeatureClassMapping(Paths.get(featureSplitMapping));
+
ClassNameMapper mapper = null;
if (proguardMap != null) {
diff --git a/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java b/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
index 8e184c0..20cf4ee 100644
--- a/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
+++ b/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
@@ -9,6 +9,9 @@
import com.android.tools.r8.D8Command;
import com.android.tools.r8.DexIndexedConsumer;
import com.android.tools.r8.DexSplitterHelper;
+import com.android.tools.r8.ResourceException;
+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 java.io.IOException;
@@ -25,6 +28,7 @@
private static class Options {
List<String> inputArchives = new ArrayList<>();
+ List<String> featureJars = new ArrayList<>();
String splitBaseName = DEFAULT_OUTPUT_ARCHIVE_FILENAME;
String featureSplitMapping;
String proguardMap;
@@ -39,6 +43,11 @@
options.inputArchives.addAll(input);
continue;
}
+ List<String> featureJars = OptionsParsing.tryParseMulti(context, "--feature-jar");
+ if (featureJars != null) {
+ options.featureJars.addAll(featureJars);
+ continue;
+ }
String output = OptionsParsing.tryParseSingle(context, "--output", "-o");
if (output != null) {
options.splitBaseName = output;
@@ -59,14 +68,27 @@
return options;
}
+ private static FeatureClassMapping createFeatureClassMapping(Options options)
+ throws IOException, FeatureMappingException, ResourceException {
+ if (options.featureSplitMapping != null) {
+ return FeatureClassMapping.fromSpecification(Paths.get(options.featureSplitMapping));
+ }
+ assert !options.featureJars.isEmpty();
+ return FeatureClassMapping.fromJarFiles(options.featureJars);
+ }
+
public static void run(String[] args)
- throws CompilationFailedException, IOException, CompilationException, ExecutionException {
+ throws CompilationFailedException, IOException, CompilationException, ExecutionException,
+ ResourceException, FeatureMappingException {
Options options = parseArguments(args);
if (options.inputArchives.isEmpty()) {
throw new RuntimeException("Need at least one --input");
}
- if (options.featureSplitMapping == null) {
- throw new RuntimeException("You must supply a feature split mapping");
+ if (options.featureSplitMapping == null && options.featureJars.isEmpty()) {
+ throw new RuntimeException("You must supply a feature split mapping or feature jars");
+ }
+ if (options.featureSplitMapping != null && !options.featureJars.isEmpty()) {
+ throw new RuntimeException("You can't supply both a feature split mapping and feature jars");
}
D8Command.Builder builder = D8Command.builder();
@@ -76,8 +98,11 @@
// 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());
+
+ FeatureClassMapping featureClassMapping = createFeatureClassMapping(options);
+
DexSplitterHelper.run(
- builder.build(), options.featureSplitMapping, options.splitBaseName, options.proguardMap);
+ builder.build(), featureClassMapping, options.splitBaseName, options.proguardMap);
}
public static void main(String[] args) {
@@ -89,7 +114,9 @@
} catch (CompilationFailedException
| IOException
| CompilationException
- | ExecutionException e) {
+ | ExecutionException
+ | ResourceException
+ | FeatureMappingException e) {
System.err.println("Splitting failed: " + e.getMessage());
System.exit(1);
}
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index 1d424bb..cccbb4b 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -92,16 +92,29 @@
}
/**
- * Determine the given {@param typeName} is valid java type name or not.
+ * Determine the given {@param typeName} is a valid jvms binary name or not (jvms 4.2.1).
*
- * @param typeName the java type name
- * @return true if and only if the given type name is valid java type
+ * @param typeName the jvms binary name
+ * @return true if and only if the given type name is valid jvms binary name
*/
public static boolean isValidJavaType(String typeName) {
- return typeName.length() > 0
- && Character.isJavaIdentifierStart(typeName.charAt(0))
- && typeName.substring(1).chars().allMatch(
- ch -> Character.isJavaIdentifierPart(ch) || ch == JAVA_PACKAGE_SEPARATOR);
+ if (typeName.length() == 0) {
+ return false;
+ }
+ char last = 0;
+ for (int i = 0; i < typeName.length(); i++) {
+ char c = typeName.charAt(i);
+ if (c == ';' ||
+ c == '[' ||
+ c == '/') {
+ return false;
+ }
+ if (c == '.' && (i == 0 || last == '.')) {
+ return false;
+ }
+ last = c;
+ }
+ return true;
}
/**
diff --git a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
index a90f193..ebb692f 100644
--- a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
+++ b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
@@ -3,8 +3,10 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
+import com.android.tools.r8.ArchiveClassFileProvider;
import java.io.IOException;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -35,9 +37,40 @@
static final String COMMENT = "#";
static final String SEPARATOR = ":";
- public FeatureClassMapping(Path file) throws IOException, FeatureMappingException {
- this(FileUtils.readAllLines(file));
- this.mappingFile = file;
+ public static FeatureClassMapping fromSpecification(Path file)
+ throws FeatureMappingException, IOException {
+ FeatureClassMapping mapping = new FeatureClassMapping();
+ List<String> lines = FileUtils.readAllLines(file);
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ mapping.parseAndAdd(line, i);
+ }
+ return mapping;
+ }
+
+ public static FeatureClassMapping fromJarFiles(List<String> jarFiles)
+ throws FeatureMappingException, IOException {
+ FeatureClassMapping mapping = new FeatureClassMapping();
+ for (String jar : jarFiles) {
+ Path jarPath = Paths.get(jar);
+ String featureName = jarPath.getFileName().toString();
+ if (featureName.endsWith(".jar") || featureName.endsWith(".zip")) {
+ featureName = featureName.substring(0, featureName.length() - 4);
+ }
+
+ ArchiveClassFileProvider provider = new ArchiveClassFileProvider(jarPath);
+ for (String javaDescriptor : provider.getClassDescriptors()) {
+ String javaType = DescriptorUtils.descriptorToJavaType(javaDescriptor);
+ mapping.addMapping(javaType, featureName);
+ }
+ }
+ return mapping;
+ }
+
+ private FeatureClassMapping() {}
+
+ public void addMapping(String clazz, String feature) throws FeatureMappingException {
+ addRule(clazz, feature, 0);
}
FeatureClassMapping(List<String> lines) throws FeatureMappingException {
@@ -79,14 +112,21 @@
error("Mapping lines can only contain one " + SEPARATOR, lineNumber);
}
- if (parsedRules.containsKey(values[0])) {
- if (!parsedRules.get(values[0]).equals(values[1])) {
- error("Redefinition of predicate not allowed", 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(values[0], values[1]);
- FeaturePredicate featurePredicate = new FeaturePredicate(values[0], values[1]);
+ parsedRules.put(predicate, feature);
+ FeaturePredicate featurePredicate = new FeaturePredicate(predicate, feature);
mappings.add(featurePredicate);
}
@@ -103,7 +143,7 @@
/** 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;
// False implies class predicate.
@@ -116,18 +156,10 @@
} else {
this.predicate = predicate;
}
- validateIdentifiers(this.predicate.split("\\."));
- this.feature = feature;
- }
-
- private void validateIdentifiers(String[] names) throws FeatureMappingException {
- // TODO(ricow): knap saa mange regexp.
- Pattern p = Pattern.compile("[A-Za-z_][A-Za-z0-9_$]*");
- for (String name : names) {
- if (!p.matcher(name).matches()) {
- throw new FeatureMappingException(name + " is not a valid identifier");
- }
+ if (!DescriptorUtils.isValidJavaType(this.predicate)) {
+ throw new FeatureMappingException(this.predicate + " is not a valid identifier");
}
+ this.feature = feature;
}
boolean match(String className) {
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
index 2e8938a..750a5c8 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
@@ -23,10 +23,13 @@
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;
@@ -71,9 +74,14 @@
"--feature-splits", splitSpec.toString()
});
- // Both classes should still work if we give all dex files to the system.
+
Path base = outputDex.getParent().resolve("output.base.zip");
Path feature = outputDex.getParent().resolve("output.feature1.zip");
+ 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());
@@ -128,6 +136,60 @@
}
@Test
+ public void splitFilesFromJar() throws IOException, CompilationFailedException {
+ // Initial normal compile to create dex files.
+ Path inputDex = temp.newFolder().toPath().resolve("input.zip");
+ D8.run(
+ D8Command.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))
+ .build());
+
+ Path outputDex = temp.getRoot().toPath().resolve("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();
+ featureStream.close();
+
+ DexSplitter.main(
+ new String[] {
+ "--input",
+ inputDex.toString(),
+ "--output",
+ outputDex.toString(),
+ "--feature-jar",
+ baseJar.toString(),
+ "--feature-jar",
+ featureJar.toString()
+ });
+ Path base = outputDex.getParent().resolve("output.base.zip");
+ Path feature = outputDex.getParent().resolve("output.feature1.zip");
+ validateUnobfuscatedOutput(base, feature);
+ }
+
+ @Test
public void splitFilesObfuscation()
throws CompilationFailedException, IOException, ExecutionException {
// Initial normal compile to create dex files.
diff --git a/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
index ba571fb..3e759d9 100644
--- a/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
+++ b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
@@ -70,9 +70,6 @@
ensureThrowsMappingException("com.google.base");
// Two colons.
ensureThrowsMappingException(ImmutableList.of("a:b:c"));
- // Invalid identifiers.
- ensureThrowsMappingException("12com.google.base:feature1");
- ensureThrowsMappingException("$com.google.base:feature1");
// Empty identifier.
ensureThrowsMappingException("com..google:feature1");