Add a new `include()` directive to MODULE.bazel files
This new directive allows the root module to divide its `MODULE.bazel` into multiple segments. This directive can only be used by root modules; only files in the main repo may be included; variable bindings are only visible in the file they occur in, not in any included or including files. See the docs for `include()` (in `ModuleFileGlobals.java`) for more details.
In follow-ups, we'll need to address:
1. Enforcing the loaded files to have some sort of naming format (tentatively `foo.MODULE.bazel` where `foo` is anything)
2. Making `bazel mod tidy` work with included files
RELNOTES: Added a new `include()` directive to `MODULE.bazel` files.
Fixes https://github.com/bazelbuild/bazel/issues/17880.
Closes #21855.
PiperOrigin-RevId: 627034184
Change-Id: Ifc2f616cf0791445daeeac9ca5ec4478e83382aa
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 32146c8..750742a 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
@@ -123,6 +123,7 @@
"BazelModuleResolutionEvent.java",
"BazelModuleResolutionValue.java",
"BzlmodFlagsAndEnvVars.java",
+ "CompiledModuleFile.java",
"GitOverride.java",
"InterimModule.java",
"LocalPathOverride.java",
@@ -149,6 +150,7 @@
],
deps = [
":common",
+ ":exception",
":inspection",
":module_extension",
":module_extension_metadata",
@@ -169,6 +171,7 @@
"//src/main/java/net/starlark/java/annot",
"//src/main/java/net/starlark/java/eval",
"//src/main/java/net/starlark/java/syntax",
+ "//src/main/protobuf:failure_details_java_proto",
"//third_party:auto_value",
"//third_party:gson",
"//third_party:guava",
@@ -232,6 +235,8 @@
"//src/main/java/com/google/devtools/build/lib/skyframe:bzl_load_value",
"//src/main/java/com/google/devtools/build/lib/skyframe:client_environment_function",
"//src/main/java/com/google/devtools/build/lib/skyframe:client_environment_value",
+ "//src/main/java/com/google/devtools/build/lib/skyframe:package_lookup_function",
+ "//src/main/java/com/google/devtools/build/lib/skyframe:package_lookup_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/skyframe:skyframe_cluster",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyFunction.java
index 6cdf50a..7f9e184 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyFunction.java
@@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.RepositoryName;
@@ -50,6 +51,11 @@
@Nullable
public SkyValue compute(SkyKey skyKey, Environment env)
throws InterruptedException, SkyFunctionException {
+ RootModuleFileValue rootModuleFileValue =
+ (RootModuleFileValue) env.getValue(ModuleFileValue.KEY_FOR_ROOT_MODULE);
+ if (rootModuleFileValue == null) {
+ return null;
+ }
BazelDepGraphValue depGraphValue = (BazelDepGraphValue) env.getValue(BazelDepGraphValue.KEY);
if (depGraphValue == null) {
return null;
@@ -112,6 +118,7 @@
return BazelModTidyValue.create(
buildozer.asPath(),
+ rootModuleFileValue.getIncludeLabelToCompiledModuleFile(),
MODULE_OVERRIDES.get(env),
IGNORE_DEV_DEPS.get(env),
LOCKFILE_MODE.get(env),
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyValue.java
index 281d45c..cfd34f1 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyValue.java
@@ -35,6 +35,8 @@
/** The path of the buildozer binary provided by the "buildozer" module. */
public abstract Path buildozer();
+ public abstract ImmutableMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile();
+
/** The value of {@link ModuleFileFunction#MODULE_OVERRIDES}. */
public abstract ImmutableMap<String, ModuleOverride> moduleOverrides();
@@ -52,12 +54,14 @@
static BazelModTidyValue create(
Path buildozer,
+ ImmutableMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile,
Map<String, ModuleOverride> moduleOverrides,
boolean ignoreDevDeps,
LockfileMode lockfileMode,
StarlarkSemantics starlarkSemantics) {
return new AutoValue_BazelModTidyValue(
buildozer,
+ includeLabelToCompiledModuleFile,
ImmutableMap.copyOf(moduleOverrides),
ignoreDevDeps,
lockfileMode,
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/CompiledModuleFile.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/CompiledModuleFile.java
new file mode 100644
index 0000000..8d5b57e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/CompiledModuleFile.java
@@ -0,0 +1,170 @@
+// 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.bazel.bzlmod;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.packages.BazelStarlarkEnvironment;
+import com.google.devtools.build.lib.packages.DotBazelFileSyntaxChecker;
+import com.google.devtools.build.lib.server.FailureDetails.ExternalDeps.Code;
+import net.starlark.java.eval.EvalException;
+import net.starlark.java.eval.Module;
+import net.starlark.java.eval.Starlark;
+import net.starlark.java.eval.StarlarkSemantics;
+import net.starlark.java.eval.StarlarkThread;
+import net.starlark.java.syntax.Argument;
+import net.starlark.java.syntax.AssignmentStatement;
+import net.starlark.java.syntax.CallExpression;
+import net.starlark.java.syntax.DotExpression;
+import net.starlark.java.syntax.ExpressionStatement;
+import net.starlark.java.syntax.Identifier;
+import net.starlark.java.syntax.Location;
+import net.starlark.java.syntax.ParserInput;
+import net.starlark.java.syntax.Program;
+import net.starlark.java.syntax.StarlarkFile;
+import net.starlark.java.syntax.StringLiteral;
+import net.starlark.java.syntax.SyntaxError;
+
+/**
+ * Represents a compiled MODULE.bazel file, ready to be executed on a {@link StarlarkThread}. It's
+ * been successfully checked for syntax errors.
+ *
+ * <p>Use the {@link #parseAndCompile} factory method instead of directly instantiating this record.
+ */
+public record CompiledModuleFile(
+ ModuleFile moduleFile,
+ Program program,
+ Module predeclaredEnv,
+ ImmutableList<IncludeStatement> includeStatements) {
+ public static final String INCLUDE_IDENTIFIER = "include";
+
+ record IncludeStatement(String includeLabel, Location location) {}
+
+ /** Parses and compiles a given module file, checking it for syntax errors. */
+ public static CompiledModuleFile parseAndCompile(
+ ModuleFile moduleFile,
+ ModuleKey moduleKey,
+ StarlarkSemantics starlarkSemantics,
+ BazelStarlarkEnvironment starlarkEnv,
+ ExtendedEventHandler eventHandler)
+ throws ExternalDepsException {
+ StarlarkFile starlarkFile =
+ StarlarkFile.parse(ParserInput.fromUTF8(moduleFile.getContent(), moduleFile.getLocation()));
+ if (!starlarkFile.ok()) {
+ Event.replayEventsOn(eventHandler, starlarkFile.errors());
+ throw ExternalDepsException.withMessage(
+ Code.BAD_MODULE, "error parsing MODULE.bazel file for %s", moduleKey);
+ }
+ try {
+ ImmutableList<IncludeStatement> includeStatements = checkModuleFileSyntax(starlarkFile);
+ Module predeclaredEnv =
+ Module.withPredeclared(starlarkSemantics, starlarkEnv.getModuleBazelEnv());
+ Program program = Program.compileFile(starlarkFile, predeclaredEnv);
+ return new CompiledModuleFile(moduleFile, program, predeclaredEnv, includeStatements);
+ } catch (SyntaxError.Exception e) {
+ Event.replayEventsOn(eventHandler, e.errors());
+ throw ExternalDepsException.withMessage(
+ Code.BAD_MODULE, "syntax error in MODULE.bazel file for %s", moduleKey);
+ }
+ }
+
+ /**
+ * Checks the given `starlarkFile` for module file syntax, and returns the list of `include`
+ * statements it contains. This is a somewhat crude sweep over the AST; we loudly complain about
+ * any usage of `include` that is not in a top-level function call statement with one single
+ * string literal positional argument, *except* that we don't do this check once `include` is
+ * assigned to, due to backwards compatibility concerns.
+ */
+ @VisibleForTesting
+ static ImmutableList<IncludeStatement> checkModuleFileSyntax(StarlarkFile starlarkFile)
+ throws SyntaxError.Exception {
+ var includeStatements = ImmutableList.<IncludeStatement>builder();
+ new DotBazelFileSyntaxChecker("MODULE.bazel files", /* canLoadBzl= */ false) {
+ // Once `include` the identifier is assigned to, we no longer care about its appearance
+ // anywhere. This allows `include` to be used as a module extension proxy (and technically
+ // any other variable binding).
+ private boolean includeWasAssigned = false;
+
+ @Override
+ public void visit(ExpressionStatement node) {
+ // We can assume this statement isn't nested in any block, since we don't allow
+ // `if`/`def`/`for` in MODULE.bazel.
+ if (!includeWasAssigned
+ && node.getExpression() instanceof CallExpression call
+ && call.getFunction() instanceof Identifier id
+ && id.getName().equals(INCLUDE_IDENTIFIER)) {
+ // Found a top-level call to `include`!
+ if (call.getArguments().size() == 1
+ && call.getArguments().getFirst() instanceof Argument.Positional pos
+ && pos.getValue() instanceof StringLiteral str) {
+ includeStatements.add(new IncludeStatement(str.getValue(), call.getStartLocation()));
+ // Nothing else to check, we can stop visiting sub-nodes now.
+ return;
+ }
+ error(
+ node.getStartLocation(),
+ "the `include` directive MUST be called with exactly one positional argument that "
+ + "is a string literal");
+ return;
+ }
+ super.visit(node);
+ }
+
+ @Override
+ public void visit(AssignmentStatement node) {
+ visit(node.getRHS());
+ if (!includeWasAssigned
+ && node.getLHS() instanceof Identifier id
+ && id.getName().equals(INCLUDE_IDENTIFIER)) {
+ includeWasAssigned = true;
+ // Technically someone could do something like
+ // (include, myvar) = (print, 3)
+ // and work around our check, but at that point IDGAF.
+ } else {
+ visit(node.getLHS());
+ }
+ }
+
+ @Override
+ public void visit(DotExpression node) {
+ visit(node.getObject());
+ if (includeWasAssigned || !node.getField().getName().equals(INCLUDE_IDENTIFIER)) {
+ // This is fine: `whatever.include`
+ // (so `include` can be used as a tag class name)
+ visit(node.getField());
+ }
+ }
+
+ @Override
+ public void visit(Identifier node) {
+ if (!includeWasAssigned && node.getName().equals(INCLUDE_IDENTIFIER)) {
+ // If we somehow reach the `include` identifier but NOT as the other allowed cases above,
+ // cry foul.
+ error(
+ node.getStartLocation(),
+ "the `include` directive MUST be called directly at the top-level");
+ }
+ super.visit(node);
+ }
+ }.check(starlarkFile);
+ return includeStatements.build();
+ }
+
+ public void runOnThread(StarlarkThread thread) throws EvalException, InterruptedException {
+ Starlark.execFileProgram(program, predeclaredEnv, thread);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
index 85b210e..6b97bb5 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
@@ -17,6 +17,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
@@ -24,16 +25,17 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.actions.FileValue;
+import com.google.devtools.build.lib.bazel.bzlmod.CompiledModuleFile.IncludeStatement;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.NonRootModuleFileValue;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelConstants;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.packages.BazelStarlarkEnvironment;
-import com.google.devtools.build.lib.packages.DotBazelFileSyntaxChecker;
import com.google.devtools.build.lib.packages.StarlarkExportable;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
@@ -42,6 +44,8 @@
import com.google.devtools.build.lib.server.FailureDetails.ExternalDeps.Code;
import com.google.devtools.build.lib.skyframe.ClientEnvironmentFunction;
import com.google.devtools.build.lib.skyframe.ClientEnvironmentValue;
+import com.google.devtools.build.lib.skyframe.PackageLookupFunction;
+import com.google.devtools.build.lib.skyframe.PackageLookupValue;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.skyframe.PrecomputedValue.Precomputed;
import com.google.devtools.build.lib.util.Fingerprint;
@@ -52,10 +56,14 @@
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.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.SkyframeLookupResult;
import com.google.errorprone.annotations.FormatMethod;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -63,14 +71,9 @@
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Mutability;
-import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.SymbolGenerator;
-import net.starlark.java.syntax.ParserInput;
-import net.starlark.java.syntax.Program;
-import net.starlark.java.syntax.StarlarkFile;
-import net.starlark.java.syntax.SyntaxError;
/**
* Takes a {@link ModuleKey} and its override (if any), retrieves the module file from a registry or
@@ -90,13 +93,14 @@
private final ImmutableMap<String, NonRegistryOverride> builtinModules;
private static final String BZLMOD_REMINDER =
- "###############################################################################\n"
- + "# Bazel now uses Bzlmod by default to manage external dependencies.\n"
- + "# Please consider migrating your external dependencies from WORKSPACE to"
- + " MODULE.bazel.\n"
- + "#\n"
- + "# For more details, please check https://github.com/bazelbuild/bazel/issues/18958\n"
- + "###############################################################################\n";
+ """
+ ###############################################################################
+ # Bazel now uses Bzlmod by default to manage external dependencies.
+ # Please consider migrating your external dependencies from WORKSPACE to MODULE.bazel.
+ #
+ # For more details, please check https://github.com/bazelbuild/bazel/issues/18958
+ ###############################################################################
+ """;
/**
* @param builtinModules A list of "built-in" modules that are treated as implicit dependencies of
@@ -113,7 +117,17 @@
}
private static class State implements Environment.SkyKeyComputeState {
+ // The following field is used during non-root module file evaluation. We store the module file
+ // here so that a later attempt to retrieve yanked versions wouldn't be overly expensive.
GetModuleFileResult getModuleFileResult;
+
+ // The following fields are used during root module file evaluation. We try to compile the root
+ // module file itself first, and then read, parse, and compile any included module files layer
+ // by layer, in a BFS fashion (hence the `horizon` field). Finally, everything is collected into
+ // the `includeLabelToCompiledModuleFile` map for use during actual Starlark execution.
+ CompiledModuleFile compiledRootModuleFile;
+ ImmutableList<IncludeStatement> horizon;
+ HashMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile = new HashMap<>();
}
@Nullable
@@ -181,10 +195,28 @@
.addBytes(state.getModuleFileResult.moduleFile.getContent())
.hexDigestAndReset();
+ CompiledModuleFile compiledModuleFile;
+ try {
+ compiledModuleFile =
+ CompiledModuleFile.parseAndCompile(
+ state.getModuleFileResult.moduleFile,
+ moduleKey,
+ starlarkSemantics,
+ starlarkEnv,
+ env.getListener());
+ } catch (ExternalDepsException e) {
+ throw new ModuleFileFunctionException(e, Transience.PERSISTENT);
+ }
+ if (!compiledModuleFile.includeStatements().isEmpty()) {
+ throw errorf(
+ Code.BAD_MODULE,
+ "include() directive found at %s, but it can only be used in the root module",
+ compiledModuleFile.includeStatements().getFirst().location());
+ }
ModuleThreadContext moduleThreadContext =
execModuleFile(
- state.getModuleFileResult.moduleFile,
- state.getModuleFileResult.registry,
+ compiledModuleFile,
+ /* includeLabelToParsedModuleFile= */ null,
moduleKey,
// Dev dependencies should always be ignored if the current module isn't the root module
/* ignoreDevDeps= */ true,
@@ -192,14 +224,13 @@
// We try to prevent most side effects of yanked modules, in particular print().
/* printIsNoop= */ yankedInfo.isPresent(),
starlarkSemantics,
- starlarkEnv,
env.getListener(),
SymbolGenerator.create(skyKey));
// Perform some sanity checks.
InterimModule module;
try {
- module = moduleThreadContext.buildModule();
+ module = moduleThreadContext.buildModule(state.getModuleFileResult.registry);
} catch (EvalException e) {
env.getListener().handle(Event.error(e.getMessageWithStack()));
throw errorf(Code.BAD_MODULE, "error executing MODULE.bazel file for %s", moduleKey);
@@ -243,69 +274,206 @@
private SkyValue computeForRootModule(
StarlarkSemantics starlarkSemantics, Environment env, SymbolGenerator<?> symbolGenerator)
throws ModuleFileFunctionException, InterruptedException {
- RootedPath moduleFilePath = getModuleFilePath(workspaceRoot);
- if (env.getValue(FileValue.key(moduleFilePath)) == null) {
- return null;
+ State state = env.getState(State::new);
+ if (state.compiledRootModuleFile == null) {
+ RootedPath moduleFilePath = getModuleFilePath(workspaceRoot);
+ if (env.getValue(FileValue.key(moduleFilePath)) == null) {
+ return null;
+ }
+ byte[] moduleFileContents;
+ if (moduleFilePath.asPath().exists()) {
+ moduleFileContents = readModuleFile(moduleFilePath.asPath());
+ } else {
+ moduleFileContents = BZLMOD_REMINDER.getBytes(UTF_8);
+ createModuleFile(moduleFilePath.asPath(), moduleFileContents);
+ env.getListener()
+ .handle(
+ Event.warn(
+ "--enable_bzlmod is set, but no MODULE.bazel file was found at the workspace"
+ + " root. Bazel will create an empty MODULE.bazel file. Please consider"
+ + " migrating your external dependencies from WORKSPACE to MODULE.bazel."
+ + " For more details, please refer to"
+ + " https://github.com/bazelbuild/bazel/issues/18958."));
+ }
+ try {
+ state.compiledRootModuleFile =
+ CompiledModuleFile.parseAndCompile(
+ ModuleFile.create(moduleFileContents, moduleFilePath.asPath().toString()),
+ ModuleKey.ROOT,
+ starlarkSemantics,
+ starlarkEnv,
+ env.getListener());
+ } catch (ExternalDepsException e) {
+ throw new ModuleFileFunctionException(e, Transience.PERSISTENT);
+ }
+ state.horizon = state.compiledRootModuleFile.includeStatements();
}
- byte[] moduleFileContents;
- if (moduleFilePath.asPath().exists()) {
- moduleFileContents = readModuleFile(moduleFilePath.asPath());
- } else {
- moduleFileContents = BZLMOD_REMINDER.getBytes(UTF_8);
- createModuleFile(moduleFilePath.asPath(), moduleFileContents);
- env.getListener()
- .handle(
- Event.warn(
- "--enable_bzlmod is set, but no MODULE.bazel file was found at the workspace"
- + " root. Bazel will create an empty MODULE.bazel file. Please consider"
- + " migrating your external dependencies from WORKSPACE to MODULE.bazel. For"
- + " more details, please refer to"
- + " https://github.com/bazelbuild/bazel/issues/18958."));
+ while (!state.horizon.isEmpty()) {
+ var newHorizon =
+ advanceHorizon(
+ state.includeLabelToCompiledModuleFile,
+ state.horizon,
+ env,
+ starlarkSemantics,
+ starlarkEnv);
+ if (newHorizon == null) {
+ return null;
+ }
+ state.horizon = newHorizon;
}
return evaluateRootModuleFile(
- moduleFileContents,
- moduleFilePath,
+ state.compiledRootModuleFile,
+ ImmutableMap.copyOf(state.includeLabelToCompiledModuleFile),
builtinModules,
MODULE_OVERRIDES.get(env),
IGNORE_DEV_DEPS.get(env),
starlarkSemantics,
- starlarkEnv,
env.getListener(),
symbolGenerator);
}
+ /**
+ * Reads, parses, and compiles all included module files named by {@code horizon}, stores the
+ * result in {@code includeLabelToCompiledModuleFile}, and finally returns the include statements
+ * of these newly compiled module files as a new "horizon".
+ */
+ @Nullable
+ private static ImmutableList<IncludeStatement> advanceHorizon(
+ HashMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile,
+ ImmutableList<IncludeStatement> horizon,
+ Environment env,
+ StarlarkSemantics starlarkSemantics,
+ BazelStarlarkEnvironment starlarkEnv)
+ throws ModuleFileFunctionException, InterruptedException {
+ var includeLabels = new ArrayList<Label>(horizon.size());
+ for (var includeStatement : horizon) {
+ if (!includeStatement.includeLabel().startsWith("//")) {
+ throw errorf(
+ Code.BAD_MODULE,
+ "bad include label '%s' at %s: include() must be called with main repo labels "
+ + "(starting with double slashes)",
+ includeStatement.includeLabel(),
+ includeStatement.location());
+ }
+ try {
+ includeLabels.add(Label.parseCanonical(includeStatement.includeLabel()));
+ } catch (LabelSyntaxException e) {
+ throw errorf(
+ Code.BAD_MODULE,
+ "bad include label '%s' at %s: %s",
+ includeStatement.includeLabel(),
+ includeStatement.location(),
+ e.getMessage());
+ }
+ }
+ SkyframeLookupResult result =
+ env.getValuesAndExceptions(
+ includeLabels.stream()
+ .map(l -> (SkyKey) PackageLookupValue.key(l.getPackageIdentifier()))
+ .collect(toImmutableSet()));
+ var rootedPaths = new ArrayList<RootedPath>(horizon.size());
+ for (int i = 0; i < horizon.size(); i++) {
+ Label includeLabel = includeLabels.get(i);
+ PackageLookupValue pkgLookupValue =
+ (PackageLookupValue)
+ result.get(PackageLookupValue.key(includeLabel.getPackageIdentifier()));
+ if (pkgLookupValue == null) {
+ return null;
+ }
+ if (!pkgLookupValue.packageExists()) {
+ String message = pkgLookupValue.getErrorMsg();
+ if (pkgLookupValue == PackageLookupValue.NO_BUILD_FILE_VALUE) {
+ message =
+ PackageLookupFunction.explainNoBuildFileValue(
+ includeLabel.getPackageIdentifier(), env);
+ }
+ throw errorf(
+ Code.BAD_MODULE,
+ "unable to load package for '%s' included at %s: %s",
+ horizon.get(i).includeLabel(),
+ horizon.get(i).location(),
+ message);
+ }
+ rootedPaths.add(
+ RootedPath.toRootedPath(pkgLookupValue.getRoot(), includeLabel.toPathFragment()));
+ }
+ result =
+ env.getValuesAndExceptions(
+ rootedPaths.stream().map(FileValue::key).collect(toImmutableSet()));
+ var newHorizon = ImmutableList.<IncludeStatement>builder();
+ for (int i = 0; i < horizon.size(); i++) {
+ FileValue fileValue = (FileValue) result.get(FileValue.key(rootedPaths.get(i)));
+ if (fileValue == null) {
+ return null;
+ }
+ if (!fileValue.isFile()) {
+ throw errorf(
+ Code.BAD_MODULE,
+ "error reading '%s' included at %s: not a regular file",
+ horizon.get(i).includeLabel(),
+ horizon.get(i).location());
+ }
+ byte[] bytes;
+ try {
+ bytes = FileSystemUtils.readContent(rootedPaths.get(i).asPath());
+ } catch (IOException e) {
+ throw errorf(
+ Code.BAD_MODULE,
+ "error reading '%s' included at %s: %s",
+ horizon.get(i).includeLabel(),
+ horizon.get(i).location(),
+ e.getMessage());
+ }
+ try {
+ var compiledModuleFile =
+ CompiledModuleFile.parseAndCompile(
+ ModuleFile.create(bytes, rootedPaths.get(i).asPath().toString()),
+ ModuleKey.ROOT,
+ starlarkSemantics,
+ starlarkEnv,
+ env.getListener());
+ includeLabelToCompiledModuleFile.put(horizon.get(i).includeLabel(), compiledModuleFile);
+ newHorizon.addAll(compiledModuleFile.includeStatements());
+ } catch (ExternalDepsException e) {
+ throw new ModuleFileFunctionException(e, Transience.PERSISTENT);
+ }
+ }
+ return newHorizon.build();
+ }
+
public static RootedPath getModuleFilePath(Path workspaceRoot) {
return RootedPath.toRootedPath(
Root.fromPath(workspaceRoot), LabelConstants.MODULE_DOT_BAZEL_FILE_NAME);
}
public static RootModuleFileValue evaluateRootModuleFile(
- byte[] moduleFileContents,
- RootedPath moduleFilePath,
+ CompiledModuleFile compiledRootModuleFile,
+ ImmutableMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile,
ImmutableMap<String, NonRegistryOverride> builtinModules,
Map<String, ModuleOverride> commandOverrides,
boolean ignoreDevDeps,
StarlarkSemantics starlarkSemantics,
- BazelStarlarkEnvironment starlarkEnv,
ExtendedEventHandler eventHandler,
SymbolGenerator<?> symbolGenerator)
throws ModuleFileFunctionException, InterruptedException {
- String moduleFileHash = new Fingerprint().addBytes(moduleFileContents).hexDigestAndReset();
+ String moduleFileHash =
+ new Fingerprint()
+ .addBytes(compiledRootModuleFile.moduleFile().getContent())
+ .hexDigestAndReset();
ModuleThreadContext moduleThreadContext =
execModuleFile(
- ModuleFile.create(moduleFileContents, moduleFilePath.asPath().toString()),
- /* registry= */ null,
+ compiledRootModuleFile,
+ includeLabelToCompiledModuleFile,
ModuleKey.ROOT,
ignoreDevDeps,
builtinModules,
/* printIsNoop= */ false,
starlarkSemantics,
- starlarkEnv,
eventHandler,
symbolGenerator);
InterimModule module;
try {
- module = moduleThreadContext.buildModule();
+ module = moduleThreadContext.buildModule(/* registry= */ null);
} catch (EvalException e) {
eventHandler.handle(Event.error(e.getMessageWithStack()));
throw errorf(Code.BAD_MODULE, "error executing MODULE.bazel file for the root module");
@@ -347,40 +515,31 @@
ModuleKey.create(name, Version.EMPTY).getCanonicalRepoNameWithoutVersion(),
name -> name));
return RootModuleFileValue.create(
- module, moduleFileHash, overrides, nonRegistryOverrideCanonicalRepoNameLookup);
+ module,
+ moduleFileHash,
+ overrides,
+ nonRegistryOverrideCanonicalRepoNameLookup,
+ includeLabelToCompiledModuleFile);
}
private static ModuleThreadContext execModuleFile(
- ModuleFile moduleFile,
- @Nullable Registry registry,
+ CompiledModuleFile compiledRootModuleFile,
+ @Nullable ImmutableMap<String, CompiledModuleFile> includeLabelToParsedModuleFile,
ModuleKey moduleKey,
boolean ignoreDevDeps,
ImmutableMap<String, NonRegistryOverride> builtinModules,
boolean printIsNoop,
StarlarkSemantics starlarkSemantics,
- BazelStarlarkEnvironment starlarkEnv,
ExtendedEventHandler eventHandler,
SymbolGenerator<?> symbolGenerator)
throws ModuleFileFunctionException, InterruptedException {
- StarlarkFile starlarkFile =
- StarlarkFile.parse(ParserInput.fromUTF8(moduleFile.getContent(), moduleFile.getLocation()));
- if (!starlarkFile.ok()) {
- Event.replayEventsOn(eventHandler, starlarkFile.errors());
- throw errorf(Code.BAD_MODULE, "error parsing MODULE.bazel file for %s", moduleKey);
- }
-
ModuleThreadContext context =
- new ModuleThreadContext(builtinModules, moduleKey, registry, ignoreDevDeps);
+ new ModuleThreadContext(
+ builtinModules, moduleKey, ignoreDevDeps, includeLabelToParsedModuleFile);
try (SilentCloseable c =
Profiler.instance()
.profile(ProfilerTask.BZLMOD, () -> "evaluate module file: " + moduleKey);
Mutability mu = Mutability.create("module file", moduleKey)) {
- new DotBazelFileSyntaxChecker("MODULE.bazel files", /* canLoadBzl= */ false)
- .check(starlarkFile);
- net.starlark.java.eval.Module predeclaredEnv =
- net.starlark.java.eval.Module.withPredeclared(
- starlarkSemantics, starlarkEnv.getModuleBazelEnv());
- Program program = Program.compileFile(starlarkFile, predeclaredEnv);
StarlarkThread thread =
StarlarkThread.create(
mu, starlarkSemantics, /* contextDescription= */ "", symbolGenerator);
@@ -398,10 +557,7 @@
}
}
});
- Starlark.execFileProgram(program, predeclaredEnv, thread);
- } catch (SyntaxError.Exception e) {
- Event.replayEventsOn(eventHandler, e.errors());
- throw errorf(Code.BAD_MODULE, "error executing MODULE.bazel file for %s", moduleKey);
+ compiledRootModuleFile.runOnThread(thread);
} catch (EvalException e) {
eventHandler.handle(Event.error(e.getMessageWithStack()));
throw errorf(Code.BAD_MODULE, "error executing MODULE.bazel file for %s", moduleKey);
@@ -409,11 +565,12 @@
return context;
}
- private static class GetModuleFileResult {
- ModuleFile moduleFile;
- // `registry` can be null if this module has a non-registry override.
- @Nullable Registry registry;
- }
+ /**
+ * Result of a {@link #getModuleFile} call.
+ *
+ * @param registry can be null if this module has a non-registry override.
+ */
+ private record GetModuleFileResult(ModuleFile moduleFile, @Nullable Registry registry) {}
@Nullable
private GetModuleFileResult getModuleFile(
@@ -436,16 +593,15 @@
if (env.getValue(FileValue.key(moduleFilePath)) == null) {
return null;
}
- GetModuleFileResult result = new GetModuleFileResult();
Label moduleFileLabel =
Label.createUnvalidated(
PackageIdentifier.create(canonicalRepoName, PathFragment.EMPTY_FRAGMENT),
LabelConstants.MODULE_DOT_BAZEL_FILE_NAME.getBaseName());
- result.moduleFile =
+ return new GetModuleFileResult(
ModuleFile.create(
readModuleFile(moduleFilePath.asPath()),
- moduleFileLabel.getUnambiguousCanonicalForm());
- return result;
+ moduleFileLabel.getUnambiguousCanonicalForm()),
+ /* registry= */ null);
}
// Otherwise, we should get the module file from a registry.
@@ -489,16 +645,13 @@
// Now go through the list of registries and use the first one that contains the requested
// module.
- GetModuleFileResult result = new GetModuleFileResult();
for (Registry registry : registryObjects) {
try {
Optional<ModuleFile> moduleFile = registry.getModuleFile(key, env.getListener());
if (moduleFile.isEmpty()) {
continue;
}
- result.moduleFile = moduleFile.get();
- result.registry = registry;
- return result;
+ return new GetModuleFileResult(moduleFile.get(), registry);
} catch (IOException e) {
throw errorf(
Code.ERROR_ACCESSING_REGISTRY, e, "Error accessing registry %s", registry.getUrl());
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java
index 2d74793..1f367f2 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java
@@ -79,7 +79,8 @@
"Declares certain properties of the Bazel module represented by the current Bazel repo."
+ " These properties are either essential metadata of the module (such as the name"
+ " and version), or affect behavior of the current module and its dependents. <p>It"
- + " should be called at most once. It can be omitted only if this module is the root"
+ + " should be called at most once, and if called, it must be the very first directive"
+ + " in the MODULE.bazel file. It can be omitted only if this module is the root"
+ " module (as in, if it's not going to be depended on by another module).",
parameters = {
@Param(
@@ -664,7 +665,7 @@
}
@StarlarkBuiltin(name = "repo_rule_proxy", documented = false)
- class RepoRuleProxy implements StarlarkValue {
+ static class RepoRuleProxy implements StarlarkValue {
private final ModuleExtensionUsageBuilder usageBuilder;
private final String tagName;
@@ -699,6 +700,36 @@
}
@StarlarkMethod(
+ name = CompiledModuleFile.INCLUDE_IDENTIFIER,
+ doc =
+ "Includes the contents of another MODULE.bazel-like file. Effectively,"
+ + " <code>include()</code> behaves as if the included file is textually placed at the"
+ + " location of the <code>include()</code> call, except that variable bindings (such"
+ + " as those used for <code>use_extension</code>) are only ever visible in the file"
+ + " they occur in, not in any included or including files.<p>Only the root module may"
+ + " use <code>include()</code>; it is an error if a <code>bazel_dep</code>'s MODULE"
+ + " file uses <code>include()</code>.<p>Only files in the main repo may be"
+ + " included.<p><code>include()</code> allows you to segment the root module file"
+ + " into multiple parts, to avoid having an enormous MODULE.bazel file or to better"
+ + " manage access control for individual semantic segments.",
+ parameters = {
+ @Param(
+ name = "label",
+ doc =
+ "The label pointing to the file to include. The label must point to a file in the"
+ + " main repo; in other words, it <strong>must<strong> start with double"
+ + " slashes (<code>//</code>)."),
+ },
+ useStarlarkThread = true)
+ public void include(String label, StarlarkThread thread)
+ throws InterruptedException, EvalException {
+ ModuleThreadContext context =
+ ModuleThreadContext.fromOrFail(thread, CompiledModuleFile.INCLUDE_IDENTIFIER + "()");
+ context.setNonModuleCalled();
+ context.include(label, thread);
+ }
+
+ @StarlarkMethod(
name = "single_version_override",
doc =
"Specifies that a dependency should still come from a registry, but its version should"
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileValue.java
index 4fe216a..7c7e38f 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileValue.java
@@ -69,13 +69,26 @@
public abstract ImmutableMap<RepositoryName, String>
getNonRegistryOverrideCanonicalRepoNameLookup();
+ /**
+ * TODO: This field is a hack. It's not needed by anything other than {@code ModCommand}, during
+ * the {@code bazel mod tidy} command. Doing it this way assumes that {@code bazel mod tidy}
+ * cannot touch any included segments. This is unsatisfactory; we should do it properly at some
+ * point, although that seems quite difficult.
+ */
+ public abstract ImmutableMap<String, CompiledModuleFile> getIncludeLabelToCompiledModuleFile();
+
public static RootModuleFileValue create(
InterimModule module,
String moduleFileHash,
ImmutableMap<String, ModuleOverride> overrides,
- ImmutableMap<RepositoryName, String> nonRegistryOverrideCanonicalRepoNameLookup) {
+ ImmutableMap<RepositoryName, String> nonRegistryOverrideCanonicalRepoNameLookup,
+ ImmutableMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile) {
return new AutoValue_ModuleFileValue_RootModuleFileValue(
- module, moduleFileHash, overrides, nonRegistryOverrideCanonicalRepoNameLookup);
+ module,
+ moduleFileHash,
+ overrides,
+ nonRegistryOverrideCanonicalRepoNameLookup,
+ includeLabelToCompiledModuleFile);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleThreadContext.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleThreadContext.java
index a4de08f..8126ffc 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleThreadContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleThreadContext.java
@@ -34,13 +34,14 @@
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.syntax.Location;
-/** Context object for a Starlark thread evaluating the MODULE.bazel file and its imports. */
+/** Context object for a Starlark thread evaluating the MODULE.bazel file and files it includes. */
public class ModuleThreadContext {
private boolean moduleCalled = false;
private boolean hadNonModuleCall = false;
private final boolean ignoreDevDeps;
private final InterimModule.Builder module;
private final ImmutableMap<String, NonRegistryOverride> builtinModules;
+ @Nullable private final ImmutableMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile;
private final Map<String, DepSpec> deps = new LinkedHashMap<>();
private final List<ModuleExtensionUsageBuilder> extensionUsageBuilders = new ArrayList<>();
private final Map<String, ModuleOverride> overrides = new HashMap<>();
@@ -50,7 +51,7 @@
throws EvalException {
ModuleThreadContext context = thread.getThreadLocal(ModuleThreadContext.class);
if (context == null) {
- throw Starlark.errorf("%s can only be called from MODULE.bazel and its imports", what);
+ throw Starlark.errorf("%s can only be called from MODULE.bazel and files it includes", what);
}
return context;
}
@@ -62,11 +63,12 @@
public ModuleThreadContext(
ImmutableMap<String, NonRegistryOverride> builtinModules,
ModuleKey key,
- @Nullable Registry registry,
- boolean ignoreDevDeps) {
- module = InterimModule.builder().setKey(key).setRegistry(registry);
+ boolean ignoreDevDeps,
+ @Nullable ImmutableMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile) {
+ module = InterimModule.builder().setKey(key);
this.ignoreDevDeps = ignoreDevDeps;
this.builtinModules = builtinModules;
+ this.includeLabelToCompiledModuleFile = includeLabelToCompiledModuleFile;
}
record RepoNameUsage(String how, Location where) {}
@@ -224,6 +226,22 @@
}
}
+ public void include(String includeLabel, StarlarkThread thread)
+ throws InterruptedException, EvalException {
+ if (includeLabelToCompiledModuleFile == null) {
+ // This should never happen because compiling the non-root module file should have failed, way
+ // before evaluation started.
+ throw Starlark.errorf("trying to call `include()` from a non-root module");
+ }
+ var compiledModuleFile = includeLabelToCompiledModuleFile.get(includeLabel);
+ if (compiledModuleFile == null) {
+ // This should never happen because the file we're trying to include should have already been
+ // compiled before evaluation started.
+ throw Starlark.errorf("internal error; included file %s not compiled", includeLabel);
+ }
+ compiledModuleFile.runOnThread(thread);
+ }
+
public void addOverride(String moduleName, ModuleOverride override) throws EvalException {
ModuleOverride existingOverride = overrides.putIfAbsent(moduleName, override);
if (existingOverride != null) {
@@ -231,7 +249,7 @@
}
}
- public InterimModule buildModule() throws EvalException {
+ public InterimModule buildModule(@Nullable Registry registry) throws EvalException {
// Add builtin modules as default deps of the current module.
for (String builtinModule : builtinModules.keySet()) {
if (module.getKey().getName().equals(builtinModule)) {
@@ -257,6 +275,7 @@
extensionUsages.add(extensionUsageBuilder.buildUsage());
}
return module
+ .setRegistry(registry)
.setDeps(ImmutableMap.copyOf(deps))
.setOriginalDeps(ImmutableMap.copyOf(deps))
.setExtensionUsages(extensionUsages.build())
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD
index 5c074c8..09bb1dc 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD
@@ -31,6 +31,7 @@
"//src/main/java/com/google/devtools/build/lib/analysis:no_build_request_finished_event",
"//src/main/java/com/google/devtools/build/lib/bazel:resolved_event",
"//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common",
+ "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:exception",
"//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:inspection",
"//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:module_extension",
"//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:module_file_fixup_event",
@@ -47,9 +48,6 @@
"//src/main/java/com/google/devtools/build/lib/packages",
"//src/main/java/com/google/devtools/build/lib/packages/semantics",
"//src/main/java/com/google/devtools/build/lib/pkgcache",
- "//src/main/java/com/google/devtools/build/lib/query2",
- "//src/main/java/com/google/devtools/build/lib/query2/common:cquery-node",
- "//src/main/java/com/google/devtools/build/lib/query2/engine",
"//src/main/java/com/google/devtools/build/lib/rules:repository/repository_directory_value",
"//src/main/java/com/google/devtools/build/lib/rules:repository/repository_function",
"//src/main/java/com/google/devtools/build/lib/rules:repository/resolved_file_value",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java
index 4d40a27..d2e4e32 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java
@@ -37,7 +37,10 @@
import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule;
import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleResolutionEvent;
import com.google.devtools.build.lib.bazel.bzlmod.BzlmodRepoRuleValue;
+import com.google.devtools.build.lib.bazel.bzlmod.CompiledModuleFile;
+import com.google.devtools.build.lib.bazel.bzlmod.ExternalDepsException;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleExtensionId;
+import com.google.devtools.build.lib.bazel.bzlmod.ModuleFile;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileFunction;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleKey;
@@ -633,18 +636,33 @@
"Unexpected error while reading module file after running buildozer: " + e.getMessage(),
Code.BUILDOZER_FAILED);
}
+ CompiledModuleFile compiledModuleFile;
+ try {
+ compiledModuleFile =
+ CompiledModuleFile.parseAndCompile(
+ ModuleFile.create(moduleFileContents, moduleFilePath.asPath().getPathString()),
+ ModuleKey.ROOT,
+ modTidyValue.starlarkSemantics(),
+ env.getRuntime().getRuleClassProvider().getBazelStarlarkEnvironment(),
+ env.getReporter());
+ } catch (ExternalDepsException e) {
+ return reportAndCreateFailureResult(
+ env,
+ "Unexpected error while compiling module file after running buildozer: "
+ + e.getMessage(),
+ Code.BUILDOZER_FAILED);
+ }
ModuleFileValue.RootModuleFileValue newRootModuleFileValue;
try {
newRootModuleFileValue =
ModuleFileFunction.evaluateRootModuleFile(
- moduleFileContents,
- moduleFilePath,
+ compiledModuleFile,
+ modTidyValue.includeLabelToCompiledModuleFile(),
ModuleFileFunction.getBuiltinModules(
env.getDirectories().getEmbeddedBinariesRoot()),
modTidyValue.moduleOverrides(),
modTidyValue.ignoreDevDeps(),
modTidyValue.starlarkSemantics(),
- env.getRuntime().getRuleClassProvider().getBazelStarlarkEnvironment(),
env.getReporter(),
// Not persisted to Skyframe.
SymbolGenerator.createTransient());
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/CompiledModuleFileTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/CompiledModuleFileTest.java
new file mode 100644
index 0000000..6a7125b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/CompiledModuleFileTest.java
@@ -0,0 +1,178 @@
+// 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.bazel.bzlmod;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.bazel.bzlmod.CompiledModuleFile.IncludeStatement;
+import net.starlark.java.syntax.Location;
+import net.starlark.java.syntax.ParserInput;
+import net.starlark.java.syntax.StarlarkFile;
+import net.starlark.java.syntax.SyntaxError;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CompiledModuleFileTest {
+
+ private static ImmutableList<IncludeStatement> checkSyntax(String str) throws Exception {
+ return CompiledModuleFile.checkModuleFileSyntax(
+ StarlarkFile.parse(ParserInput.fromString(str, "test file")));
+ }
+
+ @Test
+ public void checkSyntax_good() throws Exception {
+ String program =
+ """
+ abc()
+ include("hullo")
+ foo = bar
+ """;
+ assertThat(checkSyntax(program))
+ .containsExactly(
+ new IncludeStatement("hullo", Location.fromFileLineColumn("test file", 2, 1)));
+ }
+
+ @Test
+ public void checkSyntax_good_multiple() throws Exception {
+ String program =
+ """
+ abc()
+ include("hullo")
+ foo = bar
+ include('world')
+ """;
+ assertThat(checkSyntax(program))
+ .containsExactly(
+ new IncludeStatement("hullo", Location.fromFileLineColumn("test file", 2, 1)),
+ new IncludeStatement("world", Location.fromFileLineColumn("test file", 4, 1)));
+ }
+
+ @Test
+ public void checkSyntax_good_multilineLiteral() throws Exception {
+ String program =
+ """
+ abc()
+ # Ludicrous as this may be, it's still valid syntax. Your funeral, etc...
+ include(\"""hullo
+ world\""")
+ """;
+ assertThat(checkSyntax(program))
+ .containsExactly(
+ new IncludeStatement("hullo\nworld", Location.fromFileLineColumn("test file", 3, 1)));
+ }
+
+ @Test
+ public void checkSyntax_good_benignUsageOfInclude() throws Exception {
+ String program =
+ """
+ myext = use_extension('whatever')
+ myext.include(include="hullo")
+ """;
+ assertThat(checkSyntax(program)).isEmpty();
+ }
+
+ @Test
+ public void checkSyntax_good_includeIdentifierReassigned() throws Exception {
+ String program =
+ """
+ include = print
+ # from this point on, we no longer check anything about `include` usage.
+ include('hello')
+ str(include)
+ exclude = include
+ """;
+ assertThat(checkSyntax(program)).isEmpty();
+ }
+
+ @Test
+ public void checkSyntax_bad_if() throws Exception {
+ String program =
+ """
+ abc()
+ if d > 3:
+ pass
+ """;
+ var ex = assertThrows(SyntaxError.Exception.class, () -> checkSyntax(program));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains("`if` statements are not allowed in MODULE.bazel files");
+ }
+
+ @Test
+ public void checkSyntax_bad_assignIncludeResult() throws Exception {
+ String program =
+ """
+ foo = include('hello')
+ """;
+ var ex = assertThrows(SyntaxError.Exception.class, () -> checkSyntax(program));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains("the `include` directive MUST be called directly at the top-level");
+ }
+
+ @Test
+ public void checkSyntax_bad_assignIncludeIdentifier() throws Exception {
+ String program =
+ """
+ foo = include
+ foo('hello')
+ """;
+ var ex = assertThrows(SyntaxError.Exception.class, () -> checkSyntax(program));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains("the `include` directive MUST be called directly at the top-level");
+ }
+
+ @Test
+ public void checkSyntax_bad_multipleArgumentsToInclude() throws Exception {
+ String program =
+ """
+ include('hello', 'world')
+ """;
+ var ex = assertThrows(SyntaxError.Exception.class, () -> checkSyntax(program));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains("the `include` directive MUST be called with exactly one positional");
+ }
+
+ @Test
+ public void checkSyntax_bad_keywordArgumentToInclude() throws Exception {
+ String program =
+ """
+ include(label='hello')
+ """;
+ var ex = assertThrows(SyntaxError.Exception.class, () -> checkSyntax(program));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains("the `include` directive MUST be called with exactly one positional");
+ }
+
+ @Test
+ public void checkSyntax_bad_nonLiteralArgumentToInclude() throws Exception {
+ String program =
+ """
+ foo = 'hello'
+ include(foo)
+ """;
+ var ex = assertThrows(SyntaxError.Exception.class, () -> checkSyntax(program));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains("the `include` directive MUST be called with exactly one positional");
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
index 8e8ea8b..76d6f4e 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
@@ -48,6 +48,10 @@
import com.google.devtools.build.lib.skyframe.ExternalFilesHelper.ExternalFileAction;
import com.google.devtools.build.lib.skyframe.FileFunction;
import com.google.devtools.build.lib.skyframe.FileStateFunction;
+import com.google.devtools.build.lib.skyframe.IgnoredPackagePrefixesFunction;
+import com.google.devtools.build.lib.skyframe.LocalRepositoryLookupFunction;
+import com.google.devtools.build.lib.skyframe.PackageLookupFunction;
+import com.google.devtools.build.lib.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy;
import com.google.devtools.build.lib.skyframe.PrecomputedFunction;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
@@ -56,6 +60,7 @@
import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
import com.google.devtools.build.lib.vfs.FileStateKey;
+import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.SyscallCache;
import com.google.devtools.build.skyframe.EvaluationContext;
@@ -143,6 +148,21 @@
ruleClassProvider.getBazelStarlarkEnvironment(),
rootDirectory,
builtinModules))
+ .put(
+ SkyFunctions.PACKAGE_LOOKUP,
+ new PackageLookupFunction(
+ new AtomicReference<>(ImmutableSet.of()),
+ CrossRepositoryLabelViolationStrategy.ERROR,
+ BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY,
+ BazelSkyframeExecutorConstants.EXTERNAL_PACKAGE_HELPER))
+ .put(
+ SkyFunctions.IGNORED_PACKAGE_PREFIXES,
+ new IgnoredPackagePrefixesFunction(
+ /* ignoredPackagePrefixesFile= */ PathFragment.EMPTY_FRAGMENT))
+ .put(
+ SkyFunctions.LOCAL_REPOSITORY_LOOKUP,
+ new LocalRepositoryLookupFunction(
+ BazelSkyframeExecutorConstants.EXTERNAL_PACKAGE_HELPER))
.put(SkyFunctions.PRECOMPUTED, new PrecomputedFunction())
.put(
SkyFunctions.REPOSITORY_DIRECTORY,
@@ -306,8 +326,178 @@
ModuleOverride bazelToolsOverride =
result.get(ModuleFileValue.KEY_FOR_ROOT_MODULE).getOverrides().get("bazel_tools");
assertThat(bazelToolsOverride).isInstanceOf(LocalPathOverride.class);
- assertThat((LocalPathOverride) bazelToolsOverride)
- .isEqualTo(LocalPathOverride.create("./bazel_tools_new"));
+ assertThat(bazelToolsOverride).isEqualTo(LocalPathOverride.create("./bazel_tools_new"));
+ }
+
+ @Test
+ public void testRootModule_include_good() throws Exception {
+ scratch.overwriteFile(
+ rootDirectory.getRelative("MODULE.bazel").getPathString(),
+ "module(name='aaa')",
+ "include('//java:MODULE.bazel.segment')",
+ "bazel_dep(name='foo', version='1.0')",
+ "register_toolchains('//:whatever')",
+ "include('//python:MODULE.bazel.segment')");
+ scratch.overwriteFile(rootDirectory.getRelative("java/BUILD").getPathString());
+ scratch.overwriteFile(
+ rootDirectory.getRelative("java/MODULE.bazel.segment").getPathString(),
+ "bazel_dep(name='java-foo', version='1.0')");
+ scratch.overwriteFile(rootDirectory.getRelative("python/BUILD").getPathString());
+ scratch.overwriteFile(
+ rootDirectory.getRelative("python/MODULE.bazel.segment").getPathString(),
+ "bazel_dep(name='py-foo', version='1.0', repo_name='python-foo')",
+ "single_version_override(module_name='java-foo', version='2.0')",
+ "include('//python:toolchains/MODULE.bazel.segment')");
+ scratch.overwriteFile(
+ rootDirectory.getRelative("python/toolchains/MODULE.bazel.segment").getPathString(),
+ "register_toolchains('//:python-whatever')");
+ FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
+ ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+
+ EvaluationResult<RootModuleFileValue> result =
+ evaluator.evaluate(
+ ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+ if (result.hasError()) {
+ fail(result.getError().toString());
+ }
+ RootModuleFileValue rootModuleFileValue = result.get(ModuleFileValue.KEY_FOR_ROOT_MODULE);
+ InterimModule expectedModule =
+ InterimModuleBuilder.create("aaa", "")
+ .setKey(ModuleKey.ROOT)
+ .addDep("java-foo", createModuleKey("java-foo", "1.0"))
+ .addDep("foo", createModuleKey("foo", "1.0"))
+ .addDep("python-foo", createModuleKey("py-foo", "1.0"))
+ .addToolchainsToRegister(ImmutableList.of("//:whatever", "//:python-whatever"))
+ .build();
+ assertThat(rootModuleFileValue.getModule()).isEqualTo(expectedModule);
+ // specifically assert the order of deps, which is significant; Map.equals semantics don't test
+ // this.
+ assertThat(rootModuleFileValue.getModule().getDeps())
+ .containsExactlyEntriesIn(expectedModule.getDeps())
+ .inOrder();
+ assertThat(rootModuleFileValue.getOverrides())
+ .containsExactly(
+ "java-foo",
+ SingleVersionOverride.create(
+ Version.parse("2.0"), "", ImmutableList.of(), ImmutableList.of(), 0));
+ }
+
+ @Test
+ public void testRootModule_include_bad_otherRepoLabel() throws Exception {
+ scratch.overwriteFile(
+ rootDirectory.getRelative("MODULE.bazel").getPathString(),
+ "module(name='aaa')",
+ "include('@haha//java:MODULE.bazel.segment')");
+ FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
+ ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+
+ EvaluationResult<RootModuleFileValue> result =
+ evaluator.evaluate(
+ ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+ assertThat(result.hasError()).isTrue();
+ assertThat(result.getError().toString()).contains("starting with double slashes");
+ }
+
+ @Test
+ public void testRootModule_include_bad_relativeLabel() throws Exception {
+ scratch.overwriteFile(
+ rootDirectory.getRelative("MODULE.bazel").getPathString(),
+ "module(name='aaa')",
+ "include(':MODULE.bazel.segment')");
+ FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
+ ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+
+ EvaluationResult<RootModuleFileValue> result =
+ evaluator.evaluate(
+ ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+ assertThat(result.hasError()).isTrue();
+ assertThat(result.getError().toString()).contains("starting with double slashes");
+ }
+
+ @Test
+ public void testRootModule_include_bad_badLabelSyntax() throws Exception {
+ scratch.overwriteFile(
+ rootDirectory.getRelative("MODULE.bazel").getPathString(),
+ "module(name='aaa')",
+ "include('//haha/:::')");
+ FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
+ ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+
+ reporter.removeHandler(failFastHandler); // expect failures
+ EvaluationResult<RootModuleFileValue> result =
+ evaluator.evaluate(
+ ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+ assertThat(result.hasError()).isTrue();
+ assertThat(result.getError().toString()).contains("bad include label");
+ }
+
+ @Test
+ public void testRootModule_include_bad_moduleAfterInclude() throws Exception {
+ scratch.overwriteFile(
+ rootDirectory.getRelative("MODULE.bazel").getPathString(),
+ "include('//java:MODULE.bazel.segment')");
+ scratch.overwriteFile(rootDirectory.getRelative("java/BUILD").getPathString());
+ scratch.overwriteFile(
+ rootDirectory.getRelative("java/MODULE.bazel.segment").getPathString(),
+ "module(name='bet-you-didnt-expect-this-didya')",
+ "bazel_dep(name='java-foo', version='1.0', repo_name='foo')");
+ FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
+ ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+
+ reporter.removeHandler(failFastHandler); // expect failures
+ EvaluationResult<RootModuleFileValue> result =
+ evaluator.evaluate(
+ ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+ assertThat(result.hasError()).isTrue();
+ assertContainsEvent("if module() is called, it must be called before any other functions");
+ }
+
+ @Test
+ public void testRootModule_include_bad_repoNameCollision() throws Exception {
+ scratch.overwriteFile(
+ rootDirectory.getRelative("MODULE.bazel").getPathString(),
+ "module(name='aaa')",
+ "include('//java:MODULE.bazel.segment')",
+ "include('//python:MODULE.bazel.segment')");
+ scratch.overwriteFile(rootDirectory.getRelative("java/BUILD").getPathString());
+ scratch.overwriteFile(
+ rootDirectory.getRelative("java/MODULE.bazel.segment").getPathString(),
+ "bazel_dep(name='java-foo', version='1.0', repo_name='foo')");
+ scratch.overwriteFile(rootDirectory.getRelative("python/BUILD").getPathString());
+ scratch.overwriteFile(
+ rootDirectory.getRelative("python/MODULE.bazel.segment").getPathString(),
+ "bazel_dep(name='python-foo', version='1.0', repo_name='foo')");
+ FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
+ ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+
+ reporter.removeHandler(failFastHandler); // expect failures
+ EvaluationResult<RootModuleFileValue> result =
+ evaluator.evaluate(
+ ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+ assertThat(result.hasError()).isTrue();
+ assertContainsEvent("The repo name 'foo' is already being used");
+ }
+
+ @Test
+ public void testRootModule_include_bad_tryingToLeakBindings() throws Exception {
+ scratch.overwriteFile(
+ rootDirectory.getRelative("MODULE.bazel").getPathString(),
+ "module(name='aaa')",
+ "FOO_NAME = 'foo'",
+ "include('//java:MODULE.bazel.segment')");
+ scratch.overwriteFile(rootDirectory.getRelative("java/BUILD").getPathString());
+ scratch.overwriteFile(
+ rootDirectory.getRelative("java/MODULE.bazel.segment").getPathString(),
+ "bazel_dep(name=FOO_NAME, version='1.0')");
+ FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
+ ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+
+ reporter.removeHandler(failFastHandler); // expect failures
+ EvaluationResult<RootModuleFileValue> result =
+ evaluator.evaluate(
+ ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
+ assertThat(result.hasError()).isTrue();
+ assertContainsEvent("name 'FOO_NAME' is not defined");
}
@Test
@@ -359,6 +549,25 @@
}
@Test
+ public void testNonRootModuleCannotUseInclude() throws Exception {
+ FakeRegistry registry =
+ registryFactory
+ .newFakeRegistry("/foo")
+ .addModule(
+ createModuleKey("foo", "1.0"),
+ "module(name='foo',version='1.0')",
+ "include('//java:MODULE.bazel.segment')");
+ ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+
+ EvaluationResult<RootModuleFileValue> result =
+ evaluator.evaluate(
+ ImmutableList.of(ModuleFileValue.key(createModuleKey("foo", "1.0"), null)),
+ evaluationContext);
+ assertThat(result.hasError()).isTrue();
+ assertThat(result.getError().toString()).contains("but it can only be used in the root module");
+ }
+
+ @Test
public void testLocalPathOverride() throws Exception {
// There is an override for B to use the local path "code_for_b", so we shouldn't even be
// looking at the registry.
diff --git a/src/test/py/bazel/bzlmod/bazel_module_test.py b/src/test/py/bazel/bzlmod/bazel_module_test.py
index 344d660..2b30d03 100644
--- a/src/test/py/bazel/bzlmod/bazel_module_test.py
+++ b/src/test/py/bazel/bzlmod/bazel_module_test.py
@@ -905,6 +905,44 @@
':bar',
])
+ def testInclude(self):
+ self.ScratchFile(
+ 'MODULE.bazel',
+ [
+ 'module(name="foo")',
+ 'bazel_dep(name="bbb", version="1.0")',
+ 'include("//java:MODULE.bazel.segment")',
+ ],
+ )
+ self.ScratchFile('java/BUILD')
+ self.ScratchFile(
+ 'java/MODULE.bazel.segment',
+ [
+ 'bazel_dep(name="aaa", version="1.0", repo_name="lol")',
+ ],
+ )
+ self.ScratchFile(
+ 'BUILD',
+ [
+ 'cc_binary(',
+ ' name = "main",',
+ ' srcs = ["main.cc"],',
+ ' deps = ["@lol//:lib_aaa"],',
+ ')',
+ ],
+ )
+ self.ScratchFile(
+ 'main.cc',
+ [
+ '#include "aaa.h"',
+ 'int main() {',
+ ' hello_aaa("main function");',
+ '}',
+ ],
+ )
+ _, stdout, _ = self.RunBazel(['run', '//:main'])
+ self.assertIn('main function => aaa@1.0', stdout)
+
if __name__ == '__main__':
absltest.main()