aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/rules/android/DataBinding.java
blob: 9f295ba832daba668fe096c086a41e71c449f77e (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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
// 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.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.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;
import java.util.Objects;
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. */
  public static final String DATABINDING_ANNOTATION_PROCESSOR_ATTR =
      "$databinding_annotation_processor";

  public static final String ENABLE_DATA_BINDING_ATTR = "enable_data_binding";

  /** Contains Android Databinding configuration and resource generation information. */
  public interface DataBindingContext {
    void supplyLayoutInfo(Consumer<Artifact> consumer);
  }

  private static final class EnabledDataBindingContext implements DataBindingContext {

    private final ActionConstructionContext actionConstructionContext;

    private EnabledDataBindingContext(ActionConstructionContext actionConstructionContext) {
      this.actionConstructionContext = actionConstructionContext;
    }

    @Override
    public void supplyLayoutInfo(Consumer<Artifact> consumer) {
      consumer.accept(getLayoutInfoFile(actionConstructionContext));
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      EnabledDataBindingContext that = (EnabledDataBindingContext) o;
      return Objects.equals(actionConstructionContext, that.actionConstructionContext);
    }

    @Override
    public int hashCode() {
      return actionConstructionContext.hashCode();
    }
  }

  private static final class DisabledDataBindingContext implements DataBindingContext {

    @Override
    public void supplyLayoutInfo(Consumer<Artifact> consumer) {
      // pass
    }
  }

  /** Supplies a databinding context from a rulecontext. */
  public static DataBindingContext contextFrom(RuleContext ruleContext) {
    if (isEnabled(ruleContext)) {
      return asEnabledDataBindingContextFrom(ruleContext);
    }
    return asDisabledDataBindingContext();
  }

  /** Supplies a databinding context from a rulecontext. */
  public static DataBindingContext contextFrom(boolean enabled, ActionConstructionContext context) {
    if (enabled) {
      return asEnabledDataBindingContextFrom(context);
    }
    return asDisabledDataBindingContext();
  }

  /** Supplies an enabled DataBindingContext from the action context. */
  public static DataBindingContext asEnabledDataBindingContextFrom(
      ActionConstructionContext actionContext) {
    return new EnabledDataBindingContext(actionContext);
  }

  /** Supplies a disabled (no-op) DataBindingContext from the action context. */
  public static DataBindingContext asDisabledDataBindingContext() {
    return new DisabledDataBindingContext();
  }

  /**
   * 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>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.
   */
  public static boolean isEnabled(RuleContext ruleContext) {
    return ruleContext.attributes().has(ENABLE_DATA_BINDING_ATTR, Type.BOOLEAN)
        && 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());
  }

  /**
   * 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(ActionConstructionContext context) {
    return getSuffixedInfoFile(context, "");
  }

  /** Gets a layout info file with the specified suffix (for use in having different outputs) */
  static Artifact getSuffixedInfoFile(ActionConstructionContext context, String suffix) {
    return context.getUniqueDirectoryArtifact("databinding", "layout-info" + suffix + ".zip");
  }

  /**
   * 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 =
        JavaInfo.getProvider(
            JavaPluginInfoProvider.class,
            ruleContext.getPrerequisite(
                DATABINDING_ANNOTATION_PROCESSOR_ATTR, RuleConfiguredTarget.Mode.HOST));

    attributes.addPlugin(plugin);
    attributes.addAdditionalOutputs(getMetadataOutputs(ruleContext));
  }

  /** The javac flags that are needed to configure data binding's annotation processor. */
  static ImmutableList<String> getJavacOpts(RuleContext ruleContext, boolean isBinary) {
    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, the annotation processor reports detailed output about its activities.
    // addProcessorFlag(attributes, "enableDebugLogs", "1");
    // If enabled, produces cleaner output for Android Studio.
    flags.add(createProcessorFlag("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);
    return flags.build();
  }

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

  /**
   * 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 separately in {@link #getJavacopts}.
   */
  static Artifact createAnnotationFile(RuleContext ruleContext) {
    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 (!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.<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 (AndroidResources.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;
  }
}