aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/DependencyModule.java
blob: 9fb2cac9ff9376036d5837343fd88f00d9d71eb9 (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
// Copyright 2014 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.buildjar.javac.plugins.dependency;

import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.buildjar.javac.plugins.BlazeJavaCompilerPlugin;
import com.google.devtools.build.lib.view.proto.Deps;
import com.google.devtools.build.lib.view.proto.Deps.Dependency.Kind;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Wrapper class for managing dependencies on top of
 * {@link com.google.devtools.build.buildjar.javac.BlazeJavaCompiler}. If strict_java_deps is
 * enabled, it keeps two maps between jar names (as they appear on the classpath) and their
 * originating targets, one for direct dependencies and the other for transitive (indirect)
 * dependencies, and enables the {@link StrictJavaDepsPlugin} to perform the actual checks. The
 * plugin also collects dependency information during compilation, and DependencyModule generates a
 * .jdeps artifact summarizing the discovered dependencies.
 */
public final class DependencyModule {

  public static enum StrictJavaDeps {
    /** Legacy behavior: Silently allow referencing transitive dependencies. */
    OFF(false),
    /** Warn about transitive dependencies being used directly. */
    WARN(true),
    /** Fail the build when transitive dependencies are used directly. */
    ERROR(true);

    private boolean enabled;

    StrictJavaDeps(boolean enabled) {
      this.enabled = enabled;
    }

    /** Convenience method for just checking if it's not OFF */
    public boolean isEnabled() {
      return enabled;
    }
  }

  private StrictJavaDeps strictJavaDeps = StrictJavaDeps.OFF;
  private final Map<String, String> directJarsToTargets;
  private final Map<String, String> indirectJarsToTargets;
  private boolean strictClasspathMode;
  private final Set<String> depsArtifacts;
  private final String ruleKind;
  private final String targetLabel;
  private final String outputDepsFile;
  private final String outputDepsProtoFile;
  private final Set<String> usedClasspath;
  private final Map<String, Deps.Dependency> explicitDependenciesMap;
  private final Map<String, Deps.Dependency> implicitDependenciesMap;
  Set<String> requiredClasspath;
  private final String fixMessage;

  DependencyModule(StrictJavaDeps strictJavaDeps,
                   Map<String, String> directJarsToTargets,
                   Map<String, String> indirectJarsToTargets,
                   boolean strictClasspathMode,
                   Set<String> depsArtifacts,
                   String ruleKind,
                   String targetLabel,
                   String outputDepsFile,
                   String outputDepsProtoFile,
                   String fixMessage) {
    this.strictJavaDeps = strictJavaDeps;
    this.directJarsToTargets = directJarsToTargets;
    this.indirectJarsToTargets = indirectJarsToTargets;
    this.strictClasspathMode = strictClasspathMode;
    this.depsArtifacts = depsArtifacts;
    this.ruleKind = ruleKind;
    this.targetLabel = targetLabel;
    this.outputDepsFile = outputDepsFile;
    this.outputDepsProtoFile = outputDepsProtoFile;
    this.explicitDependenciesMap = new HashMap<>();
    this.implicitDependenciesMap = new HashMap<>();
    this.usedClasspath = new HashSet<>();
    this.fixMessage = fixMessage;
  }

  /**
   * Returns a plugin to be enabled in the compiler.
   */
  public BlazeJavaCompilerPlugin getPlugin() {
    return new StrictJavaDepsPlugin(this);
  }

  /**
   * Writes the true, used compile-time classpath to the deps file, if specified.
   */
  public void emitUsedClasspath(String classpath) throws IOException {
    if (outputDepsFile != null) {
      try (BufferedWriter out = new BufferedWriter(new FileWriter(outputDepsFile))) {
        // Filter using the original classpath, to preserve ordering.
        for (String entry : classpath.split(":")) {
          if (usedClasspath.contains(entry)) {
            out.write(entry);
            out.newLine();
          }
        }
      } catch (IOException ex) {
        throw new IOException("Cannot write dependencies to " + outputDepsFile, ex);
      }
    }
  }

  /**
   * Writes dependency information to the deps file in proto format, if specified.
   *
   * This is a replacement for {@link #emitUsedClasspath} above, which only outputs the used
   * classpath. We collect more precise dependency information to allow Blaze to analyze both
   * strict and unused dependencies based on the new deps.proto.
   */
  public void emitDependencyInformation(String classpath, boolean successful) throws IOException {
    if (outputDepsProtoFile == null) {
      return;
    }

    try (BufferedOutputStream out =
             new BufferedOutputStream(new FileOutputStream(outputDepsProtoFile))) {
      buildDependenciesProto(classpath, successful).writeTo(out);
    } catch (IOException ex) {
      throw new IOException("Cannot write dependencies to " + outputDepsProtoFile, ex);
    }
  }

  @VisibleForTesting
  Deps.Dependencies buildDependenciesProto(String classpath, boolean successful) {
    Deps.Dependencies.Builder deps = Deps.Dependencies.newBuilder();
    if (targetLabel != null) {
      deps.setRuleLabel(targetLabel);
    }
    deps.setSuccess(successful);
    // Filter using the original classpath, to preserve ordering.
    for (String entry : classpath.split(":")) {
      if (explicitDependenciesMap.containsKey(entry)) {
        deps.addDependency(explicitDependenciesMap.get(entry));
      } else if (implicitDependenciesMap.containsKey(entry)) {
        deps.addDependency(implicitDependenciesMap.get(entry));
      }
    }
    return deps.build();
  }

  /**
   * Returns whether strict dependency checks (strictJavaDeps) are enabled.
   */
  public boolean isStrictDepsEnabled() {
    return strictJavaDeps.isEnabled();
  }

  /**
   * Returns the mapping for jars of direct dependencies. The keys are full
   * paths (as seen on the classpath), and the values are build target names.
   */
  public Map<String, String> getDirectMapping() {
    return directJarsToTargets;
  }

  /**
   * Returns the mapping for jars of indirect dependencies. The keys are full
   * paths (as seen on the classpath), and the values are build target names.
   */
  public Map<String, String> getIndirectMapping() {
    return indirectJarsToTargets;
  }

  /**
   * Returns the strict dependency checking (strictJavaDeps) setting.
   */
  public StrictJavaDeps getStrictJavaDeps() {
    return strictJavaDeps;
  }

  /**
   * Returns the map collecting precise explicit dependency information.
   */
  public Map<String, Deps.Dependency> getExplicitDependenciesMap() {
    return explicitDependenciesMap;
  }

  /**
   * Returns the map collecting precise implicit dependency information.
   */
  public Map<String, Deps.Dependency> getImplicitDependenciesMap() {
    return implicitDependenciesMap;
  }

  /**
   * Returns the type (rule kind) of the originating target.
   */
  public String getRuleKind() {
    return ruleKind;
  }

  /**
   * Returns the name (label) of the originating target.
   */
  public String getTargetLabel() {
    return targetLabel;
  }

  /**
   * Returns the file name collecting dependency information.
   */
  public String getOutputDepsFile() {
    return outputDepsFile;
  }

  @VisibleForTesting
  Set<String> getUsedClasspath() {
    return usedClasspath;
  }

  /**
   * Returns a message to suggest fix when a missing indirect dependency is found.
   */
  public String getFixMessage() {
    return fixMessage;
  }

  /**
   * Returns whether classpath reduction is enabled for this invocation.
   */
  public boolean reduceClasspath() {
    return strictClasspathMode;
  }

  /**
   * Computes a reduced compile-time classpath from the union of direct dependencies and their
   * dependencies, as listed in the associated .deps artifacts.
   */
  public String computeStrictClasspath(String originalClasspath, String classDir)
      throws IOException {
    if (!strictClasspathMode) {
      return originalClasspath;
    }

    // Classpath = direct deps + runtime direct deps + their .deps
    requiredClasspath = new HashSet<>(directJarsToTargets.keySet());

    for (String depsArtifact : depsArtifacts) {
       collectDependenciesFromArtifact(depsArtifact);
    }

    // Filter the initial classpath and keep the original order, with classDir as the last entry.
    StringBuilder sb = new StringBuilder();
    String[] originalClasspathEntries = originalClasspath.split(":");

    for (String entry : originalClasspathEntries) {
      if (requiredClasspath.contains(entry)) {
        sb.append(entry).append(":");
      }
    }
    sb.append(classDir);
    return sb.toString();
  }

  @VisibleForTesting
  void setStrictClasspath(Set<String> strictClasspath) {
    this.requiredClasspath = strictClasspath;
  }

  /**
   * Updates {@link #requiredClasspath} to include dependencies from the given output artifact.
   */
  private void collectDependenciesFromArtifact(String path) throws IOException {
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path))) {
      Deps.Dependencies deps = Deps.Dependencies.parseFrom(bis);
      // Sanity check to make sure we have a valid proto.
      if (!deps.hasRuleLabel()) {
        throw new IOException("Could not parse Deps.Dependencies message from proto.");
      }
      for (Deps.Dependency dep : deps.getDependencyList()) {
        if (dep.getKind() == Kind.EXPLICIT || dep.getKind() == Kind.IMPLICIT) {
          requiredClasspath.add(dep.getPath());
        }
      }
    }
  }

  /**
   * Builder for {@link DependencyModule}.
   */
  public static class Builder {

    private StrictJavaDeps strictJavaDeps = StrictJavaDeps.OFF;
    private final Map<String, String> directJarsToTargets = new HashMap<>();
    private final Map<String, String> indirectJarsToTargets = new HashMap<>();
    private final Set<String> depsArtifacts = new HashSet<>();
    private String ruleKind;
    private String targetLabel;
    private String outputDepsFile;
    private String outputDepsProtoFile;
    private boolean strictClasspathMode = false;
    private String fixMessage = "%s** Please add the following dependencies:%s\n"
        + "  %s to %s\n\n";

    /**
     * Constructs the DependencyModule, guaranteeing that the maps are
     * never null (they may be empty), and the default strictJavaDeps setting
     * is OFF.
     *
     * @return an instance of DependencyModule
     */
    public DependencyModule build() {
      return new DependencyModule(strictJavaDeps, directJarsToTargets, indirectJarsToTargets,
          strictClasspathMode, depsArtifacts, ruleKind, targetLabel, outputDepsFile,
          outputDepsProtoFile, fixMessage);
    }

    /**
     * Sets the strictness level for dependency checking.
     *
     * @param strictJavaDeps level, as specified by {@link StrictJavaDeps}
     * @return this Builder instance
     */
    public Builder setStrictJavaDeps(String strictJavaDeps) {
      this.strictJavaDeps = StrictJavaDeps.valueOf(strictJavaDeps);
      return this;
    }

    /**
     * Sets the type (rule kind) of the originating target.
     *
     * @param ruleKind kind, such as the rule kind of a RuleConfiguredTarget
     * @return this Builder instance
     */
    public Builder setRuleKind(String ruleKind) {
      this.ruleKind = ruleKind;
      return this;
    }

    /**
     * Sets the name (label) of the originating target.
     *
     * @param targetLabel label, such as the label of a RuleConfiguredTarget.
     * @return this Builder instance.
     */
    public Builder setTargetLabel(String targetLabel) {
      this.targetLabel = targetLabel;
      return this;
    }

    /**
     * Adds a direct mapping to the existing map for direct dependencies.
     *
     * @param jar path of jar artifact, as seen on classpath.
     * @param target full name of build target providing the jar.
     * @return this Builder instance.
     */
    public Builder addDirectMapping(String jar, String target) {
      directJarsToTargets.put(jar, target);
      return this;
    }

    /**
     * Adds direct mappings to the existing map for direct dependencies.
     *
     * @param directMappings a map of paths of jar artifacts, as seen on classpath, to full names of
     *     build targets providing the jar.
     * @return this Builder instance
     */
    public Builder addDirectMappings(Map<String, String> directMappings) {
      directJarsToTargets.putAll(directMappings);
      return this;
    }

    /**
     * Adds an indirect mapping to the existing map for indirect dependencies.
     *
     * @param jar path of jar artifact, as seen on classpath.
     * @param target full name of build target providing the jar.
     * @return this Builder instance
     */
    public Builder addIndirectMapping(String jar, String target) {
      indirectJarsToTargets.put(jar, target);
      return this;
    }

    /**
     * Adds indirect mappings to the existing map for indirect dependencies.
     *
     * @param indirectMappings a map of paths of jar artifacts, as seen on classpath, to full names
     *     of build targets providing the jar.
     * @return this Builder instance
     */
    public Builder addIndirectMappings(Map<String, String> indirectMappings) {
      indirectJarsToTargets.putAll(indirectMappings);
      return this;
    }

    /**
     * Sets the name of the file that will contain dependency information.
     *
     * @param outputDepsFile output file name for dependency information
     * @return this Builder instance
     */
    public Builder setOutputDepsFile(String outputDepsFile) {
      this.outputDepsFile = outputDepsFile;
      return this;
    }

    /**
     * Sets the name of the file that will contain dependency information in the protocol buffer
     * format.
     *
     * @param outputDepsProtoFile output file name for dependency information
     * @return this Builder instance
     */
    public Builder setOutputDepsProtoFile(String outputDepsProtoFile) {
      this.outputDepsProtoFile = outputDepsProtoFile;
      return this;
    }

    /**
     * Adds a collection of dependency artifacts to use when reducing the compile-time classpath.
     *
     * @param depsArtifacts dependency artifacts
     * @return this Builder instance
     */
    public Builder addDepsArtifacts(Collection<String> depsArtifacts) {
      this.depsArtifacts.addAll(depsArtifacts);
      return this;
    }

    /**
     * Requests compile-time classpath reduction based on provided dependency artifacts.
     *
     * @return this Builder instance
     */
    public Builder setReduceClasspath() {
      this.strictClasspathMode = true;
      return this;
    }

    /**
     * Set the message to display when a missing indirect dependency is found.
     * 
     * @param fixMessage the fix message
     * @return this Builder instance
     */
    public Builder setFixMessage(String fixMessage) {
      this.fixMessage = fixMessage;
      return this;
    }
  }
}