diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkBaseExternalContext.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkBaseExternalContext.java index 4a540a0ff7c47d..9f332744d7d69e 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkBaseExternalContext.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkBaseExternalContext.java @@ -1189,6 +1189,9 @@ public String readFile(Object path, String watch, StarlarkThread thread) p.toString(), getIdentifyingStringForLogging(), thread.getCallerLocation()); env.getListener().post(w); maybeWatch(p, ShouldWatch.fromString(watch)); + if (p.isDir()) { + throw Starlark.errorf("attempting to read() a directory: %s", p); + } try { return FileSystemUtils.readContent(p.getPath(), ISO_8859_1); } catch (IOException e) { @@ -1274,9 +1277,6 @@ protected void maybeWatch(StarlarkPath starlarkPath, ShouldWatch shouldWatch) if (fileValue == null) { throw new NeedsSkyframeRestartException(); } - if (!fileValue.isFile() || fileValue.isSpecialFile()) { - throw Starlark.errorf("Not a regular file: %s", pair.second.asPath().getPathString()); - } recordedFileInputs.put(pair.first, RepoRecordedInput.File.fileValueToMarkerValue(fileValue)); } catch (IOException e) { @@ -1287,12 +1287,19 @@ protected void maybeWatch(StarlarkPath starlarkPath, ShouldWatch shouldWatch) @StarlarkMethod( name = "watch", doc = - "Tells Bazel to watch for changes to the given file. Any changes to the file will " + "Tells Bazel to watch for changes to the given path, whether or not it exists, or " + + "whether it's a file or a directory. Any changes to the file or directory will " + "invalidate this repository or module extension, and cause it to be refetched or " - + "re-evaluated next time.

Note that attempting to watch files inside the repo " - + "currently being fetched, or inside the working directory of the current module " - + "extension, will result in an error. A module extension attempting to watch a file " - + "outside the current Bazel workspace will also result in an error.", + + "re-evaluated next time.

\"Changes\" include changes to the contents of the file " + + "(if the path is a file); if the path was a file but is now a directory, or vice " + + "versa; and if the path starts or stops existing. Notably, this does not " + + "include changes to any files under the directory if the path is a directory. For " + // TODO: add `watch_dir()` + + "that, use watch_dir() instead.

Note " + + "that attempting to watch paths inside the repo currently being fetched, or inside " + + "the working directory of the current module extension, will result in an error. A " + + "module extension attempting to watch a path outside the current Bazel workspace " + + "will also result in an error.", parameters = { @Param( name = "path", diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkPath.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkPath.java index 8f0193410bbb4d..4823c294c1188a 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkPath.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkPath.java @@ -77,7 +77,6 @@ public String getBasename() { @StarlarkMethod( name = "readdir", - structField = false, doc = "The list of entries in the directory denoted by this path.") public ImmutableList readdir() throws IOException { ImmutableList.Builder builder = ImmutableList.builder(); @@ -118,11 +117,27 @@ public StarlarkPath getChild(Tuple relativePaths) throws EvalException { @StarlarkMethod( name = "exists", structField = true, - doc = "Returns true if the file denoted by this path exists.") + doc = + "Returns true if the file or directory denoted by this path exists.

Note that " + + "accessing this field does not cause the path to be watched. If you'd " + + "like the repo rule or module extension to be sensitive to the path's existence, " + + "use the watch() method on the context object.") public boolean exists() { return path.exists(); } + @StarlarkMethod( + name = "is_dir", + structField = true, + doc = + "Returns true if this path points to a directory.

Note that accessing this field does " + + "not cause the path to be watched. If you'd like the repo rule or module " + + "extension to be sensitive to whether the path is a directory or a file, use the " + + "watch() method on the context object.") + public boolean isDir() { + return path.isDirectory(); + } + @StarlarkMethod( name = "realpath", structField = true, diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContext.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContext.java index 8e051c7981f0dd..6d69087374669d 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContext.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContext.java @@ -198,9 +198,21 @@ private StarlarkPath externalPath(String method, Object pathObject) @ParamType(type = Label.class), @ParamType(type = StarlarkPath.class) }, - doc = "The path of the symlink to create, relative to the repository directory."), + doc = "The path of the symlink to create."), + @Param( + name = "watch_target", + defaultValue = "'auto'", + positional = false, + named = true, + doc = + "whether to watch the symlink target. Can be the string " + + "'yes', 'no', or 'auto'. Passing 'yes' is equivalent to immediately invoking " + + "the watch() method; passing 'no' does " + + "not attempt to watch the path; passing 'auto' will only attempt to watch " + + "the file when it is legal to do so (see watch() docs for more " + + "information."), }) - public void symlink(Object target, Object linkName, StarlarkThread thread) + public void symlink(Object target, Object linkName, String watchTarget, StarlarkThread thread) throws RepositoryFunctionException, EvalException, InterruptedException { StarlarkPath targetPath = getPath("symlink()", target); StarlarkPath linkPath = getPath("symlink()", linkName); @@ -211,6 +223,7 @@ public void symlink(Object target, Object linkName, StarlarkThread thread) getIdentifyingStringForLogging(), thread.getCallerLocation()); env.getListener().post(w); + maybeWatch(targetPath, ShouldWatch.fromString(watchTarget)); try { checkInOutputDirectory("write", linkPath); makeDirectories(linkPath.getPath()); @@ -269,12 +282,25 @@ public void symlink(Object target, Object linkName, StarlarkThread thread) defaultValue = "True", named = true, doc = "set the executable flag on the created file, true by default."), + @Param( + name = "watch_template", + defaultValue = "'auto'", + positional = false, + named = true, + doc = + "whether to watch the template file. Can be the string " + + "'yes', 'no', or 'auto'. Passing 'yes' is equivalent to immediately invoking " + + "the watch() method; passing 'no' does " + + "not attempt to watch the file; passing 'auto' will only attempt to watch " + + "the file when it is legal to do so (see watch() docs for more " + + "information."), }) public void createFileFromTemplate( Object path, Object template, Dict substitutions, // expected Boolean executable, + String watchTemplate, StarlarkThread thread) throws RepositoryFunctionException, EvalException, InterruptedException { StarlarkPath p = getPath("template()", path); @@ -290,6 +316,10 @@ public void createFileFromTemplate( getIdentifyingStringForLogging(), thread.getCallerLocation()); env.getListener().post(w); + if (t.isDir()) { + throw Starlark.errorf("attempting to use a directory as template: %s", t); + } + maybeWatch(t, ShouldWatch.fromString(watchTemplate)); try { checkInOutputDirectory("write", p); makeDirectories(p.getPath()); @@ -385,8 +415,20 @@ public boolean delete(Object pathObject, StarlarkThread thread) named = true, defaultValue = "0", doc = "strip the specified number of leading components from file names."), + @Param( + name = "watch_patch", + defaultValue = "'auto'", + positional = false, + named = true, + doc = + "whether to watch the patch file. Can be the string " + + "'yes', 'no', or 'auto'. Passing 'yes' is equivalent to immediately invoking " + + "the watch() method; passing 'no' does " + + "not attempt to watch the file; passing 'auto' will only attempt to watch " + + "the file when it is legal to do so (see watch() docs for more " + + "information."), }) - public void patch(Object patchFile, StarlarkInt stripI, StarlarkThread thread) + public void patch(Object patchFile, StarlarkInt stripI, String watchPatch, StarlarkThread thread) throws EvalException, RepositoryFunctionException, InterruptedException { int strip = Starlark.toInt(stripI, "strip"); StarlarkPath starlarkPath = getPath("patch()", patchFile); @@ -397,6 +439,10 @@ public void patch(Object patchFile, StarlarkInt stripI, StarlarkThread thread) getIdentifyingStringForLogging(), thread.getCallerLocation()); env.getListener().post(w); + if (starlarkPath.isDir()) { + throw Starlark.errorf("attempting to use a directory as patch file: %s", starlarkPath); + } + maybeWatch(starlarkPath, ShouldWatch.fromString(watchPatch)); try { PatchUtil.apply(starlarkPath.getPath(), strip, workingDirectory); } catch (PatchFailedException e) { @@ -457,12 +503,25 @@ public void patch(Object patchFile, StarlarkInt stripI, StarlarkThread thread) + " any directory prefix adjustment. This can be used to extract archives that" + " contain non-Unicode filenames, or which have files that would extract to" + " the same path on case-insensitive filesystems."), + @Param( + name = "watch_archive", + defaultValue = "'auto'", + positional = false, + named = true, + doc = + "whether to watch the archive file. Can be the string " + + "'yes', 'no', or 'auto'. Passing 'yes' is equivalent to immediately invoking " + + "the watch() method; passing 'no' does " + + "not attempt to watch the file; passing 'auto' will only attempt to watch " + + "the file when it is legal to do so (see watch() docs for more " + + "information."), }) public void extract( Object archive, Object output, String stripPrefix, Dict renameFiles, // expected + String watchArchive, StarlarkThread thread) throws RepositoryFunctionException, InterruptedException, EvalException { StarlarkPath archivePath = getPath("extract()", archive); @@ -471,6 +530,10 @@ public void extract( throw new RepositoryFunctionException( Starlark.errorf("Archive path '%s' does not exist.", archivePath), Transience.TRANSIENT); } + if (archivePath.isDir()) { + throw Starlark.errorf("attempting to extract a directory: %s", archivePath); + } + maybeWatch(archivePath, ShouldWatch.fromString(watchArchive)); StarlarkPath outputPath = getPath("extract()", output); checkInOutputDirectory("write", outputPath); diff --git a/src/main/java/com/google/devtools/build/lib/rules/repository/RepoRecordedInput.java b/src/main/java/com/google/devtools/build/lib/rules/repository/RepoRecordedInput.java index 2168b163068a8a..78ee43978d31de 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/repository/RepoRecordedInput.java +++ b/src/main/java/com/google/devtools/build/lib/rules/repository/RepoRecordedInput.java @@ -194,12 +194,16 @@ public Parser getParser() { /** * Convert to a {@link com.google.devtools.build.lib.actions.FileValue} to a String appropriate - * for placing in a repository marker file. - * - * @param fileValue The value to convert. It must correspond to a regular file. + * for placing in a repository marker file. The file need not exist, and can be a file or a + * directory. */ public static String fileValueToMarkerValue(FileValue fileValue) throws IOException { - Preconditions.checkArgument(fileValue.isFile() && !fileValue.isSpecialFile()); + if (fileValue.isDirectory()) { + return "DIR"; + } + if (!fileValue.exists()) { + return "ENOENT"; + } // Return the file content digest in hex. fileValue may or may not have the digest available. byte[] digest = fileValue.realFileStateValue().getDigest(); if (digest == null) { diff --git a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java index 979cbd9df3b7e6..05dd374c08a588 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java +++ b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java @@ -93,7 +93,7 @@ public final class RepositoryDelegatorFunction implements SkyFunction { // The marker file version is inject in the rule key digest so the rule key is always different // when we decide to update the format. - private static final int MARKER_FILE_VERSION = 5; + private static final int MARKER_FILE_VERSION = 6; // Mapping of rule class name to RepositoryFunction. private final ImmutableMap handlers; diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContextTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContextTest.java index dbe3d0987b38f0..d18e197cf0e4f2 100644 --- a/src/test/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContextTest.java +++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContextTest.java @@ -327,7 +327,7 @@ public void testPatch() throws Exception { StarlarkPath patchFile = context.path("my.patch"); context.createFile( context.path("my.patch"), "--- foo\n+++ foo\n" + ONE_LINE_PATCH, false, true, thread); - context.patch(patchFile, StarlarkInt.of(0), thread); + context.patch(patchFile, StarlarkInt.of(0), "auto", thread); testOutputFile(foo.getPath(), "line one\nline two\n"); } @@ -338,7 +338,7 @@ public void testCannotFindFileToPatch() throws Exception { context.createFile( context.path("my.patch"), "--- foo\n+++ foo\n" + ONE_LINE_PATCH, false, true, thread); try { - context.patch(patchFile, StarlarkInt.of(0), thread); + context.patch(patchFile, StarlarkInt.of(0), "auto", thread); fail("Expected RepositoryFunctionException"); } catch (RepositoryFunctionException ex) { assertThat(ex) @@ -361,7 +361,7 @@ public void testPatchOutsideOfExternalRepository() throws Exception { true, thread); try { - context.patch(patchFile, StarlarkInt.of(0), thread); + context.patch(patchFile, StarlarkInt.of(0), "auto", thread); fail("Expected RepositoryFunctionException"); } catch (RepositoryFunctionException ex) { assertThat(ex) @@ -382,7 +382,7 @@ public void testPatchErrorWasThrown() throws Exception { context.createFile( context.path("my.patch"), "--- foo\n+++ foo\n" + ONE_LINE_PATCH, false, true, thread); try { - context.patch(patchFile, StarlarkInt.of(0), thread); + context.patch(patchFile, StarlarkInt.of(0), "auto", thread); fail("Expected RepositoryFunctionException"); } catch (RepositoryFunctionException ex) { assertThat(ex) @@ -459,7 +459,7 @@ public void testSymlink() throws Exception { setUpContextForRule("test"); context.createFile(context.path("foo"), "foobar", true, true, thread); - context.symlink(context.path("foo"), context.path("bar"), thread); + context.symlink(context.path("foo"), context.path("bar"), "auto", thread); testOutputFile(outputDirectory.getChild("bar"), "foobar"); assertThat(context.path("bar").realpath()).isEqualTo(context.path("foo")); diff --git a/src/test/py/bazel/bzlmod/bazel_fetch_test.py b/src/test/py/bazel/bzlmod/bazel_fetch_test.py index c171234df2071e..a93d99a5008182 100644 --- a/src/test/py/bazel/bzlmod/bazel_fetch_test.py +++ b/src/test/py/bazel/bzlmod/bazel_fetch_test.py @@ -17,6 +17,7 @@ import os import tempfile from absl.testing import absltest + from src.test.py.bazel import test_base from src.test.py.bazel.bzlmod.test_utils import BazelRegistry diff --git a/src/test/shell/bazel/starlark_repository_test.sh b/src/test/shell/bazel/starlark_repository_test.sh index f5826d94f75f86..7134697a91cbc6 100755 --- a/src/test/shell/bazel/starlark_repository_test.sh +++ b/src/test/shell/bazel/starlark_repository_test.sh @@ -501,7 +501,7 @@ function test_starlark_repository_environ() { def _impl(repository_ctx): print(repository_ctx.os.environ["FOO"]) # Symlink so a repository is created - repository_ctx.symlink(repository_ctx.path("$repo2"), repository_ctx.path("")) + repository_ctx.symlink(repository_ctx.path("$repo2"), repository_ctx.path(""), watch_target="no") repo = repository_rule(implementation=_impl, local=False) EOF @@ -544,7 +544,7 @@ EOF def _impl(repository_ctx): print(repository_ctx.os.environ["BAR"]) # Symlink so a repository is created - repository_ctx.symlink(repository_ctx.path("$repo2"), repository_ctx.path("")) + repository_ctx.symlink(repository_ctx.path("$repo2"), repository_ctx.path(""), watch_target="no") repo = repository_rule(implementation=_impl, local=True) EOF BAR=BEZ bazel build @foo//:bar >& $TEST_log || fail "Failed to build" @@ -556,7 +556,7 @@ EOF def _impl(repository_ctx): print(repository_ctx.os.environ["BAZ"]) # Symlink so a repository is created - repository_ctx.symlink(repository_ctx.path("$repo2"), repository_ctx.path("")) + repository_ctx.symlink(repository_ctx.path("$repo2"), repository_ctx.path(""), watch_target="no") repo = repository_rule(implementation=_impl, local=True) EOF BAZ=BOZ bazel build @foo//:bar >& $TEST_log || fail "Failed to build" @@ -2755,7 +2755,7 @@ EOF function test_file_watching_outside_workspace() { # when reading a file outside the Bazel workspace, we should watch it. - local outside_dir="${TEST_TMPDIR}/outside_dir" + local outside_dir=$(mktemp -d "${TEST_TMPDIR}/testXXXXXX") mkdir -p "${outside_dir}" echo nothing > ${outside_dir}/data.txt @@ -2854,4 +2854,150 @@ EOF expect_log "Circular definition of repositories" } +function test_watch_file_status_change() { + local outside_dir=$(mktemp -d "${TEST_TMPDIR}/testXXXXXX") + mkdir -p "${outside_dir}" + echo something > ${outside_dir}/data.txt + + create_new_workspace + cat > MODULE.bazel < r.bzl <& $TEST_log || fail "expected bazel to succeed" + expect_log "I see: something" + + # test that all kinds of transitions between file, dir, and noent are watched + + rm ${outside_dir}/data.txt + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see nothing" + + mkdir ${outside_dir}/data.txt + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see a directory" + + rm -r ${outside_dir}/data.txt + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see nothing" + + echo something again > ${outside_dir}/data.txt + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see: something again" + + rm ${outside_dir}/data.txt + mkdir ${outside_dir}/data.txt + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see a directory" + + rm -r ${outside_dir}/data.txt + echo something yet again > ${outside_dir}/data.txt + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see: something yet again" +} + +function test_watch_file_status_change_dangling_symlink() { + if "$is_windows"; then + # symlinks on Windows... annoying + return + fi + local outside_dir=$(mktemp -d "${TEST_TMPDIR}/testXXXXXX") + mkdir -p "${outside_dir}" + ln -s ${outside_dir}/pointee ${outside_dir}/pointer + + create_new_workspace + cat > MODULE.bazel < r.bzl <& $TEST_log || fail "expected bazel to succeed" + expect_log "I see nothing" + + echo haha > ${outside_dir}/pointee + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see: haha" + + rm ${outside_dir}/pointee + mkdir ${outside_dir}/pointee + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see a directory" +} + +function test_watch_file_status_change_symlink_parent() { + if "$is_windows"; then + # symlinks on Windows... annoying + return + fi + local outside_dir=$(mktemp -d "${TEST_TMPDIR}/testXXXXXX") + mkdir -p "${outside_dir}/a" + + create_new_workspace + cat > MODULE.bazel < r.bzl <& $TEST_log || fail "expected bazel to succeed" + expect_log "I see nothing" + + mkdir -p ${outside_dir}/a/b + echo blah > ${outside_dir}/a/b/c + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see: blah" + + rm -rf ${outside_dir}/a/b + ln -s ${outside_dir}/d ${outside_dir}/a/b + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see nothing" + + mkdir ${outside_dir}/d + echo bleh > ${outside_dir}/d/c + bazel build @r >& $TEST_log || fail "expected bazel to succeed" + expect_log "I see: bleh" +} + run_suite "local repository tests"