Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs/external/faq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,18 @@ expected when sorting (the empty string is treated as the highest version).
[non-registry override]: module.md#non-registry_overrides
[`rules-template`]: https://github.com/bazel-contrib/rules-template

### When should I increment the compatibility level? {:#incrementing-compatibility-level}
### What is a compatibility level?

You should stop using `compatibility_level`.

Increasing `compatibility_level` leads to version conflicts that are difficult
for end users to resolve. Therefore, starting with Bazel 8.6.0 and 9.1.0, both
`compatibility_level` and `max_compatibility_level` are no-ops.

Module maintainers introducing major breaking changes should ensure that build
failures provide clear error messages and actionable migration paths.

**Legacy documentation:**

The [`compatibility_level`](module.md#compatibility_level) of a Bazel module
should be incremented _in the same commit_ that introduces a backwards
Expand Down
37 changes: 4 additions & 33 deletions docs/external/module.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ differences include:
In Bazel, this is loosened to allow letters too, and the comparison
semantics match the "identifiers" in the "prerelease" part.
* Additionally, the semantics of major, minor, and patch version increases are
not enforced. However, see [compatibility level](#compatibility_level) for
details on how we denote backwards compatibility.
not enforced.

Any valid SemVer version is a valid Bazel module version. Additionally, two
SemVer versions `a` and `b` compare `a < b` if and only if the same holds when
Expand Down Expand Up @@ -89,26 +88,6 @@ non-yanked version, or use the
[`--allow_yanked_versions`](/reference/command-line-reference#flag--allow_yanked_versions)
flag to explicitly allow the yanked version.

## Compatibility level

In Go, MVS's assumption about backwards compatibility works because it treats
backwards incompatible versions of a module as a separate module. In terms of
SemVer, that means `A 1.x` and `A 2.x` are considered distinct modules, and can
coexist in the resolved dependency graph. This is, in turn, made possible by
encoding the major version in the package path in Go, so there aren't any
compile-time or linking-time conflicts. Bazel, however, cannot provide such
guarantees because it follows [a relaxed version of SemVer](#version-format).

Thus, Bazel needs the equivalent of the SemVer major version number to detect
backwards incompatible ("breaking") versions. This number is called the
*compatibility level*, and is specified by each module version in its
[`module()`](/rule/lib/globals/module#module) directive. With this information,
Bazel can throw an error if it detects that versions of the _same module_ with
_different compatibility levels_ exist in the resolved dependency graph.

Finally, incrementing the compatibility level can be disruptive to the users.
To learn more about when and how to increment it, [check the `MODULE.bazel`
FAQ](faq#incrementing-compatibility-level).

## Overrides

Expand Down Expand Up @@ -143,23 +122,15 @@ A [`multiple_version_override`](/rules/lib/globals/module#multiple_version_overr
can be specified to allow multiple versions of the same module to coexist in the
resolved dependency graph.

You can specify an explicit list of allowed versions for the module, which must
all be present in the dependency graph before resolution — there must exist
*some* transitive dependency depending on each allowed version. After
resolution, only the allowed versions of the module remain, while Bazel upgrades
other versions of the module to the nearest higher allowed version at the same
compatibility level. If no higher allowed version at the same compatibility
level exists, Bazel throws an error.
If there are multiple versions of the same module remaining in the dependency
graph, Bazel will pick the nearest higher allowed version for each dependent.

For example, if versions `1.1`, `1.3`, `1.5`, `1.7`, and `2.0` exist in the
dependency graph before resolution and the major version is the compatibility
level:
dependency graph before resolution:

* A multiple-version override allowing `1.3`, `1.7`, and `2.0` results in
`1.1` being upgraded to `1.3`, `1.5` being upgraded to `1.7`, and other
versions remaining the same.
* A multiple-version override allowing `1.5` and `2.0` results in an error, as
`1.7` has no higher version at the same compatibility level to upgrade to.
* A multiple-version override allowing `1.9` and `2.0` results in an error, as
`1.9` is not present in the dependency graph before resolution.

Expand Down
13 changes: 12 additions & 1 deletion site/en/external/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,18 @@ expected when sorting (the empty string is treated as the highest version).
[non-registry override]: module.md#non-registry_overrides
[`rules-template`]: https://github.com/bazel-contrib/rules-template

### When should I increment the compatibility level? {:#incrementing-compatibility-level}
### What is a compatibility level? {:#compatibility-level}

You should stop using `compatibility_level`.

Increasing `compatibility_level` leads to version conflicts that are difficult
for end users to resolve. Therefore, starting with Bazel 8.6.0 and 9.1.0, both
`compatibility_level` and `max_compatibility_level` are no-ops.

Module maintainers introducing major breaking changes should ensure that build
failures provide clear error messages and actionable migration paths.

**Legacy documentation:**

The [`compatibility_level`](module.md#compatibility_level) of a Bazel module
should be incremented _in the same commit_ that introduces a backwards
Expand Down
37 changes: 4 additions & 33 deletions site/en/external/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ differences include:
In Bazel, this is loosened to allow letters too, and the comparison
semantics match the "identifiers" in the "prerelease" part.
* Additionally, the semantics of major, minor, and patch version increases are
not enforced. However, see [compatibility level](#compatibility_level) for
details on how we denote backwards compatibility.
not enforced.

Any valid SemVer version is a valid Bazel module version. Additionally, two
SemVer versions `a` and `b` compare `a < b` if and only if the same holds when
Expand Down Expand Up @@ -92,26 +91,6 @@ non-yanked version, or use the
[`--allow_yanked_versions`](/reference/command-line-reference#flag--allow_yanked_versions)
flag to explicitly allow the yanked version.

## Compatibility level

In Go, MVS's assumption about backwards compatibility works because it treats
backwards incompatible versions of a module as a separate module. In terms of
SemVer, that means `A 1.x` and `A 2.x` are considered distinct modules, and can
coexist in the resolved dependency graph. This is, in turn, made possible by
encoding the major version in the package path in Go, so there aren't any
compile-time or linking-time conflicts. Bazel, however, cannot provide such
guarantees because it follows [a relaxed version of SemVer](#version-format).

Thus, Bazel needs the equivalent of the SemVer major version number to detect
backwards incompatible ("breaking") versions. This number is called the
*compatibility level*, and is specified by each module version in its
[`module()`](/rule/lib/globals/module#module) directive. With this information,
Bazel can throw an error if it detects that versions of the _same module_ with
_different compatibility levels_ exist in the resolved dependency graph.

Finally, incrementing the compatibility level can be disruptive to the users.
To learn more about when and how to increment it, [check the `MODULE.bazel`
FAQ](faq#incrementing-compatibility-level).

## Overrides

Expand Down Expand Up @@ -146,23 +125,15 @@ A [`multiple_version_override`](/rules/lib/globals/module#multiple_version_overr
can be specified to allow multiple versions of the same module to coexist in the
resolved dependency graph.

You can specify an explicit list of allowed versions for the module, which must
all be present in the dependency graph before resolution — there must exist
*some* transitive dependency depending on each allowed version. After
resolution, only the allowed versions of the module remain, while Bazel upgrades
other versions of the module to the nearest higher allowed version at the same
compatibility level. If no higher allowed version at the same compatibility
level exists, Bazel throws an error.
If there are multiple versions of the same module remaining in the dependency
graph, Bazel will pick the nearest higher allowed version for each dependent.

For example, if versions `1.1`, `1.3`, `1.5`, `1.7`, and `2.0` exist in the
dependency graph before resolution and the major version is the compatibility
level:
dependency graph before resolution:

* A multiple-version override allowing `1.3`, `1.7`, and `2.0` results in
`1.1` being upgraded to `1.3`, `1.5` being upgraded to `1.7`, and other
versions remaining the same.
* A multiple-version override allowing `1.5` and `2.0` results in an error, as
`1.7` has no higher version at the same compatibility level to upgrade to.
* A multiple-version override allowing `1.9` and `2.0` results in an error, as
`1.9` is not present in the dependency graph before resolution.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,9 @@ private static ModuleThreadContext execModuleFile(

compiledRootModuleFile.runOnThread(thread);
injectRepos(injectedRepositories, context, thread);
for (Event warning : context.getWarnings()) {
eventHandler.handle(warning);
}
} catch (EvalException e) {
eventHandler.handle(Event.error(e.getInnermostLocation(), e.getMessageWithStack()));
throw errorf(Code.BAD_MODULE, "error executing MODULE.bazel file for %s", moduleKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
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.google.devtools.build.lib.packages.StarlarkExportable;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
Expand Down Expand Up @@ -110,20 +111,10 @@ static void validateModuleName(String moduleName) throws EvalException {
defaultValue = "''"),
@Param(
name = "compatibility_level",
doc =
"The compatibility level of the module; this should be changed every time a major"
+ " incompatible change is introduced. This is essentially the \"major"
+ " version\" of the module in terms of SemVer, except that it's not embedded"
+ " in the version string itself, but exists as a separate field. Modules with"
+ " different compatibility levels participate in version resolution as if"
+ " they're modules with different names, but the final dependency graph cannot"
+ " contain multiple modules with the same name but different compatibility"
+ " levels (unless <code>multiple_version_override</code> is in effect). See <a"
+ " href=\"/external/module#compatibility_level\">the documentation</a> for"
+ " more details.",
doc = "Deprecated. This is now a no-op and has no effect.",
named = true,
positional = false,
defaultValue = "0"),
defaultValue = "-1"),
@Param(
name = "repo_name",
doc =
Expand Down Expand Up @@ -166,6 +157,14 @@ public void module(
if (context.isModuleCalled()) {
throw Starlark.errorf("the module() directive can only be called once");
}
if (compatibilityLevel.toInt("compatibility_level") != -1
&& context.getModuleBuilder().getKey().equals(ModuleKey.ROOT)) {
Comment on lines +160 to +161

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The warning for using the deprecated compatibility_level attribute is only issued for the root module. This check is too restrictive, as it won't warn users who are using a non-registry override (e.g., local_path_override) and have specified this attribute in the overridden module's MODULE.bazel file. Since developers can edit these files, they should also be warned about the deprecation.

A similar issue exists for the max_compatibility_level warning on lines 291-292.

To address this, you could consider passing information about whether a module is from a non-registry override into the ModuleThreadContext and using that in this condition.

context.addWarning(
Event.warn(
thread.getCallerLocation(),
"The attribute 'compatibility_level' in module() is a no-op and will be removed in a"
+ " future Bazel release. Please remove it from your MODULE.bazel file."));
}
if (context.hadNonModuleCall()) {
throw Starlark.errorf("if module() is called, it must be called before any other functions");
}
Expand All @@ -190,7 +189,7 @@ public void module(
.getModuleBuilder()
.setName(name)
.setVersion(parsedVersion)
.setCompatibilityLevel(compatibilityLevel.toInt("compatibility_level"))
.setCompatibilityLevel(0)
.addBazelCompatibilityValues(
checkAllCompatibilityVersions(bazelCompatibility, "bazel_compatibility"))
.setRepoName(repoName);
Expand Down Expand Up @@ -241,11 +240,7 @@ private static ImmutableList<String> checkAllCompatibilityVersions(
defaultValue = "''"),
@Param(
name = "max_compatibility_level",
doc =
"The maximum <code>compatibility_level</code> supported for the module to be added"
+ " as a direct dependency. The version of the module implies the minimum"
+ " compatibility_level supported, as well as the maximum if this attribute is"
+ " not specified.",
doc = "Deprecated. This is now a no-op and has no effect.",
named = true,
positional = false,
defaultValue = "-1"),
Expand Down Expand Up @@ -293,6 +288,15 @@ public void bazelDep(
} catch (ParseException e) {
throw new EvalException("Invalid version in bazel_dep()", e);
}
if (maxCompatibilityLevel.toInt("max_compatibility_level") != -1
&& context.getModuleBuilder().getKey().equals(ModuleKey.ROOT)) {
context.addWarning(
Event.warn(
thread.getCallerLocation(),
"The attribute 'max_compatibility_level' in bazel_dep() is a no-op and will be"
+ " removed in a future Bazel release. Please remove it from your MODULE.bazel"
+ " file."));
}

Optional<String> repoName =
switch (repoNameArg) {
Expand All @@ -306,9 +310,7 @@ public void bazelDep(
};

if (!(context.shouldIgnoreDevDeps() && devDependency)) {
context.addDep(
repoName,
new DepSpec(name, parsedVersion, maxCompatibilityLevel.toInt("max_compatibility_level")));
context.addDep(repoName, new DepSpec(name, parsedVersion, -1));
}

if (repoName.isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.google.devtools.build.lib.cmdline.LabelConstants;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.cmdline.StarlarkThreadContext;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -55,6 +56,7 @@ public class ModuleThreadContext extends StarlarkThreadContext {
private final List<ModuleExtensionUsageBuilder> extensionUsageBuilders = new ArrayList<>();
private final Map<String, ModuleOverride> overrides = new LinkedHashMap<>();
private final Map<String, RepoNameUsage> repoNameUsages = new HashMap<>();
private final List<Event> warnings = new ArrayList<>();

private final Map<String, RepoOverride> overriddenRepos = new HashMap<>();
private final Map<String, RepoOverride> overridingRepos = new HashMap<>();
Expand Down Expand Up @@ -139,6 +141,14 @@ public InterimModule.Builder getModuleBuilder() {
return module;
}

public void addWarning(Event event) {
warnings.add(event);
}

public ImmutableList<Event> getWarnings() {
return ImmutableList.copyOf(warnings);
}

public boolean shouldIgnoreDevDeps() {
return ignoreDevDeps;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,77 +498,4 @@ public void nodep_wouldBeFulfilledIfNonDevDep() throws Exception {
.isEqualTo(Version.parse("1.0"));
assertThat(depGraph.get(createModuleKey("c", "1.0")).getDeps()).doesNotContainKey("d");
}

@Test
public void nodep_crossesCompatLevelBoundary() throws Exception {
scratch.overwriteFile(
"MODULE.bazel",
"""
bazel_dep(name='b',version='1.0')
bazel_dep(name='c',version='1.0')
""");

registry
.addModule(
createModuleKey("b", "1.0"),
"module(name='b', version='1.0')",
"bazel_dep(name='d', version='1.0')")
.addModule(
createModuleKey("c", "1.0"),
"module(name='c', version='1.0')",
"bazel_dep(name='d',version='2.0',repo_name=None)")
.addModule(createModuleKey("d", "1.0"), "module(name='d', version='1.0')")
.addModule(
createModuleKey("d", "2.0"), "module(name='d', version='2.0', compatibility_level=2)");
reporter.removeHandler(failFastHandler);
invalidatePackages(false);

EvaluationResult<BazelModuleResolutionValue> result =
SkyframeExecutorTestUtils.evaluate(
skyframeExecutor, BazelModuleResolutionValue.KEY, false, reporter);

assertThat(result.hasError()).isTrue();
assertThat(result.getError().getException())
.hasMessageThat()
.isEqualTo(
"c@1.0 depends on d@2.0 with compatibility level 2, but b@1.0 depends on d@1.0 with"
+ " compatibility level 0 which is different");
}

@Test
public void nodep_crossesCompatLevelBoundary_butWithMaxCompatibilityLevel() throws Exception {
scratch.overwriteFile(
"MODULE.bazel",
"""
bazel_dep(name='b',version='1.0')
bazel_dep(name='c',version='1.0')
""");

registry
.addModule(
createModuleKey("b", "1.0"),
"module(name='b', version='1.0')",
"bazel_dep(name='d', version='1.0', max_compatibility_level=2)")
.addModule(
createModuleKey("c", "1.0"),
"module(name='c', version='1.0')",
"bazel_dep(name='d',version='2.0',repo_name=None)")
.addModule(createModuleKey("d", "1.0"), "module(name='d', version='1.0')")
.addModule(
createModuleKey("d", "2.0"), "module(name='d', version='2.0', compatibility_level=2)");
invalidatePackages(false);

EvaluationResult<BazelModuleResolutionValue> result =
SkyframeExecutorTestUtils.evaluate(
skyframeExecutor, BazelModuleResolutionValue.KEY, false, reporter);

if (result.hasError()) {
fail(result.getError().toString());
}
var depGraph = result.get(BazelModuleResolutionValue.KEY).getResolvedDepGraph();
assertThat(depGraph).doesNotContainKey(createModuleKey("d", "1.0"));
assertThat(depGraph.get(createModuleKey("b", "1.0")).getDeps().get("d").version())
.isEqualTo(Version.parse("2.0"));
assertThat(depGraph.get(createModuleKey("c", "1.0")).getDeps()).doesNotContainKey("d");
}
}
Loading
Loading