[8.4.0] Fix runfiles directory staleness issues caused by symlinking the output manifest (#26868)
`SymlinkTreeAction` is only executed for its side effect of updating the
runfiles tree, but its up-to-dateness is tracked solely by the digest of
its output, the manifest under the runfiles directory. If this manifest
is a symlink to the input manifest and a previous build modified the
runfiles directory while not updating the action cache entry for
`SymlinkTreeAction`, this would cause Bazel to consider the action
up-to-date even though the runfiles directory is stale.
The fix consists of two parts:
* Always copy the manifest instead of symlinking it, matching the
behavior of the former `--noexperimental_inprocess_symlink_creation`
flag. This fixes staleness issues when only using versions of Bazel with
this fix.
* Detect stale state left behind by older versions of Bazel by skipping
action cache hits if the output manifest is a symlink. Since this
situation is no longer created by fixed versions of Bazel, we could
consider removing this logic in the future.
Fixes #26818
Closes #26837.
PiperOrigin-RevId: 801798862
Change-Id: I02c6d1b353981249a98df224efc4f46c5e4866d7
(cherry picked from commit 585e16c701a6eef9a0b96cce31cae56893f04278)
Also includes:
* a3eb3516b26ddaa2aa81d15a3cbfb6876a07d359
Fixes #26824
---------
Co-authored-by: Googler <tjgq@google.com>
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeAction.java
index 7f89438..c2d25a6 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeAction.java
@@ -27,6 +27,7 @@
import com.google.devtools.build.lib.actions.ActionResult;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactExpander;
+import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue.RunfileSymlinksMode;
@@ -42,7 +43,7 @@
* trees.
*/
@Immutable
-public final class SymlinkTreeAction extends AbstractAction {
+public final class SymlinkTreeAction extends AbstractAction implements NotifyOnActionCacheHit {
private static final String GUID = "7a16371c-cd4a-494d-b622-963cd89f5212";
@@ -235,4 +236,14 @@
public boolean mayInsensitivelyPropagateInputs() {
return true;
}
+
+ @Override
+ public boolean actionCacheHit(NotifyOnActionCacheHit.ActionCachedContext context) {
+ // Before 8.4.0, Bazel created the output manifest as a symlink to the input manifest, which
+ // makes it infeasible to reliably detect stale runfiles directories, which are only created as
+ // a side effect of this action and not tracked in the action cache.
+ // https://github.com/bazelbuild/bazel/issues/26818
+ // TODO: Consider removing this when Bazel 7 is no longer supported.
+ return !context.getPathResolver().toPath(getPrimaryOutput()).isSymbolicLink();
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java b/src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java
index 38ac043..5a0979b 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java
@@ -22,9 +22,9 @@
import com.google.devtools.build.lib.actions.RunfilesTree;
import com.google.devtools.build.lib.analysis.RunfilesSupport;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
-import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.DigestUtils;
+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.Symlinks;
@@ -127,19 +127,19 @@
// Avoid rebuilding the runfiles directory if the manifest in it matches the input manifest,
// implying the symlinks exist and are already up to date. If the output manifest is a
// symbolic link, it is likely a symbolic link to the input manifest, so we cannot trust it as
- // an up-to-date check.
- // On Windows, where symlinks may be silently replaced by copies, a previous run in SKIP mode
- // could have resulted in an output manifest that is an identical copy of the input manifest,
- // which we must not treat as up to date, but we also don't want to unnecessarily rebuild the
- // runfiles directory all the time. Instead, check for the presence of the first runfile in
- // the manifest. If it is present, we can be certain that the previous mode wasn't SKIP.
+ // an up-to-date check. Ignoring external interference, this situation can only arise if an
+ // older version of Bazel created the runfiles tree - before 8.4.0, Bazel created a symlink.
+ // A previous run in SKIP mode could have resulted in an output manifest that is an
+ // identical copy of the input manifest, which we must not treat as up to date. Since we also
+ // don't want to unnecessarily rebuild the runfiles directory all the time, instead check for
+ // the presence of the first runfile in the manifest. If it is present, we can be certain that
+ // the previous mode wasn't SKIP.
if (tree.getSymlinksMode() != SKIP
&& !outputManifest.isSymbolicLink()
&& Arrays.equals(
DigestUtils.getDigestWithManualFallback(outputManifest, xattrProvider),
DigestUtils.getDigestWithManualFallback(inputManifest, xattrProvider))
- && (OS.getCurrent() != OS.WINDOWS
- || isRunfilesDirectoryPopulated(runfilesDir, outputManifest))) {
+ && isRunfilesDirectoryPopulated(runfilesDir, outputManifest)) {
return;
}
} catch (IOException e) {
@@ -164,7 +164,8 @@
case EXTERNAL -> helper.createSymlinksUsingCommand(binTools, env, outErr);
case INTERNAL -> {
helper.createRunfilesSymlinksDirectly(tree.getMapping());
- outputManifest.createSymbolicLink(inputManifest);
+ // See SymlinkTreeStrategy#createOutput for why we copy instead of hardlink or symlink.
+ FileSystemUtils.copyFile(inputManifest, outputManifest);
}
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
index cb4ee3f..53dd654 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
@@ -165,12 +165,12 @@
}
/**
- * Ensures that the runfiles directory is empty except for the symlinked MANIFEST and the
- * workspace subdirectory. This is the expected state with --noenable_runfiles.
+ * Ensures that the runfiles directory is empty except for the copied MANIFEST and the workspace
+ * subdirectory. This is the expected state with --noenable_runfiles.
*/
public void clearRunfilesDirectory() throws ExecException {
deleteRunfilesDirectory();
- linkManifest();
+ copyManifest();
try {
createWorkspaceSubdirectory();
} catch (IOException e) {
@@ -187,15 +187,15 @@
}
}
- /** Links the output manifest to the input manifest. */
- private void linkManifest() throws ExecException {
- // Pretend we created the runfiles tree by symlinking the output manifest to the input manifest.
+ /** Copies the output manifest to the input manifest. */
+ private void copyManifest() throws ExecException {
+ // Pretend we created the runfiles tree by copying the output manifest to the input manifest.
try {
symlinkTreeRoot.createDirectoryAndParents();
outputManifest.delete();
- outputManifest.createSymbolicLink(inputManifest);
+ FileSystemUtils.copyFile(inputManifest, outputManifest);
} catch (IOException e) {
- throw new EnvironmentalExecException(e, Code.SYMLINK_TREE_MANIFEST_LINK_IO_EXCEPTION);
+ throw new EnvironmentalExecException(e, Code.SYMLINK_TREE_MANIFEST_COPY_IO_EXCEPTION);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
index 219b6dc..13967d2 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
@@ -34,6 +34,7 @@
import com.google.devtools.build.lib.server.FailureDetails.Execution;
import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.OutputService;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
@@ -151,13 +152,16 @@
SymlinkTreeAction action, ActionExecutionContext actionExecutionContext, Path inputManifest)
throws EnvironmentalExecException {
Path outputManifest = actionExecutionContext.getInputPath(action.getOutputManifest());
- // Link output manifest on success. We avoid a file copy as these manifests may be
- // large. Note that this step has to come last because the OutputService may delete any
- // pre-existing symlink tree before creating a new one.
+ // Copy output manifest on success. We can't use a symlink or hardlink here because
+ // SymlinkTreeAction is executed for its side effect (the symlink tree creation) and could
+ // erroneously be considered up-to-date if the input manifest changed back to a different state
+ // that had previously been recorded in the action cache.
+ // Note that this step has to come last because the OutputService may delete any pre-existing
+ // symlink tree before creating a new one.
try {
- outputManifest.createSymbolicLink(inputManifest);
+ FileSystemUtils.copyFile(inputManifest, outputManifest);
} catch (IOException e) {
- throw createLinkFailureException(outputManifest, e);
+ throw createCopyFailureException(outputManifest, e);
}
}
@@ -178,14 +182,15 @@
workspaceName);
}
- private static EnvironmentalExecException createLinkFailureException(
+ private static EnvironmentalExecException createCopyFailureException(
Path outputManifest, IOException e) {
return new EnvironmentalExecException(
e,
FailureDetail.newBuilder()
- .setMessage("Failed to link output manifest '" + outputManifest.getPathString() + "'")
+ .setMessage(
+ "Failed to copy output manifest '%s'".formatted(outputManifest.getPathString()))
.setExecution(
- Execution.newBuilder().setCode(Code.SYMLINK_TREE_MANIFEST_LINK_IO_EXCEPTION))
+ Execution.newBuilder().setCode(Code.SYMLINK_TREE_MANIFEST_COPY_IO_EXCEPTION))
.build());
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java
index aeb2745..853dea4 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java
@@ -723,12 +723,17 @@
@Override
public long getLastModifiedTime() {
- return m.getModifiedTime();
+ try {
+ return m.getModifiedTime();
+ } catch (UnsupportedOperationException e) {
+ // Not every FileArtifactValue supports getModifiedTime.
+ return 0;
+ }
}
@Override
public long getLastChangeTime() {
- return m.getModifiedTime();
+ return getLastModifiedTime();
}
@Override
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index fa8d524..4c9bd6f 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -468,8 +468,7 @@
TEST_OUT_ERR_IO_EXCEPTION = 14 [(metadata) = { exit_code: 36 }];
SYMLINK_TREE_MANIFEST_COPY_IO_EXCEPTION = 15
[(metadata) = { exit_code: 36 }];
- SYMLINK_TREE_MANIFEST_LINK_IO_EXCEPTION = 16
- [(metadata) = { exit_code: 36 }];
+ reserved 16; // was SYMLINK_TREE_MANIFEST_LINK_IO_EXCEPTION
SYMLINK_TREE_CREATION_IO_EXCEPTION = 17 [(metadata) = { exit_code: 36 }];
SYMLINK_TREE_CREATION_COMMAND_EXCEPTION = 18
[(metadata) = { exit_code: 36 }];
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategyTest.java b/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategyTest.java
index 2a55962..ed7ea2c 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategyTest.java
@@ -74,6 +74,8 @@
when(outputService.canCreateSymlinkTree()).thenReturn(true);
Artifact inputManifest = getBinArtifactWithNoOwner("dir/manifest.in");
+ inputManifest.getPath().getParentDirectory().createDirectoryAndParents();
+ FileSystemUtils.createEmptyFile(inputManifest.getPath());
Artifact outputManifest = getBinArtifactWithNoOwner("dir.runfiles/MANIFEST");
Artifact runfile = getBinArtifactWithNoOwner("dir/runfile");
doAnswer(
@@ -137,6 +139,8 @@
when(outputService.canCreateSymlinkTree()).thenReturn(false);
Artifact inputManifest = getBinArtifactWithNoOwner("dir/manifest.in");
+ inputManifest.getPath().getParentDirectory().createDirectoryAndParents();
+ FileSystemUtils.createEmptyFile(inputManifest.getPath());
Artifact outputManifest = getBinArtifactWithNoOwner("dir.runfiles/MANIFEST");
Artifact runfile = getBinArtifactWithNoOwner("dir/runfile");
diff --git a/src/test/shell/bazel/cc_integration_test.sh b/src/test/shell/bazel/cc_integration_test.sh
index 936151b..8459b9a 100755
--- a/src/test/shell/bazel/cc_integration_test.sh
+++ b/src/test/shell/bazel/cc_integration_test.sh
@@ -1004,7 +1004,7 @@
# Null build.
bazel build --experimental_sibling_repository_layout //baz:binary &> "$TEST_log" \
|| fail "expected build success"
- expect_log "INFO: 1 process: 1 internal"
+ expect_log "INFO: 1 process: 1 action cache hit, 1 internal"
}
function test_execroot_sibling_layout_header_scanning_in_external_subpackage() {
diff --git a/src/test/shell/integration/runfiles_test.sh b/src/test/shell/integration/runfiles_test.sh
index 2601ed0..f545f03 100755
--- a/src/test/shell/integration/runfiles_test.sh
+++ b/src/test/shell/integration/runfiles_test.sh
@@ -840,4 +840,140 @@
//pkg:gen >&$TEST_log || fail "build failed"
}
+function setup_runfile_manifest_only_change() {
+ mkdir -p pkg
+ cat > pkg/constants.bzl <<'EOF'
+SYMLINK_PATH = "old_name"
+EOF
+ cat > pkg/defs.bzl <<'EOF'
+load(":constants.bzl", "SYMLINK_PATH")
+
+def _my_script_impl(ctx):
+ out = ctx.actions.declare_file(ctx.label.name)
+ print(SYMLINK_PATH)
+ ctx.actions.write(
+ out,
+ """#!/usr/bin/env sh
+set -eu
+[ -f {} ] || exit 1
+""".format(SYMLINK_PATH),
+ is_executable = True,
+ )
+ runfiles = ctx.runfiles(
+ symlinks = {
+ SYMLINK_PATH: out,
+ },
+ )
+ return [DefaultInfo(executable = out, runfiles = runfiles)]
+
+my_script = rule(
+ implementation = _my_script_impl,
+ executable = True,
+)
+EOF
+ cat > pkg/BUILD <<'EOF'
+load(":defs.bzl", "my_script")
+
+my_script(
+ name = "foo",
+)
+EOF
+}
+
+function test_runfile_manifest_only_change_not_stale_no_action_cache_entry() {
+ if "$is_windows"; then
+ # Can't run shell scripts directly.
+ return
+ fi
+
+ setup_runfile_manifest_only_change
+
+ bazel run //pkg:foo $EXTRA_BUILD_FLAGS >&$TEST_log || fail "first run failed"
+
+ # Modify the symlink path only while not overwriting the existing action cache
+ # entry for the SymlinkTreeAction.
+ inplace-sed 's/old_name/new_name/' pkg/constants.bzl
+ bazel run //pkg:foo --nouse_action_cache --nobuild_runfile_links \
+ $EXTRA_BUILD_FLAGS >&$TEST_log || fail "second run failed"
+
+ # Revert the symlink path.
+ inplace-sed 's/new_name/old_name/' pkg/constants.bzl
+ bazel run //pkg:foo $EXTRA_BUILD_FLAGS >&$TEST_log || fail "third run failed"
+}
+
+function test_runfile_manifest_only_change_not_stale_no_action_cache_entry_nobuild_runfile_links() {
+ if "$is_windows"; then
+ # Can't run shell scripts directly.
+ return
+ fi
+
+ setup_runfile_manifest_only_change
+
+ bazel run //pkg:foo --nobuild_runfile_links $EXTRA_BUILD_FLAGS >&$TEST_log \
+ || fail "first run failed"
+
+ # Modify the symlink path only while not overwriting the existing action cache
+ # entry for the SymlinkTreeAction.
+ inplace-sed 's/old_name/new_name/' pkg/constants.bzl
+ bazel run //pkg:foo --nouse_action_cache --nobuild_runfile_links \
+ $EXTRA_BUILD_FLAGS >&$TEST_log || fail "second run failed"
+
+ # Revert the symlink path.
+ inplace-sed 's/new_name/old_name/' pkg/constants.bzl
+ bazel run //pkg:foo --nobuild_runfile_links $EXTRA_BUILD_FLAGS >&$TEST_log \
+ || fail "third run failed"
+}
+
+function test_runfile_manifest_only_change_not_stale_legacy_bazel_version() {
+ if "$is_windows"; then
+ # Can't run shell scripts directly.
+ return
+ fi
+
+ setup_runfile_manifest_only_change
+
+ bazel run //pkg:foo $EXTRA_BUILD_FLAGS >&$TEST_log || fail "first run failed"
+
+ # Simulate the effect of an old version of Bazel that doesn't override the
+ # action cache entry (because it stores the action cache in a different path)
+ # and also creates the output manifest as a symlink instead of a copy.
+ inplace-sed 's/old_name/new_name/' bazel-bin/pkg/foo${EXT}.runfiles_manifest
+ rm bazel-bin/pkg/foo${EXT}.runfiles/MANIFEST
+ ln -s "$(pwd)/bazel-bin/pkg/foo${EXT}.runfiles_manifest" bazel-bin/pkg/foo${EXT}.runfiles/MANIFEST
+ local -r old_path="$(find bazel-bin/pkg/foo${EXT}.runfiles -name old_name)"
+ local -r new_path="${old_path/old_name/new_name}"
+ mv "$old_path" "$new_path"
+
+ # Revert the symlink path.
+ inplace-sed 's/new_name/old_name/' pkg/constants.bzl
+ bazel run //pkg:foo $EXTRA_BUILD_FLAGS >&$TEST_log || fail "second run failed"
+}
+
+function test_runfile_manifest_only_change_not_stale_legacy_bazel_version_nobuild_runfile_links() {
+ if "$is_windows"; then
+ # Can't run shell scripts directly.
+ return
+ fi
+
+ setup_runfile_manifest_only_change
+
+ bazel run //pkg:foo --nobuild_runfile_links $EXTRA_BUILD_FLAGS >&$TEST_log \
+ || fail "first run failed"
+
+ # Simulate the effect of an old version of Bazel that doesn't override the
+ # action cache entry (because it stores the action cache in a different path)
+ # and also creates the output manifest as a symlink instead of a copy.
+ inplace-sed 's/old_name/new_name/' bazel-bin/pkg/foo${EXT}.runfiles_manifest
+ rm bazel-bin/pkg/foo${EXT}.runfiles/MANIFEST
+ ln -s "$(pwd)/bazel-bin/pkg/foo${EXT}.runfiles_manifest" bazel-bin/pkg/foo${EXT}.runfiles/MANIFEST
+ local -r old_path="$(find bazel-bin/pkg/foo${EXT}.runfiles -name old_name)"
+ local -r new_path="${old_path/old_name/new_name}"
+ mv "$old_path" "$new_path"
+
+ # Revert the symlink path.
+ inplace-sed 's/new_name/old_name/' pkg/constants.bzl
+ bazel run //pkg:foo --nobuild_runfile_links $EXTRA_BUILD_FLAGS >&$TEST_log \
+ || fail "second run failed"
+}
+
run_suite "runfiles"