Skip to content

Commit b46d382

Browse files
[7.6.0] Bzlmod: nodep deps (#25509)
This PR implements the "nodep" edges from https://docs.google.com/document/d/1JsfbH9kdMe3dyOY-IR8SUakS541A7OM8pQcKpxTRMRs/edit?tab=t.0, using the syntax of `bazel_dep(..., repo_name=None)`. The behavior is that these edges are "unfulfilled" unless the module they refer to already exist in the dep graph by some other means. Most of the changes are in the Discovery class -- I reorganized the code in there to hopefully help with readability, given the new multi-round discovery logic. Also changed `ModuleFileValue.Key` to no longer take the applicable override next to the module key -- `ModuleFileFunction` now gets the root module from Skyframe itself and looks up the correct override. The old setup was always weird (what does it mean to request `[email protected]` with an incorrect override??). Work towards #25214 RELNOTES: The `repo_name` parameter of `bazel_dep` can now be set to `None` to mark it a "nodep" dependency -- that is, the `bazel_dep` specification is only honored if the target module already exists in the dependency graph by some other means. PiperOrigin-RevId: 730962587 Change-Id: I1ca7a7a1228da5e4c2e4b5d6983a2ec97b31a4b8 --------- Co-authored-by: Yun Peng <[email protected]>
1 parent fea1188 commit b46d382

17 files changed

+747
-390
lines changed

src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModTidyFunction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,6 @@ public SkyValue compute(SkyKey skyKey, Environment env)
102102
}
103103

104104
return BazelModTidyValue.create(
105-
fixups.build(), buildozer.asPath(), rootModuleFileValue.getModuleFilePaths());
105+
fixups.build(), buildozer.asPath(), rootModuleFileValue.moduleFilePaths());
106106
}
107107
}

src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorFunction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedExcept
6060
if (resolutionValue == null) {
6161
return null;
6262
}
63-
ImmutableMap<String, ModuleOverride> overrides = root.getOverrides();
63+
ImmutableMap<String, ModuleOverride> overrides = root.overrides();
6464
ImmutableMap<ModuleKey, InterimModule> unprunedDepGraph = resolutionValue.getUnprunedDepGraph();
6565
ImmutableMap<ModuleKey, Module> resolvedDepGraph = resolutionValue.getResolvedDepGraph();
6666

src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunction.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public SkyValue compute(SkyKey skyKey, Environment env)
113113
SequencedMap<String, Optional<Checksum>> registryFileHashes =
114114
new LinkedHashMap<>(state.discoverAndSelectResult.registryFileHashes);
115115
ImmutableSet<RepoSpecKey> repoSpecKeys =
116-
state.discoverAndSelectResult.selectionResult.getResolvedDepGraph().values().stream()
116+
state.discoverAndSelectResult.selectionResult.resolvedDepGraph().values().stream()
117117
// Modules with a null registry have a non-registry override. We don't need to
118118
// fetch or store the repo spec in this case.
119119
.filter(module -> module.getRegistry() != null)
@@ -135,14 +135,14 @@ public SkyValue compute(SkyKey skyKey, Environment env)
135135
Profiler.instance().profile(ProfilerTask.BZLMOD, "compute final dep graph")) {
136136
finalDepGraph =
137137
computeFinalDepGraph(
138-
state.discoverAndSelectResult.selectionResult.getResolvedDepGraph(),
139-
root.getOverrides(),
138+
state.discoverAndSelectResult.selectionResult.resolvedDepGraph(),
139+
root.overrides(),
140140
remoteRepoSpecs.buildOrThrow());
141141
}
142142

143143
return BazelModuleResolutionValue.create(
144144
finalDepGraph,
145-
state.discoverAndSelectResult.selectionResult.getUnprunedDepGraph(),
145+
state.discoverAndSelectResult.selectionResult.unprunedDepGraph(),
146146
ImmutableMap.copyOf(registryFileHashes),
147147
state.discoverAndSelectResult.selectedYankedVersions);
148148
}
@@ -163,15 +163,15 @@ private static Result discoverAndSelect(
163163
return null;
164164
}
165165

166-
verifyAllOverridesAreOnExistentModules(discoveryResult.depGraph(), root.getOverrides());
166+
verifyAllOverridesAreOnExistentModules(discoveryResult.depGraph(), root.overrides());
167167

