aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/rules/android/DataBinding.java
blob: a5c4c33624f6d59a8208555ffe4ac6b4c17187a7 (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
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
// 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;
  }
}