Skip to content

Commit 9a1d385

Browse files
SalmaSamycopybara-github
authored andcommitted
Add fetch --repo option
Allow fetching only one repo using the canonical or apparent repo names. Only works with bzlmod. PiperOrigin-RevId: 570675543 Change-Id: Idf9c412161a2403e8b8b9a4f0ca9e76f63e94de0
1 parent e67e1eb commit 9a1d385

File tree

4 files changed

+183
-13
lines changed

4 files changed

+183
-13
lines changed

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

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
package com.google.devtools.build.lib.bazel.commands;
1515

1616
import static com.google.common.primitives.Booleans.countTrue;
17+
import static java.util.stream.Collectors.joining;
1718

1819
import com.google.common.base.Joiner;
1920
import com.google.common.collect.ImmutableList;
2021
import com.google.common.collect.ImmutableSet;
2122
import com.google.devtools.build.lib.analysis.NoBuildEvent;
2223
import com.google.devtools.build.lib.analysis.NoBuildRequestFinishedEvent;
2324
import com.google.devtools.build.lib.bazel.bzlmod.BazelFetchAllValue;
25+
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
2426
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
2527
import com.google.devtools.build.lib.cmdline.RepositoryName;
2628
import com.google.devtools.build.lib.cmdline.TargetPattern;
@@ -38,6 +40,7 @@
3840
import com.google.devtools.build.lib.query2.engine.QueryExpression;
3941
import com.google.devtools.build.lib.query2.engine.QuerySyntaxException;
4042
import com.google.devtools.build.lib.query2.engine.ThreadSafeOutputFormatterCallback;
43+
import com.google.devtools.build.lib.rules.repository.RepositoryDirectoryValue;
4144
import com.google.devtools.build.lib.runtime.BlazeCommand;
4245
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
4346
import com.google.devtools.build.lib.runtime.Command;
@@ -56,10 +59,13 @@
5659
import com.google.devtools.build.lib.util.InterruptedFailureDetails;
5760
import com.google.devtools.build.skyframe.EvaluationContext;
5861
import com.google.devtools.build.skyframe.EvaluationResult;
62+
import com.google.devtools.build.skyframe.SkyKey;
5963
import com.google.devtools.build.skyframe.SkyValue;
6064
import com.google.devtools.common.options.OptionsParsingResult;
6165
import java.io.IOException;
6266
import java.util.EnumSet;
67+
import java.util.List;
68+
import net.starlark.java.eval.EvalException;
6369

