Skip to content

Commit c64421b

Browse files
ulfjackCopybara-Service
authored andcommitted
For source directory action inputs, depend on all files in the subtree
This fixes a number of long-standing correctness issues where Bazel did not notice if files within the purview of an action changed. Consider this BUILD rule: genrule( name = "bad", outs = ["ls.txt"], srcs = ["dir"], cmd = "ls -l test/dir > $@", ) If "dir" is a directory, then Bazel does not notice changes to files underneath dir, and also does not give an error message. It only emits a warning like this: "WARNING: /path/to/BUILD:1:1: input 'test/dir' to //test:bad is a directory; dependency checking of directories is unsound" We use a startup flag for rollout since the new code requires additional Skyframe dependencies. Use like this: bazel --host_jvm_args=-DBAZEL_TRACK_SOURCE_DIRECTORIES=1 build ... With this change, Bazel assumes that all the files underneath dir/ are action inputs (i.e., it implicitly globs all the files) and reruns the action whenever any of these files change. The main differences to glob(["dir/**"]) are: - the file names do not have to be valid labels; the label syntax currently restricts the allowed characters to a small subset of UTF-8 (mainly ASCII) - the files cannot be directly referenced from another package unless they are also explicitly mentioned in the package - globbing happens during execution, not loading; this can have a significant effect on performance for packages that have a lot of globs / directory references - globs are evaluated eagerly during loading, even if only a single rule from that package is needed This change does not affect remote execution, which continues to not allow such inputs, although this could be changed subsequently. We may in the future require that directory references in BUILD files end with a trailing forward slash ('/') to indicate this fact to a reader of the BUILD file. PiperOrigin-RevId: 218817101
1 parent 73415f2 commit c64421b

File tree

4 files changed

+154
-4
lines changed

4 files changed

+154
-4
lines changed

src/main/java/com/google/devtools/build/lib/actions/FileArtifactValue.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ public static FileArtifactValue createNormalFile(byte[] digest, long size) {
248248
return createNormalFile(digest, /*proxy=*/ null, size);
249249
}
250250

251+
public static FileArtifactValue createDirectoryWithHash(byte[] digest) {
252+
return new HashedDirectoryArtifactValue(digest);
253+
}
254+
251255
public static FileArtifactValue createDirectory(long mtime) {
252256
return new DirectoryArtifactValue(mtime);
253257
}
@@ -300,6 +304,65 @@ public String toString() {
300304
}
301305
}
302306

