15
15
16
16
package com .google .devtools .build .lib .bazel .bzlmod ;
17
17
18
+ import static com .google .common .collect .ImmutableSet .toImmutableSet ;
18
19
import static java .util .stream .Collectors .joining ;
19
20
20
21
import com .google .common .collect .ImmutableMap ;
22
+ import com .google .common .collect .ImmutableSet ;
21
23
import com .google .devtools .build .lib .bazel .bzlmod .InterimModule .DepSpec ;
22
24
import com .google .devtools .build .lib .bazel .bzlmod .ModuleFileValue .RootModuleFileValue ;
23
25
import com .google .devtools .build .lib .bazel .repository .downloader .Checksum ;
24
26
import com .google .devtools .build .lib .server .FailureDetails ;
25
27
import com .google .devtools .build .skyframe .SkyFunction .Environment ;
26
- import com .google .devtools .build .skyframe .SkyKey ;
27
28
import com .google .devtools .build .skyframe .SkyframeLookupResult ;
28
- import java .util .ArrayDeque ;
29
29
import java .util .ArrayList ;
30
30
import java .util .Collections ;
31
31
import java .util .HashMap ;
32
+ import java .util .HashSet ;
32
33
import java .util .LinkedHashMap ;
33
- import java .util .LinkedHashSet ;
34
34
import java .util .List ;
35
35
import java .util .Map ;
36
36
import java .util .Optional ;
37
- import java .util .Queue ;
38
37
import java .util .SequencedMap ;
39
38
import java .util .Set ;
40
39
import javax .annotation .Nullable ;
@@ -58,82 +57,179 @@ public record Result(
58
57
@ Nullable
59
58
public static Result run (Environment env , RootModuleFileValue root )
60
59
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.
77
177
for (DepSpec depSpec : module .getDeps ().values ()) {
78
- if (depGraph .containsKey (depSpec .toModuleKey ())) {
178
+ ModuleKey depKey = depSpec .toModuleKey ();
179
+ if (depGraph .containsKey (depKey )) {
79
180
continue ;
80
181
}
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 ));
84
184
}
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 ;
101
192
}
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 ;
109
196
}
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 ));
131
199
}
132
200
}
201
+ return nextHorizon .build ();
133
202
}
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 );
136
233
}
137
- return new Result (ImmutableMap .copyOf (depGraph ), ImmutableMap .copyOf (registryFileHashes ));
138
234
}
139
235
}
0 commit comments