/*
* Copyright 2012-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.android;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.jvm.java.JavaRuntimeLauncher;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.shell.ShellStep;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepExecutionResult;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.TouchStep;
import com.facebook.buck.zip.CustomZipOutputStream;
import com.facebook.buck.zip.ZipOutputStreams;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
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.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.zip.ZipEntry;
public final class ProGuardObfuscateStep extends ShellStep {
enum SdkProguardType {
DEFAULT,
OPTIMIZED,
NONE,
}
private final JavaRuntimeLauncher javaRuntimeLauncher;
private final ProjectFilesystem filesystem;
private final Map<Path, Path> inputAndOutputEntries;
private final Path pathToProGuardCommandLineArgsFile;
private final boolean skipProguard;
private final Optional<Path> proguardJarOverride;
private final String proguardMaxHeapSize;
private final Optional<List<String>> proguardJvmArgs;
private final Optional<String> proguardAgentPath;
/**
* Create steps that write out ProGuard's command line arguments to a text file and then run
* ProGuard using those arguments. We write the arguments to a file to avoid blowing out exec()'s
* ARG_MAX limit.
*
* @param steps Where to append the generated steps.
*/
public static void create(
JavaRuntimeLauncher javaRuntimeLauncher,
ProjectFilesystem filesystem,
Optional<Path> proguardJarOverride,
String proguardMaxHeapSize,
Optional<String> proguardAgentPath,
Path generatedProGuardConfig,
Set<Path> customProguardConfigs,
SdkProguardType sdkProguardConfig,
Optional<Integer> optimizationPasses,
Optional<List<String>> proguardJvmArgs,
Map<Path, Path> inputAndOutputEntries,
Set<Path> additionalLibraryJarsForProguard,
Path proguardDirectory,
BuildableContext buildableContext,
boolean skipProguard,
ImmutableList.Builder<Step> steps) {
steps.addAll(MakeCleanDirectoryStep.of(filesystem, proguardDirectory));
Path pathToProGuardCommandLineArgsFile = proguardDirectory.resolve("command-line.txt");
CommandLineHelperStep commandLineHelperStep =
new CommandLineHelperStep(
filesystem,
generatedProGuardConfig,
customProguardConfigs,
sdkProguardConfig,
optimizationPasses,
inputAndOutputEntries,
additionalLibraryJarsForProguard,
proguardDirectory,
pathToProGuardCommandLineArgsFile);
if (skipProguard) {
steps.add(
commandLineHelperStep, new TouchStep(filesystem, commandLineHelperStep.getMappingTxt()));
} else {
ProGuardObfuscateStep proGuardStep =
new ProGuardObfuscateStep(
javaRuntimeLauncher,
filesystem,
inputAndOutputEntries,
pathToProGuardCommandLineArgsFile,
skipProguard,
proguardJarOverride,
proguardMaxHeapSize,
proguardJvmArgs,
proguardAgentPath);
buildableContext.recordArtifact(commandLineHelperStep.getConfigurationTxt());
buildableContext.recordArtifact(commandLineHelperStep.getMappingTxt());
buildableContext.recordArtifact(commandLineHelperStep.getSeedsTxt());
steps.add(
commandLineHelperStep,
proGuardStep,
// Some proguard configs can propagate the "-dontobfuscate" flag which disables
// obfuscation and prevents the mapping.txt file from being generated. So touch it
// here to guarantee it's around when we go to cache this rule.
new TouchStep(filesystem, commandLineHelperStep.getMappingTxt()));
}
}
/**
* @param inputAndOutputEntries Map of input/output pairs to proguard. The key represents an input
* jar (-injars); the value an output jar (-outjars).
* @param pathToProGuardCommandLineArgsFile Path to file containing arguments to ProGuard.
*/
private ProGuardObfuscateStep(
JavaRuntimeLauncher javaRuntimeLauncher,
ProjectFilesystem filesystem,
Map<Path, Path> inputAndOutputEntries,
Path pathToProGuardCommandLineArgsFile,
boolean skipProguard,
Optional<Path> proguardJarOverride,
String proguardMaxHeapSize,
Optional<List<String>> proguardJvmArgs,
Optional<String> proguardAgentPath) {
super(filesystem.getRootPath());
this.javaRuntimeLauncher = javaRuntimeLauncher;
this.filesystem = filesystem;
this.inputAndOutputEntries = ImmutableMap.copyOf(inputAndOutputEntries);
this.pathToProGuardCommandLineArgsFile = pathToProGuardCommandLineArgsFile;
this.skipProguard = skipProguard;
this.proguardJarOverride = proguardJarOverride;
this.proguardMaxHeapSize = proguardMaxHeapSize;
this.proguardJvmArgs = proguardJvmArgs;
this.proguardAgentPath = proguardAgentPath;
}
@Override
public String getShortName() {
return "proguard_obfuscation";
}
@Override
protected ImmutableList<String> getShellCommandInternal(ExecutionContext context) {
// Run ProGuard as a standalone executable JAR file.
Path proguardJar;
if (proguardJarOverride.isPresent()) {
proguardJar = filesystem.getPathForRelativePath(proguardJarOverride.get());
} else {
AndroidPlatformTarget androidPlatformTarget = context.getAndroidPlatformTarget();
proguardJar = androidPlatformTarget.getProguardJar();
}
ImmutableList.Builder<String> args = ImmutableList.builder();
args.add(javaRuntimeLauncher.getCommand());
if (proguardAgentPath.isPresent()) {
args.add("-agentpath:" + proguardAgentPath.get());
}
if (proguardJvmArgs.isPresent()) {
args.addAll(proguardJvmArgs.get());
}
args.add("-Xmx" + proguardMaxHeapSize)
.add("-jar")
.add(proguardJar.toString())
.add("@" + pathToProGuardCommandLineArgsFile);
return args.build();
}
@Override
public StepExecutionResult execute(ExecutionContext context)
throws IOException, InterruptedException {
StepExecutionResult executionResult = super.execute(context);
// proguard has a peculiar behaviour when multiple -injars/outjars pairs are specified in which
// any -injars that would have been fully stripped away will not produce their matching -outjars
// as requested (so the file won't exist). Our build steps are not sophisticated enough to
// account for this and remove those entries from the classes to dex so we hack things here to
// ensure that the files exist but are empty.
if (executionResult.isSuccess() && !this.skipProguard) {
return StepExecutionResult.of(ensureAllOutputsExist(context));
}
return executionResult;
}
private int ensureAllOutputsExist(ExecutionContext context) {
for (Path outputJar : inputAndOutputEntries.values()) {
if (!Files.exists(outputJar)) {
try {
createEmptyZip(outputJar);
} catch (IOException e) {
context.logError(e, "Error creating empty zip file at: %s.", outputJar);
return 1;
}
}
}
return 0;
}
@VisibleForTesting
static void createEmptyZip(Path file) throws IOException {
Files.createDirectories(file.getParent());
CustomZipOutputStream out = ZipOutputStreams.newOutputStream(file);
// Sun's java 6 runtime doesn't allow us to create a truly empty zip, but this should be enough
// to pass through dx/split-zip without any issue.
// ...and Sun's java 7 runtime doesn't let us use an empty string for the zip entry name.
out.putNextEntry(new ZipEntry("proguard_no_result"));
out.close();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof ProGuardObfuscateStep)) {
return false;
}
ProGuardObfuscateStep that = (ProGuardObfuscateStep) obj;
return Objects.equal(this.inputAndOutputEntries, that.inputAndOutputEntries)
&& Objects.equal(
this.pathToProGuardCommandLineArgsFile, that.pathToProGuardCommandLineArgsFile);
}
@Override
public int hashCode() {
return Objects.hashCode(inputAndOutputEntries, pathToProGuardCommandLineArgsFile);
}
/**
* Helper class to run as a step before ProGuardObfuscateStep to write out the command-line
* parameters to a file. The ProGuardObfuscateStep references this file when it runs using
* ProGuard's '@' syntax. This allows for longer command-lines than would otherwise be supported.
*/
@VisibleForTesting
static class CommandLineHelperStep extends AbstractExecutionStep {
private final ProjectFilesystem filesystem;
private final Path generatedProGuardConfig;
private final Set<Path> customProguardConfigs;
private final Map<Path, Path> inputAndOutputEntries;
private final ImmutableSet<Path> additionalLibraryJarsForProguard;
private final SdkProguardType sdkProguardConfig;
private final Optional<Integer> optimizationPasses;
private final Path proguardDirectory;
private final Path pathToProGuardCommandLineArgsFile;
/**
* @param generatedProGuardConfig Proguard configuration as produced by aapt.
* @param customProguardConfigs Main rule and its dependencies proguard configurations.
* @param sdkProguardConfig Which proguard config from the Android SDK to use.
* @param inputAndOutputEntries Map of input/output pairs to proguard. The key represents an
* input jar (-injars); the value an output jar (-outjars).
* @param additionalLibraryJarsForProguard Libraries that are not operated upon by proguard but
* needed to resolve symbols.
* @param proguardDirectory Output directory for various proguard-generated meta artifacts.
* @param pathToProGuardCommandLineArgsFile Path to file containing arguments to ProGuard.
*/
private CommandLineHelperStep(
ProjectFilesystem filesystem,
Path generatedProGuardConfig,
Set<Path> customProguardConfigs,
SdkProguardType sdkProguardConfig,
Optional<Integer> optimizationPasses,
Map<Path, Path> inputAndOutputEntries,
Set<Path> additionalLibraryJarsForProguard,
Path proguardDirectory,
Path pathToProGuardCommandLineArgsFile) {
super("write_proguard_command_line_parameters");
this.filesystem = filesystem;
this.generatedProGuardConfig = generatedProGuardConfig;
this.customProguardConfigs = ImmutableSet.copyOf(customProguardConfigs);
this.sdkProguardConfig = sdkProguardConfig;
this.optimizationPasses = optimizationPasses;
this.inputAndOutputEntries = ImmutableMap.copyOf(inputAndOutputEntries);
this.additionalLibraryJarsForProguard = ImmutableSet.copyOf(additionalLibraryJarsForProguard);
this.proguardDirectory = proguardDirectory;
this.pathToProGuardCommandLineArgsFile = pathToProGuardCommandLineArgsFile;
}
@Override
public StepExecutionResult execute(ExecutionContext context) {
String proGuardArguments =
Joiner.on('\n').join(getParameters(context, filesystem.getRootPath()));
try {
filesystem.writeContentsToPath(proGuardArguments, pathToProGuardCommandLineArgsFile);
} catch (IOException e) {
context.logError(
e, "Error writing ProGuard arguments to file: %s.", pathToProGuardCommandLineArgsFile);
return StepExecutionResult.ERROR;
}
return StepExecutionResult.SUCCESS;
}
/** @return the list of arguments to pass to ProGuard. */
@VisibleForTesting
ImmutableList<String> getParameters(ExecutionContext context, Path workingDirectory) {
ImmutableList.Builder<String> args = ImmutableList.builder();
AndroidPlatformTarget androidPlatformTarget = context.getAndroidPlatformTarget();
// Relative paths should be interpreted relative to project directory root, not the
// written parameters file.
args.add("-basedirectory").add(workingDirectory.toAbsolutePath().toString());
// -include
switch (sdkProguardConfig) {
case OPTIMIZED:
args.add("-include").add(androidPlatformTarget.getOptimizedProguardConfig().toString());
if (optimizationPasses.isPresent()) {
args.add("-optimizationpasses").add(optimizationPasses.get().toString());
}
break;
case DEFAULT:
args.add("-include").add(androidPlatformTarget.getProguardConfig().toString());
break;
case NONE:
break;
default:
throw new RuntimeException("Illegal value for sdkProguardConfig: " + sdkProguardConfig);
}
for (Path proguardConfig : customProguardConfigs) {
args.add("-include").add(proguardConfig.toString());
}
args.add("-include").add(generatedProGuardConfig.toString());
// -injars and -outjars paired together for each input.
for (Map.Entry<Path, Path> inputOutputEntry : inputAndOutputEntries.entrySet()) {
args.add("-injars").add(inputOutputEntry.getKey().toString());
args.add("-outjars").add(inputOutputEntry.getValue().toString());
}
// -libraryjars
Iterable<Path> bootclasspathPaths = androidPlatformTarget.getBootclasspathEntries();
Iterable<Path> libraryJars =
Iterables.concat(bootclasspathPaths, additionalLibraryJarsForProguard);
Character separator = File.pathSeparatorChar;
args.add("-libraryjars").add(Joiner.on(separator).join(libraryJars));
// -dump
args.add("-printmapping").add(getMappingTxt().toString());
args.add("-printconfiguration").add(getConfigurationTxt().toString());
args.add("-printseeds").add(getSeedsTxt().toString());
return args.build();
}
public Path getConfigurationTxt() {
return proguardDirectory.resolve("configuration.txt");
}
public Path getMappingTxt() {
return proguardDirectory.resolve("mapping.txt");
}
public Path getSeedsTxt() {
return proguardDirectory.resolve("seeds.txt");
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof CommandLineHelperStep)) {
return false;
}
CommandLineHelperStep that = (CommandLineHelperStep) obj;
return Objects.equal(sdkProguardConfig, that.sdkProguardConfig)
&& Objects.equal(additionalLibraryJarsForProguard, that.additionalLibraryJarsForProguard)
&& Objects.equal(customProguardConfigs, that.customProguardConfigs)
&& Objects.equal(generatedProGuardConfig, that.generatedProGuardConfig)
&& Objects.equal(inputAndOutputEntries, that.inputAndOutputEntries)
&& Objects.equal(proguardDirectory, that.proguardDirectory)
&& Objects.equal(
pathToProGuardCommandLineArgsFile, that.pathToProGuardCommandLineArgsFile);
}
@Override
public int hashCode() {
return Objects.hashCode(
sdkProguardConfig,
additionalLibraryJarsForProguard,
customProguardConfigs,
generatedProGuardConfig,
inputAndOutputEntries,
proguardDirectory,
pathToProGuardCommandLineArgsFile);
}
}
}