Skip to content

Commit

Permalink
Add rctx.watch_tree() to watch a directory tree
Browse files Browse the repository at this point in the history
- Added `rctx.watch_tree()` to watch a directory tree, which includes all transitive descendants' names, and if they're files, their contents.
- Added a new SkyFunction DirectoryTreeDigestFunction to do the heavy lifting.
  - In the future, for performance, we could try to get this skyfunction to have a mode where it only digests stat(), to use as heuristics (similar to #21044)

Work towards #20952.
  • Loading branch information
Wyverald committed Feb 15, 2024
1 parent 8874358 commit 01a297c
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/shell",
"//src/main/java/com/google/devtools/build/lib/skyframe:action_environment_function",
"//src/main/java/com/google/devtools/build/lib/skyframe:directory_listing_value",
"//src/main/java/com/google/devtools/build/lib/skyframe:directory_tree_digest_value",
"//src/main/java/com/google/devtools/build/lib/skyframe:ignored_package_prefixes_value",
"//src/main/java/com/google/devtools/build/lib/skyframe:precomputed_value",
"//src/main/java/com/google/devtools/build/lib/starlarkbuildapi/repository",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,16 @@
import com.google.devtools.build.lib.repository.RepositoryFetchProgress;
import com.google.devtools.build.lib.rules.repository.NeedsSkyframeRestartException;
import com.google.devtools.build.lib.rules.repository.RepoRecordedInput;
import com.google.devtools.build.lib.rules.repository.RepoRecordedInput.Dirents;
import com.google.devtools.build.lib.rules.repository.RepoRecordedInput.RepoCacheFriendlyPath;
import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException;
import com.google.devtools.build.lib.rules.repository.WorkspaceAttributeMapper;
import com.google.devtools.build.lib.runtime.ProcessWrapper;
import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutor;
import com.google.devtools.build.lib.skyframe.DirectoryListingValue;
import com.google.devtools.build.lib.skyframe.DirectoryTreeDigestValue;
import com.google.devtools.build.lib.util.StringUtilities;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.lib.vfs.SyscallCache;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
Expand Down Expand Up @@ -84,6 +83,7 @@ public class StarlarkRepositoryContext extends StarlarkBaseExternalContext {
private final StructImpl attrObject;
private final ImmutableSet<PathFragment> ignoredPatterns;
private final SyscallCache syscallCache;
private final HashMap<RepoRecordedInput.DirTree, String> recordedDirTreeInputs = new HashMap<>();

/**
* Create a new context (repository_ctx) object for a Starlark repository rule ({@code rule}
Expand Down Expand Up @@ -137,6 +137,10 @@ protected String getIdentifyingStringForLogging() {
return RepositoryFetchProgress.repositoryFetchContextString(repoName);
}

public ImmutableMap<RepoRecordedInput.DirTree, String> getRecordedDirTreeInputs() {
return ImmutableMap.copyOf(recordedDirTreeInputs);
}

@StarlarkMethod(
name = "name",
structField = true,
Expand Down Expand Up @@ -572,6 +576,49 @@ public void extract(
env.getListener().post(new ExtractProgress(outputPath.getPath().toString()));
}

@StarlarkMethod(
name = "watch_tree",
doc = "watches a tree!",
parameters = {
@Param(
name = "path",
allowedTypes = {
@ParamType(type = String.class),
@ParamType(type = Label.class),
@ParamType(type = StarlarkPath.class)
},
doc = "path of the directory tree to watch."),
})
public void watchTree(Object path)
throws EvalException, InterruptedException, RepositoryFunctionException {
StarlarkPath p = getPath("watch_tree()", path);
if (!p.isDir()) {
throw Starlark.errorf("can't call watch_tree() on non-directory %s", p);
}
RepoCacheFriendlyPath repoCacheFriendlyPath =
toRepoCacheFriendlyPath(p.getPath(), ShouldWatch.YES);
if (repoCacheFriendlyPath == null) {
return;
}
RootedPath rootedPath = repoCacheFriendlyPath.getRootedPath(env, directories);
if (rootedPath == null) {
throw new NeedsSkyframeRestartException();
}
try {
DirectoryTreeDigestValue digestValue =
(DirectoryTreeDigestValue)
env.getValueOrThrow(DirectoryTreeDigestValue.key(rootedPath), IOException.class);
if (digestValue == null) {
throw new NeedsSkyframeRestartException();
}

recordedDirTreeInputs.put(
new RepoRecordedInput.DirTree(repoCacheFriendlyPath), digestValue.hexDigest());
} catch (IOException e) {
throw new RepositoryFunctionException(e, Transience.TRANSIENT);
}
}

@Override
public String toString() {
return "repository_ctx[" + rule.getLabel() + "]";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ private RepositoryDirectoryValue.Builder fetchInternal(
// Modify marker data to include the files/dirents used by the rule's implementation function.
recordedInputValues.putAll(starlarkRepositoryContext.getRecordedFileInputs());
recordedInputValues.putAll(starlarkRepositoryContext.getRecordedDirentsInputs());
recordedInputValues.putAll(starlarkRepositoryContext.getRecordedDirTreeInputs());

// Ditto for environment variables accessed via `getenv`.
for (String envKey : starlarkRepositoryContext.getAccumulatedEnvKeys()) {
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/google/devtools/build/lib/rules/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/skyframe:action_environment_function",
"//src/main/java/com/google/devtools/build/lib/skyframe:client_environment_value",
"//src/main/java/com/google/devtools/build/lib/skyframe:directory_listing_value",
"//src/main/java/com/google/devtools/build/lib/skyframe:directory_tree_digest_value",
"//src/main/java/com/google/devtools/build/lib/skyframe:precomputed_value",
"//src/main/java/com/google/devtools/build/lib/skyframe:repository_mapping_value",
"//src/main/java/com/google/devtools/build/lib/util",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.google.devtools.build.lib.skyframe.ActionEnvironmentFunction;
import com.google.devtools.build.lib.skyframe.ClientEnvironmentValue;
import com.google.devtools.build.lib.skyframe.DirectoryListingValue;
import com.google.devtools.build.lib.skyframe.DirectoryTreeDigestValue;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.skyframe.RepositoryMappingValue;
import com.google.devtools.build.lib.util.Fingerprint;
Expand Down Expand Up @@ -439,6 +440,76 @@ public static String getDirentsMarkerValue(Path path) throws IOException {
}
}

public static final class DirTree extends RepoRecordedInput {
public static final Parser PARSER =
new Parser() {
@Override
public String getPrefix() {
return "DIRTREE";
}

@Override
public RepoRecordedInput parse(String s) {
return new DirTree(RepoCacheFriendlyPath.parse(s));
}
};

private final RepoCacheFriendlyPath path;

public DirTree(RepoCacheFriendlyPath path) {
this.path = path;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof DirTree)) {
return false;
}
DirTree that = (DirTree) o;
return Objects.equals(path, that.path);
}

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

@Override
public String toStringInternal() {
return path.toString();
}

@Override
public Parser getParser() {
return PARSER;
}

@Nullable
@Override
public SkyKey getSkyKey(BlazeDirectories directories) {
return path.getRepoDirSkyKeyOrNull();
}

@Override
public boolean isUpToDate(
Environment env, BlazeDirectories directories, @Nullable String oldValue)
throws InterruptedException {
RootedPath rootedPath = path.getRootedPath(env, directories);
if (rootedPath == null) {
return false;
}
DirectoryTreeDigestValue value =
(DirectoryTreeDigestValue) env.getValue(DirectoryTreeDigestValue.key(rootedPath));
if (value == null) {
return false;
}
return oldValue.equals(value.hexDigest());
}
}

/** Represents an environment variable accessed during the repo fetch. */
public static final class EnvVar extends RepoRecordedInput {
static final Parser PARSER =
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/google/devtools/build/lib/skyframe/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ java_library(
":diff_awareness_manager",
":directory_listing_function",
":directory_listing_state_value",
":directory_tree_digest_function",
":ephemeral_check_if_output_consumed",
":exclusive_test_build_driver_value",
":execution_finished_event",
Expand Down Expand Up @@ -1248,6 +1249,34 @@ java_library(
],
)

java_library(
name = "directory_tree_digest_function",
srcs = ["DirectoryTreeDigestFunction.java"],
deps = [
":directory_listing_value",
":directory_tree_digest_value",
":dirents",
"//src/main/java/com/google/devtools/build/lib/actions:file_metadata",
"//src/main/java/com/google/devtools/build/lib/util",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/skyframe",
"//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
"//third_party:guava",
"//third_party:jsr305",
],
)

java_library(
name = "directory_tree_digest_value",
srcs = ["DirectoryTreeDigestValue.java"],
deps = [
":sky_functions",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
"//third_party:auto_value",
],
)

java_library(
name = "dirents",
srcs = ["Dirents.java"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2024 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.skyframe;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.vfs.Dirent;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.io.IOException;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;

/** A {@link SkyFunction} for {@link DirectoryTreeDigestValue}s. */
public final class DirectoryTreeDigestFunction implements SkyFunction {
@Override
@Nullable
public SkyValue compute(SkyKey skyKey, Environment env)
throws InterruptedException, DirectoryTreeDigestFunctionException {
RootedPath rootedPath = (RootedPath) skyKey.argument();
DirectoryListingValue dirListingValue =
(DirectoryListingValue) env.getValue(DirectoryListingValue.key(rootedPath));
if (dirListingValue == null) {
return null;
}

// Get the names of entries directly in this directory, and sort them. This sets the basis for
// subsequent digests.
ImmutableSet<String> sortedDirents =
StreamSupport.stream(dirListingValue.getDirents().spliterator(), /* parallel= */ false)
.sorted()
.map(Dirent::getName)
.sorted()
.collect(toImmutableSet());

// Turn each entry into a FileValue.
ImmutableList<FileValue> fileValues = getFileValues(env, sortedDirents, rootedPath);
if (fileValues == null) {
return null;
}

// For each entry that is a directory (or a symlink to a directory), find its own
// DirectoryTreeDigestValue.
ImmutableList<String> subDirTreeDigests = getSubDirTreeDigests(env, fileValues);
if (subDirTreeDigests == null) {
return null;
}

// Finally, we're ready to digest everything together!
Fingerprint fp = new Fingerprint();
fp.addStrings(sortedDirents);
fp.addStrings(subDirTreeDigests);
try {
for (FileValue fileValue : fileValues) {
fp.addBoolean(fileValue.isDirectory());
if (!fileValue.isDirectory()) {
byte[] digest = fileValue.realFileStateValue().getDigest();
if (digest == null) {
// Fast digest not available, or it would have been in the FileValue.
digest = fileValue.realRootedPath().asPath().getDigest();
}
fp.addBytes(digest);
}
}
} catch (IOException e) {
throw new DirectoryTreeDigestFunctionException(e);
}

return DirectoryTreeDigestValue.of(fp.hexDigestAndReset());
}

@Nullable
private static ImmutableList<FileValue> getFileValues(
Environment env, ImmutableSet<String> sortedDirents, RootedPath rootedPath)
throws InterruptedException {
ImmutableSet<FileValue.Key> fileValueKeys =
sortedDirents.stream()
.map(
dirent ->
FileValue.key(
RootedPath.toRootedPath(
rootedPath.getRoot(),
rootedPath.getRootRelativePath().getRelative(dirent))))
.collect(toImmutableSet());
SkyframeLookupResult result = env.getValuesAndExceptions(fileValueKeys);
if (env.valuesMissing()) {
return null;
}
ImmutableList<FileValue> fileValues =
fileValueKeys.stream()
.map(result::get)
.map(FileValue.class::cast)
.collect(toImmutableList());
if (env.valuesMissing()) {
return null;
}
return fileValues;
}

@Nullable
private static ImmutableList<String> getSubDirTreeDigests(
Environment env, ImmutableList<FileValue> fileValues) throws InterruptedException {
ImmutableSet<SkyKey> dirTreeDigestValueKeys =
fileValues.stream()
.filter(FileValue::isDirectory)
.map(fv -> DirectoryTreeDigestValue.key(fv.realRootedPath()))
.collect(toImmutableSet());
SkyframeLookupResult result = env.getValuesAndExceptions(dirTreeDigestValueKeys);
if (env.valuesMissing()) {
return null;
}
ImmutableList<String> dirTreeDigests =
dirTreeDigestValueKeys.stream()
.map(result::get)
.map(DirectoryTreeDigestValue.class::cast)
.map(DirectoryTreeDigestValue::hexDigest)
.collect(toImmutableList());
if (env.valuesMissing()) {
return null;
}
return dirTreeDigests;
}

private static final class DirectoryTreeDigestFunctionException extends SkyFunctionException {
public DirectoryTreeDigestFunctionException(IOException e) {
super(e, Transience.TRANSIENT);
}
}
}
Loading

0 comments on commit 01a297c

Please sign in to comment.