blob: 9f6ac28ea6620bbf05940f46db55ec4228bec0fb [file] [log] [blame]
// Copyright 2016 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.collect.ImmutableList;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.rules.java.JavaInfo;
import com.google.devtools.build.lib.rules.java.JavaPluginInfoProvider;
import com.google.devtools.build.lib.rules.java.JavaTargetAttributes;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.ResourceFileLoader;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Support logic for Bazel's <a
* href="https://developer.android.com/topic/libraries/data-binding/index.html">data binding</a>
* integration.
*
* <p>In short, data binding in Bazel works as follows:
*
* <ol>
* <li>If a rule enables data binding and has layout resources with data binding expressions,
* resource processing invokes the data binding library to preprocess these expressions, then
* strips them out before feeding the resources into aapt. A separate "layout info" XML file
* gets produced that contains the bindings.
* <li>The data binding annotation processor gets activated on Java compilation. This processor
* reads a custom-generated <code>DataBindingInfo.java</code> which specifies the path to the
* layout info file (as an annotation). The processor reads that file and produces the
* corresponding Java classes that end-user code uses to access the resources.
* <li>The data binding compile-time and runtime support libraries get linked into the binary's
* deploy jar.
* </ol>
*
* <p>For data binding to work, the corresponding support libraries must be checked into the depot
* via the implicit dependencies specified inside this class.
*
* <p>Unless otherwise specified, all methods in this class assume the current rule applies data
* binding. Callers can intelligently trigger this logic by checking {@link #isEnabled}.
*/
public final class DataBinding {
/** The rule attribute supplying data binding's annotation processor. */
public static final String DATABINDING_ANNOTATION_PROCESSOR_ATTR =
"$databinding_annotation_processor";
/**
* Annotation processing creates the following metadata files that describe how data binding is
* applied. The full file paths include prefixes as implemented in {@link #getMetadataOutputs}.
*/
private static final ImmutableList<String> METADATA_OUTPUT_SUFFIXES =
ImmutableList.<String>of("setter_store.bin", "layoutinfo.bin", "br.bin");
/** The directory where the annotation processor looks for dep metadata. */
private static final String DEP_METADATA_INPUT_DIR = "dependent-lib-artifacts";
/** The directory where the annotation processor write metadata output for the current rule. */
private static final String METADATA_OUTPUT_DIR = "bin-files";
/**
* Should data binding support be enabled for this rule?
*
* <p>Data binding incurs additional resource processing and compilation work as well as
* additional compile/runtime dependencies. But rules with data binding disabled will fail if data
* binding expressions appear in their layout resources.
*/
public static boolean isEnabled(RuleContext ruleContext) {
return ruleContext.attributes().has("enable_data_binding", Type.BOOLEAN)
&& ruleContext.attributes().get("enable_data_binding", Type.BOOLEAN);
}
/** Returns this rule's data binding base output dir (as an execroot-relative path). */
private static PathFragment getDataBindingExecPath(RuleContext ruleContext) {
return ruleContext
.getBinOrGenfilesDirectory()
.getExecPath()
.getRelative(ruleContext.getUniqueDirectory("databinding"));
}
/** Returns an artifact for the specified output under a standardized data binding base dir. */
private static Artifact getDataBindingArtifact(RuleContext ruleContext, String relativePath) {
PathFragment binRelativeBasePath =
getDataBindingExecPath(ruleContext)
.relativeTo(ruleContext.getBinOrGenfilesDirectory().getExecPath());
return ruleContext.getDerivedArtifact(
binRelativeBasePath.getRelative(relativePath), ruleContext.getBinOrGenfilesDirectory());
}
/**
* Returns the file where data binding's resource processing produces binding xml. For example,
* given:
*
* <pre>{@code
* <layout>
* <data>
* <variable name="foo" type="String" />
* </data>
* </layout>
* <LinearLayout>
* ...
* </LinearLayout>
* }</pre>
*
* <p>data binding strips out and processes this part:
*
* <pre>{@code
* <data>
* <variable name="foo" type="String" />
* </data>
* }</pre>
*
* for each layout file with data binding expressions. Since this may produce multiple files,
* outputs are zipped up into a single container.
*/
static Artifact getLayoutInfoFile(RuleContext ruleContext) {
return getSuffixedInfoFile(ruleContext, "");
}
/** Gets a layout info file with the specified suffix (for use in having different outputs) */
static Artifact getSuffixedInfoFile(RuleContext ruleContext, String suffix) {
return ruleContext.getUniqueDirectoryArtifact(
"databinding", "layout-info" + suffix + ".zip", ruleContext.getBinOrGenfilesDirectory());
}
/**
* Adds data binding's annotation processor as a plugin to the given Java compilation context.
*
* <p>This, in conjunction with {@link #createAnnotationFile} extends the Java compilation to
* translate data binding .xml into corresponding classes.
*/
static void addAnnotationProcessor(
RuleContext ruleContext, JavaTargetAttributes.Builder attributes) {
JavaPluginInfoProvider plugin =
JavaInfo.getProvider(
JavaPluginInfoProvider.class,
ruleContext.getPrerequisite(
DATABINDING_ANNOTATION_PROCESSOR_ATTR, RuleConfiguredTarget.Mode.HOST));
for (String name : plugin.getProcessorClasses()) {
// For header compilation (see JavaHeaderCompileAction):
attributes.addApiGeneratingProcessorName(name);
// For full compilation:
attributes.addProcessorName(name);
}
// For header compilation (see JavaHeaderCompileAction):
attributes.addApiGeneratingProcessorPath(plugin.getProcessorClasspath());
// For full compilation:
attributes.addProcessorPath(plugin.getProcessorClasspath());
attributes.addAdditionalOutputs(getMetadataOutputs(ruleContext));
}
/** The javac flags that are needed to configure data binding's annotation processor. */
static ImmutableList<String> getJavacOpts(RuleContext ruleContext, boolean isBinary) {
ImmutableList.Builder<String> flags = ImmutableList.builder();
String metadataOutputDir = getDataBindingExecPath(ruleContext).getPathString();
// Directory where the annotation processor looks for deps metadata output. The annotation
// processor automatically appends {@link DEP_METADATA_INPUT_DIR} to this path. Individual
// files can be anywhere under this directory, recursively.
flags.add(createProcessorFlag("bindingBuildFolder", metadataOutputDir));
// Directory where the annotation processor should write this rule's metadata output. The
// annotation processor automatically appends {@link METADATA_OUTPUT_DIR} to this path.
flags.add(createProcessorFlag("generationalFileOutDir", metadataOutputDir));
// Path to the Android SDK installation (if available).
flags.add(createProcessorFlag("sdkDir", "/not/used"));
// Whether the current rule is a library or binary.
flags.add(createProcessorFlag("artifactType", isBinary ? "APPLICATION" : "LIBRARY"));
// The path where data binding's resource processor wrote its output (the data binding XML
// expressions). The annotation processor reads this file to translate that XML into Java.
flags.add(createProcessorFlag("xmlOutDir", getDataBindingExecPath(ruleContext).toString()));
// Unused.
flags.add(createProcessorFlag("exportClassListTo", "/tmp/exported_classes"));
// The Java package for the current rule.
flags.add(createProcessorFlag("modulePackage", AndroidCommon.getJavaPackage(ruleContext)));
// The minimum Android SDK compatible with this rule.
flags.add(createProcessorFlag("minApi", "14")); // TODO(gregce): update this
// If enabled, the annotation processor reports detailed output about its activities.
// addProcessorFlag(attributes, "enableDebugLogs", "1");
// If enabled, produces cleaner output for Android Studio.
flags.add(createProcessorFlag("printEncodedErrors", "0"));
// Specifies whether the current rule is a test. Currently unused.
// addDataBindingProcessorFlag(attributes, "isTestVariant", "false");
// Specifies that data binding is only used for test instrumentation. Currently unused.
// addDataBindingProcessorFlag(attributes, "enableForTests", null);
return flags.build();
}
/** Turns a key/value pair into a javac annotation processor flag received by data binding. */
private static String createProcessorFlag(String flag, String value) {
return String.format("-Aandroid.databinding.%s=%s", flag, value);
}
/**
* Creates and returns the generated Java source that data binding's annotation processor reads to
* translate layout info xml (from {@link #getLayoutInfoFile} into the classes that end user code
* consumes.
*
* <p>This mostly just triggers the annotation processor. Annotation processor settings are
* configured separately in {@link #getJavacopts}.
*/
static Artifact createAnnotationFile(RuleContext ruleContext) {
String contents;
try {
contents =
ResourceFileLoader.loadResource(DataBinding.class, "databinding_annotation_template.txt");
} catch (IOException e) {
ruleContext.ruleError("Cannot load annotation processor template: " + e.getMessage());
return null;
}
Artifact output = getDataBindingArtifact(ruleContext, "DataBindingInfo.java");
ruleContext.registerAction(FileWriteAction.create(ruleContext, output, contents, false));
return output;
}
/**
* Adds the appropriate {@link UsesDataBindingProvider} for a rule if it should expose one.
*
* <p>A rule exposes {@link UsesDataBindingProvider} if either it or its deps set {@code
* enable_data_binding = 1}.
*/
public static void maybeAddProvider(
RuleConfiguredTargetBuilder builder, RuleContext ruleContext) {
// Expose the data binding provider if this rule either applies data binding or exports a dep
// that applies it.
List<Artifact> dataBindingMetadataOutputs = new ArrayList<>();
if (DataBinding.isEnabled(ruleContext)) {
dataBindingMetadataOutputs.addAll(getMetadataOutputs(ruleContext));
}
dataBindingMetadataOutputs.addAll(getTransitiveMetadata(ruleContext, "exports"));
if (!AndroidResources.definesAndroidResources(ruleContext.attributes())) {
// If this rule doesn't declare direct resources, no resource processing is run so no data
// binding outputs are produced. In that case, we need to explicitly propagate data binding
// outputs from the deps to make sure they continue up the build graph.
dataBindingMetadataOutputs.addAll(getTransitiveMetadata(ruleContext, "deps"));
}
if (!dataBindingMetadataOutputs.isEmpty()) {
builder.addProvider(
UsesDataBindingProvider.class, new UsesDataBindingProvider(dataBindingMetadataOutputs));
}
}
/** Returns the data binding resource processing output from deps under the given attribute. */
private static List<Artifact> getTransitiveMetadata(RuleContext ruleContext, String attr) {
ImmutableList.Builder<Artifact> dataBindingMetadataOutputs = ImmutableList.builder();
if (ruleContext.attributes().has(attr, BuildType.LABEL_LIST)) {
for (UsesDataBindingProvider provider :
ruleContext.getPrerequisites(
attr, RuleConfiguredTarget.Mode.TARGET, UsesDataBindingProvider.class)) {
dataBindingMetadataOutputs.addAll(provider.getMetadataOutputs());
}
}
return dataBindingMetadataOutputs.build();
}
/**
* Returns metadata outputs from this rule's annotation processing that describe what it did with
* data binding. This is used by parent rules to ensure consistent binding patterns.
*
* <p>>For example, if {@code foo.AndroidBinary} depends on {@code foo.lib.AndroidLibrary} and the
* library defines data binding expression {@code Bar}, compiling the library produces Java class
* {@code foo.lib.Bar}. But since the binary applies data binding over the merged resources of its
* deps, that means the binary also sees {@code Bar}, so it compiles it into {@code foo.Bar}. This
* would be a class redefinition conflict. But by feeding the library's metadata outputs into the
* binary's compilation, enough information is available to only use the first version.
*/
private static List<Artifact> getMetadataOutputs(RuleContext ruleContext) {
if (!AndroidResources.definesAndroidResources(ruleContext.attributes())) {
// If this rule doesn't define local resources, no resource processing was done, so it
// doesn't produce data binding output.
return ImmutableList.<Artifact>of();
}
ImmutableList.Builder<Artifact> outputs = ImmutableList.<Artifact>builder();
String javaPackage = AndroidCommon.getJavaPackage(ruleContext);
for (String suffix : METADATA_OUTPUT_SUFFIXES) {
// The annotation processor automatically creates files with this naming pattern under the
// {@code -Aandroid.databinding.generationalFileOutDir} base directory.
outputs.add(
getDataBindingArtifact(
ruleContext,
String.format("%s/%s-%s-%s", METADATA_OUTPUT_DIR, javaPackage, javaPackage, suffix)));
}
return outputs.build();
}
/**
* Processes deps that also apply data binding.
*
* @param ruleContext the current rule
* @return the deps' metadata outputs. These need to be staged as compilation inputs to the
* current rule.
*/
static ImmutableList<Artifact> processDeps(RuleContext ruleContext) {
ImmutableList.Builder<Artifact> dataBindingJavaInputs = ImmutableList.<Artifact>builder();
if (AndroidResources.definesAndroidResources(ruleContext.attributes())) {
dataBindingJavaInputs.add(DataBinding.getLayoutInfoFile(ruleContext));
}
for (Artifact dataBindingDepMetadata : getTransitiveMetadata(ruleContext, "deps")) {
dataBindingJavaInputs.add(
symlinkDepsMetadataIntoOutputTree(ruleContext, dataBindingDepMetadata));
}
return dataBindingJavaInputs.build();
}
/**
* Data binding's annotation processor reads the transitive metadata outputs of the target's deps
* (see {@link #getMetadataOutputs(RuleContext)}) in the directory specified by the processor flag
* {@code -Aandroid.databinding.bindingBuildFolder}. Since dependencies don't generate their
* outputs under a common directory, we symlink them into a common place here.
*
* @return the symlink paths of the transitive dep metadata outputs for this rule
*/
private static Artifact symlinkDepsMetadataIntoOutputTree(
RuleContext ruleContext, Artifact depMetadata) {
Label ruleLabel = ruleContext.getRule().getLabel();
Artifact symlink =
getDataBindingArtifact(
ruleContext,
String.format(
"%s/%s", DEP_METADATA_INPUT_DIR, depMetadata.getRootRelativePathString()));
ruleContext.registerAction(
new SymlinkAction(
ruleContext.getActionOwner(),
depMetadata,
symlink,
String.format(
"Symlinking dep metadata output %s for %s", depMetadata.getFilename(), ruleLabel)));
return symlink;
}
}