blob: 4ee416cd4cb7abb1a114ee6fd24231f9b48f6f72 [file] [log] [blame]
// 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("");
}
}