blob: f602ed24da26828b10fd0a80d488484e1ceaeb91 [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.databinding;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.rules.android.AndroidCommon;
import com.google.devtools.build.lib.rules.android.AndroidConfiguration;
import com.google.devtools.build.lib.rules.android.AndroidDataContext;
import com.google.devtools.build.lib.rules.android.AndroidResources;
import com.google.devtools.build.lib.util.ResourceFileLoader;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.List;
import javax.annotation.Nullable;
/**
* 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.
*/
public final class DataBinding {
/** The rule attribute supplying data binding's annotation processor. */
public static final String DATABINDING_ANNOTATION_PROCESSOR_ATTR =
"$databinding_annotation_processor";
/** The rule attribute supplying data binding's build helper (exec). */
public static final String DATABINDING_EXEC_PROCESSOR_ATTR = "$databinding_exec";
private static final String ENABLE_DATA_BINDING_ATTR = "enable_data_binding";
/** The directory where the annotation processor looks for dep metadata. */
public static final String DEP_METADATA_INPUT_DIR = "dependent-lib-artifacts";
/** The directory where the annotation processor writes metadata output for the current rule. */
public static final String METADATA_OUTPUT_DIR = "bin-files";
@VisibleForTesting
public static final DataBindingContext DISABLED_V1_CONTEXT = new DisabledDataBindingV1Context();
private static final DataBindingContext DISABLED_V2_CONTEXT = new DisabledDataBindingV2Context();
/** Supplies a databinding context from a rulecontext. */
public static DataBindingContext contextFrom(
RuleContext ruleContext, AndroidConfiguration androidConfig) {
return contextFrom(isEnabled(ruleContext), ruleContext, androidConfig);
}
/** Supplies a databinding context from an action context. */
public static DataBindingContext contextFrom(
boolean enabled, ActionConstructionContext context, AndroidConfiguration androidConfig) {
if (enabled) {
if (androidConfig.useDataBindingV2()) {
return new DataBindingV2Context(
context,
androidConfig.useDataBindingUpdatedArgs(),
androidConfig.useDataBindingAndroidX());
} else {
return new DataBindingV1Context(context, androidConfig.useDataBindingUpdatedArgs());
}
} else {
if (androidConfig.useDataBindingV2()) {
return DISABLED_V2_CONTEXT;
} else {
return DISABLED_V1_CONTEXT;
}
}
}
/**
* 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.
*/
private static boolean isEnabled(RuleContext ruleContext) {
return ruleContext.attributes().has(ENABLE_DATA_BINDING_ATTR, Type.BOOLEAN)
&& Boolean.TRUE.equals(
ruleContext.attributes().get(ENABLE_DATA_BINDING_ATTR, Type.BOOLEAN));
}
/** Supplies a disabled (no-op) DataBindingContext. */
public static DataBindingContext getDisabledDataBindingContext(AndroidDataContext ctx) {
if (ctx.useDataBindingV2()) {
return DISABLED_V2_CONTEXT;
} else {
return DISABLED_V1_CONTEXT;
}
}
/** Returns this rule's data binding base output dir (as an execroot-relative path). */
static PathFragment getDataBindingExecPath(RuleContext ruleContext) {
return ruleContext
.getBinOrGenfilesDirectory()
.getExecPath()
.getRelative(ruleContext.getUniqueDirectory("databinding"));
}
@VisibleForTesting
public static Artifact getLayoutInfoFile(ActionConstructionContext actionConstructionContext) {
return actionConstructionContext.getUniqueDirectoryArtifact("databinding", "layout-info.zip");
}
/** Returns an artifact for the specified output under a standardized data binding base dir. */
static Artifact getDataBindingArtifact(RuleContext ruleContext, String relativePath) {
PathFragment binRelativeBasePath =
getDataBindingExecPath(ruleContext)
.relativeTo(ruleContext.getBinOrGenfilesDirectory().getExecPath());
return ruleContext.getDerivedArtifact(
binRelativeBasePath.getRelative(relativePath), ruleContext.getBinOrGenfilesDirectory());
}
static ImmutableList<Artifact> getAnnotationFile(RuleContext ruleContext, boolean useAndroidX) {
// Add this rule's annotation processor input. If the rule doesn't have direct resources,
// there's no direct data binding info, so there's strictly no need for annotation processing.
// But it's still important to process the deps' .bin files so any Java class references get
// re-referenced so they don't get filtered out of the compilation classpath by JavaBuilder
// (which filters out classpath .jars that "aren't used": see --reduce_classpath). If data
// binding didn't reprocess a library's data binding expressions redundantly up the dependency
// chain (meaning each depender processes them again as if they were its own), this problem
// wouldn't happen.
try {
String contents =
ResourceFileLoader.loadResource(
DataBinding.class,
useAndroidX
? "databinding_annotation_template_androidx.txt"
: "databinding_annotation_template_support_lib.txt");
Artifact annotationFile = getDataBindingArtifact(ruleContext, "DataBindingInfo.java");
ruleContext.registerAction(
FileWriteAction.create(ruleContext, annotationFile, contents, false));
return ImmutableList.of(annotationFile);
} catch (IOException e) {
ruleContext.ruleError("Cannot load annotation processor template: " + e.getMessage());
return ImmutableList.of();
}
}
/** Returns the data binding resource processing output from deps under the given attribute. */
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, UsesDataBindingProvider.PROVIDER)) {
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.
*/
static ImmutableList<Artifact> getMetadataOutputs(
RuleContext ruleContext, boolean useUpdatedArgs, List<String> metadataOutputSuffixes) {
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.of();
}
ImmutableList.Builder<Artifact> outputs = ImmutableList.<Artifact>builder();
String javaPackage = AndroidCommon.getJavaPackage(ruleContext);
for (String suffix : metadataOutputSuffixes) {
// The annotation processor automatically creates files with this naming pattern under the
// {@code -Aandroid.databinding.generationalFileOutDir} base directory.
if (useUpdatedArgs) {
outputs.add(
getDataBindingArtifact(
ruleContext, String.format("%s/%s-%s", METADATA_OUTPUT_DIR, javaPackage, suffix)));
} else {
outputs.add(
getDataBindingArtifact(
ruleContext,
String.format(
"%s/%s-%s-%s", METADATA_OUTPUT_DIR, javaPackage, javaPackage, suffix)));
}
}
return outputs.build();
}
@Nullable
static Artifact getMetadataOutput(
RuleContext ruleContext, boolean useUpdatedArgs, String metadataOutputSuffix) {
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 null;
}
String javaPackage = AndroidCommon.getJavaPackage(ruleContext);
// The annotation processor automatically creates files with this naming pattern under the
// {@code -Aandroid.databinding.generationalFileOutDir} base directory.
if (useUpdatedArgs) {
return getDataBindingArtifact(
ruleContext,
String.format("%s/%s-%s", METADATA_OUTPUT_DIR, javaPackage, metadataOutputSuffix));
} else {
return getDataBindingArtifact(
ruleContext,
String.format(
"%s/%s-%s-%s", METADATA_OUTPUT_DIR, javaPackage, javaPackage, metadataOutputSuffix));
}
}
/**
* Data binding's annotation processor reads the transitive metadata outputs of the target's deps
* (see {@link #getMetadataOutputs(RuleContext, List<String>)}) 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
*/
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(
SymlinkAction.toArtifact(
ruleContext.getActionOwner(),
depMetadata,
symlink,
String.format(
"Symlinking dep metadata output %s for %s", depMetadata.getFilename(), ruleLabel)));
return symlink;
}
}