Skip to content

Commit ac0f830

Browse files
iancha1992fmeum
andauthored
Allow module extension usages to be isolated (#18727)
If `isolate = True` is specified on `use_extension`, that particular usage will be isolated from all other usages, both in the same and other modules. Module extensions can check whether they are isolated (e.g. in case they can only be used in this way) via `module_ctx.is_isolated`. Closes #18529. PiperOrigin-RevId: 541823020 Change-Id: I68a7b49914bbc1fd50df2fda7a0af1e47421bb92 Co-authored-by: Fabian Meumertzheim <[email protected]>
1 parent 457047e commit ac0f830

File tree

11 files changed

+627
-36
lines changed

11 files changed

+627
-36
lines changed

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ private ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> getEx
214214
try {
215215
moduleExtensionId =
216216
ModuleExtensionId.create(
217-
labelConverter.convert(usage.getExtensionBzlFile()), usage.getExtensionName());
217+
labelConverter.convert(usage.getExtensionBzlFile()),
218+
usage.getExtensionName(),
219+
usage.getIsolationKey());
218220
} catch (LabelSyntaxException e) {
219221
throw new BazelDepGraphFunctionException(
220222
ExternalDepsException.withCauseAndMessage(
@@ -248,12 +250,31 @@ private ImmutableBiMap<String, ModuleExtensionId> calculateUniqueNameForUsedExte
248250
// not start with a tilde.
249251
RepositoryName repository = id.getBzlFileLabel().getRepository();
250252
String nonEmptyRepoPart = repository.isMain() ? "_main" : repository.getName();
251-
String bestName = nonEmptyRepoPart + "~" + id.getExtensionName();
253+
// When using a namespace, prefix the extension name with "_" to distinguish the prefix from
254+
// those generated by non-namespaced extension usages. Extension names are identified by their
255+
// Starlark identifier, which in the case of an exported symbol cannot start with "_".
256+
// We also include whether the isolated usage is a dev usage as well as its index in the
257+
// MODULE.bazel file to ensure that canonical repository names don't change depending on
258+
// whether dev dependencies are ignored. This removes potential for confusion and also
259+
// prevents unnecessary refetches when --ignore_dev_dependency is toggled.
260+
String bestName =
261+
id.getIsolationKey()
262+
.map(
263+
namespace ->
264+
String.format(
265+
"%s~_%s~%s~%s~%s%d",
266+
nonEmptyRepoPart,
267+
id.getExtensionName(),
268+
namespace.getModule().getName(),
269+
namespace.getModule().getVersion(),
270+
namespace.isDevUsage() ? "dev" : "",
271+
namespace.getIsolatedUsageIndex()))
272+
.orElse(nonEmptyRepoPart + "~" + id.getExtensionName());
252273
if (extensionUniqueNames.putIfAbsent(bestName, id) == null) {
253274
continue;
254275
}
255276
int suffix = 2;
256-
while (extensionUniqueNames.putIfAbsent(bestName + suffix, id) != null) {
277+
while (extensionUniqueNames.putIfAbsent(bestName + "~" + suffix, id) != null) {
257278
suffix++;
258279
}
259280
}

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,25 @@
2020
import static com.google.devtools.build.lib.bazel.bzlmod.DelegateTypeAdapterFactory.IMMUTABLE_MAP;
2121
import static com.google.devtools.build.lib.bazel.bzlmod.DelegateTypeAdapterFactory.IMMUTABLE_SET;
2222

23+
import com.google.common.base.Preconditions;
2324
import com.google.common.base.Splitter;
2425
import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException;
2526
import com.google.gson.Gson;
2627
import com.google.gson.GsonBuilder;
2728
import com.google.gson.JsonParseException;
2829
import com.google.gson.TypeAdapter;
30+
import com.google.gson.TypeAdapterFactory;
31+
import com.google.gson.reflect.TypeToken;
2932
import com.google.gson.stream.JsonReader;
33+
import com.google.gson.stream.JsonToken;
3034
import com.google.gson.stream.JsonWriter;
3135
import com.ryanharter.auto.value.gson.GenerateTypeAdapter;
3236
import java.io.IOException;
37+
import java.lang.reflect.ParameterizedType;
38+
import java.lang.reflect.Type;
3339
import java.util.List;
40+
import java.util.Optional;
41+
import javax.annotation.Nullable;
3442

3543
/**
3644
* Utility class to hold type adapters and helper methods to get gson registered with type adapters
@@ -88,6 +96,56 @@ public ModuleKey read(JsonReader jsonReader) throws IOException {
8896
}
8997
};
9098

99+
public static final TypeAdapterFactory OPTIONAL =
100+
new TypeAdapterFactory() {
101+
@Nullable
102+
@Override
103+
@SuppressWarnings("unchecked")
104+
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
105+
if (typeToken.getRawType() != Optional.class) {
106+
return null;
107+
}
108+
Type type = typeToken.getType();
109+
if (!(type instanceof ParameterizedType)) {
110+
return null;
111+
}
112+
Type elementType = ((ParameterizedType) typeToken.getType()).getActualTypeArguments()[0];
113+
var elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
114+
if (elementTypeAdapter == null) {
115+
return null;
116+
}
117+
return (TypeAdapter<T>) new OptionalTypeAdapter<>(elementTypeAdapter);
118+
}
119+
};
120+
121+
private static final class OptionalTypeAdapter<T> extends TypeAdapter<Optional<T>> {
122+
private final TypeAdapter<T> elementTypeAdapter;
123+
124+
public OptionalTypeAdapter(TypeAdapter<T> elementTypeAdapter) {
125+
this.elementTypeAdapter = elementTypeAdapter;
126+
}
127+
128+
@Override
129+
public void write(JsonWriter jsonWriter, Optional<T> t) throws IOException {
130+
Preconditions.checkNotNull(t);
131+
if (t.isEmpty()) {
132+
jsonWriter.nullValue();
133+
} else {
134+
elementTypeAdapter.write(jsonWriter, t.get());
135+
}
136+
}
137+
138+
@Override
139+
public Optional<T> read(JsonReader jsonReader) throws IOException {
140+
if (jsonReader.peek() == JsonToken.NULL) {
141+
jsonReader.nextNull();
142+
return Optional.empty();
143+
} else {
144+
return Optional.of(elementTypeAdapter.read(jsonReader));
145+
}
146+
}
147+
}
148+
91149
public static final Gson LOCKFILE_GSON =
92150
new GsonBuilder()
93151
.setPrettyPrinting()
@@ -97,6 +155,7 @@ public ModuleKey read(JsonReader jsonReader) throws IOException {
97155
.registerTypeAdapterFactory(IMMUTABLE_LIST)
98156
.registerTypeAdapterFactory(IMMUTABLE_BIMAP)
99157
.registerTypeAdapterFactory(IMMUTABLE_SET)
158+
.registerTypeAdapterFactory(OPTIONAL)
100159
.registerTypeAdapter(Version.class, VERSION_TYPE_ADAPTER)
101160
.registerTypeAdapter(ModuleKey.class, MODULE_KEY_TYPE_ADAPTER)
102161
.registerTypeAdapter(AttributeValues.class, new AttributeValuesAdapter())

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ public boolean isDevDependency(TypeCheckedTag tag) {
120120
return tag.isDevDependency();
121121
}
122122

123+
@StarlarkMethod(
124+
name = "is_isolated",
125+
doc =
126+
"Whether this particular usage of the extension had <code>isolate = True</code> "
127+
+ "specified and is thus isolated from all other usages.",
128+
structField = true)
129+
public boolean isIsolated() {
130+
return extensionId.getIsolationKey().isPresent();
131+
}
132+
123133
@StarlarkMethod(
124134
name = "extension_metadata",
125135
doc =
@@ -181,6 +191,6 @@ public ModuleExtensionMetadata extensionMetadata(
181191
Object rootModuleDirectDepsUnchecked, Object rootModuleDirectDevDepsUnchecked)
182192
throws EvalException {
183193
return ModuleExtensionMetadata.create(
184-
rootModuleDirectDepsUnchecked, rootModuleDirectDevDepsUnchecked);
194+
rootModuleDirectDepsUnchecked, rootModuleDirectDevDepsUnchecked, extensionId);
185195
}
186196
}

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,41 @@
1717

1818
import com.google.auto.value.AutoValue;
1919
import com.google.devtools.build.lib.cmdline.Label;
20+
import java.util.Optional;
2021

2122
/** A unique identifier for a {@link ModuleExtension}. */
2223
@AutoValue
2324
public abstract class ModuleExtensionId {
25+
26+
/** A unique identifier for a single isolated usage of a fixed module extension. */
27+
@AutoValue
28+
abstract static class IsolationKey {
29+
/** The module which contains this isolated usage of a module extension. */
30+
public abstract ModuleKey getModule();
31+
32+
/** Whether this isolated usage specified {@code dev_dependency = True}. */
33+
public abstract boolean isDevUsage();
34+
35+
/**
36+
* The 0-based index of this isolated usage within the module's isolated usages of the same
37+
* module extension and with the same {@link #isDevUsage()} value.
38+
*/
39+
public abstract int getIsolatedUsageIndex();
40+
41+
public static IsolationKey create(
42+
ModuleKey module, boolean isDevUsage, int isolatedUsageIndex) {
43+
return new AutoValue_ModuleExtensionId_IsolationKey(module, isDevUsage, isolatedUsageIndex);
44+
}
45+
}
46+
2447
public abstract Label getBzlFileLabel();
2548

2649
public abstract String getExtensionName();
2750

28-
public static ModuleExtensionId create(Label bzlFileLabel, String extensionName) {
29-
return new AutoValue_ModuleExtensionId(bzlFileLabel, extensionName);
51+
public abstract Optional<IsolationKey> getIsolationKey();
52+
53+
public static ModuleExtensionId create(
54+
Label bzlFileLabel, String extensionName, Optional<IsolationKey> isolationKey) {
55+
return new AutoValue_ModuleExtensionId(bzlFileLabel, extensionName, isolationKey);
3056
}
3157
}

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

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ private ModuleExtensionMetadata(
6969
}
7070

7171
static ModuleExtensionMetadata create(
72-
Object rootModuleDirectDepsUnchecked, Object rootModuleDirectDevDepsUnchecked)
72+
Object rootModuleDirectDepsUnchecked,
73+
Object rootModuleDirectDevDepsUnchecked,
74+
ModuleExtensionId extensionId)
7375
throws EvalException {
7476
if (rootModuleDirectDepsUnchecked == Starlark.NONE
7577
&& rootModuleDirectDevDepsUnchecked == Starlark.NONE) {
@@ -80,11 +82,23 @@ static ModuleExtensionMetadata create(
8082
// root_module_direct_dev_deps = [], but not root_module_direct_dev_deps = ["some_repo"].
8183
if (rootModuleDirectDepsUnchecked.equals("all")
8284
&& rootModuleDirectDevDepsUnchecked.equals(StarlarkList.immutableOf())) {
85+
if (extensionId.getIsolationKey().isPresent()
86+
&& extensionId.getIsolationKey().get().isDevUsage()) {
87+
throw Starlark.errorf(
88+
"root_module_direct_deps must be empty for an isolated extension usage with "
89+
+ "dev_dependency = True");
90+
}
8391
return new ModuleExtensionMetadata(null, null, UseAllRepos.REGULAR);
8492
}
8593

8694
if (rootModuleDirectDevDepsUnchecked.equals("all")
8795
&& rootModuleDirectDepsUnchecked.equals(StarlarkList.immutableOf())) {
96+
if (extensionId.getIsolationKey().isPresent()
97+
&& !extensionId.getIsolationKey().get().isDevUsage()) {
98+
throw Starlark.errorf(
99+
"root_module_direct_dev_deps must be empty for an isolated extension usage with "
100+
+ "dev_dependency = False");
101+
}
88102
return new ModuleExtensionMetadata(null, null, UseAllRepos.DEV);
89103
}
90104

@@ -114,6 +128,20 @@ static ModuleExtensionMetadata create(
114128
Sequence.cast(
115129
rootModuleDirectDevDepsUnchecked, String.class, "root_module_direct_dev_deps");
116130

131+
if (extensionId.getIsolationKey().isPresent()) {
132+
ModuleExtensionId.IsolationKey isolationKey = extensionId.getIsolationKey().get();
133+
if (isolationKey.isDevUsage() && !rootModuleDirectDeps.isEmpty()) {
134+
throw Starlark.errorf(
135+
"root_module_direct_deps must be empty for an isolated extension usage with "
136+
+ "dev_dependency = True");
137+
}
138+
if (!isolationKey.isDevUsage() && !rootModuleDirectDevDeps.isEmpty()) {
139+
throw Starlark.errorf(
140+
"root_module_direct_dev_deps must be empty for an isolated extension usage with "
141+
+ "dev_dependency = False");
142+
}
143+
}
144+
117145
Set<String> explicitRootModuleDirectDeps = new LinkedHashSet<>();
118146
for (String dep : rootModuleDirectDeps) {
119147
try {
@@ -257,13 +285,33 @@ private static Optional<Event> generateFixupMessage(
257285
var fixupCommands =
258286
Stream.of(
259287
makeUseRepoCommand(
260-
"use_repo_add", false, importsToAdd, extensionBzlFile, extensionName),
288+
"use_repo_add",
289+
false,
290+
importsToAdd,
291+
extensionBzlFile,
292+
extensionName,
293+
firstUsage.getIsolationKey()),
261294
makeUseRepoCommand(
262-
"use_repo_remove", false, importsToRemove, extensionBzlFile, extensionName),
295+
"use_repo_remove",
296+
false,
297+
importsToRemove,
298+
extensionBzlFile,
299+
extensionName,
300+
firstUsage.getIsolationKey()),
263301
makeUseRepoCommand(
264-
"use_repo_add", true, devImportsToAdd, extensionBzlFile, extensionName),
302+
"use_repo_add",
303+
true,
304+
devImportsToAdd,
305+
extensionBzlFile,
306+
extensionName,
307+
firstUsage.getIsolationKey()),
265308
makeUseRepoCommand(
266-
"use_repo_remove", true, devImportsToRemove, extensionBzlFile, extensionName))
309+
"use_repo_remove",
310+
true,
311+
devImportsToRemove,
312+
extensionBzlFile,
313+
extensionName,
314+
firstUsage.getIsolationKey()))
267315
.flatMap(Optional::stream);
268316

269317
return Optional.of(
@@ -284,17 +332,28 @@ private static Optional<String> makeUseRepoCommand(
284332
boolean devDependency,
285333
Collection<String> repos,
286334
String extensionBzlFile,
287-
String extensionName) {
335+
String extensionName,
336+
Optional<ModuleExtensionId.IsolationKey> isolationKey) {
337+
288338
if (repos.isEmpty()) {
289339
return Optional.empty();
290340
}
341+
342+
String extensionUsageIdentifier = extensionName;
343+
if (isolationKey.isPresent()) {
344+
// We verified in create() that the extension did not report root module deps of a kind that
345+
// does not match the isolated (and hence only) usage. It also isn't possible for users to
346+
// specify repo usages of the wrong kind, so we can't get here.
347+
Preconditions.checkState(isolationKey.get().isDevUsage() == devDependency);
348+
extensionUsageIdentifier += ":" + isolationKey.get().getIsolatedUsageIndex();
349+
}
291350
return Optional.of(
292351
String.format(
293352
"buildozer '%s%s %s %s %s' //MODULE.bazel:all",
294353
cmd,
295354
devDependency ? " dev" : "",
296355
extensionBzlFile,
297-
extensionName,
356+
extensionUsageIdentifier,
298357
String.join(" ", repos)));
299358
}
300359

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.common.collect.ImmutableSet;
2121
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2222
import com.ryanharter.auto.value.gson.GenerateTypeAdapter;
23+
import java.util.Optional;
2324
import net.starlark.java.syntax.Location;
2425

2526
/**
@@ -35,6 +36,12 @@ public abstract class ModuleExtensionUsage {
3536
/** The name of the extension. */
3637
public abstract String getExtensionName();
3738

39+
/**
40+
* The isolation key of this module extension usage. This is present if and only if the usage is
41+
* created with {@code isolate = True}.
42+
*/
43+
public abstract Optional<ModuleExtensionId.IsolationKey> getIsolationKey();
44+
3845
/** The module that contains this particular extension usage. */
3946
public abstract ModuleKey getUsingModule();
4047

@@ -73,6 +80,8 @@ public abstract static class Builder {
7380

7481
public abstract Builder setExtensionName(String value);
7582

83+
public abstract Builder setIsolationKey(Optional<ModuleExtensionId.IsolationKey> value);
84+
7685
public abstract Builder setUsingModule(ModuleKey value);
7786

7887
public abstract Builder setLocation(Location value);

0 commit comments

Comments
 (0)