blob: aea3e82d3bc797c4eef0c7c27c70458177dcbd1d [file] [log] [blame]
// Copyright (c) 2020, 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.relocator;
import com.android.tools.r8.BaseCompilerCommandParser;
import com.android.tools.r8.ClassFileConsumer;
import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.Diagnostic;
import com.android.tools.r8.DiagnosticsHandler;
import com.android.tools.r8.Keep;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.references.PackageReference;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardPathList;
import com.android.tools.r8.utils.AbortException;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.Box;
import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.ExceptionUtils;
import com.android.tools.r8.utils.FlagFile;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
@Keep
public class RelocatorCommand {
private static final String THREAD_COUNT_FLAG = "--thread-count";
private static final Set<String> OPTIONS_WITH_PARAMETER =
ImmutableSet.of("--output", "--input", "--map", THREAD_COUNT_FLAG);
static final String USAGE_MESSAGE =
String.join(
"\n",
Iterables.concat(
Arrays.asList(
"The Relocator CLI is EXPERIMENTAL and is subject to change",
"Usage: relocator [options]",
" where options are:",
" --input <file> # Input file to remap, class, zip or jar.",
" --output <file> # Output result in <outfile>.",
" --map <from->to> # Registers a mapping.",
" --thread-count <number> # A specified number of threads to run with.",
" --version # Print the version of d8.",
" --help # Print this message.")));
private final boolean printHelp;
private final boolean printVersion;
private final Reporter reporter;
private final DexItemFactory factory;
private final ClassFileConsumer consumer;
private final AndroidApp app;
private final ImmutableMap<PackageReference, PackageReference> mapping;
private final int threadCount;
private RelocatorCommand(boolean printHelp, boolean printVersion) {
this.printHelp = printHelp;
this.printVersion = printVersion;
reporter = null;
factory = null;
consumer = null;
app = null;
mapping = null;
threadCount = ThreadUtils.NOT_SPECIFIED;
}
private RelocatorCommand(
ImmutableMap<PackageReference, PackageReference> mapping,
AndroidApp app,
Reporter reporter,
DexItemFactory factory,
ClassFileConsumer consumer,
int threadCount) {
this.printHelp = false;
this.printVersion = false;
this.mapping = mapping;
this.app = app;
this.reporter = reporter;
this.factory = factory;
this.consumer = consumer;
this.threadCount = threadCount;
}
public static Builder builder() {
return new Builder();
}
public static Builder parse(String[] args, Origin origin) {
return Builder.parse(args, origin);
}
public static Builder builder(DiagnosticsHandler diagnosticsHandler) {
return new Builder(diagnosticsHandler);
}
public Reporter getReporter() {
return reporter;
}
public DexItemFactory getFactory() {
return factory;
}
public ClassFileConsumer getConsumer() {
return consumer;
}
public int getThreadCount() {
return threadCount;
}
public AndroidApp getApp() {
return app;
}
public boolean isPrintHelp() {
return printHelp;
}
public boolean isPrintVersion() {
return printVersion;
}
public InternalOptions getInternalOptions() {
// We are using the proguard configuration for adapting resources.
InternalOptions options =
new InternalOptions(
ProguardConfiguration.builder(factory, getReporter())
.addKeepAttributePatterns(ImmutableList.of("*"))
.addAdaptResourceFilenames(ProguardPathList.builder().addFileName("**").build())
.build(),
getReporter());
options.relocatorCompilation = true;
options.threadCount = getThreadCount();
options.programConsumer = consumer;
assert consumer != null;
options.dataResourceConsumer = consumer.getDataResourceConsumer();
// Set debug to ensure that we are writing all information to the application writer.
options.debug = true;
return options;
}
public Map<PackageReference, PackageReference> getMapping() {
return mapping;
}
@Keep
public static class Builder {
private final AndroidApp.Builder app;
private final Reporter reporter;
private final ImmutableMap.Builder<PackageReference, PackageReference> mapping =
ImmutableMap.builder();
private ClassFileConsumer consumer = null;
private int threadCount = ThreadUtils.NOT_SPECIFIED;
private boolean printVersion;
private boolean printHelp;
Builder() {
this(AndroidApp.builder());
}
Builder(DiagnosticsHandler handler) {
this(AndroidApp.builder(new Reporter(handler)));
}
Builder(AndroidApp.Builder builder) {
this.app = builder;
this.reporter = builder.getReporter();
}
/**
* Setting output to a path.
*
* <p>Setting the output path will override any previous set consumer or any previous set output
* path.
*
* @param outputPath Output path to write output to. A null argument will clear the program
* consumer / output.
*/
public Builder setOutputPath(Path outputPath) {
if (outputPath == null) {
this.consumer = null;
return this;
}
this.consumer = new ArchiveConsumer(outputPath, true);
return this;
}
public Builder setPrintHelp(boolean printHelp) {
this.printHelp = printHelp;
return this;
}
public Builder setPrintVersion(boolean printVersion) {
this.printVersion = printVersion;
return this;
}
/** Signal an error. */
public void error(Diagnostic diagnostic) {
reporter.error(diagnostic);
}
/** Set the number of threads to use for the compilation */
public Builder setThreadCount(int threadCount) {
if (threadCount <= 0) {
reporter.error("Invalid threadCount: " + threadCount);
} else {
this.threadCount = threadCount;
}
return this;
}
/** Add program file resources. */
public Builder addProgramFiles(Path... files) {
return addProgramFiles(Arrays.asList(files));
}
/** Add program file resources. */
public Builder addProgramFiles(Collection<Path> files) {
guard(
() -> {
for (Path path : files) {
try {
app.addProgramFile(path);
} catch (CompilationError e) {
error(new PathOrigin(path), e);
}
}
});
return this;
}
/** Add program file resource. */
public Builder addProgramFile(Path file) {
guard(
() -> {
try {
app.addProgramFile(file);
} catch (CompilationError e) {
error(new PathOrigin(file), e);
}
});
return this;
}
public Builder addPackageMapping(PackageReference source, PackageReference destination) {
mapping.put(source, destination);
return this;
}
/**
* Set the program consumer.
*
* <p>Setting the program consumer will override any previous set consumer or any previous set
* output path.
*
* @param consumer ClassFile consumer to set as current. A null argument will clear the program
* consumer / output.
*/
public Builder setConsumer(ClassFileConsumer consumer) {
// Setting an explicit program consumer resets any output-path/mode setup.
this.consumer = consumer;
return this;
}
private void validate() {
if (consumer == null) {
reporter.error(new StringDiagnostic("No output path or consumer has been specified"));
}
}
public RelocatorCommand build() throws CompilationFailedException {
Box<RelocatorCommand> result = new Box<>();
ExceptionUtils.withCompilationHandler(
reporter,
() -> {
if (printHelp || printVersion) {
result.set(new RelocatorCommand(printHelp, printVersion));
return;
}
reporter.failIfPendingErrors();
validate();
reporter.failIfPendingErrors();
DexItemFactory factory = new DexItemFactory();
result.set(
new RelocatorCommand(
mapping.build(), app.build(), reporter, factory, consumer, threadCount));
});
return result.get();
}
// Helper to signify an error.
void error(Origin origin, Throwable throwable) {
reporter.error(new ExceptionDiagnostic(throwable, origin));
}
// Helper to guard and handle exceptions.
void guard(Runnable action) {
try {
action.run();
} catch (CompilationError e) {
reporter.error(e.toStringDiagnostic());
} catch (AbortException e) {
// Error was reported and exception will be thrown by build.
}
}
/**
* Parse the Relocator command-line.
*
* <p>Parsing will set the supplied options or their default value if they have any.
*
* @param args Command-line arguments array.
* @param origin Origin description of the command-line arguments.
* @return Relocator command builder with state set up according to parsed command line.
*/
public static Builder parse(String[] args, Origin origin) {
return parse(args, origin, RelocatorCommand.builder());
}
/**
* Parse the Relocator command-line.
*
* <p>Parsing will set the supplied options or their default value if they have any.
*
* @param args Command-line arguments array.
* @param origin Origin description of the command-line arguments.
* @param handler Custom defined diagnostics handler.
* @return Relocator command builder with state set up according to parsed command line.
*/
public static Builder parse(String[] args, Origin origin, DiagnosticsHandler handler) {
return parse(args, origin, RelocatorCommand.builder(handler));
}
private static Builder parse(String[] args, Origin origin, Builder builder) {
String[] expandedArgs = FlagFile.expandFlagFiles(args, builder::error);
Path outputPath = null;
for (int i = 0; i < expandedArgs.length; i++) {
String arg = expandedArgs[i].trim();
String nextArg = null;
if (OPTIONS_WITH_PARAMETER.contains(arg)) {
if (++i < expandedArgs.length) {
nextArg = expandedArgs[i];
} else {
builder.error(
new StringDiagnostic("Missing parameter for " + expandedArgs[i - 1] + ".", origin));
break;
}
}
if (arg.length() == 0) {
continue;
}
switch (arg) {
case "--help":
builder.setPrintHelp(true);
break;
case "--version":
builder.setPrintVersion(true);
break;
case "--output":
assert nextArg != null;
if (outputPath != null) {
builder.error(
new StringDiagnostic(
"Cannot output both to '" + outputPath.toString() + "' and '" + nextArg + "'",
origin));
continue;
}
outputPath = Paths.get(nextArg);
break;
case "--input":
assert nextArg != null;
builder.addProgramFile(Paths.get(nextArg));
break;
case THREAD_COUNT_FLAG:
BaseCompilerCommandParser.parsePositiveIntArgument(
builder::error, arg, nextArg, origin, builder::setThreadCount);
break;
case "--map":
assert nextArg != null;
int separator = nextArg.indexOf("->");
if (separator < 0) {
builder.error(
new StringDiagnostic("--map " + nextArg + " is not on the form from->to"));
continue;
}
// TODO(b/155047633): Handle invalid package names.
builder.addPackageMapping(
Reference.packageFromString(nextArg.substring(0, separator)),
Reference.packageFromString(nextArg.substring(separator + 2)));
break;
default:
builder.error(new StringDiagnostic("Unknown argument: " + arg, origin));
}
}
if (outputPath == null) {
outputPath = Paths.get(".");
}
builder.setOutputPath(outputPath);
return builder;
}
}
}