Skip to content

Commit 39481ad

Browse files
fmeumcopybara-github
authored andcommitted
Let repo rule attributes reference extension apparent names
Fixes bazelbuild#19055 RELNOTES: Repository rules instantiated in the same module extensions can now refer to each other by their extension-specified names in label attributes. Closes bazelbuild#23369. PiperOrigin-RevId: 666893202 Change-Id: Ib2eaa55fcb563adc86e16dc4a357ac808228ebda
1 parent 22ba72c commit 39481ad

16 files changed

+355
-119
lines changed

site/en/external/extension.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ several repo visibility rules:
165165
the apparent name `foo`, and the extension generates a repo with the
166166
specified name `foo`, then for all repos generated by that extension
167167
`foo` refers to the former.
168+
* Similarly, in a module extension's implementation function, repos created
169+
by the extension can refer to each other by their apparent names in
170+
attributes, regardless of the order in which they are created.
171+
* In case of a conflict with a repository visible to the module, labels
172+
passed to repository rule attributes can be wrapped in a call to
173+
[`Label`](/rules/lib/toplevel/attr#label) to ensure that they refer to
174+
the repo visible to the module instead of the extension-generated repo
175+
of the same name.
168176

169177
## Best practices
170178

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

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,25 @@ public static AttributeValues create(Map<String, Object> attribs) {
4545

4646
public abstract Dict<String, Object> attributes();
4747

48-
public static void validateAttrs(AttributeValues attributes, String what) throws EvalException {
48+
public static void validateAttrs(AttributeValues attributes, String where, String what)
49+
throws EvalException {
4950
for (var entry : attributes.attributes().entrySet()) {
50-
validateSingleAttr(entry.getKey(), entry.getValue(), what);
51+
validateSingleAttr(entry.getKey(), entry.getValue(), where, what);
5152
}
5253
}
5354

54-
public static void validateSingleAttr(String attrName, Object attrValue, String what)
55-
throws EvalException {
55+
public static void validateSingleAttr(
56+
String attrName, Object attrValue, String where, String what) throws EvalException {
5657
var maybeNonVisibleLabel = getFirstNonVisibleLabel(attrValue);
5758
if (maybeNonVisibleLabel.isEmpty()) {
5859
return;
5960
}
6061
Label label = maybeNonVisibleLabel.get();
6162
String repoName = label.getRepository().getName();
6263
throw Starlark.errorf(
63-
"no repository visible as '@%s' to the %s, but referenced by label '@%s//%s:%s' in"
64-
+ " attribute '%s' of %s. Is the %s missing a bazel_dep or use_repo(..., \"%s\")?",
65-
repoName,
66-
label.getRepository().getOwnerRepoDisplayString(),
67-
repoName,
68-
label.getPackageName(),
69-
label.getName(),
70-
attrName,
71-
what,
72-
label.getRepository().getOwnerModuleDisplayString(),
73-
repoName);
64+
"no repository visible as '@%s' %s, but referenced by label '@%s//%s:%s' in"
65+
+ " attribute '%s' of %s.",
66+
repoName, where, repoName, label.getPackageName(), label.getName(), attrName, what);
7467
}
7568

7669
private static Optional<Label> getFirstNonVisibleLabel(Object nativeAttrValue) {

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

Lines changed: 129 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515

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

18-
import com.google.auto.value.AutoValue;
18+
import com.google.common.collect.ImmutableList;
1919
import com.google.common.collect.ImmutableMap;
2020
import com.google.common.collect.Maps;
2121
import com.google.devtools.build.lib.analysis.BlazeDirectories;
22+
import com.google.devtools.build.lib.cmdline.Label;
2223
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
2324
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
2425
import com.google.devtools.build.lib.cmdline.RepositoryName;
@@ -29,12 +30,16 @@
2930
import com.google.devtools.build.lib.packages.RuleClass;
3031
import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException;
3132
import com.google.devtools.build.lib.packages.StarlarkNativeModule.ExistingRulesShouldBeNoOp;
32-
import java.util.HashMap;
33+
import java.util.LinkedHashMap;
3334
import java.util.Map;
3435
import javax.annotation.Nullable;
3536
import net.starlark.java.eval.Dict;
3637
import net.starlark.java.eval.EvalException;
38+
import net.starlark.java.eval.NoneType;
3739
import net.starlark.java.eval.Starlark;
40+
import net.starlark.java.eval.StarlarkInt;
41+
import net.starlark.java.eval.StarlarkList;
42+
import net.starlark.java.eval.StarlarkSemantics;
3843
import net.starlark.java.eval.StarlarkThread;
3944
import net.starlark.java.syntax.Location;
4045

@@ -56,94 +61,163 @@ public static ModuleExtensionEvalStarlarkThreadContext fromOrNull(StarlarkThread
5661
return ctx instanceof ModuleExtensionEvalStarlarkThreadContext c ? c : null;
5762
}
5863

59-
@AutoValue
60-
abstract static class RepoSpecAndLocation {
61-
abstract RepoSpec getRepoSpec();
62-
63-
abstract Location getLocation();
64-
65-
static RepoSpecAndLocation create(RepoSpec repoSpec, Location location) {
66-
return new AutoValue_ModuleExtensionEvalStarlarkThreadContext_RepoSpecAndLocation(
67-
repoSpec, location);
68-
}
69-
}
64+
record RepoRuleCall(
65+
RuleClass ruleClass,
66+
Dict<String, Object> kwargs,
67+
Location location,
68+
ImmutableList<StarlarkThread.CallStackEntry> callStack) {}
7069

70+
private final ModuleExtensionId extensionId;
7171
private final String repoPrefix;
7272
private final PackageIdentifier basePackageId;
73-
private final RepositoryMapping repoMapping;
73+
private final RepositoryMapping baseRepoMapping;
7474
private final BlazeDirectories directories;
7575
private final ExtendedEventHandler eventHandler;
76-
private final Map<String, RepoSpecAndLocation> generatedRepos = new HashMap<>();
76+
private final Map<String, RepoRuleCall> deferredRepos = new LinkedHashMap<>();
7777

7878
public ModuleExtensionEvalStarlarkThreadContext(
79+
ModuleExtensionId extensionId,
7980
String repoPrefix,
8081
PackageIdentifier basePackageId,
81-
RepositoryMapping repoMapping,
82+
RepositoryMapping baseRepoMapping,
8283
RepositoryMapping mainRepoMapping,
8384
BlazeDirectories directories,
8485
ExtendedEventHandler eventHandler) {
8586
super(() -> mainRepoMapping);
87+
this.extensionId = extensionId;
8688
this.repoPrefix = repoPrefix;
8789
this.basePackageId = basePackageId;
88-
this.repoMapping = repoMapping;
90+
this.baseRepoMapping = baseRepoMapping;
8991
this.directories = directories;
9092
this.eventHandler = eventHandler;
9193
}
9294

93-
public void createRepo(StarlarkThread thread, Dict<String, Object> kwargs, RuleClass ruleClass)
94-
throws InterruptedException, EvalException {
95+
/**
96+
* Records a call to a repo rule that should be created at the end of the module extension
97+
* evaluation.
98+
*/
99+
@SuppressWarnings("unchecked")
100+
public void lazilyCreateRepo(
101+
StarlarkThread thread, Dict<String, Object> kwargs, RuleClass ruleClass)
102+
throws EvalException {
95103
Object nameValue = kwargs.getOrDefault("name", Starlark.NONE);
96104
if (!(nameValue instanceof String name)) {
97105
throw Starlark.errorf(
98106
"expected string for attribute 'name', got '%s'", Starlark.type(nameValue));
99107
}
100108
RepositoryName.validateUserProvidedRepoName(name);
101-
RepoSpecAndLocation conflict = generatedRepos.get(name);
109+
RepoRuleCall conflict = deferredRepos.get(name);
102110
if (conflict != null) {
103111
throw Starlark.errorf(
104112
"A repo named %s is already generated by this module extension at %s",
105-
name, conflict.getLocation());
113+
name, conflict.location());
106114
}
107-
String prefixedName = repoPrefix + name;
108-
try {
109-
Rule rule =
110-
BzlmodRepoRuleCreator.createRule(
111-
basePackageId,
112-
repoMapping,
113-
directories,
114-
thread.getSemantics(),
115-
eventHandler,
116-
thread.getCallStack(),
117-
ruleClass,
118-
Maps.transformEntries(kwargs, (k, v) -> k.equals("name") ? prefixedName : v));
115+
deferredRepos.put(
116+
name,
117+
new RepoRuleCall(
118+
ruleClass,
119+
// The extension may mutate the values of the kwargs after this function returns.
120+
(Dict<String, Object>) deepCloneAttrValue(kwargs),
121+
thread.getCallerLocation(),
122+
thread.getCallStack()));
123+
}
119124

120-
Map<String, Object> attributes =
121-
Maps.filterKeys(
122-
Maps.transformEntries(kwargs, (k, v) -> rule.getAttr(k)), k -> !k.equals("name"));
123-
String bzlFile = ruleClass.getRuleDefinitionEnvironmentLabel().getUnambiguousCanonicalForm();
124-
var attributesValue = AttributeValues.create(attributes);
125-
AttributeValues.validateAttrs(
126-
attributesValue, String.format("%s '%s'", rule.getRuleClass(), name));
127-
RepoSpec repoSpec =
128-
RepoSpec.builder()
129-
.setBzlFile(bzlFile)
130-
.setRuleClassName(ruleClass.getName())
131-
.setAttributes(attributesValue)
132-
.build();
125+
/**
126+
* Evaluates the repo rule calls recorded by {@link #lazilyCreateRepo} and returns all repos
127+
* generated by the extension. The key is the "internal name" (as specified by the extension) of
128+
* the repo, and the value is the {@link RepoSpec}.
129+
*/
130+
public ImmutableMap<String, RepoSpec> createRepos(StarlarkSemantics starlarkSemantics)
131+
throws EvalException, InterruptedException {
132+
// LINT.IfChange
133+
// Make it possible to refer to extension repos in the label attributes of another extension
134+
// repo. Wrapping a label in Label(...) ensures that it is evaluated with respect to the
135+
// containing module's repo mapping instead.
136+
var extensionRepos =
137+
Maps.asMap(
138+
deferredRepos.keySet(),
139+
apparentName -> RepositoryName.createUnvalidated(repoPrefix + apparentName));
140+
RepositoryMapping fullRepoMapping =
141+
RepositoryMapping.create(extensionRepos, baseRepoMapping.ownerRepo())
142+
.withAdditionalMappings(baseRepoMapping);
143+
// LINT.ThenChange(//src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionRepoMappingEntriesFunction.java)
144+
145+
ImmutableMap.Builder<String, RepoSpec> repoSpecs = ImmutableMap.builder();
146+
for (var entry : deferredRepos.entrySet()) {
147+
String name = entry.getKey();
148+
RepoRuleCall repoRuleCall = entry.getValue();
149+
try {
150+
String prefixedName = repoPrefix + name;
151+
Rule rule =
152+
BzlmodRepoRuleCreator.createRule(
153+
basePackageId,
154+
fullRepoMapping,
155+
directories,
156+
starlarkSemantics,
157+
eventHandler,
158+
repoRuleCall.callStack,
159+
repoRuleCall.ruleClass,
160+
Maps.transformEntries(
161+
repoRuleCall.kwargs, (k, v) -> k.equals("name") ? prefixedName : v));
133162

134-
generatedRepos.put(name, RepoSpecAndLocation.create(repoSpec, thread.getCallerLocation()));
135-
} catch (InvalidRuleException | NoSuchPackageException e) {
136-
throw Starlark.errorf("%s", e.getMessage());
163+
Map<String, Object> attributes =
164+
Maps.filterKeys(
165+
Maps.transformEntries(repoRuleCall.kwargs, (k, v) -> rule.getAttr(k)),
166+
k -> !k.equals("name"));
167+
String bzlFile =
168+
repoRuleCall
169+
.ruleClass
170+
.getRuleDefinitionEnvironmentLabel()
171+
.getUnambiguousCanonicalForm();
172+
var attributesValue = AttributeValues.create(attributes);
173+
AttributeValues.validateAttrs(
174+
attributesValue,
175+
String.format("in the extension '%s'", extensionId.asTargetString()),
176+
String.format("%s '%s'", rule.getRuleClass(), name));
177+
RepoSpec repoSpec =
178+
RepoSpec.builder()
179+
.setBzlFile(bzlFile)
180+
.setRuleClassName(repoRuleCall.ruleClass.getName())
181+
.setAttributes(attributesValue)
182+
.build();
183+
repoSpecs.put(name, repoSpec);
184+
} catch (EvalException e) {
185+
throw e.withCallStack(repoRuleCall.callStack);
186+
} catch (InvalidRuleException | NoSuchPackageException e) {
187+
throw new EvalException(e).withCallStack(repoRuleCall.callStack);
188+
}
137189
}
190+
return repoSpecs.buildOrThrow();
138191
}
139192

140193
/**
141-
* Returns the repos generated by the extension so far. The key is the "internal name" (as
142-
* specified by the extension) of the repo, and the value is the package containing (only) the
143-
* repo rule.
194+
* Deep-clones a potentially mutable Starlark object that is a valid repo rule attribute.
195+
* Immutable (sub-)objects are not cloned.
144196
*/
145-
public ImmutableMap<String, RepoSpec> getGeneratedRepoSpecs() {
146-
return ImmutableMap.copyOf(
147-
Maps.transformValues(generatedRepos, RepoSpecAndLocation::getRepoSpec));
197+
private static Object deepCloneAttrValue(Object x) throws EvalException {
198+
if (x instanceof NoneType
199+
|| x instanceof Boolean
200+
|| x instanceof StarlarkInt
201+
|| x instanceof String
202+
|| x instanceof Label) {
203+
return x;
204+
}
205+
// Mutable Starlark values have to be cloned deeply.
206+
if (x instanceof Dict<?, ?> dict) {
207+
Dict.Builder<Object, Object> newDict = Dict.builder();
208+
for (Map.Entry<?, ?> e : dict.entrySet()) {
209+
newDict.put(deepCloneAttrValue(e.getKey()), deepCloneAttrValue(e.getValue()));
210+
}
211+
return newDict.buildImmutable();
212+
}
213+
if (x instanceof Iterable<?> iterable) {
214+
ImmutableList.Builder<Object> newList = ImmutableList.builder();
215+
for (Object item : iterable) {
216+
newList.add(deepCloneAttrValue(item));
217+
}
218+
return StarlarkList.immutableCopyOf(newList.build());
219+
}
220+
throw Starlark.errorf(
221+
"unexpected Starlark value: %s (of type %s)", Starlark.repr(x), Starlark.type(x));
148222
}
149223
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@ private ModuleExtensionRepoMappingEntriesValue computeRepoMappingEntries(
7070
// extension generates an internal repo name "bar", then within a repo generated by the
7171
// extension, "bar" will refer to the latter. We should explore a way to differentiate between
7272
// the two to avoid any surprises.
73+
// LINT.IfChange
7374
ImmutableMap.Builder<String, RepositoryName> entries = ImmutableMap.builder();
7475
entries.putAll(bazelDepGraphValue.getFullRepoMapping(moduleKey).entries());
7576
entries.putAll(extensionEvalValue.getCanonicalRepoNameToInternalNames().inverse());
7677
return ModuleExtensionRepoMappingEntriesValue.create(entries.buildKeepingLast(), moduleKey);
78+
// LINT.ThenChange(//src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionEvalStarlarkThreadContext.java)
7779
}
7880
}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public SkyValue compute(SkyKey skyKey, Environment env)
202202
try {
203203
module = moduleThreadContext.buildModule(getModuleFileResult.registry);
204204
} catch (EvalException e) {
205-
env.getListener().handle(Event.error(e.getMessageWithStack()));
205+
env.getListener().handle(Event.error(e.getInnermostLocation(), e.getMessageWithStack()));
206206
throw errorf(Code.BAD_MODULE, "error executing MODULE.bazel file for %s", moduleKey);
207207
}
208208
if (!module.getName().equals(moduleKey.getName())) {
@@ -482,10 +482,7 @@ public static RootModuleFileValue evaluateRootModuleFile(
482482
.map(label -> Label.parseCanonicalUnchecked(label).toPathFragment()))
483483
.collect(toImmutableSet());
484484
return RootModuleFileValue.create(
485-
module,
486-
overrides,
487-
nonRegistryOverrideCanonicalRepoNameLookup,
488-
moduleFilePaths);
485+
module, overrides, nonRegistryOverrideCanonicalRepoNameLookup, moduleFilePaths);
489486
}
490487

491488
private static ModuleThreadContext execModuleFile(

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ public final String toString() {
7373
return getName() + "@" + (getVersion().isEmpty() ? "_" : getVersion().toString());
7474
}
7575

76+
/** Returns a string such as "root module" or "module [email protected]" for display purposes. */
77+
public final String toDisplayString() {
78+
if (this.equals(ROOT)) {
79+
return "root module";
80+
}
81+
return String.format("module '%s'", this);
82+
}
83+
7684
/**
7785
* Returns the canonical name of the repo backing this module, including its version. This name is
7886
* always guaranteed to be unique.

0 commit comments

Comments
 (0)