aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/rules/android/DataBinding.java
blob: c52bc88938fab67e6cb72fbce0188b64730abbd5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// 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.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Template;
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 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 the data binding runtime/compile-time support libraries.
   */
  private static final String DATABINDING_RUNTIME_ATTR = "$databinding_runtime";

  /**
   * The rule attribute supplying the data binding annotation processor.
   */
  private static final String DATABINDING_ANNOTATION_PROCESSOR_ATTR =
      "$databinding_annotation_processor";

  /**
   * 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 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 the support libraries needed to compile/run Java code with data binding.
   *
   * <p>This excludes the annotation processor, which is injected separately as a Java plugin
   * (see {@link #addAnnotationProcessor}).
   */
  static ImmutableList<TransitiveInfoCollection> addSupportLibs(RuleContext ruleContext,
      List<? extends TransitiveInfoCollection> deps) {
    RuleConfiguredTarget.Mode mode = RuleConfiguredTarget.Mode.TARGET;
    return ImmutableList.<TransitiveInfoCollection>builder()
        .addAll(deps)
        .addAll(ruleContext.getPrerequisites(DATABINDING_RUNTIME_ATTR, mode))
        .build();
  }

  /**
   * 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) {
    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));
  }

  /**
   * 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.
   */
  static Artifact createAnnotationFile(RuleContext ruleContext, boolean isLibrary) {
    Template template =
        Template.forResource(DataBinding.class, "databinding_annotation_template.txt");

    List<Substitution> subs = new ArrayList<>();
    subs.add(Substitution.of("%module_package%", AndroidCommon.getJavaPackage(ruleContext)));
    // TODO(gregce): clarify or remove the sdk root
    subs.add(Substitution.of("%sdk_root%", "/not/used"));
    subs.add(Substitution.of("%layout_info_dir%",
        getLayoutInfoFile(ruleContext).getExecPath().getParentDirectory().toString()));
    subs.add(Substitution.of("%export_class_list_to%", "/tmp/exported_classes")); // Unused.
    subs.add(Substitution.of("%is_library%", Boolean.toString(isLibrary)));
    subs.add(Substitution.of("%min_sdk%", "14")); // TODO(gregce): update this

    Artifact output = ruleContext.getPackageRelativeArtifact(
        String.format("databinding/%s/DataBindingInfo.java", ruleContext.getLabel().getName()),
        ruleContext.getConfiguration().getGenfilesDirectory());

    ruleContext.registerAction
        (new TemplateExpansionAction(ruleContext.getActionOwner(), output, template, subs, 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));
    }
    if (ruleContext.attributes().has("exports", BuildType.LABEL_LIST)) {
      for (UsesDataBindingProvider provider : ruleContext.getPrerequisites("exports",
          RuleConfiguredTarget.Mode.TARGET, UsesDataBindingProvider.class)) {
        dataBindingMetadataOutputs.addAll(provider.getMetadataOutputs());
      }
    }
    if (!dataBindingMetadataOutputs.isEmpty()) {
      // QUESTION(gregce): does a rule need to propagate the metadata outputs of its deps, or do
      // they get integrated automatically into its own outputs?
      builder.addProvider(UsesDataBindingProvider.class,
          new UsesDataBindingProvider(dataBindingMetadataOutputs));
    }
  }

  /**
   * 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");

  /**
   * 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 an {@code android_binary} depends on an {@code android_library} in a
   * different package, the {@code android_library}'s version gets packaged with the application
   * jar, even though (due to resource merging) both modules compile against their own instances.
   */
  public static List<Artifact> getMetadataOutputs(RuleContext ruleContext) {
    ImmutableList.Builder<Artifact> outputs = ImmutableList.<Artifact>builder();
    String javaPackage = AndroidCommon.getJavaPackage(ruleContext);
    Label ruleLabel = ruleContext.getRule().getLabel();
    String pathPrefix =
        String.format(
            "_javac/%s/lib%s_classes/%s/%s-",
            ruleLabel.getName(),
            ruleLabel.getName(),
            javaPackage.replace('.', '/'),
            javaPackage);
    for (String suffix : METADATA_OUTPUT_SUFFIXES) {
      outputs.add(ruleContext.getBinArtifact(pathPrefix + suffix));
    }
    return outputs.build();
  }

  /**
   * Processes deps that also apply data binding.
   *
   * @param ruleContext the current rule
   * @param attributes java compilation attributes. The directories of the deps' metadata outputs
   *     (see {@link #getMetadataOutputs}) are added to this rule's annotation processor classpath.
   * @return the deps' metadata outputs. These need to be staged as compilation inputs to the
   *     current rule.
   */
  static ImmutableList<Artifact> processDeps(RuleContext ruleContext,
      JavaTargetAttributes.Builder attributes) {
    ImmutableList.Builder<Artifact> dataBindingJavaInputs = ImmutableList.<Artifact>builder();
    dataBindingJavaInputs.add(DataBinding.getLayoutInfoFile(ruleContext));
    for (UsesDataBindingProvider p : ruleContext.getPrerequisites("deps",
        RuleConfiguredTarget.Mode.TARGET, UsesDataBindingProvider.class)) {
      for (Artifact dataBindingDepMetadata : p.getMetadataOutputs()) {
        attributes.addProcessorPathDir(dataBindingDepMetadata.getExecPath().getParentDirectory());
        dataBindingJavaInputs.add(dataBindingDepMetadata);
      }
    }
    return dataBindingJavaInputs.build();
  }
}