// Copyright 2015 The Bazel Authors. All rights reserved. // // 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.google.devtools.build.lib.rules.android; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.FileProvider; import com.google.devtools.build.lib.analysis.FilesToRunProvider; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.rules.android.AndroidRuleClasses.MultidexMode; import com.google.devtools.build.lib.rules.java.JavaSemantics; import com.google.devtools.build.lib.util.FileType; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; /** * Builds Jack actions for a Java or Android target. * *
Jack is the new Android toolchain which integrates proguard-compatible code minification
* et al. and has an intermediate library format to front-load the dexing work.
*
* @see Jack documentation
* @see JackLibraryProvider
*/
public final class JackCompilationHelper {
private static final String PARTIAL_JACK_DIRECTORY = "_jill";
private static final String JACK_DIRECTORY = "_jack";
/** Filetype for the intermediate library created by Jack. */
public static final FileType JACK_LIBRARY_TYPE = FileType.of(".jack");
/** Flag to indicate that the next argument is a Jack property. */
static final String JACK_PROPERTY = "-D";
/** Flag to indicate that resource conflicts should be resolved by taking the first element. */
static final String PROPERTY_KEEP_FIRST_RESOURCE = "jack.import.resource.policy=keep-first";
/** Flag to indicate that type conflicts should be resolved by taking the first element. */
static final String PROPERTY_KEEP_FIRST_TYPE = "jack.import.type.policy=keep-first";
/** Flag to turn on/off sanity checks in Jack. */
static final String SANITY_CHECKS = "--sanity-checks";
/** Value of the sanity checks flag which disables sanity checks. */
static final String SANITY_CHECKS_OFF = "off";
/** Value of the sanity checks flag which enables sanity checks. */
static final String SANITY_CHECKS_ON = "on";
/** Flag to indicate the classpath of Jack libraries, separated by semicolons. */
static final String CLASSPATH = "-cp";
/** Flag to load a Jack library into the Jack compiler so it will be part of the output. */
static final String IMPORT_JACK_LIBRARY = "--import";
/** Flag to import a zip file of Java sources into a Jack library. */
static final String IMPORT_SOURCE_ZIP = "--import-source-zip";
/** Flag to import a single file into a Jack library's resources. */
static final String IMPORT_RESOURCE_FILE = "--import-resource-file";
/** Flag to add a zip file full of resources to the Jack library. */
static final String IMPORT_RESOURCE_ZIP = "--import-resource-zip";
/** Flag to add the names of annotation processors. */
static final String PROCESSOR_NAMES = "--processor";
/** Flag to add the classpath of annotation processors. */
static final String PROCESSOR_CLASSPATH = "--processorpath";
/** Flag to include a Proguard configuration. */
static final String CONFIG_PROGUARD = "--config-proguard";
/** Flag to set the multidex mode when compiling to dex with Jack. */
static final String MULTI_DEX = "--multi-dex";
/** Flag to specify the path to the main dex list in manual main dex mode. */
static final String MAIN_DEX_LIST = "--main-dex-list";
/** Flag indicating the filename Jill should output the converted jar to. */
static final String JILL_OUTPUT = "--output";
/** Flag to output a jack library. */
static final String OUTPUT_JACK = "--output-jack";
/** Flag to output a zip file containing dex files and resources for packaging. */
static final String OUTPUT_DEX_ZIP = "--output-dex-zip";
/** Name of the zip file containing the dex files and java resources for packaging. */
static final String ZIP_OUTPUT_FILENAME = "classes.dex.zip";
/** Rule context used to build and register actions. */
private final RuleContext ruleContext;
/** True to use Jack's internal sanity checks, trading speed for crash-on-bugs. */
private final boolean useSanityChecks;
/** Binary used to extract resources from a jar file. */
private final FilesToRunProvider resourceExtractorBinary;
/** Binary used to build Jack libraries and dex files. */
private final FilesToRunProvider jackBinary;
/** Binary used to convert jars to Jack libraries. */
private final FilesToRunProvider jillBinary;
/** Jack library containing Android base classes. This will be placed first on the classpath. */
private final Artifact androidBaseLibraryForJack;
/** The destination for the Jack artifact to be created. */
private final Artifact outputArtifact;
/** Java files for the rule's Jack library. */
private final ImmutableSet This method should only be called once, as it will generate the same artifact each time.
* It will fail if called a second time.
*
* @param multidexMode The multidex flag to send to Jack.
* @param manualMainDexList Iff multidexMode is MANUAL_MAIN_DEX, an artifact representing the file
* with the list of class filenames which should go in the main dex. Else, absent.
* @param proguardSpecs A collection of Proguard configuration files to be used to process the
* Jack libraries before building a dex out of them.
* @returns A zip file containing the dex file(s) and Java resource(s) generated by Jack.
*/
// TODO(bazel-team): this method (compile to jack library, compile all transitive jacks to dex)
// may be too much overhead for manydex (dex per library) mode.
// Instead, consider running jack --output-dex right on the source files, bypassing the
// intermediate jack library format.
public Artifact compileAsDex(
MultidexMode multidexMode,
Optional Partial Jack format does not contain resources or pre-dex files.
*
* @see #postprocessPartialJackAndAddResources(Artifact,Artifact)
*/
private Artifact convertJarToPartialJack(Artifact jar) {
Artifact result = ruleContext.getUniqueDirectoryArtifact(
PARTIAL_JACK_DIRECTORY,
FileSystemUtils.replaceExtension(jar.getRootRelativePath(), ".jack"),
ruleContext.getBinOrGenfilesDirectory());
ruleContext.registerAction(
new SpawnAction.Builder()
.setExecutable(jillBinary)
.addArgument(JILL_OUTPUT)
.addOutputArgument(result)
.addInputArgument(jar)
.setProgressMessage(
"Converting " + jar.getExecPath().getBaseName() + " to Jack library with Jill")
.setMnemonic("AndroidJill")
.build(ruleContext));
return result;
}
/**
* Generates an action which creates a zip file from the contents of the input jar, filtering out
* non-resource files and returning a zip file containing only resources.
*/
private Artifact extractResourcesFromJar(Artifact jar) {
Artifact result = ruleContext.getUniqueDirectoryArtifact(
PARTIAL_JACK_DIRECTORY,
FileSystemUtils.replaceExtension(jar.getRootRelativePath(), "-resources.zip"),
ruleContext.getBinOrGenfilesDirectory());
ruleContext.registerAction(
new SpawnAction.Builder()
.setExecutable(resourceExtractorBinary)
.addInputArgument(jar)
.addOutputArgument(result)
.setProgressMessage("Extracting resources from " + jar.getExecPath().getBaseName())
.setMnemonic("AndroidJillResources")
.build(ruleContext));
return result;
}
/**
* Generates an action to finish processing a partial Jack library generated by
* {@link #convertJarToPartialJack(Artifact)} and add resources from
* {@link #extractResourcesFromJar(Artifact)}, then returns the final library.
*/
private Artifact postprocessPartialJackAndAddResources(
Artifact partialJackLibrary, Artifact resources) {
Artifact result = ruleContext.getUniqueDirectoryArtifact(
JACK_DIRECTORY,
partialJackLibrary.getRootRelativePath().relativeTo(
ruleContext.getUniqueDirectory(PARTIAL_JACK_DIRECTORY)),
ruleContext.getBinOrGenfilesDirectory());
CustomCommandLine.Builder builder =
CustomCommandLine.builder()
// Have jack double-check its behavior and crash rather than producing invalid output
.add(SANITY_CHECKS)
.add(useSanityChecks ? SANITY_CHECKS_ON : SANITY_CHECKS_OFF)
.addExecPath(IMPORT_JACK_LIBRARY, partialJackLibrary)
.addExecPath(IMPORT_RESOURCE_ZIP, resources)
.addExecPath(OUTPUT_JACK, result);
ruleContext.registerAction(
new SpawnAction.Builder()
.setExecutable(jackBinary)
.addInput(partialJackLibrary)
.addInput(resources)
.addOutput(result)
.setCommandLine(builder.build())
.setProgressMessage(
"Processing " + partialJackLibrary.getExecPath().getBaseName() + " as Jack library")
.setMnemonic("AndroidJillPostprocess")
.build(ruleContext));
return result;
}
/**
* Creates an action to build an empty jack library given by outputArtifact.
*/
private void buildEmptyJackAction() {
ruleContext.registerAction(
new SpawnAction.Builder()
.setExecutable(jackBinary)
.addArgument(OUTPUT_JACK)
.addOutputArgument(outputArtifact)
.setProgressMessage("Compiling " + ruleContext.getLabel() + " as Jack library")
.setMnemonic("AndroidJackLibraryNull")
.build(ruleContext));
}
/**
* Creates an action to compile the given sources as a jack library given by outputArtifact.
*
* @param javaSources Iterable of .java files to compile using jack.
* @param sourceJars Iterable of .srcjar files to unpack and compile using jack.
* @param resources Mapping from library paths to resource files to be imported into the jack
* library.
* @param classpathJackLibraries Libraries used for compilation.
* @returns An artifact representing the combined jack library, or null if none was created.
*/
private void buildJackAction(
Iterable The compilation tools will be loaded automatically from the appropriate attributes:
* Jack as $jack, Jill as $jill, and the resource extractor as $resource_extractor.
*
* Jack's sanity checks will be enabled or disabled according to the AndroidConfiguration
* accessed through this context.
*/
public JackCompilationHelper.Builder setRuleContext(RuleContext ruleContext) {
this.ruleContext = Preconditions.checkNotNull(ruleContext);
return this;
}
/**
* Sets the artifact the final Jack library should be output to.
*
* The artifact specified will always be generated, although it may be empty if there are no
* sources.
*/
public JackCompilationHelper.Builder setOutputArtifact(Artifact outputArtifact) {
this.outputArtifact = Preconditions.checkNotNull(outputArtifact);
return this;
}
/**
* Sets the tools bundle containing Jack, Jill, the resource extractor, and the Android base
* library in Jack format.
*/
public JackCompilationHelper.Builder setAndroidSdk(AndroidSdkProvider androidSdk) {
this.androidSdk = Preconditions.checkNotNull(androidSdk);
return this;
}
/**
* Adds a collection of Java source files to be compiled by Jack.
*/
public JackCompilationHelper.Builder addJavaSources(Collection The Jack libraries created from these jar files will be used both as
* dependencies on the classpath of this rule and exports to the classpath of depending rules,
* as with jar files in the sources of a Java rule.
* They will also be available to the compilation of the final dex file(s).
* It has an identical effect as if these jars were specified in both deps and exports.
*/
public JackCompilationHelper.Builder addCompiledJars(Collection These dependencies will be considered direct dependencies of this rule,
* and indirect dependencies of any rules depending on this one.
* They will also be available to the compilation of the final dex file(s).
*/
public JackCompilationHelper.Builder addDeps(
Iterable extends TransitiveInfoCollection> deps) {
return addClasspathDeps(deps).addRuntimeDeps(deps);
}
/**
* Adds a set of dependencies which will be exported to Jack rules which depend on this one.
*
* These dependencies will be considered direct dependencies of rules depending on this one,
* but not of this rule itself, in line with Java rule semantics.
* They will also be available to the compilation of the final dex file(s).
*/
public JackCompilationHelper.Builder addExports(
Iterable extends TransitiveInfoCollection> exports) {
return addExportedDeps(exports).addRuntimeDeps(exports);
}
/**
* Adds a set of dependencies which will be used in dexing.
*
* Unless {@link #addClasspathDeps} or {@link #addExportedDeps} are also called,
* runtimeDeps will not be provided on the classpath of this rule or rules which depend on it.
*/
public JackCompilationHelper.Builder addRuntimeDeps(
Iterable extends TransitiveInfoCollection> runtimeDeps) {
for (TransitiveInfoCollection dep : Preconditions.checkNotNull(runtimeDeps)) {
JackLibraryProvider jackLibraryProvider = dep.getProvider(JackLibraryProvider.class);
if (jackLibraryProvider != null) {
dexJacks.addTransitive(jackLibraryProvider.getTransitiveJackLibrariesToLink());
} else {
NestedSet Unless {@link #addRuntimeDeps} is also called, classpathDeps will only be used for
* compilation of this rule and will not be built into the final dex.
*
* @see #addDeps
*/
public JackCompilationHelper.Builder addClasspathDeps(
Iterable extends TransitiveInfoCollection> classpathDeps) {
return addDependenciesInternal(
Preconditions.checkNotNull(classpathDeps),
classpathNonLibraryFiles,
classpathJackLibraries);
}
/**
* Adds a set of dependencies to be placed on the classpath of rules depending on this rule.
*
* Unless {@link #addRuntimeDeps} is also called, exportedDeps will only be used for
* compilation of depending rules and will not be built into the final dex.
*
* @see #addExports
*/
public JackCompilationHelper.Builder addExportedDeps(
Iterable extends TransitiveInfoCollection> exportedDeps) {
return addDependenciesInternal(
Preconditions.checkNotNull(exportedDeps), exportedNonLibraryFiles, exportedJackLibraries);
}
/**
* Adds all libraries from deps to nonLibraryFiles and jackLibs based on their type.
*
* Those exporting JackLibraryProvider have their jackClasspathLibraries added to jackLibs.
* Others will have any jars or jacks in their filesToBuild added to nonLibraryFiles.
*
* {@link #addRuntimeDeps} should also be called on deps for dependencies which will be built
* into the final dex file(s).
*/
private JackCompilationHelper.Builder addDependenciesInternal(
Iterable extends TransitiveInfoCollection> deps,
Collection It's not recommended to call build() more than once, as the resulting
* JackCompilationHelpers will attempt to generate the same actions.
*/
public JackCompilationHelper build() {
Preconditions.checkNotNull(ruleContext);
Preconditions.checkNotNull(androidSdk);
boolean useSanityChecks =
ruleContext
.getFragment(AndroidConfiguration.class)
.isJackSanityChecked();
FilesToRunProvider jackBinary = androidSdk.getJack();
FilesToRunProvider jillBinary = androidSdk.getJill();
FilesToRunProvider resourceExtractorBinary = androidSdk.getResourceExtractor();
Artifact androidBaseLibraryForJack = androidSdk.getAndroidJack();
return new JackCompilationHelper(
ruleContext,
useSanityChecks,
Preconditions.checkNotNull(resourceExtractorBinary),
Preconditions.checkNotNull(jackBinary),
Preconditions.checkNotNull(jillBinary),
Preconditions.checkNotNull(androidBaseLibraryForJack),
Preconditions.checkNotNull(outputArtifact),
ImmutableSet.copyOf(javaSources),
ImmutableSet.copyOf(sourceJars),
ImmutableMap.copyOf(resources),
processorClasspathJars.build(),
ImmutableSet.copyOf(processorNames),
exportedJackLibraries.build(),
ImmutableSet.copyOf(exportedNonLibraryFiles),
classpathJackLibraries.build(),
ImmutableSet.copyOf(classpathNonLibraryFiles),
dexJacks.build(),
ImmutableSet.copyOf(dexJars));
}
}
}