307+
private static final class HashedDirectoryArtifactValue extends FileArtifactValue {
308+
private final byte[] digest;
309+
310+
private HashedDirectoryArtifactValue(byte[] digest) {
311+
this.digest = digest;
312+
}
313+
314+
@Override
315+
public FileStateType getType() {
316+
return FileStateType.DIRECTORY;
317+
}
318+
319+
@Nullable
320+
@Override
321+
public byte[] getDigest() {
322+
return digest;
323+
}
324+
325+
@Override
326+
public long getModifiedTime() {
327+
return 0;
328+
}
329+
330+
@Override
331+
public long getSize() {
332+
return 0;
333+
}
334+
335+
@Override
336+
public boolean wasModifiedSinceDigest(Path path) throws IOException {
337+
// TODO(ulfjack): Ideally, we'd attempt to detect intra-build modifications here. I'm
338+
// consciously deferring work here as this code will most likely change again, and we're
339+
// already doing better than before by detecting inter-build modifications.
340+
return false;
341+
}
342+
343+
@Override
344+
public String toString() {
345+
return MoreObjects.toStringHelper(this).add("digest", digest).toString();
346+
}
347+
348+
@Override
349+
public boolean equals(Object o) {
350+
if (this == o) {
351+
return true;
352+
}
353+
if (!(o instanceof HashedDirectoryArtifactValue)) {
354+
return false;
355+
}
356+
HashedDirectoryArtifactValue r = (HashedDirectoryArtifactValue) o;
357+
return Arrays.equals(digest, r.digest);
358+
}
359+
360+
@Override
361+
public int hashCode() {
362+
return Arrays.hashCode(digest);
363+
}
364+
}
365+
303366
private static final class RegularFileArtifactValue extends FileArtifactValue {
304367
private final byte[] digest;
305368
@Nullable private final FileContentsProxy proxy;

src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleAction.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.devtools.build.lib.actions.SpawnResult;
3131
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
3232
import com.google.devtools.build.lib.events.EventHandler;
33+
import com.google.devtools.build.lib.skyframe.TrackSourceDirectoriesFlag;
3334
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
3435
import java.util.List;
3536

@@ -74,7 +75,9 @@ public GenRuleAction(
7475
protected List<SpawnResult> internalExecute(ActionExecutionContext actionExecutionContext)
7576
throws ExecException, InterruptedException {
7677
EventHandler reporter = actionExecutionContext.getEventHandler();
77-
checkInputsForDirectories(reporter, actionExecutionContext.getMetadataProvider());
78+
if (!TrackSourceDirectoriesFlag.trackSourceDirectories()) {
79+
checkInputsForDirectories(reporter, actionExecutionContext.getMetadataProvider());
80+
}
7881
List<SpawnResult> spawnResults = ImmutableList.of();
7982
try {
8083
spawnResults = super.internalExecute(actionExecutionContext);

src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@
3333
import com.google.devtools.build.lib.actions.ArtifactSkyKey;
3434
import com.google.devtools.build.lib.actions.FileArtifactValue;
3535
import com.google.devtools.build.lib.actions.FileValue;
36+
import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversalRoot;
37+
import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode;
3638
import com.google.devtools.build.lib.actions.MissingInputFileException;
3739
import com.google.devtools.build.lib.cmdline.Label;
3840
import com.google.devtools.build.lib.events.Event;
3941
import com.google.devtools.build.lib.events.EventHandler;
42+
import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.RecursiveFilesystemTraversalException;
43+
import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile;
44+
import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.TraversalRequest;
45+
import com.google.devtools.build.lib.util.Fingerprint;
4046
import com.google.devtools.build.lib.util.Pair;
4147
import com.google.devtools.build.lib.vfs.RootedPath;
4248
import com.google.devtools.build.skyframe.SkyFunction;
@@ -73,6 +79,8 @@ public SkyValue compute(SkyKey skyKey, Environment env)
7379
// the above side effect of posting an event to the EventBus. Importantly, that event
7480
// is potentially used to report root causes.
7581
throw new ArtifactFunctionException(e, Transience.TRANSIENT);
82+
} catch (IOException e) {
83+
throw new ArtifactFunctionException(e, Transience.TRANSIENT);
7684
}
7785
}
7886

@@ -217,9 +225,9 @@ public TreeFileArtifact apply(Artifact artifact) {
217225
}
218226

219227
private FileArtifactValue createSourceValue(Artifact artifact, boolean mandatory, Environment env)
220-
throws MissingInputFileException, InterruptedException {
221-
SkyKey fileSkyKey =
222-
FileValue.key(RootedPath.toRootedPath(artifact.getRoot().getRoot(), artifact.getPath()));
228+
throws MissingInputFileException, IOException, InterruptedException {
229+
RootedPath path = RootedPath.toRootedPath(artifact.getRoot().getRoot(), artifact.getPath());
230+
SkyKey fileSkyKey = FileValue.key(path);
223231
FileValue fileValue;
224232
try {
225233
fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class);
@@ -236,6 +244,45 @@ private FileArtifactValue createSourceValue(Artifact artifact, boolean mandatory
236244
throw makeMissingInputFileException(artifact, mandatory, null, env.getListener());
237245
}
238246
}
247+
// For directory artifacts that are not Filesets, we initiate a directory traversal here, and
248+
// compute a hash from the directory structure.
249+
if (fileValue.isDirectory() && TrackSourceDirectoriesFlag.trackSourceDirectories()) {
250+
// We rely on the guarantees of RecursiveFilesystemTraversalFunction for correctness.
251+
//
252+
// This approach may have unexpected interactions with --package_path. In particular, the exec
253+
// root is setup from the loading / analysis phase, and it is now too late to change it;
254+
// therefore, this may traverse a different set of files depending on which targets are built
255+
// at the same time and what the package-path layout is (this may be moot if there is only one
256+
// entry). Or this may return a set of files that's inconsistent with those actually available
257+
// to the action (for local execution).
258+
//
259+
// In the future, we need to make this result the source of truth for the files available to
260+
// the action so that we at least have consistency.
261+
TraversalRequest request = TraversalRequest.create(
262+
DirectTraversalRoot.forRootedPath(path),
263+
/*isRootGenerated=*/ false,
264+
PackageBoundaryMode.CROSS,
265+
/*strictOutputFiles=*/ true,
266+
/*skipTestingForSubpackage=*/ true,
267+
/*errorInfo=*/ null);
268+
RecursiveFilesystemTraversalValue value;
269+
try {
270+
value =
271+
(RecursiveFilesystemTraversalValue) env.getValueOrThrow(
272+
request, RecursiveFilesystemTraversalException.class);
273+
} catch (RecursiveFilesystemTraversalException e) {
274+
throw new IOException(e);
275+
}
276+
if (value == null) {
277+
return null;
278+
}
279+
Fingerprint fp = new Fingerprint();
280+
for (ResolvedFile file : value.getTransitiveFiles()) {
281+
fp.addString(file.getNameInSymlinkTree().getPathString());
282+
fp.addInt(file.getMetadata().hashCode());
283+
}
284+
return FileArtifactValue.createDirectoryWithHash(fp.digestAndReset());
285+
}
239286
try {
240287
return FileArtifactValue.create(artifact, fileValue);
241288
} catch (IOException e) {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2018 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package com.google.devtools.build.lib.skyframe;
15+
16+
/**
17+
* A flag to enable / disable tracking of source directories. Uses a system property which can be
18+
* set via a startup flag. The intention is for this code to be temporary, so I didn't want to add
19+
* a permanent flag to startup options (and there's already --host_jvm_args, which we can use to
20+
* roll this out). The flag affects Skyframe dependencies, so it needs to clear the Skyframe graph -
21+
* the easiest way to do that is to restart the server, which is done when --host_jvm_args changes.
22+
*/
23+
public class TrackSourceDirectoriesFlag {
24+
private static final boolean TRACK_SOURCE_DIRECTORIES;
25+
26+
static {
27+
TRACK_SOURCE_DIRECTORIES = "1".equals(System.getProperty("BAZEL_TRACK_SOURCE_DIRECTORIES"));
28+
}
29+
30+
public static boolean trackSourceDirectories() {
31+
return TRACK_SOURCE_DIRECTORIES;
32+
}
33+
34+
// Private constructor to prevent instantiation.
35+
private TrackSourceDirectoriesFlag() {
36+
}
37+
}

0 commit comments

Comments
 (0)