Don't mistake a refetched repo for an externally modified one (#31108) ### Description After `bazel fetch --force` (or any other command that refetches a repo without loading its packages), the next `bazel build` warned that a file of the repo "has been modified externally" and fetched the repo a second time. The check for external modifications of repo files scanned all `FileStateValue` nodes in the graph via `MemoizingEvaluator#getValues()`, which also returns the last values of nodes that are already dirty. Refetching a repo re-evaluates its `RepositoryDirectoryValue`, which is not comparable and thus dirties the file state nodes of all files in the repo. A build re-evaluates these nodes when it loads the affected packages, but `fetch` doesn't request them and, unlike `BuildTool`, also doesn't delete dirty nodes at the end of the command. The stale nodes then survived until the next build, where the `ExternalDirtinessChecker` compared their old contents proxies with the rewritten files, reported an external modification and deleted the marker file to force another fetch. Only scan nodes that are done: dirty nodes are re-evaluated when they are next requested anyway, so their last values can't be used to detect changes that Skyframe doesn't already know about. ### Motivation Every `bazel fetch --force` was followed by a second fetch of the same repos in the next build, together with a misleading warning about external modifications. ### Build API Changes No ### Release Notes RELNOTES: `bazel fetch --force` no longer causes the fetched repos to be fetched again by the next build with a spurious warning about external modifications. Closes #31108 COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/bazel/pull/31108 from fmeum:fix-stale-external-repo-file-check 739a90bd6f1b0d3d1d9f2ad88078fca2762fe2bf PiperOrigin-RevId: 981245657 Change-Id: I98bc3c1162e55361ca435823c749f25759a0469f
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java index 66c9544..63faf81 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -4101,7 +4101,7 @@ try (SilentCloseable c = Profiler.instance().profile("fsvc.getDirtyKeys")) { batchDirtyResult = fsvc.getDirtyKeys( - memoizingEvaluator.getValues(), + memoizingEvaluator.getDoneValues(), new UnionDirtinessChecker(ImmutableList.copyOf(dirtinessCheckers))); } if (externalDirtinessChecker != null) {
diff --git a/src/test/py/bazel/bzlmod/bazel_fetch_test.py b/src/test/py/bazel/bzlmod/bazel_fetch_test.py index bd37ec6..37ddde6 100644 --- a/src/test/py/bazel/bzlmod/bazel_fetch_test.py +++ b/src/test/py/bazel/bzlmod/bazel_fetch_test.py
@@ -18,6 +18,7 @@ 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 @@ -328,6 +329,55 @@ _, _, stderr = self.RunBazel(['fetch', '--repo=@hello', '--force']) self.assertIn('No more Orange Juice!', ''.join(stderr)) + def testForceFetchDoesNotCauseRefetchInNextBuild(self): + self.ScratchFile( + 'MODULE.bazel', + [ + 'ext = use_extension("extension.bzl", "ext")', + 'use_repo(ext, "hello")', + ], + ) + self.ScratchFile('BUILD') + self.ScratchFile( + 'extension.bzl', + [ + 'def impl(ctx):', + ' print("JUST FETCHED")', + ' ctx.file("BUILD", "filegroup(name = \'lala\')")', + 'repo_rule = repository_rule(implementation=impl)', + '', + 'def _ext_impl(ctx):', + ' repo_rule(name="hello")', + 'ext = module_extension(implementation=_ext_impl)', + ], + ) + + _, _, stderr = self.RunBazel(['build', '@hello//:lala']) + self.assertIn('JUST FETCHED', ''.join(stderr)) + + # A forced fetch rewrites the files of the repo without the build loading + # them again. This must not be mistaken for an external modification. + _, _, stderr = self.RunBazel(['fetch', '--repo=@hello', '--force']) + self.assertIn('JUST FETCHED', ''.join(stderr)) + _, _, stderr = self.RunBazel(['build', '@hello//:lala']) + self.assertNotIn('modified externally', ''.join(stderr)) + self.assertNotIn('JUST FETCHED', ''.join(stderr)) + + # An actual external modification is still detected. + _, stdout, _ = self.RunBazel(['info', 'output_base']) + build_file = os.path.join( + stdout[0].strip(), 'external', '+ext+hello', 'BUILD' + ) + with open(build_file, 'a') as f: + f.write('# modified externally\n') + _, _, stderr = self.RunBazel(['build', '@hello//:lala']) + self.assertIn( + "Repository '@@+ext+hello' will be fetched again since the file 'BUILD'" + ' has been modified externally.', + ''.join(stderr), + ) + self.assertIn('JUST FETCHED', ''.join(stderr)) + def testForceFetchWithRepoCache(self): self.ScratchFile( 'MODULE.bazel',