| // 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 <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. */ |
| 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: |
| * |
| * <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. |
| */ |
| default void supplyLayoutInfo(Consumer<Artifact> consumer) {} |
| |
| /** The javac flags that are needed to configure data binding's annotation processor. */ |
| default void supplyJavaCoptsUsing( |
| RuleContext ruleContext, boolean isBinary, Consumer<Iterable<String>> consumer) {} |
| |
| /** |
| * Adds data binding's annotation processor as a plugin to the given Java compilation context. |
| * |
| * <p>This extends the Java compilation to translate data binding .xml into corresponding |
| * classes. |
| */ |
| default void supplyAnnotationProcessor( |
| RuleContext ruleContext, BiConsumer<JavaPluginInfoProvider, Iterable<Artifact>> 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<Artifact> 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. |
| * |
| * <p>This triggers the annotation processor. Annotation processor settings are configured |
| * separately in {@link #supplyJavaCoptsUsing(RuleContext, boolean, Consumer)}. |
| */ |
| default ImmutableList<Artifact> addAnnotationFileToSrcs( |
| ImmutableList<Artifact> srcs, RuleContext ruleContext) { |
| return srcs; |
| }; |
| |
| /** |
| * 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}. |
| */ |
| 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<Artifact> consumer) { |
| consumer.accept(layoutInfoFile()); |
| } |
| |
| Artifact layoutInfoFile() { |
| return actionConstructionContext.getUniqueDirectoryArtifact("databinding", "layout-info.zip"); |
| } |
| |
| @Override |
| public void supplyJavaCoptsUsing( |
| RuleContext ruleContext, boolean isBinary, Consumer<Iterable<String>> consumer) { |
| ImmutableList.Builder<String> 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<JavaPluginInfoProvider, Iterable<Artifact>> consumer) { |
| consumer.accept( |
| JavaInfo.getProvider( |
| JavaPluginInfoProvider.class, |
| ruleContext.getPrerequisite( |
| DATABINDING_ANNOTATION_PROCESSOR_ATTR, RuleConfiguredTarget.Mode.HOST)), |
| getMetadataOutputs(ruleContext)); |
| } |
| |
| @Override |
| public ImmutableList<Artifact> processDeps(RuleContext ruleContext) { |
| ImmutableList.Builder<Artifact> 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<Artifact> addAnnotationFileToSrcs( |
| ImmutableList<Artifact> 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.<Artifact>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<Artifact> 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<String> 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? |
| * |
| * <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)); |
| } |
| |
| /** 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. |
| * |
| * <p>A rule exposes {@link UsesDataBindingProvider} if either it or its deps set {@code |
| * enable_data_binding = 1}. |
| */ |
| private static void maybeAddProvider( |
| List<Artifact> 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<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.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. |
| */ |
| private static List<Artifact> 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<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(); |
| } |
| |
| /** |
| * 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; |
| } |
| } |