// 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.Iterables; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.RuleConfiguredTarget; 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.cmdline.Label; import com.google.devtools.build.lib.packages.BuildType; 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> * <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> * <li>The data binding compile-time and runtime support libraries get linked into the binary's * deploy jar.</li> * </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>This is true if either the rule or any of its transitive dependencies declares data binding * support in its attributes. * * <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 * any data binding expressions appear in their layout resources. */ public static boolean isEnabled(RuleContext ruleContext) { if (ruleContext.attributes().has("enable_data_binding", Type.BOOLEAN) && ruleContext.attributes().get("enable_data_binding", Type.BOOLEAN)) { return true; } else { return !Iterables.isEmpty(ruleContext.getPrerequisites("deps", RuleConfiguredTarget.Mode.TARGET, UsesDataBindingProvider.class)); } } /** * 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) { // The data binding library expects this to be called "layout-info.zip". return ruleContext.getUniqueDirectoryArtifact("databinding", "layout-info.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, boolean isBinary) { JavaPluginInfoProvider plugin = ruleContext.getPrerequisite( DATABINDING_ANNOTATION_PROCESSOR_ATTR, RuleConfiguredTarget.Mode.TARGET, JavaPluginInfoProvider.class); 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)); addProcessorFlags(ruleContext, attributes, isBinary); } /** * Adds javac flags to configure data binding's annotation processor. */ private static void addProcessorFlags(RuleContext ruleContext, JavaTargetAttributes.Builder attributes, boolean isBinary) { 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. addProcessorFlag(attributes, "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. addProcessorFlag(attributes, "generationalFileOutDir", metadataOutputDir); // Path to the Android SDK installation (if available). addProcessorFlag(attributes, "sdkDir", "/not/used"); // Whether the current rule is a library or binary. addProcessorFlag(attributes, "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. addProcessorFlag(attributes, "xmlOutDir", getDataBindingExecPath(ruleContext).toString()); // Unused. addProcessorFlag(attributes, "exportClassListTo", "/tmp/exported_classes"); // The Java package for the current rule. addProcessorFlag(attributes, "modulePackage", AndroidCommon.getJavaPackage(ruleContext)); // The minimum Android SDK compatible with this rule. addProcessorFlag(attributes, "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. addProcessorFlag(attributes, "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); } /** * Turns a key/value pair into a javac annotation processor flag received by data binding. */ private static void addProcessorFlag(JavaTargetAttributes.Builder attributes, String flag, String value) { attributes.addProcessorFlag(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 in {@link #addProcessorFlags}. */ static Artifact createAnnotationFile(RuleContext ruleContext, boolean isLibrary) { 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 (!LocalResourceContainer.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 (!LocalResourceContainer.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 (LocalResourceContainer.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; } }