blob: 6d4071c23e552d5bd27ce7747ff8d3a066fa6884 [file] [log] [blame]
// Copyright 2021 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.bzlmod;
import static com.google.common.base.StandardSystemProperty.OS_ARCH;
import static com.google.common.collect.ImmutableBiMap.toImmutableBiMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Maps.transformValues;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
import com.google.devtools.build.lib.cmdline.BazelModuleContext;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelConstants;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.profiler.SilentCloseable;
import com.google.devtools.build.lib.rules.repository.NeedsSkyframeRestartException;
import com.google.devtools.build.lib.rules.repository.RepositoryFunction;
import com.google.devtools.build.lib.runtime.ProcessWrapper;
import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutor;
import com.google.devtools.build.lib.server.FailureDetails.ExternalDeps;
import com.google.devtools.build.lib.server.FailureDetails.ExternalDeps.Code;
import com.google.devtools.build.lib.skyframe.BzlLoadFunction;
import com.google.devtools.build.lib.skyframe.BzlLoadFunction.BzlLoadFailedException;
import com.google.devtools.build.lib.skyframe.BzlLoadValue;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Module;
import net.starlark.java.eval.Mutability;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.spelling.SpellChecker;
import net.starlark.java.syntax.Location;
/**
* Evaluates a single module extension. This function loads the .bzl file containing the extension,
* runs its implementation function with a module_ctx object containing all relevant information,
* and returns the generated repos.
*/
public class SingleExtensionEvalFunction implements SkyFunction {
private final BlazeDirectories directories;
private final Supplier<Map<String, String>> clientEnvironmentSupplier;
private final DownloadManager downloadManager;
private double timeoutScaling = 1.0;
@Nullable private ProcessWrapper processWrapper = null;
@Nullable private RepositoryRemoteExecutor repositoryRemoteExecutor = null;
public SingleExtensionEvalFunction(
BlazeDirectories directories,
Supplier<Map<String, String>> clientEnvironmentSupplier,
DownloadManager downloadManager) {
this.directories = directories;
this.clientEnvironmentSupplier = clientEnvironmentSupplier;
this.downloadManager = downloadManager;
}
public void setTimeoutScaling(double timeoutScaling) {
this.timeoutScaling = timeoutScaling;
}
public void setProcessWrapper(ProcessWrapper processWrapper) {
this.processWrapper = processWrapper;
}
public void setRepositoryRemoteExecutor(RepositoryRemoteExecutor repositoryRemoteExecutor) {
this.repositoryRemoteExecutor = repositoryRemoteExecutor;
}
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env)
throws SingleExtensionEvalFunctionException, InterruptedException {
StarlarkSemantics starlarkSemantics = PrecomputedValue.STARLARK_SEMANTICS.get(env);
if (starlarkSemantics == null) {
return null;
}
ModuleExtensionId extensionId = (ModuleExtensionId) skyKey.argument();
SingleExtensionUsagesValue usagesValue =
(SingleExtensionUsagesValue) env.getValue(SingleExtensionUsagesValue.key(extensionId));
if (usagesValue == null) {
return null;
}
Location sampleUsageLocation =
usagesValue.getExtensionUsages().values().iterator().next().getLocation();
BzlLoadValue bzlLoadValue =
loadBzlFile(extensionId.getBzlFileLabel(), sampleUsageLocation, starlarkSemantics, env);
if (bzlLoadValue == null) {
return null;
}
// TODO(wyv): Consider whether there's a need to check .bzl load visibility
// (BzlLoadFunction#checkLoadVisibilities).
// TODO(wyv): Consider refactoring to use PackageFunction#loadBzlModules, or the simpler API
// that may be created by b/237658764.
// Check that the .bzl file actually exports a module extension by our name.
Object exported = bzlLoadValue.getModule().getGlobal(extensionId.getExtensionName());
if (!(exported instanceof ModuleExtension)) {
ImmutableSet<String> exportedExtensions =
bzlLoadValue.getModule().getGlobals().entrySet().stream()
.filter(e -> e.getValue() instanceof ModuleExtension)
.map(Entry::getKey)
.collect(toImmutableSet());
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withMessage(
ExternalDeps.Code.BAD_MODULE,
"%s does not export a module extension called %s, yet its use is requested at %s%s",
extensionId.getBzlFileLabel(),
extensionId.getExtensionName(),
sampleUsageLocation,
SpellChecker.didYouMean(extensionId.getExtensionName(), exportedExtensions)),
Transience.PERSISTENT);
}
ModuleExtension extension = (ModuleExtension) exported;
ImmutableMap<String, String> extensionEnvVars =
RepositoryFunction.getEnvVarValues(env, ImmutableSet.copyOf(extension.getEnvVariables()));
if (extensionEnvVars == null) {
return null;
}
byte[] bzlTransitiveDigest =
BazelModuleContext.of(bzlLoadValue.getModule()).bzlTransitiveDigest();
// Check the lockfile first for that module extension
LockfileMode lockfileMode = BazelLockFileFunction.LOCKFILE_MODE.get(env);
ModuleExtensionEvalFactors extensionFactors =
ModuleExtensionEvalFactors.create(
extension.getOsDependent() ? OS.getCurrent().toString() : "",
extension.getArchDependent() ? OS_ARCH.value() : "");
if (!lockfileMode.equals(LockfileMode.OFF)) {
BazelLockFileValue lockfile = (BazelLockFileValue) env.getValue(BazelLockFileValue.KEY);
if (lockfile == null) {
return null;
}
try {
SingleExtensionEvalValue singleExtensionEvalValue =
tryGettingValueFromLockFile(
env,
extensionId,
extensionFactors,
extensionEnvVars,
usagesValue,
bzlTransitiveDigest,
lockfile);
if (singleExtensionEvalValue != null) {
return singleExtensionEvalValue;
}
} catch (NeedsSkyframeRestartException e) {
return null;
}
}
// Run that extension!
RunModuleExtensionResult moduleExtensionResult =
runModuleExtension(
extensionId, extension, usagesValue, bzlLoadValue.getModule(), starlarkSemantics, env);
if (moduleExtensionResult == null) {
return null;
}
ImmutableMap<String, RepoSpec> generatedRepoSpecs =
moduleExtensionResult.getGeneratedRepoSpecs();
Optional<ModuleExtensionMetadata> moduleExtensionMetadata =
moduleExtensionResult.getModuleExtensionMetadata();
// At this point the extension has been evaluated successfully, but SingleExtensionEvalFunction
// may still fail if imported repositories were not generated. However, since imports do not
// influence the evaluation of the extension and the validation also runs when the extension
// result is taken from the lockfile, we can already post the update event. This is necessary to
// prevent the extension from rerunning when only the imports change.
if (lockfileMode.equals(LockfileMode.UPDATE)) {
env.getListener()
.post(
ModuleExtensionResolutionEvent.create(
extensionId,
extensionFactors,
LockFileModuleExtension.builder()
.setBzlTransitiveDigest(bzlTransitiveDigest)
.setAccumulatedFileDigests(moduleExtensionResult.getAccumulatedFileDigests())
.setEnvVariables(extensionEnvVars)
.setGeneratedRepoSpecs(generatedRepoSpecs)
.setModuleExtensionMetadata(moduleExtensionMetadata)
.build()));
}
return validateAndCreateSingleExtensionEvalValue(
generatedRepoSpecs, moduleExtensionMetadata, extensionId, usagesValue, env);
}
/**
* Tries to get the evaluation result from the lockfile, if it's still up-to-date. Otherwise,
* returns {@code null}.
*
* @throws NeedsSkyframeRestartException in case we need a skyframe restart. Note that we
* <em>don't</em> return {@code null} in this case!
*/
@Nullable
private SingleExtensionEvalValue tryGettingValueFromLockFile(
Environment env,
ModuleExtensionId extensionId,
ModuleExtensionEvalFactors extensionFactors,
ImmutableMap<String, String> envVariables,
SingleExtensionUsagesValue usagesValue,
byte[] bzlTransitiveDigest,
BazelLockFileValue lockfile)
throws SingleExtensionEvalFunctionException,
InterruptedException,
NeedsSkyframeRestartException {
LockfileMode lockfileMode = BazelLockFileFunction.LOCKFILE_MODE.get(env);
var lockedExtensionMap = lockfile.getModuleExtensions().get(extensionId);
LockFileModuleExtension lockedExtension =
lockedExtensionMap == null ? null : lockedExtensionMap.get(extensionFactors);
if (lockedExtension == null) {
if (lockfileMode.equals(LockfileMode.ERROR)) {
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withMessage(
Code.BAD_MODULE,
"The module extension '%s''%s' does not exist in the lockfile",
extensionId,
extensionFactors.isEmpty() ? "" : " for platform " + extensionFactors),
Transience.PERSISTENT);
}
return null;
}
ImmutableMap<ModuleKey, ModuleExtensionUsage> lockedExtensionUsages;
try {
// TODO(salmasamy) might be nicer to precompute this table when we construct
// BazelLockFileValue, without adding it to the json file
ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsagesById =
BazelDepGraphFunction.getExtensionUsagesById(lockfile.getModuleDepGraph());
lockedExtensionUsages = extensionUsagesById.row(extensionId);
} catch (ExternalDepsException e) {
throw new SingleExtensionEvalFunctionException(e, Transience.PERSISTENT);
}
boolean filesChanged = didFilesChange(env, lockedExtension.getAccumulatedFileDigests());
// Check extension data in lockfile is still valid, disregarding usage information that is not
// relevant for the evaluation of the extension.
var trimmedLockedUsages =
ImmutableMap.copyOf(
transformValues(lockedExtensionUsages, ModuleExtensionUsage::trimForEvaluation));
var trimmedUsages =
ImmutableMap.copyOf(
transformValues(
usagesValue.getExtensionUsages(), ModuleExtensionUsage::trimForEvaluation));
if (!filesChanged
&& Arrays.equals(bzlTransitiveDigest, lockedExtension.getBzlTransitiveDigest())
&& trimmedUsages.equals(trimmedLockedUsages)
&& envVariables.equals(lockedExtension.getEnvVariables())) {
return validateAndCreateSingleExtensionEvalValue(
lockedExtension.getGeneratedRepoSpecs(),
lockedExtension.getModuleExtensionMetadata(),
extensionId,
usagesValue,
env);
} else if (lockfileMode.equals(LockfileMode.ERROR)) {
ImmutableList<String> extDiff =
lockfile.getModuleExtensionDiff(
extensionId,
lockedExtension,
bzlTransitiveDigest,
filesChanged,
envVariables,
trimmedUsages,
trimmedLockedUsages);
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withMessage(
Code.BAD_MODULE,
"Lock file is no longer up-to-date because: %s. "
+ "Please run `bazel mod deps --lockfile_mode=update` to update your lockfile.",
String.join(", ", extDiff)),
Transience.PERSISTENT);
}
return null;
}
private boolean didFilesChange(
Environment env, ImmutableMap<Label, String> accumulatedFileDigests)
throws InterruptedException, NeedsSkyframeRestartException {
// Turn labels into FileValue keys & get those values
Map<Label, FileValue.Key> fileKeys = new HashMap<>();
for (Label label : accumulatedFileDigests.keySet()) {
try {
RootedPath rootedPath = RepositoryFunction.getRootedPathFromLabel(label, env);
fileKeys.put(label, FileValue.key(rootedPath));
} catch (NeedsSkyframeRestartException e) {
throw e;
} catch (EvalException e) {
// Consider those exception to be a cause for invalidation
return true;
}
}
SkyframeLookupResult result = env.getValuesAndExceptions(fileKeys.values());
if (env.valuesMissing()) {
throw new NeedsSkyframeRestartException();
}
// Compare the collected file values with the hashes stored in the lockfile
for (Entry<Label, String> entry : accumulatedFileDigests.entrySet()) {
FileValue fileValue = (FileValue) result.get(fileKeys.get(entry.getKey()));
try {
if (!entry.getValue().equals(RepositoryFunction.fileValueToMarkerValue(fileValue))) {
return true;
}
} catch (IOException e) {
// Consider those exception to be a cause for invalidation
return true;
}
}
return false;
}
/**
* Validates the result of the module extension evaluation against the declared imports, throwing
* an exception if validation fails, and returns a SingleExtensionEvalValue otherwise.
*
* <p>Since extension evaluation does not depend on the declared imports, the result of the
* evaluation of the extension implementation function can be reused and persisted in the lockfile
* even if validation fails.
*/
private SingleExtensionEvalValue validateAndCreateSingleExtensionEvalValue(
ImmutableMap<String, RepoSpec> generatedRepoSpecs,
Optional<ModuleExtensionMetadata> moduleExtensionMetadata,
ModuleExtensionId extensionId,
SingleExtensionUsagesValue usagesValue,
Environment env)
throws SingleExtensionEvalFunctionException {
// Evaluate the metadata before failing on invalid imports so that fixup warning are still
// emitted in case of an error.
if (moduleExtensionMetadata.isPresent()) {
try {
// TODO: ModuleExtensionMetadata#evaluate should throw ExternalDepsException instead of
// EvalException.
moduleExtensionMetadata
.get()
.evaluate(
usagesValue.getExtensionUsages().values(),
generatedRepoSpecs.keySet(),
env.getListener());
} catch (EvalException e) {
env.getListener().handle(Event.error(e.getMessageWithStack()));
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withMessage(
Code.BAD_MODULE,
"error evaluating module extension %s in %s",
extensionId.getExtensionName(),
extensionId.getBzlFileLabel()),
Transience.TRANSIENT);
}
}
// Check that all imported repos have actually been generated.
for (ModuleExtensionUsage usage : usagesValue.getExtensionUsages().values()) {
for (Entry<String, String> repoImport : usage.getImports().entrySet()) {
if (!generatedRepoSpecs.containsKey(repoImport.getValue())) {
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withMessage(
Code.BAD_MODULE,
"module extension \"%s\" from \"%s\" does not generate repository \"%s\", yet it"
+ " is imported as \"%s\" in the usage at %s%s",
extensionId.getExtensionName(),
extensionId.getBzlFileLabel(),
repoImport.getValue(),
repoImport.getKey(),
usage.getLocation(),
SpellChecker.didYouMean(repoImport.getValue(), generatedRepoSpecs.keySet())),
Transience.PERSISTENT);
}
}
}
return SingleExtensionEvalValue.create(
generatedRepoSpecs,
generatedRepoSpecs.keySet().stream()
.collect(
toImmutableBiMap(
e ->
RepositoryName.createUnvalidated(
usagesValue.getExtensionUniqueName() + "~" + e),
Function.identity())));
}
private BzlLoadValue loadBzlFile(
Label bzlFileLabel,
Location sampleUsageLocation,
StarlarkSemantics starlarkSemantics,
Environment env)
throws SingleExtensionEvalFunctionException, InterruptedException {
// Check that the .bzl label isn't crazy.
try {
BzlLoadFunction.checkValidLoadLabel(bzlFileLabel, starlarkSemantics);
} catch (LabelSyntaxException e) {
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withCauseAndMessage(
Code.BAD_MODULE, e, "invalid module extension label"),
Transience.PERSISTENT);
}
// Load the .bzl file pointed to by the label.
BzlLoadValue bzlLoadValue;
try {
bzlLoadValue =
(BzlLoadValue)
env.getValueOrThrow(
BzlLoadValue.keyForBzlmod(bzlFileLabel), BzlLoadFailedException.class);
} catch (BzlLoadFailedException e) {
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withCauseAndMessage(
Code.BAD_MODULE,
e,
"Error loading '%s' for module extensions, requested by %s: %s",
bzlFileLabel,
sampleUsageLocation,
e.getMessage()),
Transience.PERSISTENT);
}
return bzlLoadValue;
}
@Nullable
private RunModuleExtensionResult runModuleExtension(
ModuleExtensionId extensionId,
ModuleExtension extension,
SingleExtensionUsagesValue usagesValue,
Module module,
StarlarkSemantics starlarkSemantics,
Environment env)
throws SingleExtensionEvalFunctionException, InterruptedException {
ModuleExtensionEvalStarlarkThreadContext threadContext =
new ModuleExtensionEvalStarlarkThreadContext(
usagesValue.getExtensionUniqueName() + "~",
extensionId.getBzlFileLabel().getPackageIdentifier(),
BazelModuleContext.of(module).repoMapping(),
directories,
env.getListener());
ModuleExtensionContext moduleContext;
Optional<ModuleExtensionMetadata> moduleExtensionMetadata;
try (Mutability mu =
Mutability.create("module extension", usagesValue.getExtensionUniqueName())) {
StarlarkThread thread = new StarlarkThread(mu, starlarkSemantics);
thread.setPrintHandler(Event.makeDebugPrintHandler(env.getListener()));
moduleContext = createContext(env, usagesValue, starlarkSemantics, extensionId, extension);
threadContext.storeInThread(thread);
try (SilentCloseable c =
Profiler.instance()
.profile(
ProfilerTask.BZLMOD,
() -> "evaluate module extension: " + extensionId.asTargetString())) {
Object returnValue =
Starlark.fastcall(
thread, extension.getImplementation(), new Object[] {moduleContext}, new Object[0]);
if (returnValue != Starlark.NONE && !(returnValue instanceof ModuleExtensionMetadata)) {
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withMessage(
ExternalDeps.Code.BAD_MODULE,
"expected module extension %s in %s to return None or extension_metadata, got %s",
extensionId.getExtensionName(),
extensionId.getBzlFileLabel(),
Starlark.type(returnValue)),
Transience.PERSISTENT);
}
if (returnValue instanceof ModuleExtensionMetadata) {
moduleExtensionMetadata = Optional.of((ModuleExtensionMetadata) returnValue);
} else {
moduleExtensionMetadata = Optional.empty();
}
} catch (NeedsSkyframeRestartException e) {
// Clean up and restart by returning null.
try {
if (moduleContext.getWorkingDirectory().exists()) {
moduleContext.getWorkingDirectory().deleteTree();
}
} catch (IOException e1) {
ExternalDepsException externalDepsException =
ExternalDepsException.withCauseAndMessage(
ExternalDeps.Code.UNRECOGNIZED,
e1,
"Failed to clean up module context directory");
throw new SingleExtensionEvalFunctionException(
externalDepsException, Transience.TRANSIENT);
}
return null;
} catch (EvalException e) {
env.getListener().handle(Event.error(e.getMessageWithStack()));
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withMessage(
ExternalDeps.Code.BAD_MODULE,
"error evaluating module extension %s in %s",
extensionId.getExtensionName(),
extensionId.getBzlFileLabel()),
Transience.TRANSIENT);
}
}
return RunModuleExtensionResult.create(
moduleContext.getAccumulatedFileDigests(),
threadContext.getGeneratedRepoSpecs(),
moduleExtensionMetadata);
}
private ModuleExtensionContext createContext(
Environment env,
SingleExtensionUsagesValue usagesValue,
StarlarkSemantics starlarkSemantics,
ModuleExtensionId extensionId,
ModuleExtension extension)
throws SingleExtensionEvalFunctionException {
Path workingDirectory =
directories
.getOutputBase()
.getRelative(LabelConstants.MODULE_EXTENSION_WORKING_DIRECTORY_LOCATION)
.getRelative(usagesValue.getExtensionUniqueName());
ArrayList<StarlarkBazelModule> modules = new ArrayList<>();
for (AbridgedModule abridgedModule : usagesValue.getAbridgedModules()) {
ModuleKey moduleKey = abridgedModule.getKey();
try {
modules.add(
StarlarkBazelModule.create(
abridgedModule,
extension,
usagesValue.getRepoMappings().get(moduleKey),
usagesValue.getExtensionUsages().get(moduleKey)));
} catch (ExternalDepsException e) {
throw new SingleExtensionEvalFunctionException(e, Transience.PERSISTENT);
}
}
ModuleExtensionUsage rootUsage = usagesValue.getExtensionUsages().get(ModuleKey.ROOT);
boolean rootModuleHasNonDevDependency =
rootUsage != null && rootUsage.getHasNonDevUseExtension();
return new ModuleExtensionContext(
workingDirectory,
env,
clientEnvironmentSupplier.get(),
downloadManager,
timeoutScaling,
processWrapper,
starlarkSemantics,
repositoryRemoteExecutor,
extensionId,
StarlarkList.immutableCopyOf(modules),
rootModuleHasNonDevDependency);
}
static final class SingleExtensionEvalFunctionException extends SkyFunctionException {
SingleExtensionEvalFunctionException(ExternalDepsException cause, Transience transience) {
super(cause, transience);
}
}
/* Holds the result data from running a module extension */
@AutoValue
abstract static class RunModuleExtensionResult {
abstract ImmutableMap<Label, String> getAccumulatedFileDigests();
abstract ImmutableMap<String, RepoSpec> getGeneratedRepoSpecs();
abstract Optional<ModuleExtensionMetadata> getModuleExtensionMetadata();
static RunModuleExtensionResult create(
ImmutableMap<Label, String> accumulatedFileDigests,
ImmutableMap<String, RepoSpec> generatedRepoSpecs,
Optional<ModuleExtensionMetadata> moduleExtensionMetadata) {
return new AutoValue_SingleExtensionEvalFunction_RunModuleExtensionResult(
accumulatedFileDigests, generatedRepoSpecs, moduleExtensionMetadata);
}
}
}