Expose Bazel's Android data binding support to users.
The logic is already in Bazel but wasn't available to build rules.
This change makes it available, but still requires data binding's
{build|run}time libraries to be checked into appropriate depot
spots for everything to work.
Followup changes will make those libraries easily available.
Issue: #2694
PiperOrigin-RevId: 153359861
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 51b444d..a3707e7 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -979,6 +979,7 @@
),
resources = [
"rules/android/android_device_stub_template.txt",
+ "rules/android/databinding_annotation_template.txt",
],
deps = [
":build-base",
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
index 279f252..feafff5 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
@@ -740,6 +740,8 @@
builder, skylarkApiProvider, filesToBuild, classJar, ANDROID_COLLECTION_SPEC);
javaCommon.addGenJarsProvider(builder, skylarkApiProvider, genClassJar, genSourceJar);
+ DataBinding.maybeAddProvider(builder, ruleContext);
+
return builder
.setFilesToBuild(filesToBuild)
.addSkylarkTransitiveInfo(JavaSkylarkApiProvider.NAME, skylarkApiProvider.build())
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
index 7c289e3..81c141d 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
@@ -35,10 +35,12 @@
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Attribute.AllowedValueSet;
import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel;
import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.Rule;
@@ -55,6 +57,7 @@
import com.google.devtools.build.lib.rules.java.ProguardHelper;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
import com.google.devtools.build.lib.syntax.Printer;
+import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.FileType;
import java.util.List;
@@ -440,7 +443,7 @@
*/
public static final class AndroidResourceSupportRule implements RuleDefinition {
@Override
- public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
+ public RuleClass build(RuleClass.Builder builder, final RuleDefinitionEnvironment env) {
return builder
/* <!-- #BLAZE_RULE($android_resource_support).ATTRIBUTE(manifest) -->
The name of the Android manifest file, normally <code>AndroidManifest.xml</code>.
@@ -491,6 +494,43 @@
libraries that will only be detected at runtime.
<!-- #END_BLAZE_RULE.ATTRIBUTE --> */
.add(attr("custom_package", STRING))
+ /* <!-- #BLAZE_RULE($android_resource_support).ATTRIBUTE(enable_data_binding) -->
+ If true, this rule processes
+ <a href="https://developer.android.com/topic/libraries/data-binding/index.html">data
+ binding</a> expressions in layout resources included through the
+ <a href="${link android_binary.resource_files}">resource_files</a> attribute. Without this
+ setting, data binding expressions produce build failures.
+ <p>
+ To build an Android app with data binding, you must also do the following:
+ <ol>
+ <li>Set this attribute for all Android rules that transitively depend on this one.
+ This is because of resource merging: when a rule declares data binding XML expressions
+ its dependers implicitly inherit those expressions. So they also need to build with
+ data binding in order to parse those expressions correctly.
+ <li>Add a <code>deps =</code> entry for the data binding runtime library to all targets
+ that set this attribute. The location of this library depends on your depot setup.
+ </ol>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("enable_data_binding", Type.BOOLEAN))
+ // The javac annotation processor from Android's data binding library that turns
+ // processed XML expressions into Java code.
+ .add(attr(DataBinding.DATABINDING_ANNOTATION_PROCESSOR_ATTR, BuildType.LABEL)
+ // This has to be a computed default because the annotation processor is a
+ // java_plugin, which means it needs the Jvm configuration fragment. That conflicts
+ // with Android builds that use --experimental_disable_jvm.
+ // TODO(gregce): The Jvm dependency is only needed for the host configuration.
+ // --experimental_disable_jvm is really intended for target configurations without
+ // a JDK. So this case isn't conceptually a conflict. Clean this up so we can remove
+ // this computed default.
+ .value(new Attribute.ComputedDefault("enable_data_binding") {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.get("enable_data_binding", Type.BOOLEAN)
+ ? env.getToolsLabel("//tools/android:databinding_annotation_processor")
+ : null;
+ }
+ }))
+
.build();
}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/DataBinding.java b/src/main/java/com/google/devtools/build/lib/rules/android/DataBinding.java
index 6b74a72..eae6870 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/DataBinding.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/DataBinding.java
@@ -61,9 +61,9 @@
*/
public final class DataBinding {
/**
- * The rule attribute supplying the data binding annotation processor.
+ * The rule attribute supplying data binding's annotation processor.
*/
- private static final String DATABINDING_ANNOTATION_PROCESSOR_ATTR =
+ public static final String DATABINDING_ANNOTATION_PROCESSOR_ATTR =
"$databinding_annotation_processor";
/**
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
index 73b07a9..f631e5d 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -228,7 +228,9 @@
.add("java_import(name = 'idlclass_import',")
.add(" jars = [ 'idlclass.jar' ])")
.add("exports_files(['adb', 'adb_static'])")
- .add("sh_binary(name = 'android_runtest', srcs = ['empty.sh'])");
+ .add("sh_binary(name = 'android_runtest', srcs = ['empty.sh'])")
+ .add("java_plugin(name = 'databinding_annotation_processor',")
+ .add(" processor_class = 'android.databinding.annotationprocessor.ProcessDataBinding')");
return androidBuildContents.build();
}
diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidDataBindingTest.java b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidDataBindingTest.java
new file mode 100644
index 0000000..8569eae
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidDataBindingTest.java
@@ -0,0 +1,249 @@
+// Copyright 2017 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 static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.getFirstArtifactEndingWith;
+
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.rules.java.JavaCompileAction;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Bazel's Android data binding support.
+ */
+@RunWith(JUnit4.class)
+public class AndroidDataBindingTest extends AndroidBuildViewTestCase {
+ private void writeDataBindingFiles() throws Exception {
+ scratch.file("java/android/library/BUILD",
+ "android_library(",
+ " name = 'lib_with_data_binding',",
+ " enable_data_binding = 1,",
+ " manifest = 'AndroidManifest.xml',",
+ " srcs = ['MyLib.java'],",
+ " resource_files = [],",
+ ")");
+ scratch.file("java/android/library/MyLib.java",
+ "package android.library; public class MyLib {};");
+ scratch.file("java/android/binary/BUILD",
+ "android_binary(",
+ " name = 'app',",
+ " enable_data_binding = 1,",
+ " manifest = 'AndroidManifest.xml',",
+ " srcs = ['MyApp.java'],",
+ " deps = ['//java/android/library:lib_with_data_binding'],",
+ ")");
+ scratch.file("java/android/binary/MyApp.java",
+ "package android.binary; public class MyApp {};");
+ }
+
+ /**
+ * Returns the .params file contents of a {@link JavaCompileAction}
+ */
+ private Iterable<String> getParamFileContents(JavaCompileAction action) {
+ Artifact paramFile = getFirstArtifactEndingWith(action.getInputs(), "-2.params");
+ return ((ParameterFileWriteAction) getGeneratingAction(paramFile)).getContents();
+ }
+
+ @Test
+ public void basicDataBindingIntegrationParallelResourceProcessing() throws Exception {
+ useConfiguration("--experimental_use_parallel_android_resource_processing");
+ basicDataBindingIntegration();
+ }
+
+ @Test
+ public void basicDataBindingIntegrationLegacyResourceProcessing() throws Exception {
+ useConfiguration("--noexperimental_use_parallel_android_resource_processing");
+ basicDataBindingIntegration();
+ }
+
+ private void basicDataBindingIntegration() throws Exception {
+ writeDataBindingFiles();
+ ConfiguredTarget ctapp = getConfiguredTarget("//java/android/binary:app");
+ Set<Artifact> allArtifacts = actionsTestUtil().artifactClosureOf(getFilesToBuild(ctapp));
+
+ // "Data binding"-enabled targets invoke resource processing with a request for data binding
+ // output:
+ Artifact libResourceInfoOutput = getFirstArtifactEndingWith(allArtifacts,
+ "databinding/lib_with_data_binding/layout-info.zip");
+ assertThat(getGeneratingSpawnAction(libResourceInfoOutput).getArguments())
+ .containsAllOf("--dataBindingInfoOut", libResourceInfoOutput.getExecPathString())
+ .inOrder();
+
+ Artifact binResourceInfoOutput = getFirstArtifactEndingWith(allArtifacts,
+ "databinding/app/layout-info.zip");
+ assertThat(getGeneratingSpawnAction(binResourceInfoOutput).getArguments())
+ .containsAllOf("--dataBindingInfoOut", binResourceInfoOutput.getExecPathString())
+ .inOrder();
+
+ // Java compilation includes the data binding annotation processor, the resource processor's
+ // output, and the auto-generated DataBindingInfo.java the annotation processor uses to figure
+ // out what to do:
+ JavaCompileAction libCompileAction = (JavaCompileAction) getGeneratingAction(
+ getFirstArtifactEndingWith(allArtifacts, "lib_with_data_binding.jar"));
+ assertThat(libCompileAction.getProcessorNames())
+ .contains("android.databinding.annotationprocessor.ProcessDataBinding");
+ assertThat(ActionsTestUtil.prettyArtifactNames(libCompileAction.getInputs()))
+ .containsAllOf(
+ "java/android/library/databinding/lib_with_data_binding/layout-info.zip",
+ "java/android/library/databinding/lib_with_data_binding/DataBindingInfo.java");
+
+ JavaCompileAction binCompileAction = (JavaCompileAction) getGeneratingAction(
+ getFirstArtifactEndingWith(allArtifacts, "app.jar"));
+ assertThat(binCompileAction.getProcessorNames())
+ .contains("android.databinding.annotationprocessor.ProcessDataBinding");
+ assertThat(ActionsTestUtil.prettyArtifactNames(binCompileAction.getInputs()))
+ .containsAllOf(
+ "java/android/binary/databinding/app/layout-info.zip",
+ "java/android/binary/databinding/app/DataBindingInfo.java");
+ }
+
+ @Test
+ public void dataBindingCompilationUsesMetadataFromDepsParallelResourceProcessing()
+ throws Exception {
+ useConfiguration("--experimental_use_parallel_android_resource_processing");
+ dataBindingCompilationUsesMetadataFromDeps();
+ }
+
+ @Test
+ public void dataBindingCompilationUsesMetadataFromDepsLegacyResourceProcessing()
+ throws Exception {
+ useConfiguration("--noexperimental_use_parallel_android_resource_processing");
+ dataBindingCompilationUsesMetadataFromDeps();
+ }
+
+ private void dataBindingCompilationUsesMetadataFromDeps() throws Exception {
+ writeDataBindingFiles();
+ ConfiguredTarget ctapp = getConfiguredTarget("//java/android/binary:app");
+ Set<Artifact> allArtifacts = actionsTestUtil().artifactClosureOf(getFilesToBuild(ctapp));
+
+ // The library's compilation doesn't include any of the -setter_store.bin, layoutinfo.bin, etc.
+ // files that store a dependency's data binding results (since the library has no deps).
+ // We check that they don't appear as compilation inputs.
+ JavaCompileAction libCompileAction = (JavaCompileAction)
+ getGeneratingAction(getFirstArtifactEndingWith(allArtifacts, "lib_with_data_binding.jar"));
+ assertThat(
+ Iterables.filter(libCompileAction.getInputs(),
+ ActionsTestUtil.getArtifactSuffixMatcher(".bin")))
+ .isEmpty();
+
+ // The binary's compilation includes the library's data binding results.
+ JavaCompileAction binCompileAction = (JavaCompileAction) getGeneratingAction(
+ getFirstArtifactEndingWith(allArtifacts, "app.jar"));
+ Iterable<Artifact> depMetadataInputs = Iterables.filter(
+ binCompileAction.getInputs(), ActionsTestUtil.getArtifactSuffixMatcher(".bin"));
+ final String depMetadataBaseDir = Iterables.getFirst(depMetadataInputs, null).getExecPath()
+ .getParentDirectory().toString();
+ ActionsTestUtil.execPaths(Iterables.filter(
+ binCompileAction.getInputs(), ActionsTestUtil.getArtifactSuffixMatcher(".bin")));
+ assertThat(ActionsTestUtil.execPaths(depMetadataInputs)).containsExactly(
+ depMetadataBaseDir + "/android.library-android.library-setter_store.bin",
+ depMetadataBaseDir + "/android.library-android.library-layoutinfo.bin",
+ depMetadataBaseDir + "/android.library-android.library-br.bin");
+ }
+
+ @Test
+ public void dataBindingAnnotationProcessorFlags() throws Exception {
+ writeDataBindingFiles();
+ ConfiguredTarget ctapp = getConfiguredTarget("//java/android/binary:app");
+ Set<Artifact> allArtifacts = actionsTestUtil().artifactClosureOf(getFilesToBuild(ctapp));
+ JavaCompileAction binCompileAction = (JavaCompileAction) getGeneratingAction(
+ getFirstArtifactEndingWith(allArtifacts, "app.jar"));
+ String dataBindingFilesDir = targetConfig.getBinDirectory(RepositoryName.MAIN).getExecPath()
+ .getRelative("java/android/binary/databinding/app").getPathString();
+ assertThat(getParamFileContents(binCompileAction))
+ .containsAllOf(
+ "-Aandroid.databinding.bindingBuildFolder=" + dataBindingFilesDir,
+ "-Aandroid.databinding.generationalFileOutDir=" + dataBindingFilesDir,
+ "-Aandroid.databinding.sdkDir=/not/used",
+ "-Aandroid.databinding.artifactType=APPLICATION",
+ "-Aandroid.databinding.xmlOutDir=" + dataBindingFilesDir,
+ "-Aandroid.databinding.exportClassListTo=/tmp/exported_classes",
+ "-Aandroid.databinding.modulePackage=android.binary",
+ "-Aandroid.databinding.minApi=14",
+ "-Aandroid.databinding.printEncodedErrors=0");
+ }
+
+ @Test
+ public void dataBindingIncludesTransitiveDepsForLibsWithNoResources() throws Exception {
+ scratch.file("java/android/lib_with_resource_files/BUILD",
+ "android_library(",
+ " name = 'lib_with_resource_files',",
+ " enable_data_binding = 1,",
+ " manifest = 'AndroidManifest.xml',",
+ " srcs = ['LibWithResourceFiles.java'],",
+ " resource_files = glob(['res/**']),",
+ ")");
+ scratch.file("java/android/lib_with_resource_files/LibWithResourceFiles.java",
+ "package android.lib_with_resource_files; public class LibWithResourceFiles {};");
+
+ scratch.file("java/android/lib_no_resource_files/BUILD",
+ "android_library(",
+ " name = 'lib_no_resource_files',",
+ " enable_data_binding = 1,",
+ " srcs = ['LibNoResourceFiles.java'],",
+ " deps = ['//java/android/lib_with_resource_files'],",
+ ")");
+ scratch.file("java/android/lib_no_resource_files/LibNoResourceFiles.java",
+ "package android.lib_no_resource_files; public class LibNoResourceFiles {};");
+
+ scratch.file("java/android/binary/BUILD",
+ "android_binary(",
+ " name = 'app',",
+ " enable_data_binding = 1,",
+ " manifest = 'AndroidManifest.xml',",
+ " srcs = ['MyApp.java'],",
+ " deps = ['//java/android/lib_no_resource_files'],",
+ ")");
+ scratch.file("java/android/binary/MyApp.java",
+ "package android.binary; public class MyApp {};");
+
+ ConfiguredTarget ct = getConfiguredTarget("//java/android/binary:app");
+ Set<Artifact> allArtifacts = actionsTestUtil().artifactClosureOf(getFilesToBuild(ct));
+
+ // Data binding resource processing outputs are expected only for libs with resources.
+ assertThat(getFirstArtifactEndingWith(allArtifacts,
+ "databinding/lib_no_resource_files/layout-info.zip")).isNull();
+ assertThat(getFirstArtifactEndingWith(allArtifacts,
+ "databinding/lib_with_resource_files/layout-info.zip")).isNotNull();
+ assertThat(getFirstArtifactEndingWith(allArtifacts, "databinding/app/layout-info.zip"))
+ .isNotNull();
+
+ // Compiling the app's Java source includes data binding metadata from the resource-equipped
+ // lib, but not the resource-empty one.
+ JavaCompileAction binCompileAction = (JavaCompileAction) getGeneratingAction(
+ getFirstArtifactEndingWith(allArtifacts, "app.jar"));
+ List<String> appJarInputs = ActionsTestUtil.prettyArtifactNames(binCompileAction.getInputs());
+ String libWithResourcesMetadataBaseDir = "java/android/binary/databinding/app/"
+ + "dependent-lib-artifacts/java/android/lib_with_resource_files/databinding/"
+ + "lib_with_resource_files/bin-files/android.lib_with_resource_files-";
+ assertThat(appJarInputs).containsAllOf(
+ "java/android/binary/databinding/app/layout-info.zip",
+ libWithResourcesMetadataBaseDir + "android.lib_with_resource_files-setter_store.bin",
+ libWithResourcesMetadataBaseDir + "android.lib_with_resource_files-layoutinfo.bin",
+ libWithResourcesMetadataBaseDir + "android.lib_with_resource_files-br.bin");
+ for (String compileInput : appJarInputs) {
+ assertThat(compileInput).doesNotMatch(".*lib_no_resource_files.*.bin");
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/BUILD b/src/test/java/com/google/devtools/build/lib/rules/android/BUILD
index f7d7e10..3d8368d 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/android/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/rules/android/BUILD
@@ -149,3 +149,21 @@
"//third_party:truth",
],
)
+
+java_test(
+ name = "AndroidDataBindingTest",
+ srcs = ["AndroidDataBindingTest.java"],
+ deps = [
+ ":AndroidBuildViewTestCase",
+ "//src/main/java/com/google/devtools/build/lib:android-rules",
+ "//src/main/java/com/google/devtools/build/lib:build-base",
+ "//src/main/java/com/google/devtools/build/lib:java-compilation",
+ "//src/main/java/com/google/devtools/build/lib/actions",
+ "//src/main/java/com/google/devtools/build/lib/cmdline",
+ "//src/test/java/com/google/devtools/build/lib:actions_testutil",
+ "//src/test/java/com/google/devtools/build/lib:testutil",
+ "//third_party:guava",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
diff --git a/tools/android/BUILD.tools b/tools/android/BUILD.tools
index 0d684d0..782fea9 100644
--- a/tools/android/BUILD.tools
+++ b/tools/android/BUILD.tools
@@ -205,3 +205,8 @@
name = "debug_keystore",
srcs = ["bazel_debug.keystore"],
)
+
+alias(
+ name = "databinding_annotation_processor",
+ actual = "@android_databinding//:annotation_processor",
+)