blob: 361f5231fe850cf762f8c6ced72ca5b46f5d70a4 [file] [log] [blame]
// Copyright 2021 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.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
import static java.util.Comparator.naturalOrder;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Comparators;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.bazel.bzlmod.InterimModule.DepSpec;
import com.google.devtools.build.lib.server.FailureDetails.ExternalDeps.Code;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable;
/**
* Runs module selection. This step of module resolution reads the output of {@link Discovery} and
* applies the Minimal Version Selection algorithm to it, removing unselected modules from the
* dependency graph and rewriting dependencies to point to the selected versions. It also returns an
* un-pruned version of the dep graph for inspection purpose.
*
* <p>Essentially, what needs to happen is:
*
* <ul>
* <li>In the most basic case, only one version of each module is selected (ie. remains in the dep
* graph). The selected version is simply the highest among all existing versions in the dep
* graph. In other words, each module name forms a "selection group". If foo@1.5 is selected,
* then any other foo@X is removed from the dep graph, and any module depending on foo@X will
* depend on foo@1.5 instead.
* <li>As an extension of the above, we also remove any module that becomes unreachable from the
* root module because of the removal of some other module.
* <li>If, however, versions of the same module but with different compatibility levels exist in
* the dep graph, then one version is selected for each compatibility level (ie. we split the
* selection groups by compatibility level). In the end, though, still only one version can
* remain in the dep graph after the removal of unselected and unreachable modules.
* <li>Things get more complicated with multiple-version overrides. If module foo has a
* multiple-version override which allows versions [1.3, 1.5, 2.0] (using the major version as
* the compatibility level), then we further split the selection groups by the target allowed
* version (keep in mind that versions are upgraded to the nearest higher-or-equal allowed
* version at the same compatibility level). If, for example, some module depends on foo@1.0,
* then it'll depend on foo@1.3 post-selection instead (and foo@1.0 will be removed). If any
* of foo@1.7, foo@2.2, or foo@3.0 exist in the dependency graph before selection, they must
* be removed before the end of selection (by becoming unreachable, for example), otherwise
* it'll be an error since they're not allowed by the override (these versions are in
* selection groups that have no valid target allowed version).
* <li>Things get even more complicated with max_compatibility_level. The difference this
* introduces is that each "DepSpec" could be satisfied by one of multiple choices. (Without
* max_compatibility_level, there is always only one choice.) So what we do is go through all
* the combinations of possible choices for each distinct DepSpec, and for each combination,
* see if the resulting dep graph is valid. As soon as we find a valid combination, we return
* that result. The distinct DepSpecs are sorted by the order they first appear in the dep
* graph if we BFS from the root module. The combinations are attempted in the typical
* cartesian product order (see {@link Lists#cartesianProduct}); the "version choices" of each
* DepSpec are sorted from low to high.
* </ul>
*/
final class Selection {
private Selection() {}
/** The result of selection. */
@AutoValue
abstract static class Result {
/** Final dep graph sorted in BFS iteration order, with unused modules removed. */
abstract ImmutableMap<ModuleKey, InterimModule> getResolvedDepGraph();
/**
* Un-pruned dep graph, with updated dep keys, and additionally containing the unused modules
* which were initially discovered (and their MODULE.bazel files loaded). Does not contain
* modules overridden by {@code single_version_override} or {@link NonRegistryOverride}, only by
* {@code multiple_version_override}.
*/
abstract ImmutableMap<ModuleKey, InterimModule> getUnprunedDepGraph();
static Result create(
ImmutableMap<ModuleKey, InterimModule> resolvedDepGraph,
ImmutableMap<ModuleKey, InterimModule> unprunedDepGraph) {
return new AutoValue_Selection_Result(resolvedDepGraph, unprunedDepGraph);
}
}
/** During selection, a version is selected for each distinct "selection group". */
@AutoValue
abstract static class SelectionGroup {
static SelectionGroup create(
String moduleName, int compatibilityLevel, Version targetAllowedVersion) {
return new AutoValue_Selection_SelectionGroup(
moduleName, compatibilityLevel, targetAllowedVersion);
}
abstract String getModuleName();
abstract int getCompatibilityLevel();
/** This is only used for modules with multiple-version overrides. */
abstract Version getTargetAllowedVersion();
}
@AutoValue
abstract static class ModuleNameAndCompatibilityLevel {
static ModuleNameAndCompatibilityLevel create(String moduleName, int compatibilityLevel) {
return new AutoValue_Selection_ModuleNameAndCompatibilityLevel(
moduleName, compatibilityLevel);
}
abstract String getModuleName();
abstract int getCompatibilityLevel();
}
/**
* Computes a mapping from (moduleName, compatibilityLevel) to the set of allowed versions. This
* is only performed for modules with multiple-version overrides.
*/
private static ImmutableMap<ModuleNameAndCompatibilityLevel, ImmutableSortedSet<Version>>
computeAllowedVersionSets(
ImmutableMap<String, ModuleOverride> overrides,
ImmutableMap<ModuleKey, InterimModule> depGraph)
throws ExternalDepsException {
Map<ModuleNameAndCompatibilityLevel, ImmutableSortedSet.Builder<Version>> allowedVersionSets =
new HashMap<>();
for (Map.Entry<String, ModuleOverride> overrideEntry : overrides.entrySet()) {
String moduleName = overrideEntry.getKey();
ModuleOverride override = overrideEntry.getValue();
if (!(override instanceof MultipleVersionOverride)) {
continue;
}
ImmutableList<Version> allowedVersions = ((MultipleVersionOverride) override).getVersions();
for (Version allowedVersion : allowedVersions) {
InterimModule allowedVersionModule =
depGraph.get(ModuleKey.create(moduleName, allowedVersion));
if (allowedVersionModule == null) {
throw ExternalDepsException.withMessage(
Code.VERSION_RESOLUTION_ERROR,
"multiple_version_override for module %s contains version %s, but it doesn't"
+ " exist in the dependency graph",
moduleName,
allowedVersion);
}
ImmutableSortedSet.Builder<Version> allowedVersionSet =
allowedVersionSets.computeIfAbsent(
ModuleNameAndCompatibilityLevel.create(
moduleName, allowedVersionModule.getCompatibilityLevel()),
// Remember that the empty version compares greater than any other version, so we
// can use it as a sentinel value.
k -> ImmutableSortedSet.<Version>naturalOrder().add(Version.EMPTY));
allowedVersionSet.add(allowedVersion);
}
}
return ImmutableMap.copyOf(
Maps.transformValues(allowedVersionSets, ImmutableSortedSet.Builder::build));
}
/**
* Computes the {@link SelectionGroup} for the given module. If the module has a multiple-version
* override (which would be reflected in the allowedVersionSets), information in there will be
* used to compute its targetAllowedVersion.
*/
private static SelectionGroup computeSelectionGroup(
InterimModule module,
ImmutableMap<ModuleNameAndCompatibilityLevel, ImmutableSortedSet<Version>>
allowedVersionSets) {
ImmutableSortedSet<Version> allowedVersionSet =
allowedVersionSets.get(
ModuleNameAndCompatibilityLevel.create(
module.getName(), module.getCompatibilityLevel()));
if (allowedVersionSet == null) {
// This means that this module has no multiple-version override.
return SelectionGroup.create(
module.getKey().getName(), module.getCompatibilityLevel(), Version.EMPTY);
}
return SelectionGroup.create(
module.getKey().getName(),
module.getCompatibilityLevel(),
// We use the `ceiling` method here to quickly locate the lowest allowed version that's
// still no lower than this module's version.
// If this module's version is higher than any allowed version (in which case EMPTY is
// returned), it should result in an error. We don't immediately throw here because it might
// still become unreferenced later.
allowedVersionSet.ceiling(module.getVersion()));
}
/**
* Computes the possible list of ModuleKeys a single given DepSpec can resolve to. This is
* normally just one ModuleKey, but when max_compatibility_level is involved, multiple choices may
* be possible.
*/
private static ImmutableList<ModuleKey> computePossibleResolutionResultsForOneDepSpec(
DepSpec depSpec,
ImmutableMap<ModuleKey, SelectionGroup> selectionGroups,
Map<SelectionGroup, Version> selectedVersions) {
int minCompatibilityLevel = selectionGroups.get(depSpec.toModuleKey()).getCompatibilityLevel();
int maxCompatibilityLevel =
depSpec.getMaxCompatibilityLevel() < 0
? minCompatibilityLevel
: depSpec.getMaxCompatibilityLevel();
// First find the selection groups that this DepSpec could use.
return Maps.filterKeys(
selectedVersions,
group ->
group.getModuleName().equals(depSpec.getName())
&& group.getCompatibilityLevel() >= minCompatibilityLevel
&& group.getCompatibilityLevel() <= maxCompatibilityLevel
&& group.getTargetAllowedVersion().compareTo(depSpec.getVersion()) >= 0)
.entrySet()
.stream()
// Collect into an ImmutableSortedMap so that:
// 1. The final list is sorted by compatibility level, guaranteeing lowest version first;
// 2. Only one ModuleKey is attempted per compatibility level, so that in the case of a
// multiple-version override, we only try the lowest allowed version in that
// compatibility level (note the Comparators::min call).
.collect(
toImmutableSortedMap(
naturalOrder(),
e -> e.getKey().getCompatibilityLevel(),
e -> e.getValue(),
Comparators::min))
.values()
.stream()
.map(v -> ModuleKey.create(depSpec.getName(), v))
.collect(toImmutableList());
}
/**
* Computes the possible list of ModuleKeys a DepSpec can resolve to, for all distinct DepSpecs in
* the dependency graph.
*/
private static ImmutableMap<DepSpec, ImmutableList<ModuleKey>> computePossibleResolutionResults(
ImmutableMap<ModuleKey, InterimModule> depGraph,
ImmutableMap<ModuleKey, SelectionGroup> selectionGroups,
Map<SelectionGroup, Version> selectedVersions) {
// Important that we use a LinkedHashMap here to ensure reproducibility.
Map<DepSpec, ImmutableList<ModuleKey>> results = new LinkedHashMap<>();
for (InterimModule module : depGraph.values()) {
for (DepSpec depSpec : module.getDeps().values()) {
results.computeIfAbsent(
depSpec,
ds ->
computePossibleResolutionResultsForOneDepSpec(
ds, selectionGroups, selectedVersions));
}
}
return ImmutableMap.copyOf(results);
}
/**
* Given the possible list of ModuleKeys each DepSpec can resolve to, enumerate through all the
* possible resolution strategies. Each strategy assigns each DepSpec to a single ModuleKey out of
* its possible list.
*/
private static List<Function<DepSpec, ModuleKey>> enumerateStrategies(
ImmutableMap<DepSpec, ImmutableList<ModuleKey>> possibleResolutionResults) {
Map<DepSpec, Integer> depSpecToPosition = new HashMap<>();
int position = 0;
for (DepSpec depSpec : possibleResolutionResults.keySet()) {
depSpecToPosition.put(depSpec, position++);
}
return Lists.transform(
Lists.cartesianProduct(possibleResolutionResults.values().asList()),
(List<ModuleKey> choices) ->
(DepSpec depSpec) -> choices.get(depSpecToPosition.get(depSpec)));
// TODO(wyv): There are some strategies that we could eliminate earlier. For example, the
// strategy where (foo@1.1, maxCL=3) resolves to foo@2.0 and (foo@1.2, maxCL=3) resolves to
// foo@3.0 is obviously not valid. All foo@? should resolve to the same version (assuming no
// multiple-version override).
}
/** Runs module selection (aka version resolution). */
public static Result run(
ImmutableMap<ModuleKey, InterimModule> depGraph,
ImmutableMap<String, ModuleOverride> overrides)
throws ExternalDepsException {
// For any multiple-version overrides, build a mapping from (moduleName, compatibilityLevel) to
// the set of allowed versions.
ImmutableMap<ModuleNameAndCompatibilityLevel, ImmutableSortedSet<Version>> allowedVersionSets =
computeAllowedVersionSets(overrides, depGraph);
// For each module in the dep graph, pre-compute its selection group. For most modules this is
// simply its (moduleName, compatibilityLevel) tuple; for modules with multiple-version
// overrides, it additionally includes the targetAllowedVersion, which denotes the version to
// "snap" to during selection.
ImmutableMap<ModuleKey, SelectionGroup> selectionGroups =
ImmutableMap.copyOf(
Maps.transformValues(
depGraph, module -> computeSelectionGroup(module, allowedVersionSets)));
// Figure out the version to select for every selection group.
Map<SelectionGroup, Version> selectedVersions = new HashMap<>();
for (Map.Entry<ModuleKey, SelectionGroup> entry : selectionGroups.entrySet()) {
ModuleKey key = entry.getKey();
SelectionGroup selectionGroup = entry.getValue();
selectedVersions.merge(selectionGroup, key.getVersion(), Comparators::max);
}
// Compute the possible list of ModuleKeys that each DepSpec could resolve to.
ImmutableMap<DepSpec, ImmutableList<ModuleKey>> possibleResolutionResults =
computePossibleResolutionResults(depGraph, selectionGroups, selectedVersions);
for (Map.Entry<DepSpec, ImmutableList<ModuleKey>> e : possibleResolutionResults.entrySet()) {
if (e.getValue().isEmpty()) {
throw ExternalDepsException.withMessage(
Code.VERSION_RESOLUTION_ERROR,
"Unexpected error: %s has no valid resolution result",
e.getKey());
}
}
// Each DepSpec may resolve to one or more ModuleKeys. We try out every single possible
// combination; in other words, we enumerate through the cartesian product of the "possible
// resolution result" set for every distinct DepSpec. Each element of this cartesian product is
// essentially a mapping from DepSpecs to ModuleKeys; we can call this mapping a "resolution
// strategy".
//
// Given a resolution strategy, we can walk through the graph from the root module, and see if
// the strategy yields a valid graph (only containing the nodes reachable from the root). If the
// graph is invalid (for example, because there are modules with different compatibility
// levels), we try the next resolution strategy. When all strategies are exhausted, we know
// there is no way to achieve a valid selection result, so we report the failure from the time
// we attempted to walk the graph using the first resolution strategy.
DepGraphWalker depGraphWalker = new DepGraphWalker(depGraph, overrides, selectionGroups);
ExternalDepsException firstFailure = null;
for (Function<DepSpec, ModuleKey> resolutionStrategy :
enumerateStrategies(possibleResolutionResults)) {
try {
ImmutableMap<ModuleKey, InterimModule> prunedDepGraph =
depGraphWalker.walk(resolutionStrategy);
// If the call above didn't throw, we have a valid graph. Go ahead and produce a result!
ImmutableMap<ModuleKey, InterimModule> unprunedDepGraph =
ImmutableMap.copyOf(
Maps.transformValues(
depGraph,
module ->
module.withDepSpecsTransformed(
depSpec -> DepSpec.fromModuleKey(resolutionStrategy.apply(depSpec)))));
return Result.create(prunedDepGraph, unprunedDepGraph);
} catch (ExternalDepsException e) {
if (firstFailure == null) {
firstFailure = e;
}
}
}
// firstFailure cannot be null, since enumerateStrategies(...) cannot be empty, since no
// element of possibleResolutionResults is empty.
throw firstFailure;
}
/**
* Walks the dependency graph from the root node, collecting any reachable nodes through deps into
* a new dep graph and checking that nothing conflicts.
*/
static class DepGraphWalker {
private static final Joiner JOINER = Joiner.on(", ");
private final ImmutableMap<ModuleKey, InterimModule> oldDepGraph;
private final ImmutableMap<String, ModuleOverride> overrides;
private final ImmutableMap<ModuleKey, SelectionGroup> selectionGroups;
DepGraphWalker(
ImmutableMap<ModuleKey, InterimModule> oldDepGraph,
ImmutableMap<String, ModuleOverride> overrides,
ImmutableMap<ModuleKey, SelectionGroup> selectionGroups) {
this.oldDepGraph = oldDepGraph;
this.overrides = overrides;
this.selectionGroups = selectionGroups;
}
/**
* Walks the old dep graph and builds a new dep graph containing only deps reachable from the
* root module. The returned map has a guaranteed breadth-first iteration order.
*/
ImmutableMap<ModuleKey, InterimModule> walk(Function<DepSpec, ModuleKey> resolutionStrategy)
throws ExternalDepsException {
HashMap<String, ExistingModule> moduleByName = new HashMap<>();
ImmutableMap.Builder<ModuleKey, InterimModule> newDepGraph = ImmutableMap.builder();
Set<ModuleKey> known = new HashSet<>();
Queue<ModuleKeyAndDependent> toVisit = new ArrayDeque<>();
toVisit.add(ModuleKeyAndDependent.create(ModuleKey.ROOT, null));
known.add(ModuleKey.ROOT);
while (!toVisit.isEmpty()) {
ModuleKeyAndDependent moduleKeyAndDependent = toVisit.remove();
ModuleKey key = moduleKeyAndDependent.getModuleKey();
InterimModule module =
oldDepGraph
.get(key)
.withDepSpecsTransformed(
depSpec -> DepSpec.fromModuleKey(resolutionStrategy.apply(depSpec)));
visit(key, module, moduleKeyAndDependent.getDependent(), moduleByName);
for (DepSpec depSpec : module.getDeps().values()) {
if (known.add(depSpec.toModuleKey())) {
toVisit.add(ModuleKeyAndDependent.create(depSpec.toModuleKey(), key));
}
}
newDepGraph.put(key, module);
}
return newDepGraph.buildOrThrow();
}
void visit(
ModuleKey key,
InterimModule module,
@Nullable ModuleKey from,
HashMap<String, ExistingModule> moduleByName)
throws ExternalDepsException {
ModuleOverride override = overrides.get(key.getName());
if (override instanceof MultipleVersionOverride) {
if (selectionGroups.get(key).getTargetAllowedVersion().isEmpty()) {
// This module has no target allowed version, which means that there's no allowed version
// higher than its version at the same compatibility level.
Preconditions.checkState(
from != null, "the root module cannot have a multiple version override");
throw ExternalDepsException.withMessage(
Code.VERSION_RESOLUTION_ERROR,
"%s depends on %s which is not allowed by the multiple_version_override on %s,"
+ " which allows only [%s]",
from,
key,
key.getName(),
JOINER.join(((MultipleVersionOverride) override).getVersions()));
}
} else {
ExistingModule existingModuleWithSameName =
moduleByName.put(
module.getName(), ExistingModule.create(key, module.getCompatibilityLevel(), from));
if (existingModuleWithSameName != null) {
// This has to mean that a module with the same name but a different compatibility level
// was also selected.
Preconditions.checkState(
from != null && existingModuleWithSameName.getDependent() != null,
"the root module cannot possibly exist more than once in the dep graph");
throw ExternalDepsException.withMessage(
Code.VERSION_RESOLUTION_ERROR,
"%s depends on %s with compatibility level %d, but %s depends on %s with"
+ " compatibility level %d which is different",
from,
key,
module.getCompatibilityLevel(),
existingModuleWithSameName.getDependent(),
existingModuleWithSameName.getModuleKey(),
existingModuleWithSameName.getCompatibilityLevel());
}
}
// Make sure that we don't have `module` depending on the same dependency version twice.
HashMap<ModuleKey, String> depKeyToRepoName = new HashMap<>();
for (Map.Entry<String, DepSpec> depEntry : module.getDeps().entrySet()) {
String repoName = depEntry.getKey();
DepSpec depSpec = depEntry.getValue();
String previousRepoName = depKeyToRepoName.put(depSpec.toModuleKey(), repoName);
if (previousRepoName != null) {
throw ExternalDepsException.withMessage(
Code.VERSION_RESOLUTION_ERROR,
"%s depends on %s at least twice (with repo names %s and %s). Consider adding a"
+ " multiple_version_override if you want to depend on multiple versions of"
+ " %s simultaneously",
key,
depSpec.toModuleKey(),
repoName,
previousRepoName,
key.getName());
}
}
}
@AutoValue
abstract static class ModuleKeyAndDependent {
abstract ModuleKey getModuleKey();
@Nullable
abstract ModuleKey getDependent();
static ModuleKeyAndDependent create(ModuleKey moduleKey, @Nullable ModuleKey dependent) {
return new AutoValue_Selection_DepGraphWalker_ModuleKeyAndDependent(moduleKey, dependent);
}
}
@AutoValue
abstract static class ExistingModule {
abstract ModuleKey getModuleKey();
abstract int getCompatibilityLevel();
@Nullable
abstract ModuleKey getDependent();
static ExistingModule create(
ModuleKey moduleKey, int compatibilityLevel, ModuleKey dependent) {
return new AutoValue_Selection_DepGraphWalker_ExistingModule(
moduleKey, compatibilityLevel, dependent);
}
}
}
}