// 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.common.collect.Lists; 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.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.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.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; import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; /** * Support logic for Bazel's data binding * integration. * *

In short, data binding in Bazel works as follows: * *

    *
  1. 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. *
  2. The data binding annotation processor gets activated on Java compilation. This processor * reads a custom-generated DataBindingInfo.java 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. *
  3. The data binding compile-time and runtime support libraries get linked into the binary's * deploy jar. *
* *

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. */ static final String DATABINDING_ANNOTATION_PROCESSOR_ATTR = "$databinding_annotation_processor"; private static final String ENABLE_DATA_BINDING_ATTR = "enable_data_binding"; /** Contains Android Databinding configuration and resource generation information. */ public interface DataBindingContext { /** * Returns the file where data binding's resource processing produces binding xml. For example, * given: * *

{@code
     * 
     *   
     *     
     *   
     * 
     * 
     *   ...
     * 
     * }
* *

data binding strips out and processes this part: * *

{@code
     * 
     *   
     * 
     * }
* * for each layout file with data binding expressions. Since this may produce multiple files, * outputs are zipped up into a single container. */ default void supplyLayoutInfo(Consumer consumer) {} /** The javac flags that are needed to configure data binding's annotation processor. */ default void supplyJavaCoptsUsing( RuleContext ruleContext, boolean isBinary, Consumer> consumer) {} /** * Adds data binding's annotation processor as a plugin to the given Java compilation context. * *

This extends the Java compilation to translate data binding .xml into corresponding * classes. */ default void supplyAnnotationProcessor( RuleContext ruleContext, BiConsumer> consumer) {} /** * 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. */ default ImmutableList processDeps(RuleContext ruleContext) { return ImmutableList.of(); } /** * Creates and adds the generated Java source for data binding annotation processor to read and * translate layout info xml (from {@link #supplyLayoutInfo(Consumer)} into the classes that end * user code consumes. * *

This triggers the annotation processor. Annotation processor settings are configured * separately in {@link #supplyJavaCoptsUsing(RuleContext, boolean, Consumer)}. */ default ImmutableList addAnnotationFileToSrcs( ImmutableList srcs, RuleContext ruleContext) { return srcs; }; /** * Adds the appropriate {@link UsesDataBindingProvider} for a rule if it should expose one. * *

A rule exposes {@link UsesDataBindingProvider} if either it or its deps set {@code * enable_data_binding = 1}. */ default void addProvider(RuleConfiguredTargetBuilder builder, RuleContext ruleContext) { maybeAddProvider(new ArrayList<>(), builder, ruleContext); } default AndroidResources processResources(AndroidResources resources) { return resources; } } private static final class EnabledDataBindingV1Context implements DataBindingContext { private final ActionConstructionContext actionConstructionContext; private EnabledDataBindingV1Context(ActionConstructionContext actionConstructionContext) { this.actionConstructionContext = actionConstructionContext; } @Override public void supplyLayoutInfo(Consumer consumer) { consumer.accept(layoutInfoFile()); } Artifact layoutInfoFile() { return actionConstructionContext.getUniqueDirectoryArtifact("databinding", "layout-info.zip"); } @Override public void supplyJavaCoptsUsing( RuleContext ruleContext, boolean isBinary, Consumer> consumer) { ImmutableList.Builder 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, produces cleaner output for Android Studio. flags.add(createProcessorFlag("printEncodedErrors", "0")); consumer.accept(flags.build()); } @Override public void supplyAnnotationProcessor( RuleContext ruleContext, BiConsumer> consumer) { consumer.accept( JavaInfo.getProvider( JavaPluginInfoProvider.class, ruleContext.getPrerequisite( DATABINDING_ANNOTATION_PROCESSOR_ATTR, RuleConfiguredTarget.Mode.HOST)), getMetadataOutputs(ruleContext)); } @Override public ImmutableList processDeps(RuleContext ruleContext) { ImmutableList.Builder dataBindingJavaInputs = ImmutableList.builder(); if (AndroidResources.definesAndroidResources(ruleContext.attributes())) { dataBindingJavaInputs.add(layoutInfoFile()); } for (Artifact dataBindingDepMetadata : getTransitiveMetadata(ruleContext, "deps")) { dataBindingJavaInputs.add( symlinkDepsMetadataIntoOutputTree(ruleContext, dataBindingDepMetadata)); } return dataBindingJavaInputs.build(); } @Override public ImmutableList addAnnotationFileToSrcs( ImmutableList srcs, RuleContext ruleContext) { // 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, "databinding_annotation_template.txt"); Artifact annotationFile = getDataBindingArtifact(ruleContext, "DataBindingInfo.java"); ruleContext.registerAction( FileWriteAction.create(ruleContext, annotationFile, contents, false)); return ImmutableList.builder().addAll(srcs).add(annotationFile).build(); } catch (IOException e) { ruleContext.ruleError("Cannot load annotation processor template: " + e.getMessage()); return ImmutableList.of(); } } @Override public void addProvider(RuleConfiguredTargetBuilder builder, RuleContext ruleContext) { List dataBindingMetadataOutputs = Lists.newArrayList(getMetadataOutputs(ruleContext)); maybeAddProvider(dataBindingMetadataOutputs, builder, ruleContext); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EnabledDataBindingV1Context that = (EnabledDataBindingV1Context) o; return Objects.equals(actionConstructionContext, that.actionConstructionContext); } @Override public int hashCode() { return actionConstructionContext.hashCode(); } @Override public AndroidResources processResources(AndroidResources resources) { return resources; } } private static class EnabledDataBindingV2Context implements DataBindingContext { private final ActionConstructionContext actionContext; private EnabledDataBindingV2Context(ActionConstructionContext actionContext) { this.actionContext = actionContext; throw new UnsupportedOperationException("V2 not implemented yet."); } // TODO(b/112038432): Enable databinding v2. } private static final DataBindingContext DISABLED_CONTEXT = new DataBindingContext() {}; /** Supplies a databinding context from a rulecontext. */ public static DataBindingContext contextFrom( RuleContext ruleContext, AndroidConfiguration androidConfig) { if (isEnabled(ruleContext)) { if (androidConfig.useDataBindingV2()) { return asEnabledDataBindingV2ContextFrom(ruleContext); } return asEnabledDataBindingV1ContextFrom(ruleContext); } return asDisabledDataBindingContext(); } /** Supplies a databinding context from an action context. */ public static DataBindingContext contextFrom( boolean enabled, ActionConstructionContext context, AndroidConfiguration androidConfig) { if (enabled) { if (androidConfig.useDataBindingV2()) { return asEnabledDataBindingV2ContextFrom(context); } return asEnabledDataBindingV1ContextFrom(context); } return asDisabledDataBindingContext(); } /** Supplies an enabled DataBindingContext from the action context. */ private static DataBindingContext asEnabledDataBindingV1ContextFrom( ActionConstructionContext actionContext) { return new EnabledDataBindingV1Context(actionContext); } private static DataBindingContext asEnabledDataBindingV2ContextFrom( ActionConstructionContext actionContext) { return new EnabledDataBindingV2Context(actionContext); } /** Supplies a disabled (no-op) DataBindingContext. */ public static DataBindingContext asDisabledDataBindingContext() { return DISABLED_CONTEXT; } /** * 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 METADATA_OUTPUT_SUFFIXES = ImmutableList.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? * *

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)); } /** 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()); } /** 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); } /** * Adds the appropriate {@link UsesDataBindingProvider} for a rule if it should expose one. * *

A rule exposes {@link UsesDataBindingProvider} if either it or its deps set {@code * enable_data_binding = 1}. */ private static void maybeAddProvider( List dataBindingMetadataOutputs, RuleConfiguredTargetBuilder builder, RuleContext ruleContext) { // Expose the data binding provider if there are outputs. 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.addNativeDeclaredProvider(new UsesDataBindingProvider(dataBindingMetadataOutputs)); } } /** Returns the data binding resource processing output from deps under the given attribute. */ private static List getTransitiveMetadata(RuleContext ruleContext, String attr) { ImmutableList.Builder dataBindingMetadataOutputs = ImmutableList.builder(); if (ruleContext.attributes().has(attr, BuildType.LABEL_LIST)) { for (UsesDataBindingProvider provider : ruleContext.getPrerequisites( attr, RuleConfiguredTarget.Mode.TARGET, 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. * *

>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 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.of(); } ImmutableList.Builder outputs = ImmutableList.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(); } /** * 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; } }