Skip to content

Commit 59ac9ce

Browse files
fmeumcopybara-github
authored andcommitted
Add bazel mod dump_repo_mapping
`bazel mod dump_repo_mapping` with no arguments is explicitly made an error so that a new mode that dumps all repository mappings with a single Bazel invocation can be added later if needed, e.g. to support IntelliJ's "sync" workflow. RELNOTES: `bazel mod dump_repo_mapping <canonical repo name>...` returns the repository mappings of the given repositories in NDJSON. This information can be used by IDEs and Starlark language servers to resolve labels with `--enable_bzlmod`. Work towards #20631 Closes #20686. PiperOrigin-RevId: 601332180 Change-Id: I828d7c88637bea175e11eccc52c6202f6da4c32c
1 parent ffe8e8c commit 59ac9ce

File tree

5 files changed

+178
-24
lines changed

5 files changed

+178
-24
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ public enum ModSubcommand {
172172
PATH(true),
173173
EXPLAIN(true),
174174
SHOW_REPO(false),
175-
SHOW_EXTENSION(false);
175+
SHOW_EXTENSION(false),
176+
DUMP_REPO_MAPPING(false);
176177

177178
/** Whether this subcommand produces a graph output. */
178179
private final boolean isGraph;

src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ java_library(
6868
"//src/main/java/com/google/devtools/common/options",
6969
"//src/main/java/net/starlark/java/eval",
7070
"//src/main/protobuf:failure_details_java_proto",
71+
"//third_party:gson",
7172
"//third_party:guava",
7273
],
7374
)

src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java

Lines changed: 105 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414
package com.google.devtools.build.lib.bazel.commands;
1515

16+
import static com.google.common.collect.ImmutableList.toImmutableList;
1617
import static com.google.common.collect.ImmutableMap.toImmutableMap;
1718
import static com.google.common.collect.ImmutableSet.toImmutableSet;
1819
import static com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModOptions.Charset.UTF8;
@@ -44,6 +45,7 @@
4445
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModOptions.ModSubcommandConverter;
4546
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModuleArg;
4647
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModuleArg.ModuleArgConverter;
48+
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
4749
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
4850
import com.google.devtools.build.lib.cmdline.RepositoryName;
4951
import com.google.devtools.build.lib.events.Event;
@@ -57,6 +59,7 @@
5759
import com.google.devtools.build.lib.server.FailureDetails;
5860
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
5961
import com.google.devtools.build.lib.server.FailureDetails.ModCommand.Code;
62+
import com.google.devtools.build.lib.skyframe.RepositoryMappingValue;
6063
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
6164
import com.google.devtools.build.lib.util.AbruptExitException;
6265
import com.google.devtools.build.lib.util.DetailedExitCode;
@@ -70,11 +73,17 @@
7073
import com.google.devtools.common.options.OptionsParser;
7174
import com.google.devtools.common.options.OptionsParsingException;
7275
import com.google.devtools.common.options.OptionsParsingResult;
76+
import com.google.gson.Gson;
77+
import com.google.gson.GsonBuilder;
78+
import com.google.gson.stream.JsonWriter;
79+
import java.io.IOException;
7380
import java.io.OutputStreamWriter;
81+
import java.io.Writer;
7482
import java.util.List;
7583
import java.util.Map.Entry;
7684
import java.util.Objects;
7785
import java.util.Optional;
86+
import java.util.stream.IntStream;
7887

7988
/** Queries the Bzlmod external dependency graph. */
8089
@Command(
@@ -125,8 +134,51 @@ public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult opti
125134
}
126135

