blob: 53a6e0e23e8ee04cf3e139c44ab1f7e4ea3e213a [file] [log] [blame]
// Copyright 2016 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.repository.starlark;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.RuleDefinition;
import com.google.devtools.build.lib.bazel.repository.RepositoryResolvedEvent;
import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
import com.google.devtools.build.lib.bazel.repository.starlark.RepoFetchingSkyKeyComputeState.Signal;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelConstants;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.packages.BazelStarlarkContext;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
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.repository.RepositoryFetchProgress;
import com.google.devtools.build.lib.rules.repository.NeedsSkyframeRestartException;
import com.google.devtools.build.lib.rules.repository.RepoRecordedInput;
import com.google.devtools.build.lib.rules.repository.RepositoryDirectoryValue;
import com.google.devtools.build.lib.rules.repository.RepositoryFunction;
import com.google.devtools.build.lib.rules.repository.WorkspaceFileHelper;
import com.google.devtools.build.lib.runtime.ProcessWrapper;
import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutor;
import com.google.devtools.build.lib.skyframe.IgnoredPackagePrefixesValue;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.SyscallCache;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
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.StarlarkCallable;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.SymbolGenerator;
/** A repository function to delegate work done by Starlark remote repositories. */
public final class StarlarkRepositoryFunction extends RepositoryFunction {
private final DownloadManager downloadManager;
private double timeoutScaling = 1.0;
@Nullable private ExecutorService workerExecutorService = null;
@Nullable private ProcessWrapper processWrapper = null;
@Nullable private RepositoryRemoteExecutor repositoryRemoteExecutor;
@Nullable private SyscallCache syscallCache;
public StarlarkRepositoryFunction(DownloadManager downloadManager) {
this.downloadManager = downloadManager;
}
public void setTimeoutScaling(double timeoutScaling) {
this.timeoutScaling = timeoutScaling;
}
public void setProcessWrapper(@Nullable ProcessWrapper processWrapper) {
this.processWrapper = processWrapper;
}
public void setSyscallCache(SyscallCache syscallCache) {
this.syscallCache = checkNotNull(syscallCache);
}
public void setWorkerExecutorService(@Nullable ExecutorService workerExecutorService) {
this.workerExecutorService = workerExecutorService;
}
@Override
protected void setupRepoRootBeforeFetching(Path repoRoot) throws RepositoryFunctionException {
// DON'T delete the repo root here if we're using a worker thread, since when this SkyFunction
// restarts, fetching is still happening inside the worker thread.
if (workerExecutorService == null) {
setupRepoRoot(repoRoot);
}
}
@Override
public void reportSkyframeRestart(Environment env, RepositoryName repoName) {
// DON'T report a "restarting." event if we're using a worker thread, since the actual fetch
// function run by the worker thread never restarts.
if (workerExecutorService == null) {
super.reportSkyframeRestart(env, repoName);
}
}
@Nullable
@Override
public RepositoryDirectoryValue.Builder fetch(
Rule rule,
Path outputDirectory,
BlazeDirectories directories,
Environment env,
Map<RepoRecordedInput, String> recordedInputValues,
SkyKey key)
throws RepositoryFunctionException, InterruptedException {
if (workerExecutorService == null
|| env.inErrorBubblingForSkyFunctionsThatCanFullyRecoverFromErrors()) {
// Don't use the worker thread if we're in Skyframe error bubbling. For some reason, using a
// worker thread during error bubbling very frequently causes deadlocks on Linux platforms.
// The deadlock is rather elusive and this is just the immediate thing that seems to help.
// Fortunately, no Skyframe restarts should happen during error bubbling anyway, so this
// shouldn't be a performance concern. See https://github.com/bazelbuild/bazel/issues/21238
// for more context.
return fetchInternal(rule, outputDirectory, directories, env, recordedInputValues, key);
}
var state = env.getState(RepoFetchingSkyKeyComputeState::new);
var workerFuture = state.workerFuture;
if (workerFuture == null) {
// No worker is running yet, which means we're just starting to fetch this repo. Start with a
// clean slate, and create the worker.
setupRepoRoot(outputDirectory);
Environment workerEnv = new RepoFetchingWorkerSkyFunctionEnvironment(state, env);
workerFuture =
workerExecutorService.submit(
() -> {
try {
return fetchInternal(
rule,
outputDirectory,
directories,
workerEnv,
state.recordedInputValues,
key);
} finally {
state.signalQueue.put(Signal.DONE);
}
});
state.workerFuture = workerFuture;
} else {
// A worker is already running. This can only mean one thing -- we just had a Skyframe
// restart, and need to send over a fresh Environment.
state.delegateEnvQueue.put(env);
}
Signal signal;
try {
signal = state.signalQueue.take();
} catch (InterruptedException e) {
// This means that we caught a Ctrl-C. Make sure to close the state object to interrupt the
// worker thread, wait for it to finish, and then propagate the InterruptedException.
state.close();
signal = Uninterruptibles.takeUninterruptibly(state.signalQueue);
// The call to Uninterruptibles.takeUninterruptibly() above may set the thread interrupted
// status if it suppressed an InterruptedException, so we clear it again.
Thread.interrupted();
throw new InterruptedException();
}
switch (signal) {
case RESTART:
return null;
case DONE:
try {
RepositoryDirectoryValue.Builder result = workerFuture.get();
recordedInputValues.putAll(state.recordedInputValues);
return result;
} catch (ExecutionException e) {
Throwables.throwIfInstanceOf(e.getCause(), RepositoryFunctionException.class);
Throwables.throwIfUnchecked(e.getCause());
throw new IllegalStateException(
"unexpected exception type: " + e.getClass(), e.getCause());
} catch (CancellationException e) {
// This can only happen if the state object was invalidated due to memory pressure, in
// which case we can simply reattempt the fetch.
env.getListener()
.post(
RepositoryFetchProgress.ongoing(
RepositoryName.createUnvalidated(rule.getName()),
"fetch interrupted due to memory pressure; restarting."));
return fetch(rule, outputDirectory, directories, env, recordedInputValues, key);
} finally {
// At this point, the worker thread has definitely finished. But in some corner cases (see
// b/330892334), a Skyframe restart might still happen; to ensure we're not tricked into
// a deadlock, we clean up the worker thread and so that next time we come into fetch(),
// we actually restart the entire computation.
state.close();
}
}
// TODO(wyv): use a switch expression above instead and remove this.
throw new IllegalStateException();
}
@Nullable
private RepositoryDirectoryValue.Builder fetchInternal(
Rule rule,
Path outputDirectory,
BlazeDirectories directories,
Environment env,
Map<RepoRecordedInput, String> recordedInputValues,
SkyKey key)
throws RepositoryFunctionException, InterruptedException {
String defInfo = RepositoryResolvedEvent.getRuleDefinitionInformation(rule);
env.getListener().post(new StarlarkRepositoryDefinitionLocationEvent(rule.getName(), defInfo));
StarlarkCallable function = rule.getRuleClassObject().getConfiguredTargetFunction();
if (declareEnvironmentDependencies(recordedInputValues, env, getEnviron(rule)) == null) {
return null;
}
StarlarkSemantics starlarkSemantics = PrecomputedValue.STARLARK_SEMANTICS.get(env);
if (env.valuesMissing()) {
return null;
}
PathPackageLocator packageLocator = PrecomputedValue.PATH_PACKAGE_LOCATOR.get(env);
if (env.valuesMissing()) {
return null;
}
IgnoredPackagePrefixesValue ignoredPackagesValue =
(IgnoredPackagePrefixesValue) env.getValue(IgnoredPackagePrefixesValue.key());
if (env.valuesMissing()) {
return null;
}
ImmutableSet<PathFragment> ignoredPatterns = checkNotNull(ignoredPackagesValue).getPatterns();
try (Mutability mu = Mutability.create("Starlark repository")) {
StarlarkThread thread = new StarlarkThread(mu, starlarkSemantics);
thread.setPrintHandler(Event.makeDebugPrintHandler(env.getListener()));
var repoMappingRecorder = new Label.RepoMappingRecorder();
// For repos defined in Bzlmod, record any used repo mappings in the marker file.
// Repos defined in WORKSPACE are impossible to verify given the chunked loading (we'd have to
// record which chunk the repo mapping was used in, and ain't nobody got time for that).
if (!isWorkspaceRepo(rule)) {
repoMappingRecorder.mergeEntries(
rule.getRuleClassObject().getRuleDefinitionEnvironmentRepoMappingEntries());
thread.setThreadLocal(Label.RepoMappingRecorder.class, repoMappingRecorder);
}
new BazelStarlarkContext(
BazelStarlarkContext.Phase.LOADING, // ("fetch")
SymbolGenerator.create(key))
.storeInThread(thread);
StarlarkRepositoryContext starlarkRepositoryContext =
new StarlarkRepositoryContext(
rule,
packageLocator,
outputDirectory,
ignoredPatterns,
env,
ImmutableMap.copyOf(clientEnvironment),
downloadManager,
timeoutScaling,
processWrapper,
starlarkSemantics,
repositoryRemoteExecutor,
syscallCache,
directories);
if (starlarkRepositoryContext.isRemotable()) {
// If a rule is declared remotable then invalidate it if remote execution gets
// enabled or disabled.
PrecomputedValue.REMOTE_EXECUTION_ENABLED.get(env);
}
// Since restarting a repository function can be really expensive, we first ensure that
// all label-arguments can be resolved to paths.
try {
starlarkRepositoryContext.enforceLabelAttributes();
} catch (NeedsSkyframeRestartException e) {
// Missing values are expected; just restart before we actually start the rule
return null;
}
// This rule is mainly executed for its side effect. Nevertheless, the return value is
// of importance, as it provides information on how the call has to be modified to be a
// reproducible rule.
//
// Also we do a lot of stuff in there, maybe blocking operations and we should certainly make
// it possible to return null and not block but it doesn't seem to be easy with Starlark
// structure as it is.
Object result;
boolean fetchSuccessful = false;
try (SilentCloseable c =
Profiler.instance()
.profile(ProfilerTask.STARLARK_REPOSITORY_FN, () -> rule.getLabel().toString())) {
result =
Starlark.call(
thread,
function,
/*args=*/ ImmutableList.of(starlarkRepositoryContext),
/*kwargs=*/ ImmutableMap.of());
fetchSuccessful = true;
} finally {
if (starlarkRepositoryContext.ensureNoPendingAsyncTasks(
env.getListener(), fetchSuccessful)) {
if (fetchSuccessful) {
throw new RepositoryFunctionException(
new EvalException(
"Pending asynchronous work after repository rule finished running"),
Transience.PERSISTENT);
}
}
}
RepositoryResolvedEvent resolved =
new RepositoryResolvedEvent(
rule, starlarkRepositoryContext.getAttr(), outputDirectory, result);
if (resolved.isNewInformationReturned()) {
env.getListener().handle(Event.debug(resolved.getMessage()));
env.getListener().handle(Event.debug(defInfo));
}
// Modify marker data to include the files/dirents used by the rule's implementation function.
recordedInputValues.putAll(starlarkRepositoryContext.getRecordedFileInputs());
recordedInputValues.putAll(starlarkRepositoryContext.getRecordedDirentsInputs());
recordedInputValues.putAll(starlarkRepositoryContext.getRecordedDirTreeInputs());
// Ditto for environment variables accessed via `getenv`.
for (String envKey : starlarkRepositoryContext.getAccumulatedEnvKeys()) {
recordedInputValues.put(
new RepoRecordedInput.EnvVar(envKey), clientEnvironment.get(envKey));
}
for (Table.Cell<RepositoryName, String, RepositoryName> repoMappings :
repoMappingRecorder.recordedEntries().cellSet()) {
recordedInputValues.put(
new RepoRecordedInput.RecordedRepoMapping(
repoMappings.getRowKey(), repoMappings.getColumnKey()),
repoMappings.getValue().getName());
}
env.getListener().post(resolved);
} catch (NeedsSkyframeRestartException e) {
// A dependency is missing, cleanup and returns null
try {
if (outputDirectory.exists()) {
outputDirectory.deleteTree();
}
} catch (IOException e1) {
throw new RepositoryFunctionException(e1, Transience.TRANSIENT);
}
return null;
} catch (EvalException e) {
env.getListener()
.handle(
Event.error(
"An error occurred during the fetch of repository '"
+ rule.getName()
+ "':\n "
+ e.getMessageWithStack()));
env.getListener()
.handle(Event.info(RepositoryResolvedEvent.getRuleDefinitionInformation(rule)));
throw new RepositoryFunctionException(e, Transience.TRANSIENT);
}
if (!outputDirectory.isDirectory()) {
throw new RepositoryFunctionException(
new IOException(rule + " must create a directory"), Transience.TRANSIENT);
}
// Make sure the fetched repo has a boundary file.
if (!WorkspaceFileHelper.isValidRepoRoot(outputDirectory)) {
if (outputDirectory.isSymbolicLink()) {
// The created repo is actually just a symlink to somewhere else (think local_repository).
// In this case, we shouldn't try to create the repo boundary file ourselves, but report an
// error instead.
throw new RepositoryFunctionException(
new IOException(
"No MODULE.bazel, REPO.bazel, or WORKSPACE file found in " + outputDirectory),
Transience.TRANSIENT);
}
// Otherwise, we can just create an empty REPO.bazel file.
try {
FileSystemUtils.createEmptyFile(outputDirectory.getRelative(LabelConstants.REPO_FILE_NAME));
if (starlarkSemantics.getBool(BuildLanguageOptions.ENABLE_WORKSPACE)) {
FileSystemUtils.createEmptyFile(
outputDirectory.getRelative(LabelConstants.WORKSPACE_FILE_NAME));
}
} catch (IOException e) {
throw new RepositoryFunctionException(e, Transience.TRANSIENT);
}
}
return RepositoryDirectoryValue.builder().setPath(outputDirectory);
}
@SuppressWarnings("unchecked")
private static ImmutableSet<String> getEnviron(Rule rule) {
return ImmutableSet.copyOf((Iterable<String>) rule.getAttr("$environ"));
}
@Override
protected boolean isLocal(Rule rule) {
return (Boolean) rule.getAttr("$local");
}
@Override
protected boolean isConfigure(Rule rule) {
return (Boolean) rule.getAttr("$configure");
}
/**
* Static method to determine if for a starlark repository rule {@code isConfigure} holds true. It
* also checks that the rule is indeed a Starlark rule so that this class is the appropriate
* handler for the given rule. As, however, only Starklark rules can be configure rules, this
* method can also be used as a universal check.
*/
public static boolean isConfigureRule(Rule rule) {
return rule.getRuleClassObject().isStarlark() && ((Boolean) rule.getAttr("$configure"));
}
@Nullable
@Override
public Class<? extends RuleDefinition> getRuleDefinition() {
return null; // unused so safe to return null
}
public void setRepositoryRemoteExecutor(RepositoryRemoteExecutor repositoryRemoteExecutor) {
this.repositoryRemoteExecutor = repositoryRemoteExecutor;
}
}