blob: 222eb67bf9dbff969a3413c1aa0793ee38e40fff [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.collect.ImmutableBiMap.toImmutableBiMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
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.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.rules.repository.NeedsSkyframeRestartException;
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.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.vfs.Path;
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 java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.Map.Entry;
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.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 SkyFunctionException, 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();
// Check that the .bzl label isn't crazy.
try {
BzlLoadFunction.checkValidLoadLabel(extensionId.getBzlFileLabel(), 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(extensionId.getBzlFileLabel()),
BzlLoadFailedException.class);
} catch (BzlLoadFailedException e) {
throw new SingleExtensionEvalFunctionException(
ExternalDepsException.withCauseAndMessage(
Code.BAD_MODULE,
e,
"Error loading '%s' for module extensions, requested by %s: %s",
extensionId.getBzlFileLabel(),
sampleUsageLocation,
e.getMessage()),
Transience.PERSISTENT);
}
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(
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);
}
// Run that extension!
ModuleExtension extension = (ModuleExtension) exported;
ModuleExtensionEvalStarlarkThreadContext threadContext =
new ModuleExtensionEvalStarlarkThreadContext(
usagesValue.getExtensionUniqueName() + "~",
extensionId.getBzlFileLabel().getPackageIdentifier(),
BazelModuleContext.of(bzlLoadValue.getModule()).repoMapping(),
directories,
env.getListener());
try (Mutability mu =
Mutability.create("module extension", usagesValue.getExtensionUniqueName())) {
StarlarkThread thread = new StarlarkThread(mu, starlarkSemantics);
thread.setPrintHandler(Event.makeDebugPrintHandler(env.getListener()));
ModuleExtensionContext moduleContext =
createContext(env, usagesValue, starlarkSemantics, extensionId, extension);
threadContext.storeInThread(thread);
try {
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(
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 metadata = (ModuleExtensionMetadata) returnValue;
metadata.evaluate(
usagesValue.getExtensionUsages().values(),
threadContext.getGeneratedRepos().keySet(),
env.getListener());
}
} catch (NeedsSkyframeRestartException e) {
// Clean up and restart by returning null.
try {
if (moduleContext.getWorkingDirectory().exists()) {
moduleContext.getWorkingDirectory().deleteTree();
}
} catch (IOException e1) {
throw new SingleExtensionEvalFunctionException(e1, Transience.TRANSIENT);
}
return null;
} 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 been actually generated
for (ModuleExtensionUsage usage : usagesValue.getExtensionUsages().values()) {
for (Entry<String, String> repoImport : usage.getImports().entrySet()) {
if (!threadContext.getGeneratedRepos().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(), threadContext.getGeneratedRepos().keySet())),
Transience.PERSISTENT);
}
}
}
return SingleExtensionEvalValue.create(
threadContext.getGeneratedRepos(),
threadContext.getGeneratedRepos().keySet().stream()
.collect(
toImmutableBiMap(
e ->
RepositoryName.createUnvalidated(
usagesValue.getExtensionUniqueName() + "~" + e),
Function.identity())));
}
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);
}
}
return new ModuleExtensionContext(
workingDirectory,
env,
clientEnvironmentSupplier.get(),
downloadManager,
timeoutScaling,
processWrapper,
starlarkSemantics,
repositoryRemoteExecutor,
extensionId,
StarlarkList.immutableCopyOf(modules));
}
static final class SingleExtensionEvalFunctionException extends SkyFunctionException {
SingleExtensionEvalFunctionException(Exception cause, Transience transience) {
super(cause, transience);
}
}
}