168168
Selection.Result selectionResult;
169169
try (SilentCloseable c = Profiler.instance().profile(ProfilerTask.BZLMOD, "selection")) {
170-
selectionResult = Selection.run(discoveryResult.depGraph(), root.getOverrides());
170+
selectionResult = Selection.run(discoveryResult.depGraph(), root.overrides());
171171
} catch (ExternalDepsException e) {
172172
throw new BazelModuleResolutionFunctionException(e, Transience.PERSISTENT);
173173
}
174-
ImmutableMap<ModuleKey, InterimModule> resolvedDepGraph = selectionResult.getResolvedDepGraph();
174+
ImmutableMap<ModuleKey, InterimModule> resolvedDepGraph = selectionResult.resolvedDepGraph();
175175

176176
ImmutableMap<ModuleKey, YankedVersionsValue> yankedVersionsValues;
177177
try (SilentCloseable c =

src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Discovery.java

Lines changed: 167 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,25 @@
1515

1616
package com.google.devtools.build.lib.bazel.bzlmod;
1717

18+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
1819
import static java.util.stream.Collectors.joining;
1920

2021
import com.google.common.collect.ImmutableMap;
22+
import com.google.common.collect.ImmutableSet;
2123
import com.google.devtools.build.lib.bazel.bzlmod.InterimModule.DepSpec;
2224
import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue;
2325
import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
2426
import com.google.devtools.build.lib.server.FailureDetails;
2527
import com.google.devtools.build.skyframe.SkyFunction.Environment;
26-
import com.google.devtools.build.skyframe.SkyKey;
2728
import com.google.devtools.build.skyframe.SkyframeLookupResult;
28-
import java.util.ArrayDeque;
2929
import java.util.ArrayList;
3030
import java.util.Collections;
3131
import java.util.HashMap;
32+
import java.util.HashSet;
3233
import java.util.LinkedHashMap;
33-
import java.util.LinkedHashSet;
3434
import java.util.List;
3535
import java.util.Map;
3636
import java.util.Optional;
37-
import java.util.Queue;
3837
import java.util.SequencedMap;
3938
import java.util.Set;
4039
import javax.annotation.Nullable;
@@ -58,82 +57,179 @@ public record Result(
5857
@Nullable
5958
public static Result run(Environment env, RootModuleFileValue root)
6059
throws InterruptedException, ExternalDepsException {
61-
String rootModuleName = root.getModule().getName();
62-
ImmutableMap<String, ModuleOverride> overrides = root.getOverrides();
63-
Map<ModuleKey, InterimModule> depGraph = new HashMap<>();
64-
depGraph.put(
65-
ModuleKey.ROOT,
66-
root.getModule()
67-
.withDepSpecsTransformed(InterimModule.applyOverrides(overrides, rootModuleName)));
68-
Queue<ModuleKey> unexpanded = new ArrayDeque<>();
69-
Map<ModuleKey, ModuleKey> predecessors = new HashMap<>();
70-
SequencedMap<String, Optional<Checksum>> registryFileHashes =
71-
new LinkedHashMap<>(root.getRegistryFileHashes());
72-
unexpanded.add(ModuleKey.ROOT);
73-
while (!unexpanded.isEmpty()) {
74-
Set<SkyKey> unexpandedSkyKeys = new LinkedHashSet<>();
75-
while (!unexpanded.isEmpty()) {
76-
InterimModule module = depGraph.get(unexpanded.remove());
60+
// Because of the possible existence of nodep edges, we do multiple rounds of discovery.
61+
// In each round, we keep track of unfulfilled nodep edges, and at the end of the round, if any
62+
// unfulfilled nodep edge can now be fulfilled, we run another round.
63+
ImmutableSet<String> prevRoundModuleNames = ImmutableSet.of(root.module().getName());
64+
while (true) {
65+
DiscoveryRound discoveryRound = new DiscoveryRound(env, root, prevRoundModuleNames);
66+
Result result = discoveryRound.run();
67+
if (result == null) {
68+
return null;
69+
}
70+
prevRoundModuleNames =
71+
result.depGraph().values().stream().map(InterimModule::getName).collect(toImmutableSet());
72+
if (discoveryRound.unfulfilledNodepEdgeModuleNames.stream()
73+
.noneMatch(prevRoundModuleNames::contains)) {
74+
return result;
75+
}
76+
}
77+
}
78+
79+
private static class DiscoveryRound {
80+
private final Environment env;
81+
private final RootModuleFileValue root;
82+
private final ImmutableSet<String> prevRoundModuleNames;
83+
private final Map<ModuleKey, InterimModule> depGraph = new HashMap<>();
84+
85+
/**
86+
* Stores a mapping from a module to its "predecessor" -- that is, its first dependent in BFS
87+
* order. This is used to report a dependency chain in errors (see {@link
88+
* #maybeReportDependencyChain}.
89+
*/
90+
private final Map<ModuleKey, ModuleKey> predecessors = new HashMap<>();
91+
92+
/**
93+
* For all unfulfilled nodep edges seen during this round, this set stores the module names of
94+
* those nodep edges. Remember that whether a nodep edge can be fulfilled depends on whether the
95+
* module it names already exists in the dep graph.
96+
*/
97+
private final Set<String> unfulfilledNodepEdgeModuleNames = new HashSet<>();
98+
99+
DiscoveryRound(
100+
Environment env, RootModuleFileValue root, ImmutableSet<String> prevRoundModuleNames) {
101+
this.env = env;
102+
this.root = root;
103+
this.prevRoundModuleNames = prevRoundModuleNames;
104+
}
105+
106+
/**
107+
* Runs one round of discovery. At its core, this is a simple breadth-first search: we start
108+
* from the "horizon" of just the root module, and advance the horizon by discovering the
109+
* dependencies of modules in the current horizon. Keep doing this until the horizon is empty.
110+
*/
111+
@Nullable
112+
Result run() throws InterruptedException, ExternalDepsException {
113+
SequencedMap<String, Optional<Checksum>> registryFileHashes = new LinkedHashMap<>();
114+
depGraph.put(ModuleKey.ROOT, root.module().withDepSpecsTransformed(this::applyOverrides));
115+
ImmutableSet<ModuleKey> horizon = ImmutableSet.of(ModuleKey.ROOT);
116+
while (!horizon.isEmpty()) {
117+
ImmutableSet<ModuleFileValue.Key> nextHorizonSkyKeys = advanceHorizon(horizon);
118+
SkyframeLookupResult result = env.getValuesAndExceptions(nextHorizonSkyKeys);
119+
var nextHorizon = ImmutableSet.<ModuleKey>builder();
120+
for (ModuleFileValue.Key skyKey : nextHorizonSkyKeys) {
121+
ModuleKey depKey = skyKey.moduleKey();
122+
ModuleFileValue moduleFileValue;
123+
try {
124+
moduleFileValue =
125+
(ModuleFileValue) result.getOrThrow(skyKey, ExternalDepsException.class);
126+
} catch (ExternalDepsException e) {
127+
throw maybeReportDependencyChain(e, depKey);
128+
}
129+
if (moduleFileValue == null) {
130+
// Don't return yet. Try to expand any other unexpanded nodes before returning.
131+
depGraph.put(depKey, null);
132+
} else {
133+
depGraph.put(
134+
depKey, moduleFileValue.module().withDepSpecsTransformed(this::applyOverrides));
135+
registryFileHashes.putAll(moduleFileValue.registryFileHashes());
136+
nextHorizon.add(depKey);
137+
}
138+
}
139+
horizon = nextHorizon.build();
140+
}
141+
if (env.valuesMissing()) {
142+
return null;
143+
}
144+
return new Result(ImmutableMap.copyOf(depGraph), ImmutableMap.copyOf(registryFileHashes));
145+
}
146+
147+
/**
148+
* Returns a new {@link DepSpec} that is transformed according to any existing overrides on the
149+
* dependency module.
150+
*/
151+
DepSpec applyOverrides(DepSpec depSpec) {
152+
if (root.module().getName().equals(depSpec.getName())) {
153+
return DepSpec.fromModuleKey(ModuleKey.ROOT);
154+
}
155+
Version newVersion =
156+
switch (root.overrides().get(depSpec.getName())) {
157+
case NonRegistryOverride nro -> Version.EMPTY;
158+
case SingleVersionOverride svo when !svo.getVersion().isEmpty() -> svo.getVersion();
159+
case null, default -> depSpec.getVersion();
160+
};
161+
return DepSpec.create(depSpec.getName(), newVersion, depSpec.getMaxCompatibilityLevel());
162+
}
163+
164+
/**
165+
* Given a set of module keys to discover (the current "horizon"), return the next horizon
166+
* consisting of newly discovered module keys from the current set (mostly, their dependencies).
167+
*
168+
* <p>The current horizon contains keys to modules that are already in the {@code depGraph}.
169+
* Note also that this method mutates {@code predecessors} and {@code
170+
* unfulfilledNodepEdgeModuleNames}.
171+
*/
172+
ImmutableSet<ModuleFileValue.Key> advanceHorizon(ImmutableSet<ModuleKey> horizon) {
173+
var nextHorizon = ImmutableSet.<ModuleFileValue.Key>builder();
174+
for (ModuleKey moduleKey : horizon) {
175+
InterimModule module = depGraph.get(moduleKey);
176+
// The main group of module keys to discover are the current horizon's normal deps.
77177
for (DepSpec depSpec : module.getDeps().values()) {
78-
if (depGraph.containsKey(depSpec.toModuleKey())) {
178+
ModuleKey depKey = depSpec.toModuleKey();
179+
if (depGraph.containsKey(depKey)) {
79180
continue;
80181
}
81-
predecessors.putIfAbsent(depSpec.toModuleKey(), module.getKey());
82-
unexpandedSkyKeys.add(
83-
ModuleFileValue.key(depSpec.toModuleKey(), overrides.get(depSpec.getName())));
182+
predecessors.putIfAbsent(depKey, module.getKey());
183+
nextHorizon.add(ModuleFileValue.key(depKey));
84184
}
85-
}
86-
SkyframeLookupResult result = env.getValuesAndExceptions(unexpandedSkyKeys);
87-
for (SkyKey skyKey : unexpandedSkyKeys) {
88-
ModuleKey depKey = ((ModuleFileValue.Key) skyKey).getModuleKey();
89-
ModuleFileValue moduleFileValue;
90-
try {
91-
moduleFileValue =
92-
(ModuleFileValue) result.getOrThrow(skyKey, ExternalDepsException.class);
93-
} catch (ExternalDepsException e) {
94-
if (e.getDetailedExitCode().getFailureDetail() == null
95-
|| e.getDetailedExitCode().getFailureDetail().getExternalDeps().getCode()
96-
!= FailureDetails.ExternalDeps.Code.BAD_MODULE) {
97-
// This is not due to a bad module, so don't print a dependency chain. This covers cases
98-
// such as a parse error in the lockfile or an I/O exception during registry access,
99-
// which aren't related to any particular module dep.
100-
throw e;
185+
// Any of the current horizon's nodep deps should also be discovered ("fulfilled"), iff the
186+
// module they refer to already exists in the dep graph. Otherwise, record these unfulfilled
187+
// nodep edges, so that we can later decide whether to run another round of discovery.
188+
for (DepSpec depSpec : module.getNodepDeps()) {
189+
ModuleKey depKey = depSpec.toModuleKey();
190+
if (depGraph.containsKey(depKey)) {
191+
continue;
101192
}
102-
// Trace back a dependency chain to the root module. There can be multiple paths to the
103-
// failing module, but any of those is useful for debugging.
104-
List<ModuleKey> depChain = new ArrayList<>();
105-
depChain.add(depKey);
106-
ModuleKey predecessor = depKey;
107-
while ((predecessor = predecessors.get(predecessor)) != null) {
108-
depChain.add(predecessor);
193+
if (!prevRoundModuleNames.contains(depSpec.getName())) {
194+
unfulfilledNodepEdgeModuleNames.add(depSpec.getName());
195+
continue;
109196
}
110-
Collections.reverse(depChain);
111-
String depChainString =
112-
depChain.stream().map(ModuleKey::toString).collect(joining(" -> "));
113-
throw ExternalDepsException.withCauseAndMessage(
114-
FailureDetails.ExternalDeps.Code.BAD_MODULE,
115-
e,
116-
"in module dependency chain %s",
117-
depChainString);
118-
}
119-
if (moduleFileValue == null) {
120-
// Don't return yet. Try to expand any other unexpanded nodes before returning.
121-
depGraph.put(depKey, null);
122-
} else {
123-
depGraph.put(
124-
depKey,
125-
moduleFileValue
126-
.getModule()
127-
.withDepSpecsTransformed(
128-
InterimModule.applyOverrides(overrides, rootModuleName)));
129-
registryFileHashes.putAll(moduleFileValue.getRegistryFileHashes());
130-
unexpanded.add(depKey);
197+
predecessors.putIfAbsent(depKey, module.getKey());
198+
nextHorizon.add(ModuleFileValue.key(depKey));
131199
}
132200
}
201+
return nextHorizon.build();
133202
}
134-
if (env.valuesMissing()) {
135-
return null;
203+
204+
/**
205+
* When an exception occurs while discovering a new dep, try to add information about the
206+
* dependency chain that led to that dep.
207+
*/
208+
private ExternalDepsException maybeReportDependencyChain(
209+
ExternalDepsException e, ModuleKey depKey) {
210+
if (e.getDetailedExitCode().getFailureDetail() == null
211+
|| e.getDetailedExitCode().getFailureDetail().getExternalDeps().getCode()
212+
!= FailureDetails.ExternalDeps.Code.BAD_MODULE) {
213+
// This is not due to a bad module, so don't print a dependency chain. This covers cases
214+
// such as a parse error in the lockfile or an I/O exception during registry access,
215+
// which aren't related to any particular module dep.
216+
return e;
217+
}
218+
// Trace back a dependency chain to the root module. There can be multiple paths to the
219+
// failing module, but any of those is useful for debugging.
220+
List<ModuleKey> depChain = new ArrayList<>();
221+
depChain.add(depKey);
222+
ModuleKey predecessor = depKey;
223+
while ((predecessor = predecessors.get(predecessor)) != null) {
224+
depChain.add(predecessor);
225+
}
226+
Collections.reverse(depChain);
227+
String depChainString = depChain.stream().map(ModuleKey::toString).collect(joining(" -> "));
228+
return ExternalDepsException.withCauseAndMessage(
229+
FailureDetails.ExternalDeps.Code.BAD_MODULE,
230+
e,
231+
"in module dependency chain %s",
232+
depChainString);
136233
}
137-
return new Result(ImmutableMap.copyOf(depGraph), ImmutableMap.copyOf(registryFileHashes));
138234
}
139235
}

src/main/java/com/google/devtools/build/lib/bazel/bzlmod/InterimModule.java

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ public final ModuleKey toModuleKey() {
8484
*/
8585
public abstract ImmutableMap<String, DepSpec> getOriginalDeps();
8686

87+
/**
88+
* The "nodep" dependencies of this module: these don't actually add a dependency on the specified
89+
* module, but if specified module is somehow in the dependency graph, it'll be at least at this
90+
* version.
91+
*/
92+
public abstract ImmutableList<DepSpec> getNodepDeps();
93+
8794
/**
8895
* The registry where this module came from. Must be null iff the module has a {@link
8996
* NonRegistryOverride}.
@@ -159,6 +166,16 @@ public final Builder addToolchainsToRegister(Iterable<String> values) {
159166

160167
public abstract Builder setDeps(ImmutableMap<String, DepSpec> value);
161168

169+
abstract ImmutableList.Builder<DepSpec> nodepDepsBuilder();
170+
171+
@CanIgnoreReturnValue
172+
public final Builder addNodepDep(DepSpec value) {
173+
nodepDepsBuilder().add(value);
174+
return this;
175+
}
176+
177+
public abstract Builder setNodepDeps(ImmutableList<DepSpec> value);
178+
162179
public abstract Builder setRegistry(Registry value);
163180

164181
public abstract Builder setExtensionUsages(ImmutableList<ModuleExtensionUsage> value);
@@ -229,26 +246,4 @@ private static RepoSpec maybeAppendAdditionalPatches(
229246
.setAttributes(AttributeValues.create(attrBuilder.buildOrThrow()))
230247
.build();
231248
}
232-
233-
static UnaryOperator<DepSpec> applyOverrides(
234-
ImmutableMap<String, ModuleOverride> overrides, String rootModuleName) {
235-
return depSpec -> {
236-
if (rootModuleName.equals(depSpec.getName())) {
237-
return DepSpec.fromModuleKey(ModuleKey.ROOT);
238-
}
239-
240-
Version newVersion = depSpec.getVersion();
241-
@Nullable ModuleOverride override = overrides.get(depSpec.getName());
242-
if (override instanceof NonRegistryOverride) {
243-
newVersion = Version.EMPTY;
244-
} else if (override instanceof SingleVersionOverride) {
245-
Version overrideVersion = ((SingleVersionOverride) override).getVersion();
246-
if (!overrideVersion.isEmpty()) {
247-
newVersion = overrideVersion;
248-
}
249-
}
250-
251-
return DepSpec.create(depSpec.getName(), newVersion, depSpec.getMaxCompatibilityLevel());
252-
};
253-
}
254249
}

0 commit comments

Comments
 (0)