Skip to content

Commit

Permalink
Expose Bazel's Android data binding support to users.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gregestren authored and aehlig committed Apr 18, 2017
1 parent 36355da commit ed666fc
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/main/java/com/google/devtools/build/lib/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,7 @@ java_library(
),
resources = [
"rules/android/android_device_stub_template.txt",
"rules/android/databinding_annotation_template.txt",
],
deps = [
":build-base",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,8 @@ public RuleConfiguredTargetBuilder addTransitiveInfoProviders(
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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -440,7 +443,7 @@ public Metadata getMetadata() {
*/
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>.
Expand Down Expand Up @@ -491,6 +494,43 @@ Generated files (from genrules) can be referenced by
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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ private ImmutableList<String> createAndroidBuildContents() {
.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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
18 changes: 18 additions & 0 deletions src/test/java/com/google/devtools/build/lib/rules/android/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,21 @@ java_test(
"//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",
],
)
5 changes: 5 additions & 0 deletions tools/android/BUILD.tools
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,8 @@ filegroup(
name = "debug_keystore",
srcs = ["bazel_debug.keystore"],
)

alias(
name = "databinding_annotation_processor",
actual = "@android_databinding//:annotation_processor",
)

0 comments on commit ed666fc

Please sign in to comment.