diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD index d643bdee4bdbf0..96cbe7e86a2c2a 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD @@ -137,6 +137,7 @@ java_library( ":common", ":inspection", ":module_extension", + ":module_extension_metadata", ":registry", ":repo_rule_creator", "//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories", @@ -168,7 +169,6 @@ java_library( "Discovery.java", "GsonTypeAdapterUtil.java", "ModuleExtensionContext.java", - "ModuleExtensionMetadata.java", "ModuleFileFunction.java", "ModuleFileGlobals.java", "Selection.java", @@ -182,6 +182,7 @@ java_library( ":common", ":exception", ":module_extension", + ":module_extension_metadata", ":registry", ":resolution", "//src/main/java/com/google/devtools/build/docgen/annot", @@ -282,3 +283,24 @@ java_library( "//third_party:jsr305", ], ) + +java_library( + name = "module_extension_metadata", + srcs = [ + "ModuleExtensionMetadata.java", + ], + deps = [ + ":common", + ":module_extension", + "//src/main/java/com/google/devtools/build/docgen/annot", + "//src/main/java/com/google/devtools/build/lib/cmdline", + "//src/main/java/com/google/devtools/build/lib/events", + "//src/main/java/net/starlark/java/annot", + "//src/main/java/net/starlark/java/eval", + "//src/main/java/net/starlark/java/syntax", + "//third_party:auto_value", + "//third_party:gson", + "//third_party:guava", + "//third_party:jsr305", + ], +) diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileValue.java index fc49eaeafde2ea..0f44205600d549 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileValue.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileValue.java @@ -137,7 +137,7 @@ public ImmutableList getModuleExtensionDiff( extDiff.add("One or more files the extension '" + extensionId + "' is using have changed"); } if (!extensionUsages.equals(lockedExtensionUsages)) { - extDiff.add("The usages of the extension '" + extensionId + "' has changed"); + extDiff.add("The usages of the extension '" + extensionId + "' have changed"); } if (!envVariables.equals(lockedExtension.getEnvVariables())) { extDiff.add( diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/LockFileModuleExtension.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/LockFileModuleExtension.java index 1b5eb71d60c35b..9b111974694c19 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/LockFileModuleExtension.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/LockFileModuleExtension.java @@ -19,6 +19,7 @@ import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable; import com.ryanharter.auto.value.gson.GenerateTypeAdapter; +import java.util.Optional; /** * This object serves as a container for the transitive digest (obtained from transitive .bzl files) @@ -33,7 +34,8 @@ public static Builder builder() { return new AutoValue_LockFileModuleExtension.Builder() // TODO(salmasamy) can be removed when updating lockfile version .setEnvVariables(ImmutableMap.of()) - .setAccumulatedFileDigests(ImmutableMap.of()); + .setAccumulatedFileDigests(ImmutableMap.of()) + .setModuleExtensionMetadata(Optional.empty()); } @SuppressWarnings("mutable") @@ -45,6 +47,8 @@ public static Builder builder() { public abstract ImmutableMap getGeneratedRepoSpecs(); + public abstract Optional getModuleExtensionMetadata(); + public abstract Builder toBuilder(); /** Builder type for {@link LockFileModuleExtension}. */ @@ -59,6 +63,8 @@ public abstract static class Builder { public abstract Builder setGeneratedRepoSpecs(ImmutableMap value); + public abstract Builder setModuleExtensionMetadata(Optional value); + public abstract LockFileModuleExtension build(); } } diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionMetadata.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionMetadata.java index a161cf0cff2e59..443a71c86b69db 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionMetadata.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionMetadata.java @@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; import static java.util.stream.Collectors.joining; +import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; @@ -27,6 +28,7 @@ import com.google.devtools.build.lib.cmdline.RepositoryName; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; +import com.ryanharter.auto.value.gson.GenerateTypeAdapter; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; @@ -49,24 +51,29 @@ doc = "Return values of this type from a module extension's implementation function to " + "provide metadata about the repositories generated by the extension to Bazel.") -public class ModuleExtensionMetadata implements StarlarkValue { - @Nullable private final ImmutableSet explicitRootModuleDirectDeps; - @Nullable private final ImmutableSet explicitRootModuleDirectDevDeps; - private final UseAllRepos useAllRepos; +@AutoValue +@GenerateTypeAdapter +public abstract class ModuleExtensionMetadata implements StarlarkValue { + @Nullable + abstract ImmutableSet getExplicitRootModuleDirectDeps(); - private ModuleExtensionMetadata( + @Nullable + abstract ImmutableSet getExplicitRootModuleDirectDevDeps(); + + abstract UseAllRepos getUseAllRepos(); + + private static ModuleExtensionMetadata create( @Nullable Set explicitRootModuleDirectDeps, @Nullable Set explicitRootModuleDirectDevDeps, UseAllRepos useAllRepos) { - this.explicitRootModuleDirectDeps = + return new AutoValue_ModuleExtensionMetadata( explicitRootModuleDirectDeps != null ? ImmutableSet.copyOf(explicitRootModuleDirectDeps) - : null; - this.explicitRootModuleDirectDevDeps = + : null, explicitRootModuleDirectDevDeps != null ? ImmutableSet.copyOf(explicitRootModuleDirectDevDeps) - : null; - this.useAllRepos = useAllRepos; + : null, + useAllRepos); } static ModuleExtensionMetadata create( @@ -76,19 +83,19 @@ static ModuleExtensionMetadata create( throws EvalException { if (rootModuleDirectDepsUnchecked == Starlark.NONE && rootModuleDirectDevDepsUnchecked == Starlark.NONE) { - return new ModuleExtensionMetadata(null, null, UseAllRepos.NO); + return create(null, null, UseAllRepos.NO); } // When root_module_direct_deps = "all", accept both root_module_direct_dev_deps = None and // root_module_direct_dev_deps = [], but not root_module_direct_dev_deps = ["some_repo"]. if (rootModuleDirectDepsUnchecked.equals("all") && rootModuleDirectDevDepsUnchecked.equals(StarlarkList.immutableOf())) { - return new ModuleExtensionMetadata(null, null, UseAllRepos.REGULAR); + return create(null, null, UseAllRepos.REGULAR); } if (rootModuleDirectDevDepsUnchecked.equals("all") && rootModuleDirectDepsUnchecked.equals(StarlarkList.immutableOf())) { - return new ModuleExtensionMetadata(null, null, UseAllRepos.DEV); + return create(null, null, UseAllRepos.DEV); } if (rootModuleDirectDepsUnchecked.equals("all") @@ -146,8 +153,7 @@ static ModuleExtensionMetadata create( } } - return new ModuleExtensionMetadata( - explicitRootModuleDirectDeps, explicitRootModuleDirectDevDeps, UseAllRepos.NO); + return create(explicitRootModuleDirectDeps, explicitRootModuleDirectDevDeps, UseAllRepos.NO); } public void evaluate( @@ -358,10 +364,10 @@ private static Optional makeUseRepoCommand( private Optional> getRootModuleDirectDeps(Set allRepos) throws EvalException { - switch (useAllRepos) { + switch (getUseAllRepos()) { case NO: - if (explicitRootModuleDirectDeps != null) { - Set invalidRepos = Sets.difference(explicitRootModuleDirectDeps, allRepos); + if (getExplicitRootModuleDirectDeps() != null) { + Set invalidRepos = Sets.difference(getExplicitRootModuleDirectDeps(), allRepos); if (!invalidRepos.isEmpty()) { throw Starlark.errorf( "root_module_direct_deps contained the following repositories " @@ -369,7 +375,7 @@ private Optional> getRootModuleDirectDeps(Set allRe String.join(", ", invalidRepos)); } } - return Optional.ofNullable(explicitRootModuleDirectDeps); + return Optional.ofNullable(getExplicitRootModuleDirectDeps()); case REGULAR: return Optional.of(ImmutableSet.copyOf(allRepos)); case DEV: @@ -380,10 +386,11 @@ private Optional> getRootModuleDirectDeps(Set allRe private Optional> getRootModuleDirectDevDeps(Set allRepos) throws EvalException { - switch (useAllRepos) { + switch (getUseAllRepos()) { case NO: - if (explicitRootModuleDirectDevDeps != null) { - Set invalidRepos = Sets.difference(explicitRootModuleDirectDevDeps, allRepos); + if (getExplicitRootModuleDirectDevDeps() != null) { + Set invalidRepos = + Sets.difference(getExplicitRootModuleDirectDevDeps(), allRepos); if (!invalidRepos.isEmpty()) { throw Starlark.errorf( "root_module_direct_dev_deps contained the following " @@ -391,7 +398,7 @@ private Optional> getRootModuleDirectDevDeps(Set al String.join(", ", invalidRepos)); } } - return Optional.ofNullable(explicitRootModuleDirectDevDeps); + return Optional.ofNullable(getExplicitRootModuleDirectDevDeps()); case REGULAR: return Optional.of(ImmutableSet.of()); case DEV: @@ -400,7 +407,7 @@ private Optional> getRootModuleDirectDevDeps(Set al throw new IllegalStateException("not reached"); } - private enum UseAllRepos { + enum UseAllRepos { NO, REGULAR, DEV, diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionUsage.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionUsage.java index c84f87d62529f2..9d186432afef10 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionUsage.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionUsage.java @@ -80,6 +80,8 @@ public abstract class ModuleExtensionUsage { */ public abstract boolean getHasNonDevUseExtension(); + public abstract Builder toBuilder(); + public static Builder builder() { return new AutoValue_ModuleExtensionUsage.Builder(); } diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionEvalFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionEvalFunction.java index 7f56efc2340aa4..fc2e2fc3fa14d5 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionEvalFunction.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionEvalFunction.java @@ -19,10 +19,12 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Maps; import com.google.devtools.build.lib.actions.FileValue; import com.google.devtools.build.lib.analysis.BlazeDirectories; import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode; @@ -57,6 +59,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nullable; @@ -190,9 +193,14 @@ public SkyValue compute(SkyKey skyKey, Environment env) } ImmutableMap generatedRepoSpecs = moduleExtensionResult.getGeneratedRepoSpecs(); - // Check that all imported repos have been actually generated - validateAllImportsAreGenerated(generatedRepoSpecs, usagesValue, extensionId); - + Optional moduleExtensionMetadata = + moduleExtensionResult.getModuleExtensionMetadata(); + + // At this point the extension has been evaluated successfully, but SingleExtensionEvalFunction + // may still fail if imported repositories were not generated. However, since imports do not + // influence the evaluation of the extension and the validation also runs when the extension + // result is taken from the lockfile, we can already post the update event. This is necessary to + // prevent the extension from rerunning when only the imports change. if (lockfileMode.equals(LockfileMode.UPDATE)) { env.getListener() .post( @@ -203,9 +211,11 @@ public SkyValue compute(SkyKey skyKey, Environment env) .setAccumulatedFileDigests(moduleExtensionResult.getAccumulatedFileDigests()) .setEnvVariables(extensionEnvVars) .setGeneratedRepoSpecs(generatedRepoSpecs) + .setModuleExtensionMetadata(moduleExtensionMetadata) .build())); } - return createSingleExtentionValue(generatedRepoSpecs, usagesValue); + return validateAndCreateSingleExtensionEvalValue( + generatedRepoSpecs, moduleExtensionMetadata, extensionId, usagesValue, env); } @Nullable @@ -247,12 +257,22 @@ private SingleExtensionEvalValue tryGettingValueFromLockFile( return null; } - // Check extension data in lockfile still valid + // Check extension data in lockfile is still valid, disregarding usage information that is not + // relevant for the evaluation of the extension. + ImmutableMap trimmedLockedUsages = + trimUsagesForEvaluation(lockedExtensionUsages); + ImmutableMap trimmedUsages = + trimUsagesForEvaluation(usagesValue.getExtensionUsages()); if (!filesChanged && Arrays.equals(bzlTransitiveDigest, lockedExtension.getBzlTransitiveDigest()) - && usagesValue.getExtensionUsages().equals(lockedExtensionUsages) + && trimmedUsages.equals(trimmedLockedUsages) && envVariables.equals(lockedExtension.getEnvVariables())) { - return createSingleExtentionValue(lockedExtension.getGeneratedRepoSpecs(), usagesValue); + return validateAndCreateSingleExtensionEvalValue( + lockedExtension.getGeneratedRepoSpecs(), + lockedExtension.getModuleExtensionMetadata(), + extensionId, + usagesValue, + env); } else if (lockfileMode.equals(LockfileMode.ERROR)) { ImmutableList extDiff = lockfile.getModuleExtensionDiff( @@ -260,8 +280,8 @@ private SingleExtensionEvalValue tryGettingValueFromLockFile( bzlTransitiveDigest, filesChanged, envVariables, - usagesValue.getExtensionUsages(), - lockedExtensionUsages); + trimmedUsages, + trimmedLockedUsages); throw new SingleExtensionEvalFunctionException( ExternalDepsException.withMessage( Code.BAD_MODULE, @@ -309,24 +329,46 @@ private Boolean didFilesChange( return false; } - private SingleExtensionEvalValue createSingleExtentionValue( - ImmutableMap generatedRepoSpecs, SingleExtensionUsagesValue usagesValue) { - return SingleExtensionEvalValue.create( - generatedRepoSpecs, - generatedRepoSpecs.keySet().stream() - .collect( - toImmutableBiMap( - e -> - RepositoryName.createUnvalidated( - usagesValue.getExtensionUniqueName() + "~" + e), - Function.identity()))); - } - - private void validateAllImportsAreGenerated( + /** + * Validates the result of the module extension evaluation against the declared imports, throwing + * an exception if validation fails, and returns a SingleExtensionEvalValue otherwise. + * + *

Since extension evaluation does not depend on the declared imports, the result of the + * evaluation of the extension implementation function can be reused and persisted in the lockfile + * even if validation fails. + */ + private SingleExtensionEvalValue validateAndCreateSingleExtensionEvalValue( ImmutableMap generatedRepoSpecs, + Optional moduleExtensionMetadata, + ModuleExtensionId extensionId, SingleExtensionUsagesValue usagesValue, - ModuleExtensionId extensionId) + Environment env) throws SingleExtensionEvalFunctionException { + // Evaluate the metadata before failing on invalid imports so that fixup warning are still + // emitted in case of an error. + if (moduleExtensionMetadata.isPresent()) { + try { + // TODO: ModuleExtensionMetadata#evaluate should throw ExternalDepsException instead of + // EvalException. + moduleExtensionMetadata + .get() + .evaluate( + usagesValue.getExtensionUsages().values(), + generatedRepoSpecs.keySet(), + env.getListener()); + } catch (EvalException e) { + env.getListener().handle(Event.error(e.getMessageWithStack())); + throw new SingleExtensionEvalFunctionException( + ExternalDepsException.withMessage( + Code.BAD_MODULE, + "error evaluating module extension %s in %s", + extensionId.getExtensionName(), + extensionId.getBzlFileLabel()), + Transience.TRANSIENT); + } + } + + // Check that all imported repos have actually been generated. for (ModuleExtensionUsage usage : usagesValue.getExtensionUsages().values()) { for (Entry repoImport : usage.getImports().entrySet()) { if (!generatedRepoSpecs.containsKey(repoImport.getValue())) { @@ -345,6 +387,43 @@ private void validateAllImportsAreGenerated( } } } + + return SingleExtensionEvalValue.create( + generatedRepoSpecs, + generatedRepoSpecs.keySet().stream() + .collect( + toImmutableBiMap( + e -> + RepositoryName.createUnvalidated( + usagesValue.getExtensionUniqueName() + "~" + e), + Function.identity()))); + } + + /** + * Returns usages with all information removed that does not influence the evaluation of the + * extension. + */ + private static ImmutableMap trimUsagesForEvaluation( + Map usages) { + return ImmutableMap.copyOf( + Maps.transformValues( + usages, + usage -> + // We start with the full usage and selectively remove information that does not + // influence the evaluation of the extension. Compared to explicitly copying over + // the parts that do, this preserves correctness in case new fields are added to + // ModuleExtensionUsage without updating this code. + usage.toBuilder() + // Locations are only used for error reporting and thus don't influence whether + // the evaluation of the extension is successful and what its result is + // in case of success. + .setLocation(Location.BUILTIN) + // Extension implementation functions do not see the imports, they are only + // validated against the set of generated repos in a validation step that comes + // afterward. + .setImports(ImmutableBiMap.of()) + .setDevImports(ImmutableSet.of()) + .build())); } private BzlLoadValue loadBzlFile( @@ -401,6 +480,7 @@ private RunModuleExtensionResult runModuleExtension( directories, env.getListener()); ModuleExtensionContext moduleContext; + Optional moduleExtensionMetadata; try (Mutability mu = Mutability.create("module extension", usagesValue.getExtensionUniqueName())) { StarlarkThread thread = new StarlarkThread(mu, starlarkSemantics); @@ -422,11 +502,9 @@ private RunModuleExtensionResult runModuleExtension( Transience.PERSISTENT); } if (returnValue instanceof ModuleExtensionMetadata) { - ModuleExtensionMetadata metadata = (ModuleExtensionMetadata) returnValue; - metadata.evaluate( - usagesValue.getExtensionUsages().values(), - threadContext.getGeneratedRepoSpecs().keySet(), - env.getListener()); + moduleExtensionMetadata = Optional.of((ModuleExtensionMetadata) returnValue); + } else { + moduleExtensionMetadata = Optional.empty(); } } catch (NeedsSkyframeRestartException e) { // Clean up and restart by returning null. @@ -456,7 +534,9 @@ private RunModuleExtensionResult runModuleExtension( } } return RunModuleExtensionResult.create( - moduleContext.getAccumulatedFileDigests(), threadContext.getGeneratedRepoSpecs()); + moduleContext.getAccumulatedFileDigests(), + threadContext.getGeneratedRepoSpecs(), + moduleExtensionMetadata); } private ModuleExtensionContext createContext( @@ -517,13 +597,14 @@ abstract static class RunModuleExtensionResult { abstract ImmutableMap getGeneratedRepoSpecs(); + abstract Optional getModuleExtensionMetadata(); + static RunModuleExtensionResult create( ImmutableMap accumulatedFileDigests, - ImmutableMap generatedRepoSpecs) { + ImmutableMap generatedRepoSpecs, + Optional moduleExtensionMetadata) { return new AutoValue_SingleExtensionEvalFunction_RunModuleExtensionResult( - accumulatedFileDigests, generatedRepoSpecs); + accumulatedFileDigests, generatedRepoSpecs, moduleExtensionMetadata); } } } - - diff --git a/src/test/py/bazel/bzlmod/bazel_lockfile_test.py b/src/test/py/bazel/bzlmod/bazel_lockfile_test.py index b88881661a0c8d..6053cc8d5d6eba 100644 --- a/src/test/py/bazel/bzlmod/bazel_lockfile_test.py +++ b/src/test/py/bazel/bzlmod/bazel_lockfile_test.py @@ -839,6 +839,77 @@ def testOldVersion(self): data = json.load(json_file) self.assertEqual(data['lockFileVersion'], 1) + def testExtensionEvaluationDoesNotRerunOnChangedImports(self): + self.ScratchFile( + 'MODULE.bazel', + [ + 'lockfile_ext = use_extension("extension.bzl", "lockfile_ext")', + 'use_repo(lockfile_ext, "dep", "indirect_dep", "invalid_dep")', + ], + ) + self.ScratchFile('BUILD.bazel') + self.ScratchFile( + 'extension.bzl', + [ + 'def _repo_rule_impl(ctx):', + ' ctx.file("WORKSPACE")', + ' ctx.file("BUILD", "filegroup(name=\'lala\')")', + '', + 'repo_rule = repository_rule(implementation=_repo_rule_impl)', + '', + 'def _module_ext_impl(ctx):', + ' print("I am being evaluated")', + ' repo_rule(name="dep")', + ' repo_rule(name="missing_dep")', + ' repo_rule(name="indirect_dep")', + ' return ctx.extension_metadata(', + ' root_module_direct_deps=["dep", "missing_dep"],', + ' root_module_direct_dev_deps=[],', + ' )', + '', + 'lockfile_ext = module_extension(', + ' implementation=_module_ext_impl', + ')', + ], + ) + + # The build fails due to the "invalid_dep" import, which is not generated by the extension. + # Warnings should still be shown. + _, _, stderr = self.RunBazel(['build', '@dep//:all'], allow_failure = True) + stderr = '\n'.join(stderr) + self.assertIn('I am being evaluated', stderr) + self.assertIn('Imported, but not created by the extension (will cause the build to fail):\ninvalid_dep', stderr) + self.assertIn('Not imported, but reported as direct dependencies by the extension (may cause the build to fail):\nmissing_dep', stderr) + self.assertIn('Imported, but reported as indirect dependencies by the extension:\nindirect_dep', stderr) + self.assertIn('ERROR: module extension "lockfile_ext" from "//:extension.bzl" does not generate repository "invalid_dep"', stderr) + + # Shut down bazel to empty cache and run with no changes to verify that the warnings are still shown. + self.RunBazel(['shutdown']) + _, _, stderr = self.RunBazel(['build', '@dep//:all'], allow_failure = True) + stderr = '\n'.join(stderr) + self.assertNotIn('I am being evaluated', stderr) + self.assertIn('Imported, but not created by the extension (will cause the build to fail):\ninvalid_dep', stderr) + self.assertIn('Not imported, but reported as direct dependencies by the extension (may cause the build to fail):\nmissing_dep', stderr) + self.assertIn('Imported, but reported as indirect dependencies by the extension:\nindirect_dep', stderr) + self.assertIn('ERROR: module extension "lockfile_ext" from "//:extension.bzl" does not generate repository "invalid_dep"', stderr) + + # Fix the imports, which should not trigger a rerun of the extension even though imports and locations changed. + self.ScratchFile( + 'MODULE.bazel', + [ + '# This is a comment that changes the location of the usage below.', + 'lockfile_ext = use_extension("extension.bzl", "lockfile_ext")', + 'use_repo(lockfile_ext, "dep", "missing_dep")', + ], + ) + _, _, stderr = self.RunBazel(['build', '@dep//:all']) + stderr = '\n'.join(stderr) + self.assertNotIn('I am being evaluated', stderr) + self.assertNotIn('Not imported, but reported as direct dependencies by the extension (may cause the build to fail):\nmissing_dep', stderr) + self.assertNotIn('Imported, but reported as indirect dependencies by the extension:\nindirect_dep', stderr) + self.assertNotIn('Imported, but reported as indirect dependencies by the extension:\nindirect_dep', stderr) + self.assertNotIn('ERROR: module extension "lockfile_ext" from "//:extension.bzl" does not generate repository "invalid_dep"', stderr) + if __name__ == '__main__': unittest.main()