6470
/** Fetches external repositories. Which is so fetch. */
6571
@Command(
@@ -90,10 +96,13 @@ public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult opti
9096
return createFailedBlazeCommandResult(Code.OPTIONS_INVALID, errorMessage);
9197
}
9298
FetchOptions fetchOptions = options.getOptions(FetchOptions.class);
93-
// Validate only one option is provided for fetch
94-
boolean moreThanOneOption =
95-
countTrue(fetchOptions.all, fetchOptions.configure, !options.getResidue().isEmpty()) > 1;
96-
if (moreThanOneOption) {
99+
int optionsCount =
100+
countTrue(
101+
fetchOptions.all,
102+
fetchOptions.configure,
103+
!fetchOptions.repos.isEmpty(),
104+
!options.getResidue().isEmpty());
105+
if (optionsCount > 1) {
97106
String errorMessage = "Only one fetch option should be provided for fetch command.";
98107
env.getReporter().handle(Event.error(null, errorMessage));
99108
return createFailedBlazeCommandResult(Code.OPTIONS_INVALID, errorMessage);
@@ -109,9 +118,10 @@ public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult opti
109118
/* showProgress= */ true,
110119
/* id= */ null));
111120
BlazeCommandResult result;
112-
113121
if (fetchOptions.all || fetchOptions.configure) {
114-
return fetchAll(env, options, fetchOptions.configure, threadsOption);
122+
result = fetchAll(env, options, threadsOption, fetchOptions.configure);
123+
} else if (!fetchOptions.repos.isEmpty()) {
124+
result = fetchRepo(env, options, threadsOption, fetchOptions.repos);
115125
} else {
116126
result = fetchTarget(env, options, threadsOption);
117127
}
@@ -125,8 +135,8 @@ public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult opti
125135
private BlazeCommandResult fetchAll(
126136
CommandEnvironment env,
127137
OptionsParsingResult options,
128-
boolean configureEnabled,
129-
LoadingPhaseThreadsOption threadsOption) {
138+
LoadingPhaseThreadsOption threadsOption,
139+
boolean configureEnabled) {
130140
if (!options.getOptions(BuildLanguageOptions.class).enableBzlmod) {
131141
String errorMessage =
132142
"Bzlmod has to be enabled for fetch --all to work, run with --enable_bzlmod";
@@ -168,6 +178,68 @@ private BlazeCommandResult fetchAll(
168178
}
169179
}
170180

181+
private BlazeCommandResult fetchRepo(
182+
CommandEnvironment env,
183+
OptionsParsingResult options,
184+
LoadingPhaseThreadsOption threadsOption,
185+
List<String> repos) {
186+
SkyframeExecutor skyframeExecutor = env.getSkyframeExecutor();
187+
EvaluationContext evaluationContext =
188+
EvaluationContext.newBuilder()
189+
.setParallelism(threadsOption.threads)
190+
.setEventHandler(env.getReporter())
191+
.build();
192+
try {
193+
env.syncPackageLoading(options);
194+
ImmutableSet.Builder<SkyKey> repoDelegatorKeys = ImmutableSet.builder();
195+
for (String repo : repos) {
196+
RepositoryName repoName = getRepositoryName(env, threadsOption, repo);
197+
repoDelegatorKeys.add(RepositoryDirectoryValue.key(repoName));
198+
}
199+
EvaluationResult<SkyValue> evaluationResult =
200+
skyframeExecutor.prepareAndGet(repoDelegatorKeys.build(), evaluationContext);
201+
if (evaluationResult.hasError()) {
202+
Exception e = evaluationResult.getError().getException();
203+
String errorMessage =
204+
e != null ? e.getMessage() : "Unexpected error during repository fetching.";
205+
env.getReporter().handle(Event.error(errorMessage));
206+
return BlazeCommandResult.detailedExitCode(
207+
InterruptedFailureDetails.detailedExitCode(errorMessage));
208+
}
209+
String notFoundRepos =
210+
repoDelegatorKeys.build().stream()
211+
.filter(
212+
key -> !((RepositoryDirectoryValue) evaluationResult.get(key)).repositoryExists())
213+
.map(key -> ((RepositoryDirectoryValue) evaluationResult.get(key)).getErrorMsg())
214+
.collect(joining("; "));
215+
if (!notFoundRepos.isEmpty()) {
216+
String errorMessage = "Fetching repos failed with errors: " + notFoundRepos;
217+
env.getReporter().handle(Event.error(errorMessage));
218+
return BlazeCommandResult.detailedExitCode(
219+
InterruptedFailureDetails.detailedExitCode(errorMessage));
220+
}
221+
222+
// Everything has been fetched successfully!
223+
return BlazeCommandResult.success();
224+
} catch (AbruptExitException e) {
225+
env.getReporter().handle(Event.error(null, "Unknown error: " + e.getMessage()));
226+
return BlazeCommandResult.detailedExitCode(e.getDetailedExitCode());
227+
} catch (InterruptedException e) {
228+
String errorMessage = "Fetch interrupted: " + e.getMessage();
229+
env.getReporter().handle(Event.error(errorMessage));
230+
return BlazeCommandResult.detailedExitCode(
231+
InterruptedFailureDetails.detailedExitCode(errorMessage));
232+
} catch (LabelSyntaxException | EvalException | IllegalArgumentException e) {
233+
String errorMessage = "Invalid repo name: " + e.getMessage();
234+
env.getReporter().handle(Event.error(null, errorMessage));
235+
return BlazeCommandResult.detailedExitCode(
236+
InterruptedFailureDetails.detailedExitCode(errorMessage));
237+
} catch (RepositoryMappingResolutionException e) {
238+
env.getReporter().handle(Event.error(e.getMessage()));
239+
return BlazeCommandResult.detailedExitCode(e.getDetailedExitCode());
240+
}
241+
}
242+
171243
private BlazeCommandResult fetchTarget(
172244
CommandEnvironment env,
173245
OptionsParsingResult options,
@@ -188,6 +260,7 @@ private BlazeCommandResult fetchTarget(
188260
RepositoryMapping repoMapping =
189261
env.getSkyframeExecutor()
190262
.getMainRepoMapping(keepGoing, threadsOption.threads, env.getReporter());
263+
191264
mainRepoTargetParser =
192265
new Parser(env.getRelativeWorkingDirectory(), RepositoryName.MAIN, repoMapping);
193266
} catch (RepositoryMappingResolutionException e) {
@@ -294,6 +367,29 @@ public void processOutput(Iterable<Target> partialResult) {
294367
expr));
295368
}
296369

370+
private RepositoryName getRepositoryName(
371+
CommandEnvironment env, LoadingPhaseThreadsOption threadsOption, String repoName)
372+
throws EvalException,
373+
LabelSyntaxException,
374+
RepositoryMappingResolutionException,
375+
InterruptedException {
376+
if (repoName.startsWith("@@")) { // canonical RepoName
377+
return RepositoryName.create(repoName.substring(2));
378+
} else if (repoName.startsWith("@")) { // apparent RepoName
379+
RepositoryName.validateUserProvidedRepoName(repoName.substring(1));
380+
RepositoryMapping repoMapping =
381+
env.getSkyframeExecutor()
382+
.getMainRepoMapping(
383+
env.getOptions().getOptions(KeepGoingOption.class).keepGoing,
384+
threadsOption.threads,
385+
env.getReporter());
386+
return repoMapping.get(repoName.substring(1));
387+
} else {
388+
throw new IllegalArgumentException(
389+
"The repo value has to be either apparent '@repo' or canonical '@@repo' repo name");
390+
}
391+
}
392+
297393
private static BlazeCommandResult createFailedBlazeCommandResult(
298394
Code fetchCommandCode, String message) {
299395
return BlazeCommandResult.detailedExitCode(

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.google.devtools.common.options.OptionDocumentationCategory;
1818
import com.google.devtools.common.options.OptionEffectTag;
1919
import com.google.devtools.common.options.OptionsBase;
20+
import java.util.List;
2021

2122
/** Defines the options specific to Bazel's sync command */
2223
public class FetchOptions extends OptionsBase {
@@ -36,12 +37,22 @@ public class FetchOptions extends OptionsBase {
3637
documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
3738
effectTags = {OptionEffectTag.CHANGES_INPUTS},
3839
help =
39-
"Only fetch repositories marked as 'configure' for system-configuration purpose. Only"
40+
"Only fetches repositories marked as 'configure' for system-configuration purpose. Only"
4041
+ " works when --enable_bzlmod is on.")
4142
public boolean configure;
4243

44+
@Option(
45+
name = "repo",
46+
defaultValue = "null",
47+
allowMultiple = true,
48+
documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
49+
effectTags = {OptionEffectTag.CHANGES_INPUTS},
50+
help =
51+
"Only fetches the specified repository, which can be either {@apparent_repo_name} or"
52+
+ " {@@canonical_repo_name}. Only works when --enable_bzlmod is on.")
53+
public List<String> repos;
54+
4355
/*TODO(salmasamy) add more options:
44-
* repo: to fetch a specific repo
4556
* force: to force fetch even if a repo exists
4657
*/
4758
}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11

22
Usage: %{product} %{command} [<option> ...]
33

4-
- --all: Fetches all external dependencies, only works with bzlmod
5-
- --configure: Only fetches repositories marked as 'configure', only works with bzlmod
6-
- <targets>: Fetches dependencies only for given targets
4+
- --all: Fetches all external dependencies, only works with bzlmod.
5+
- --configure: Only fetches repositories marked as 'configure', only works with bzlmod.
6+
- --repo={repo}: Only fetches the specified repository, which can be either {@apparent_repo_name} or {@@canonical_repo_name}
7+
, only works with bzlmod.
8+
- <targets>: Fetches dependencies only for given targets.
79

810
%{options}

src/test/py/bazel/bzlmod/bazel_fetch_test.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,67 @@ def testFetchFailsWithMultipleOptions(self):
157157
'ERROR: Only one fetch option should be provided for fetch command.',
158158
stderr,
159159
)
160+
exit_code, _, stderr = self.RunBazel(
161+
['fetch', '--all', '--repo=@hello'], allow_failure=True
162+
)
163+
self.AssertExitCode(exit_code, 2, stderr)
164+
self.assertIn(
165+
'ERROR: Only one fetch option should be provided for fetch command.',
166+
stderr,
167+
)
168+
169+
def testFetchRepo(self):
170+
self.main_registry.createCcModule('aaa', '1.0').createCcModule(
171+
'bbb', '1.0', {'aaa': '1.0'}
172+
).createCcModule('ccc', '1.0')
173+
self.ScratchFile(
174+
'MODULE.bazel',
175+
[
176+
'bazel_dep(name = "bbb", version = "1.0")',
177+
'bazel_dep(name = "ccc", version = "1.0", repo_name = "my_repo")',
178+
'local_path_override(module_name="bazel_tools", path="tools_mock")',
179+
'local_path_override(module_name="local_config_platform", ',
180+
'path="platforms_mock")',
181+
],
182+
)
183+
self.ScratchFile('BUILD')
184+
# Test canonical/apparent repo names & multiple repos
185+
self.RunBazel(['fetch', '--repo=@@bbb~1.0', '--repo=@my_repo'])
186+
_, stdout, _ = self.RunBazel(['info', 'output_base'])
187+
repos_fetched = os.listdir(stdout[0] + '/external')
188+
self.assertIn('bbb~1.0', repos_fetched)
189+
self.assertIn('ccc~1.0', repos_fetched)
190+
self.assertNotIn('aaa~1.0', repos_fetched)
191+
192+
def testFetchInvalidRepo(self):
193+
# Invalid repo name (not canonical or apparent)
194+
exit_code, _, stderr = self.RunBazel(
195+
['fetch', '--repo=hello'], allow_failure=True
196+
)
197+
self.AssertExitCode(exit_code, 8, stderr)
198+
self.assertIn(
199+
'ERROR: Invalid repo name: The repo value has to be either apparent'
200+
" '@repo' or canonical '@@repo' repo name",
201+
stderr,
202+
)
203+
# Repo does not exist
204+
self.ScratchFile(
205+
'MODULE.bazel',
206+
[
207+
'local_path_override(module_name="bazel_tools", path="tools_mock")',
208+
'local_path_override(module_name="local_config_platform", ',
209+
'path="platforms_mock")',
210+
],
211+
)
212+
exit_code, _, stderr = self.RunBazel(
213+
['fetch', '--repo=@@nono', '--repo=@nana'], allow_failure=True
214+
)
215+
self.AssertExitCode(exit_code, 8, stderr)
216+
self.assertIn(
217+
"ERROR: Fetching repos failed with errors: Repository '@nono' is not "
218+
"defined; No repository visible as '@nana' from main repository",
219+
stderr,
220+
)
160221

161222

162223
if __name__ == '__main__':

0 commit comments

Comments
 (0)