blob: d3a79e668c545377e45e7a0f8452787434c9e342 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.bazel.commands;
import static com.google.common.primitives.Booleans.countTrue;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.NoBuildEvent;
import com.google.devtools.build.lib.analysis.NoBuildRequestFinishedEvent;
import com.google.devtools.build.lib.bazel.bzlmod.BazelFetchAllValue;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.cmdline.TargetPattern;
import com.google.devtools.build.lib.cmdline.TargetPattern.Parser;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.packages.LabelPrinter;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.pkgcache.PackageOptions;
import com.google.devtools.build.lib.query2.common.AbstractBlazeQueryEnvironment;
import com.google.devtools.build.lib.query2.common.UniverseScope;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
import com.google.devtools.build.lib.query2.engine.QueryEvalResult;
import com.google.devtools.build.lib.query2.engine.QueryException;
import com.google.devtools.build.lib.query2.engine.QueryExpression;
import com.google.devtools.build.lib.query2.engine.QuerySyntaxException;
import com.google.devtools.build.lib.query2.engine.ThreadSafeOutputFormatterCallback;
import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
import com.google.devtools.build.lib.rules.repository.RepositoryDirectoryValue;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.KeepGoingOption;
import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption;
import com.google.devtools.build.lib.runtime.commands.QueryCommand;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.FetchCommand.Code;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.skyframe.RepositoryMappingValue.RepositoryMappingResolutionException;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.InterruptedFailureDetails;
import com.google.devtools.build.skyframe.EvaluationContext;
import com.google.devtools.build.skyframe.EvaluationResult;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.common.options.OptionsParsingResult;
import java.io.IOException;
import java.util.EnumSet;
import java.util.List;
import net.starlark.java.eval.EvalException;
/** Fetches external repositories. Which is so fetch. */
@Command(
name = FetchCommand.NAME,
options = {
FetchOptions.class,
PackageOptions.class,
KeepGoingOption.class,
LoadingPhaseThreadsOption.class
},
help = "resource:fetch.txt",
shortDescription = "Fetches external repositories that are prerequisites to the targets.",
allowResidue = true,
completion = "label")
public final class FetchCommand implements BlazeCommand {
// TODO(kchodorow): this would be a great time to check for difference and invalidate the upward
// transitive closure for local repositories.
public static final String NAME = "fetch";
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
PackageOptions pkgOptions = options.getOptions(PackageOptions.class);
if (!pkgOptions.fetch) {
return createFailedBlazeCommandResult(
env.getReporter(), Code.OPTIONS_INVALID, "You cannot run fetch with --fetch=false");
}
FetchOptions fetchOptions = options.getOptions(FetchOptions.class);
int optionsCount =
countTrue(
fetchOptions.all,
fetchOptions.configure,
!fetchOptions.repos.isEmpty(),
!options.getResidue().isEmpty());
if (optionsCount > 1) {
return createFailedBlazeCommandResult(
env.getReporter(),
Code.OPTIONS_INVALID,
"Only one fetch option should be provided for fetch command.");
}
LoadingPhaseThreadsOption threadsOption = options.getOptions(LoadingPhaseThreadsOption.class);
env.getEventBus()
.post(
new NoBuildEvent(
env.getCommandName(),
env.getCommandStartTime(),
/* separateFinishedEvent= */ true,
/* showProgress= */ true,
/* id= */ null));
BlazeCommandResult result;
if (fetchOptions.force) {
// Using commandId as the value -instead of true/false- to make sure to invalidate skyframe
// and to actually force fetch each time
env.getSkyframeExecutor()
.injectExtraPrecomputedValues(
ImmutableList.of(
PrecomputedValue.injected(
RepositoryDelegatorFunction.FORCE_FETCH, env.getCommandId().toString())));
}
if (fetchOptions.all || fetchOptions.configure) {
result = fetchAll(env, options, threadsOption, fetchOptions.configure);
} else if (!fetchOptions.repos.isEmpty()) {
result = fetchRepo(env, options, threadsOption, fetchOptions.repos);
} else {
result = fetchTarget(env, options, threadsOption);
}
env.getEventBus()
.post(
new NoBuildRequestFinishedEvent(
result.getExitCode(), env.getRuntime().getClock().currentTimeMillis()));
return result;
}
private BlazeCommandResult fetchAll(
CommandEnvironment env,
OptionsParsingResult options,
LoadingPhaseThreadsOption threadsOption,
boolean configureEnabled) {
if (!options.getOptions(BuildLanguageOptions.class).enableBzlmod) {
return createFailedBlazeCommandResult(
env.getReporter(),
"Bzlmod has to be enabled for fetch --all to work, run with --enable_bzlmod");
}
SkyframeExecutor skyframeExecutor = env.getSkyframeExecutor();
EvaluationContext evaluationContext =
EvaluationContext.newBuilder()
.setParallelism(threadsOption.threads)
.setEventHandler(env.getReporter())
.build();
try {
env.syncPackageLoading(options);
EvaluationResult<SkyValue> evaluationResult =
skyframeExecutor.prepareAndGet(
ImmutableSet.of(BazelFetchAllValue.key(configureEnabled)), evaluationContext);
if (evaluationResult.hasError()) {
Exception e = evaluationResult.getError().getException();
return createFailedBlazeCommandResult(
env.getReporter(),
e != null ? e.getMessage() : "Unexpected error during fetching all external deps.");
}
// Everything is fetched successfully!
return BlazeCommandResult.success();
} catch (AbruptExitException e) {
return createFailedBlazeCommandResult(
env.getReporter(), "Unknown error:: " + e.getMessage(), e.getDetailedExitCode());
} catch (InterruptedException e) {
return createFailedBlazeCommandResult(
env.getReporter(), "Fetch interrupted: " + e.getMessage());
}
}
private BlazeCommandResult fetchRepo(
CommandEnvironment env,
OptionsParsingResult options,
LoadingPhaseThreadsOption threadsOption,
List<String> repos) {
SkyframeExecutor skyframeExecutor = env.getSkyframeExecutor();
EvaluationContext evaluationContext =
EvaluationContext.newBuilder()
.setParallelism(threadsOption.threads)
.setEventHandler(env.getReporter())
.build();
try {
env.syncPackageLoading(options);
ImmutableSet.Builder<SkyKey> repoDelegatorKeys = ImmutableSet.builder();
for (String repo : repos) {
RepositoryName repoName = getRepositoryName(env, threadsOption, repo);
repoDelegatorKeys.add(RepositoryDirectoryValue.key(repoName));
}
EvaluationResult<SkyValue> evaluationResult =
skyframeExecutor.prepareAndGet(repoDelegatorKeys.build(), evaluationContext);
if (evaluationResult.hasError()) {
Exception e = evaluationResult.getError().getException();
return createFailedBlazeCommandResult(
env.getReporter(),
e != null ? e.getMessage() : "Unexpected error during repository fetching.");
}
String notFoundRepos =
repoDelegatorKeys.build().stream()
.filter(
key -> !((RepositoryDirectoryValue) evaluationResult.get(key)).repositoryExists())
.map(key -> ((RepositoryDirectoryValue) evaluationResult.get(key)).getErrorMsg())
.collect(joining("; "));
if (!notFoundRepos.isEmpty()) {
return createFailedBlazeCommandResult(
env.getReporter(), "Fetching repos failed with errors: " + notFoundRepos);
}
// Everything has been fetched successfully!
return BlazeCommandResult.success();
} catch (AbruptExitException e) {
return createFailedBlazeCommandResult(
env.getReporter(), "Unknown error: " + e.getMessage(), e.getDetailedExitCode());
} catch (InterruptedException e) {
return createFailedBlazeCommandResult(
env.getReporter(), "Fetch interrupted: " + e.getMessage());
} catch (LabelSyntaxException | EvalException | IllegalArgumentException e) {
return createFailedBlazeCommandResult(
env.getReporter(), "Invalid repo name: " + e.getMessage());
} catch (RepositoryMappingResolutionException e) {
return createFailedBlazeCommandResult(
env.getReporter(), "Invalid repo name: " + e.getMessage(), e.getDetailedExitCode());
}
}
private BlazeCommandResult fetchTarget(
CommandEnvironment env,
OptionsParsingResult options,
LoadingPhaseThreadsOption threadsOption) {
if (options.getResidue().isEmpty()) {
return createFailedBlazeCommandResult(
env.getReporter(),
Code.EXPRESSION_MISSING,
String.format(
"missing fetch expression. Type '%s help fetch' for syntax and help",
env.getRuntime().getProductName()));
}
boolean keepGoing = options.getOptions(KeepGoingOption.class).keepGoing;
TargetPattern.Parser mainRepoTargetParser;
try {
env.syncPackageLoading(options);
RepositoryMapping repoMapping =
env.getSkyframeExecutor()
.getMainRepoMapping(keepGoing, threadsOption.threads, env.getReporter());
mainRepoTargetParser =
new Parser(env.getRelativeWorkingDirectory(), RepositoryName.MAIN, repoMapping);
} catch (RepositoryMappingResolutionException e) {
return createFailedBlazeCommandResult(
env.getReporter(), e.getMessage(), e.getDetailedExitCode());
} catch (InterruptedException e) {
return createFailedBlazeCommandResult(
env.getReporter(), "Fetch interrupted: " + e.getMessage());
} catch (AbruptExitException e) {
return createFailedBlazeCommandResult(
env.getReporter(), "Unknown error: " + e.getMessage(), e.getDetailedExitCode());
}
// Querying for all of the dependencies of the targets has the side-effect of populating the
// Skyframe graph for external targets, which requires downloading them. The JDK is required to
// build everything but isn't counted as a dep in the build graph so we add it manually.
ImmutableList.Builder<String> labelsToLoad =
new ImmutableList.Builder<String>().addAll(options.getResidue());
String query = Joiner.on(" union ").join(labelsToLoad.build());
query = "deps(" + query + ")";
AbstractBlazeQueryEnvironment<Target> queryEnv =
QueryCommand.newQueryEnvironment(
env,
keepGoing,
false,
UniverseScope.EMPTY,
threadsOption.threads,
EnumSet.noneOf(Setting.class),
/* useGraphlessQuery= */ true,
mainRepoTargetParser,
LabelPrinter.legacy());
// 1. Parse query:
QueryExpression expr;
try {
expr = QueryExpression.parse(query, queryEnv);
} catch (QuerySyntaxException e) {
return createFailedBlazeCommandResult(
env.getReporter(),
Code.QUERY_PARSE_ERROR,
String.format(
"Error while parsing '%s': %s", QueryExpression.truncate(query), e.getMessage()));
}
env.getReporter()
.post(
new NoBuildEvent(
env.getCommandName(),
env.getCommandStartTime(),
true,
true,
env.getCommandId().toString()));
// 2. Evaluate expression:
QueryEvalResult queryEvalResult = null;
try {
queryEvalResult =
queryEnv.evaluateQuery(
expr,
new ThreadSafeOutputFormatterCallback<Target>() {
@Override
public void processOutput(Iterable<Target> partialResult) {
// Throw away the result.
}
});
} catch (InterruptedException e) {
env.getReporter()
.post(
new NoBuildRequestFinishedEvent(
ExitCode.COMMAND_LINE_ERROR, env.getRuntime().getClock().currentTimeMillis()));
return BlazeCommandResult.detailedExitCode(
InterruptedFailureDetails.detailedExitCode(e.getMessage()));
} catch (QueryException e) {
// Keep consistent with reportBuildFileError()
env.getReporter()
.post(
new NoBuildRequestFinishedEvent(
ExitCode.COMMAND_LINE_ERROR, env.getRuntime().getClock().currentTimeMillis()));
return createFailedBlazeCommandResult(
env.getReporter(), Code.QUERY_PARSE_ERROR, e.getMessage());
} catch (IOException e) {
// Should be impossible since our OutputFormatterCallback doesn't throw IOException.
throw new IllegalStateException(e);
}
if (queryEvalResult.getSuccess()) {
env.getReporter().handle(Event.info("All external dependencies fetched successfully."));
}
env.getReporter()
.post(
new NoBuildRequestFinishedEvent(
queryEvalResult.getSuccess() ? ExitCode.SUCCESS : ExitCode.COMMAND_LINE_ERROR,
env.getRuntime().getClock().currentTimeMillis()));
return queryEvalResult.getSuccess()
? BlazeCommandResult.success()
: createFailedBlazeCommandResult(
env.getReporter(),
Code.QUERY_EVALUATION_ERROR,
String.format(
"Evaluation of query \"%s\" failed but --keep_going specified, ignoring errors",
expr));
}
private RepositoryName getRepositoryName(
CommandEnvironment env, LoadingPhaseThreadsOption threadsOption, String repoName)
throws EvalException,
LabelSyntaxException,
RepositoryMappingResolutionException,
InterruptedException {
if (repoName.startsWith("@@")) { // canonical RepoName
return RepositoryName.create(repoName.substring(2));
} else if (repoName.startsWith("@")) { // apparent RepoName
RepositoryName.validateUserProvidedRepoName(repoName.substring(1));
RepositoryMapping repoMapping =
env.getSkyframeExecutor()
.getMainRepoMapping(
env.getOptions().getOptions(KeepGoingOption.class).keepGoing,
threadsOption.threads,
env.getReporter());
return repoMapping.get(repoName.substring(1));
} else {
throw new IllegalArgumentException(
"The repo value has to be either apparent '@repo' or canonical '@@repo' repo name");
}
}
private static BlazeCommandResult createFailedBlazeCommandResult(
Reporter reporter, Code fetchCommandCode, String message) {
return createFailedBlazeCommandResult(
reporter,
message,
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(message)
.setFetchCommand(
FailureDetails.FetchCommand.newBuilder().setCode(fetchCommandCode).build())
.build()));
}
private static BlazeCommandResult createFailedBlazeCommandResult(
Reporter reporter, String errorMessage) {
return createFailedBlazeCommandResult(
reporter, errorMessage, InterruptedFailureDetails.detailedExitCode(errorMessage));
}
private static BlazeCommandResult createFailedBlazeCommandResult(
Reporter reporter, String message, DetailedExitCode exitCode) {
reporter.handle(Event.error(message));
return BlazeCommandResult.detailedExitCode(exitCode);
}
}