127136
private BlazeCommandResult execInternal(CommandEnvironment env, OptionsParsingResult options) {
137+
ModOptions modOptions = options.getOptions(ModOptions.class);
138+
Preconditions.checkArgument(modOptions != null);
139+
140+
if (options.getResidue().isEmpty()) {
141+
String errorMessage =
142+
String.format(
143+
"No subcommand specified, choose one of : %s.", ModSubcommand.printValues());
144+
return reportAndCreateFailureResult(env, errorMessage, Code.MOD_COMMAND_UNKNOWN);
145+
}
146+
147+
// The first element in the residue must be the subcommand, and then comes a list of arguments.
148+
String subcommandStr = options.getResidue().get(0);
149+
ModSubcommand subcommand;
150+
try {
151+
subcommand = new ModSubcommandConverter().convert(subcommandStr);
152+
} catch (OptionsParsingException e) {
153+
String errorMessage =
154+
String.format("Invalid subcommand, choose one from : %s.", ModSubcommand.printValues());
155+
return reportAndCreateFailureResult(env, errorMessage, Code.MOD_COMMAND_UNKNOWN);
156+
}
157+
List<String> args = options.getResidue().subList(1, options.getResidue().size());
158+
159+
ImmutableList.Builder<RepositoryMappingValue.Key> repositoryMappingKeysBuilder =
160+
ImmutableList.builder();
161+
if (subcommand.equals(ModSubcommand.DUMP_REPO_MAPPING)) {
162+
if (args.isEmpty()) {
163+
// Make this case an error so that we are free to add a mode that emits all mappings in a
164+
// single JSON object later.
165+
return reportAndCreateFailureResult(
166+
env, "No repository name(s) specified", Code.INVALID_ARGUMENTS);
167+
}
168+
for (String arg : args) {
169+
try {
170+
repositoryMappingKeysBuilder.add(RepositoryMappingValue.key(RepositoryName.create(arg)));
171+
} catch (LabelSyntaxException e) {
172+
return reportAndCreateFailureResult(env, e.getMessage(), Code.INVALID_ARGUMENTS);
173+
}
174+
}
175+
}
176+
ImmutableList<RepositoryMappingValue.Key> repoMappingKeys =
177+
repositoryMappingKeysBuilder.build();
178+
128179
BazelDepGraphValue depGraphValue;
129180
BazelModuleInspectorValue moduleInspector;
181+
ImmutableList<RepositoryMappingValue> repoMappingValues;
130182

131183
SkyframeExecutor skyframeExecutor = env.getSkyframeExecutor();
132184
LoadingPhaseThreadsOption threadsOption = options.getOptions(LoadingPhaseThreadsOption.class);
@@ -140,10 +192,14 @@ private BlazeCommandResult execInternal(CommandEnvironment env, OptionsParsingRe
140192
try {
141193
env.syncPackageLoading(options);
142194

195+
ImmutableSet.Builder<SkyKey> keys = ImmutableSet.builder();
196+
if (subcommand.equals(ModSubcommand.DUMP_REPO_MAPPING)) {
197+
keys.addAll(repoMappingKeys);
198+
} else {
199+
keys.add(BazelDepGraphValue.KEY, BazelModuleInspectorValue.KEY);
200+
}
143201
EvaluationResult<SkyValue> evaluationResult =
144-
skyframeExecutor.prepareAndGet(
145-
ImmutableSet.of(BazelDepGraphValue.KEY, BazelModuleInspectorValue.KEY),
146-
evaluationContext);
202+
skyframeExecutor.prepareAndGet(keys.build(), evaluationContext);
147203

148204
if (evaluationResult.hasError()) {
149205
Exception e = evaluationResult.getError().getException();
@@ -159,6 +215,11 @@ private BlazeCommandResult execInternal(CommandEnvironment env, OptionsParsingRe
159215
moduleInspector =
160216
(BazelModuleInspectorValue) evaluationResult.get(BazelModuleInspectorValue.KEY);
161217

218+
repoMappingValues =
219+
repoMappingKeys.stream()
220+
.map(evaluationResult::get)
221+
.map(RepositoryMappingValue.class::cast)
222+
.collect(toImmutableList());
162223
} catch (InterruptedException e) {
163224
String errorMessage = "mod command interrupted: " + e.getMessage();
164225
env.getReporter().handle(Event.error(errorMessage));
@@ -169,27 +230,29 @@ private BlazeCommandResult execInternal(CommandEnvironment env, OptionsParsingRe
169230
return BlazeCommandResult.detailedExitCode(e.getDetailedExitCode());
170231
}
171232

172-
ModOptions modOptions = options.getOptions(ModOptions.class);
173-
Preconditions.checkArgument(modOptions != null);
174-
175-
if (options.getResidue().isEmpty()) {
176-
String errorMessage =
177-
String.format(
178-
"No subcommand specified, choose one of : %s.", ModSubcommand.printValues());
179-
return reportAndCreateFailureResult(env, errorMessage, Code.MOD_COMMAND_UNKNOWN);
180-
}
181-
182-
// The first element in the residue must be the subcommand, and then comes a list of arguments.
183-
String subcommandStr = options.getResidue().get(0);
184-
ModSubcommand subcommand;
185-
try {
186-
subcommand = new ModSubcommandConverter().convert(subcommandStr);
187-
} catch (OptionsParsingException e) {
188-
String errorMessage =
189-
String.format("Invalid subcommand, choose one from : %s.", ModSubcommand.printValues());
190-
return reportAndCreateFailureResult(env, errorMessage, Code.MOD_COMMAND_UNKNOWN);
233+
if (subcommand.equals(ModSubcommand.DUMP_REPO_MAPPING)) {
234+
String missingRepos =
235+
IntStream.range(0, repoMappingKeys.size())
236+
.filter(i -> repoMappingValues.get(i) == RepositoryMappingValue.NOT_FOUND_VALUE)
237+
.mapToObj(repoMappingKeys::get)
238+
.map(RepositoryMappingValue.Key::repoName)
239+
.map(RepositoryName::getName)
240+
.collect(joining(", "));
241+
if (!missingRepos.isEmpty()) {
242+
return reportAndCreateFailureResult(
243+
env, "Repositories not found: " + missingRepos, Code.INVALID_ARGUMENTS);
244+
}
245+
try {
246+
dumpRepoMappings(
247+
repoMappingValues,
248+
new OutputStreamWriter(
249+
env.getReporter().getOutErr().getOutputStream(),
250+
modOptions.charset == UTF8 ? UTF_8 : US_ASCII));
251+
} catch (IOException e) {
252+
throw new IllegalStateException(e);
253+
}
254+
return BlazeCommandResult.success();
191255
}
192-
List<String> args = options.getResidue().subList(1, options.getResidue().size());
193256

194257
// Extract and check the --base_module argument first to use it when parsing the other args.
195258
// Can only be a TargetModule or a repoName relative to the ROOT.
@@ -453,6 +516,8 @@ private BlazeCommandResult execInternal(CommandEnvironment env, OptionsParsingRe
453516
case SHOW_EXTENSION:
454517
modExecutor.showExtension(argsAsExtensions, usageKeys);
455518
break;
519+
default:
520+
throw new IllegalStateException("Unexpected subcommand: " + subcommand);
456521
}
457522

458523
return BlazeCommandResult.success();
@@ -510,4 +575,21 @@ private static BlazeCommandResult createFailureResult(String message, Code detai
510575
.setMessage(message)
511576
.build()));
512577
}
578+
579+
public static void dumpRepoMappings(List<RepositoryMappingValue> repoMappings, Writer writer)
580+
throws IOException {
581+
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
582+
for (RepositoryMappingValue repoMapping : repoMappings) {
583+
JsonWriter jsonWriter = gson.newJsonWriter(writer);
584+
jsonWriter.beginObject();
585+
for (Entry<String, RepositoryName> entry :
586+
repoMapping.getRepositoryMapping().entries().entrySet()) {
587+
jsonWriter.name(entry.getKey());
588+
jsonWriter.value(entry.getValue().getName());
589+
}
590+
jsonWriter.endObject();
591+
writer.write('\n');
592+
}
593+
writer.flush();
594+
}
513595
}

src/main/java/com/google/devtools/build/lib/bazel/commands/mod.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The command will display the external dependency graph or parts thereof, structu
1212
- explain <module>...: Prints all the places where the module is (or was) requested as a direct dependency, along with the reason why the respective final version was selected. It will display a pruned version of the `all_paths <module>...` command which only contains the direct deps of the root, the <module(s)> leaves, along with their dependants (can be modified with --depth).
1313
- show_repo <module>...: Prints the rule that generated the specified repos (i.e. http_archive()). The arguments may refer to extension-generated repos.
1414
- show_extension <extension>...: Prints information about the given extension(s). Usages can be filtered down to only those from modules in --extension_usage.
15+
- dump_repo_mapping <canonical_repo_name>...: Prints the mappings from apparent repo names to canonical repo names for the given repos in NDJSON format. The order of entries within each JSON object is unspecified. This command is intended for use by tools such as IDEs and Starlark language servers.
1516

1617

1718
<module> arguments must be one of the following:
@@ -25,4 +26,6 @@ The command will display the external dependency graph or parts thereof, structu
2526

2627
<extension> arguments must be of the form <module><label_to_bzl_file>%<extension_name>. For example, both rules_java//java:extensions.bzl%toolchains and @rules_java//java:extensions.bzl%toolchains are valid specifications of extensions.
2728

29+
<canonical_repo_name> arguments are canonical repo names without any leading @ characters. The canonical repo name of the root module repository is the empty string.
30+
2831
%{options}

src/test/py/bazel/bzlmod/mod_command_test.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515
"""Tests the mod command."""
1616

17+
import json
1718
import os
1819
import tempfile
1920
from absl.testing import absltest
@@ -454,6 +455,72 @@ def testShowRepoThrowsUnusedModule(self):
454455
stderr,
455456
)
456457

458+
def testDumpRepoMapping(self):
459+
_, stdout, _ = self.RunBazel(
460+
[
461+
'mod',
462+
'dump_repo_mapping',
463+
'',
464+
'foo~2.0',
465+
],
466+
)
467+
root_mapping, foo_mapping = [json.loads(l) for l in stdout]
468+
469+
self.assertContainsSubset(
470+
{
471+
'my_project': '',
472+
'foo1': 'foo~1.0',
473+
'foo2': 'foo~2.0',
474+
'myrepo2': 'ext2~1.0~ext~repo1',
475+
'bazel_tools': 'bazel_tools',
476+
}.items(),
477+
root_mapping.items(),
478+
)
479+
480+
self.assertContainsSubset(
481+
{
482+
'foo': 'foo~2.0',
483+
'ext_mod': 'ext~1.0',
484+
'my_repo3': 'ext~1.0~ext~repo3',
485+
'bazel_tools': 'bazel_tools',
486+
}.items(),
487+
foo_mapping.items(),
488+
)
489+
490+
def testDumpRepoMappingThrowsNoRepos(self):
491+
_, _, stderr = self.RunBazel(
492+
['mod', 'dump_repo_mapping'],
493+
allow_failure=True,
494+
)
495+
self.assertIn(
496+
"ERROR: No repository name(s) specified. Type 'bazel help mod' for"
497+
' syntax and help.',
498+
stderr,
499+
)
500+
501+
def testDumpRepoMappingThrowsInvalidRepoName(self):
502+
_, _, stderr = self.RunBazel(
503+
['mod', 'dump_repo_mapping', '{}'],
504+
allow_failure=True,
505+
)
506+
self.assertIn(
507+
"ERROR: invalid repository name '{}': repo names may contain only A-Z,"
508+
" a-z, 0-9, '-', '_', '.' and '~' and must not start with '~'. Type"
509+
" 'bazel help mod' for syntax and help.",
510+
stderr,
511+
)
512+
513+
def testDumpRepoMappingThrowsUnknownRepoName(self):
514+
_, _, stderr = self.RunBazel(
515+
['mod', 'dump_repo_mapping', 'does_not_exist'],
516+
allow_failure=True,
517+
)
518+
self.assertIn(
519+
"ERROR: Repositories not found: does_not_exist. Type 'bazel help mod'"
520+
' for syntax and help.',
521+
stderr,
522+
)
523+
457524

458525
if __name__ == '__main__':
459526
absltest.main()

0 commit comments

Comments
 (0)