| // 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.ImmutableList.toImmutableList; |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Splitter; |
| 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.common.collect.Iterables; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Table; |
| 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.bazel.repository.starlark.StarlarkRepositoryModule.RepositoryRuleFunction; |
| 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.RepositoryMapping; |
| import com.google.devtools.build.lib.cmdline.RepositoryName; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.packages.NoSuchPackageException; |
| import com.google.devtools.build.lib.packages.Rule; |
| import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException; |
| 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.skyframe.RepositoryMappingValue; |
| import com.google.devtools.build.lib.util.Fingerprint; |
| 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.HashSet; |
| import java.util.Iterator; |
| 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.Dict; |
| 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 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; |
| } |
| RunnableExtension extension; |
| if (extensionId.isInnate()) { |
| extension = loadInnateRunnableExtension(extensionId, usagesValue, starlarkSemantics, env); |
| } else { |
| extension = loadRegularRunnableExtension(extensionId, usagesValue, starlarkSemantics, env); |
| } |
| if (extension == null) { |
| return null; |
| } |
| |
| // Check the lockfile first for that module extension |
| LockfileMode lockfileMode = BazelLockFileFunction.LOCKFILE_MODE.get(env); |
| if (!lockfileMode.equals(LockfileMode.OFF)) { |
| BazelLockFileValue lockfile = (BazelLockFileValue) env.getValue(BazelLockFileValue.KEY); |
| if (lockfile == null) { |
| return null; |
| } |
| try { |
| SingleExtensionEvalValue singleExtensionEvalValue = |
| tryGettingValueFromLockFile(env, extensionId, extension, usagesValue, lockfile); |
| if (singleExtensionEvalValue != null) { |
| return singleExtensionEvalValue; |
| } |
| } catch (NeedsSkyframeRestartException e) { |
| return null; |
| } |
| } |
| |
| // Run that extension! |
| env.getListener().post(ModuleExtensionEvaluationProgress.ongoing(extensionId, "starting")); |
| RunModuleExtensionResult moduleExtensionResult = |
| extension.run(env, usagesValue, starlarkSemantics, extensionId); |
| if (moduleExtensionResult == null) { |
| return null; |
| } |
| env.getListener().post(ModuleExtensionEvaluationProgress.finished(extensionId)); |
| 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, |
| extension.getEvalFactors(), |
| LockFileModuleExtension.builder() |
| .setBzlTransitiveDigest(extension.getBzlTransitiveDigest()) |
| .setAccumulatedFileDigests(moduleExtensionResult.getAccumulatedFileDigests()) |
| .setEnvVariables(extension.getEnvVars()) |
| .setGeneratedRepoSpecs(generatedRepoSpecs) |
| .setModuleExtensionMetadata(moduleExtensionMetadata) |
| .setRecordedRepoMappingEntries( |
| moduleExtensionResult.getRecordedRepoMappingEntries()) |
| .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, |
| RunnableExtension extension, |
| SingleExtensionUsagesValue usagesValue, |
| 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(extension.getEvalFactors()); |
| 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, |
| extension.getEvalFactors().isEmpty() |
| ? "" |
| : " for platform " + extension.getEvalFactors()), |
| 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); |
| } |
| |
| DiffRecorder diffRecorder = |
| new DiffRecorder(/* recordMessages= */ lockfileMode.equals(LockfileMode.ERROR)); |
| try { |
| // Put faster diff detections earlier, so that we can short-circuit in UPDATE mode. |
| if (!Arrays.equals( |
| extension.getBzlTransitiveDigest(), lockedExtension.getBzlTransitiveDigest())) { |
| diffRecorder.record( |
| "The implementation of the extension '" |
| + extensionId |
| + "' or one of its transitive .bzl files has changed"); |
| } |
| if (!extension.getEnvVars().equals(lockedExtension.getEnvVariables())) { |
| diffRecorder.record( |
| "The environment variables the extension '" |
| + extensionId |
| + "' depends on (or their values) have changed"); |
| } |
| // Check extension data in lockfile is still valid, disregarding usage information that is not |
| // relevant for the evaluation of the extension. |
| if (!ModuleExtensionUsage.trimForEvaluation(usagesValue.getExtensionUsages()) |
| .equals(ModuleExtensionUsage.trimForEvaluation(lockedExtensionUsages))) { |
| diffRecorder.record("The usages of the extension '" + extensionId + "' have changed"); |
| } |
| if (didRepoMappingsChange(env, lockedExtension.getRecordedRepoMappingEntries())) { |
| diffRecorder.record( |
| "The repo mappings of certain repos used by the extension '" |
| + extensionId |
| + "' have changed"); |
| } |
| if (didFilesChange(env, lockedExtension.getAccumulatedFileDigests())) { |
| diffRecorder.record( |
| "One or more files the extension '" + extensionId + "' is using have changed"); |
| } |
| } catch (DiffFoundEarlyExitException ignored) { |
| // ignored |
| } |
| if (!diffRecorder.anyDiffsDetected()) { |
| return validateAndCreateSingleExtensionEvalValue( |
| lockedExtension.getGeneratedRepoSpecs(), |
| lockedExtension.getModuleExtensionMetadata(), |
| extensionId, |
| usagesValue, |
| env); |
| } |
| if (lockfileMode.equals(LockfileMode.ERROR)) { |
| throw new SingleExtensionEvalFunctionException( |
| ExternalDepsException.withMessage( |
| Code.BAD_MODULE, |
| "MODULE.bazel.lock is no longer up-to-date because: %s. " |
| + "Please run `bazel mod deps --lockfile_mode=update` to update your lockfile.", |
| diffRecorder.getRecordedDiffMessages()), |
| Transience.PERSISTENT); |
| } |
| return null; |
| } |
| |
| private static final class DiffFoundEarlyExitException extends Exception {} |
| |
| private static final class DiffRecorder { |
| private boolean diffDetected = false; |
| private final ImmutableList.Builder<String> diffMessages; |
| |
| DiffRecorder(boolean recordMessages) { |
| diffMessages = recordMessages ? ImmutableList.builder() : null; |
| } |
| |
| private void record(String message) throws DiffFoundEarlyExitException { |
| diffDetected = true; |
| if (diffMessages != null) { |
| diffMessages.add(message); |
| } else { |
| throw new DiffFoundEarlyExitException(); |
| } |
| } |
| |
| public boolean anyDiffsDetected() { |
| return diffDetected; |
| } |
| |
| public String getRecordedDiffMessages() { |
| return String.join(",", diffMessages.build()); |
| } |
| } |
| |
| private static boolean didRepoMappingsChange( |
| Environment env, ImmutableTable<RepositoryName, String, RepositoryName> recordedRepoMappings) |
| throws InterruptedException, NeedsSkyframeRestartException { |
| SkyframeLookupResult result = |
| env.getValuesAndExceptions( |
| recordedRepoMappings.rowKeySet().stream() |
| .map(RepositoryMappingValue::key) |
| .collect(toImmutableSet())); |
| if (env.valuesMissing()) { |
| // This shouldn't really happen, since the RepositoryMappingValues of any recorded repos |
| // should have already been requested by the time we load the .bzl for the extension. And this |
| // method is only called if the transitive .bzl digest hasn't changed. |
| // However, we pretend it could happen anyway because we're good citizens. |
| throw new NeedsSkyframeRestartException(); |
| } |
| for (Table.Cell<RepositoryName, String, RepositoryName> cell : recordedRepoMappings.cellSet()) { |
| RepositoryMappingValue repoMappingValue = |
| (RepositoryMappingValue) result.get(RepositoryMappingValue.key(cell.getRowKey())); |
| if (repoMappingValue == null) { |
| // Again, this shouldn't happen. But anyway. |
| throw new NeedsSkyframeRestartException(); |
| } |
| if (!cell.getValue() |
| .equals(repoMappingValue.getRepositoryMapping().get(cell.getColumnKey()))) { |
| // Wee woo wee woo -- diff detected! |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static 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; |
| } |
| |
| /** |
| * An internal abstraction to support the two "flavors" of module extensions: the "regular", which |
| * is declared using {@code module_extension} in a .bzl file; and the "innate", which is |
| * fabricated from usages of {@code use_repo_rule} in MODULE.bazel files. |
| * |
| * <p>The general idiom is to "load" such a {@link RunnableExtension} object by getting as much |
| * information about it as needed to determine whether it can be reused from the lockfile (hence |
| * methods such as {@link #getEvalFactors()}, {@link #getBzlTransitiveDigest()}, {@link |
| * #getEnvVars()}). Then the {@link #run} method can be called if it's determined that we can't |
| * reuse the cached results in the lockfile and have to re-run this extension. |
| */ |
| private interface RunnableExtension { |
| ModuleExtensionEvalFactors getEvalFactors(); |
| |
| byte[] getBzlTransitiveDigest(); |
| |
| ImmutableMap<String, String> getEnvVars(); |
| |
| @Nullable |
| RunModuleExtensionResult run( |
| Environment env, |
| SingleExtensionUsagesValue usagesValue, |
| StarlarkSemantics starlarkSemantics, |
| ModuleExtensionId extensionId) |
| throws InterruptedException, SingleExtensionEvalFunctionException; |
| } |
| |
| /** Information about a single repo to be created by an innate extension. */ |
| @AutoValue |
| abstract static class InnateExtensionRepo { |
| abstract Label bzlLabel(); |
| |
| abstract String ruleName(); |
| |
| abstract Tag tag(); |
| |
| abstract BzlLoadValue loadedBzl(); |
| |
| static Builder builder() { |
| return new AutoValue_SingleExtensionEvalFunction_InnateExtensionRepo.Builder(); |
| } |
| |
| @AutoValue.Builder |
| abstract static class Builder { |
| |
| abstract Builder setBzlLabel(Label value); |
| |
| abstract Label bzlLabel(); |
| |
| abstract Builder setRuleName(String value); |
| |
| abstract Builder setTag(Tag value); |
| |
| abstract Tag tag(); |
| |
| abstract Builder setLoadedBzl(BzlLoadValue value); |
| |
| abstract InnateExtensionRepo build(); |
| } |
| } |
| |
| @Nullable |
| private InnateRunnableExtension loadInnateRunnableExtension( |
| ModuleExtensionId extensionId, |
| SingleExtensionUsagesValue usagesValue, |
| StarlarkSemantics starlarkSemantics, |
| Environment env) |
| throws InterruptedException, SingleExtensionEvalFunctionException { |
| // An innate extension should have a singular usage. |
| if (usagesValue.getExtensionUsages().size() > 1) { |
| throw new SingleExtensionEvalFunctionException( |
| ExternalDepsException.withMessage( |
| Code.BAD_MODULE, |
| "innate module extension %s is used by multiple modules: %s", |
| extensionId, |
| usagesValue.getExtensionUsages().keySet()), |
| Transience.PERSISTENT); |
| } |
| ModuleKey moduleKey = Iterables.getOnlyElement(usagesValue.getExtensionUsages().keySet()); |
| Preconditions.checkState(moduleKey.moduleFileLabel().equals(extensionId.getBzlFileLabel())); |
| ImmutableList<Tag> tags = |
| Iterables.getOnlyElement(usagesValue.getExtensionUsages().values()).getTags(); |
| RepositoryMapping repoMapping = usagesValue.getRepoMappings().get(moduleKey); |
| |
| // Each tag of this usage defines a repo. The name of the tag is of the form |
| // "<bzl_file_label>%<rule_name>". Collect the .bzl files referenced and load them. |
| Label.RepoContext repoContext = |
| Label.RepoContext.of(moduleKey.getCanonicalRepoName(), repoMapping); |
| ArrayList<InnateExtensionRepo.Builder> repoBuilders = new ArrayList<>(tags.size()); |
| for (Tag tag : tags) { |
| Iterator<String> parts = Splitter.on('%').split(tag.getTagName()).iterator(); |
| InnateExtensionRepo.Builder repoBuilder = InnateExtensionRepo.builder().setTag(tag); |
| repoBuilders.add(repoBuilder); |
| try { |
| Label label = Label.parseWithRepoContext(parts.next(), repoContext); |
| BzlLoadFunction.checkValidLoadLabel(label, starlarkSemantics); |
| repoBuilder.setBzlLabel(label).setRuleName(parts.next()); |
| } catch (LabelSyntaxException e) { |
| throw new SingleExtensionEvalFunctionException( |
| ExternalDepsException.withCauseAndMessage( |
| Code.BAD_MODULE, e, "bad repo rule .bzl file label at %s", tag.getLocation()), |
| Transience.PERSISTENT); |
| } |
| } |
| ImmutableSet<BzlLoadValue.Key> loadKeys = |
| repoBuilders.stream() |
| .map(r -> BzlLoadValue.keyForBzlmod(r.bzlLabel())) |
| .collect(toImmutableSet()); |
| HashSet<Label> digestedLabels = new HashSet<>(); |
| Fingerprint transitiveBzlDigest = new Fingerprint(); |
| SkyframeLookupResult loadResult = env.getValuesAndExceptions(loadKeys); |
| for (InnateExtensionRepo.Builder repoBuilder : repoBuilders) { |
| BzlLoadValue loadedBzl; |
| try { |
| loadedBzl = |
| (BzlLoadValue) |
| loadResult.getOrThrow( |
| BzlLoadValue.keyForBzlmod(repoBuilder.bzlLabel()), |
| BzlLoadFailedException.class); |
| } catch (BzlLoadFailedException e) { |
| throw new SingleExtensionEvalFunctionException( |
| ExternalDepsException.withCauseAndMessage( |
| Code.BAD_MODULE, |
| e, |
| "error loading '%s' for repo rules, requested by %s", |
| repoBuilder.bzlLabel(), |
| repoBuilder.tag().getLocation()), |
| Transience.PERSISTENT); |
| } |
| if (loadedBzl == null) { |
| return null; |
| } |
| repoBuilder.setLoadedBzl(loadedBzl); |
| if (digestedLabels.add(repoBuilder.bzlLabel())) { |
| // Only digest this BzlLoadValue if we haven't seen this bzl label before. |
| transitiveBzlDigest.addBytes(loadedBzl.getTransitiveDigest()); |
| } |
| } |
| |
| return new InnateRunnableExtension( |
| moduleKey, |
| repoBuilders.stream().map(InnateExtensionRepo.Builder::build).collect(toImmutableList()), |
| transitiveBzlDigest.digestAndReset()); |
| } |
| |
| private final class InnateRunnableExtension implements RunnableExtension { |
| private final ModuleKey moduleKey; |
| private final ImmutableList<InnateExtensionRepo> repos; |
| private final byte[] transitiveBzlDigest; |
| |
| InnateRunnableExtension( |
| ModuleKey moduleKey, ImmutableList<InnateExtensionRepo> repos, byte[] transitiveBzlDigest) { |
| this.moduleKey = moduleKey; |
| this.repos = repos; |
| this.transitiveBzlDigest = transitiveBzlDigest; |
| } |
| |
| @Override |
| public ModuleExtensionEvalFactors getEvalFactors() { |
| return ModuleExtensionEvalFactors.create("", ""); |
| } |
| |
| @Override |
| public byte[] getBzlTransitiveDigest() { |
| return transitiveBzlDigest; |
| } |
| |
| @Override |
| public ImmutableMap<String, String> getEnvVars() { |
| return ImmutableMap.of(); |
| } |
| |
| @Override |
| public RunModuleExtensionResult run( |
| Environment env, |
| SingleExtensionUsagesValue usagesValue, |
| StarlarkSemantics starlarkSemantics, |
| ModuleExtensionId extensionId) |
| throws InterruptedException, SingleExtensionEvalFunctionException { |
| var generatedRepoSpecs = ImmutableMap.<String, RepoSpec>builderWithExpectedSize(repos.size()); |
| // Instiantiate the repos one by one. |
| for (InnateExtensionRepo repo : repos) { |
| Object exported = repo.loadedBzl().getModule().getGlobal(repo.ruleName()); |
| if (!(exported instanceof RepositoryRuleFunction)) { |
| ImmutableSet<String> exportedRepoRules = |
| repo.loadedBzl().getModule().getGlobals().entrySet().stream() |
| .filter(e -> e.getValue() instanceof RepositoryRuleFunction) |
| .map(Entry::getKey) |
| .collect(toImmutableSet()); |
| throw new SingleExtensionEvalFunctionException( |
| ExternalDepsException.withMessage( |
| Code.BAD_MODULE, |
| "%s does not export a repository_rule called %s, yet its use is requested at" |
| + " %s%s", |
| repo.bzlLabel(), |
| repo.ruleName(), |
| repo.tag().getLocation(), |
| SpellChecker.didYouMean(repo.ruleName(), exportedRepoRules)), |
| Transience.PERSISTENT); |
| } |
| RepositoryRuleFunction repoRule = (RepositoryRuleFunction) exported; |
| Dict<String, Object> kwargs = repo.tag().getAttributeValues().attributes(); |
| // This cast should be safe since it should have been verified at tag creation time. |
| String name = (String) kwargs.get("name"); |
| String prefixedName = usagesValue.getExtensionUniqueName() + "~" + name; |
| Rule ruleInstance; |
| try { |
| ruleInstance = |
| BzlmodRepoRuleCreator.createRule( |
| extensionId.getBzlFileLabel().getPackageIdentifier(), |
| usagesValue.getRepoMappings().get(moduleKey), |
| directories, |
| starlarkSemantics, |
| env.getListener(), |
| "SingleExtensionEval.createInnateExtensionRepoRule", |
| repoRule.getRuleClass(), |
| Maps.transformEntries(kwargs, (k, v) -> k.equals("name") ? prefixedName : v)); |
| } catch (InvalidRuleException | NoSuchPackageException | EvalException e) { |
| throw new SingleExtensionEvalFunctionException( |
| ExternalDepsException.withCauseAndMessage( |
| Code.BAD_MODULE, |
| e, |
| "error creating repo %s requested at %s", |
| name, |
| repo.tag().getLocation()), |
| Transience.PERSISTENT); |
| } |
| RepoSpec repoSpec = |
| RepoSpec.builder() |
| .setBzlFile( |
| repoRule |
| .getRuleClass() |
| .getRuleDefinitionEnvironmentLabel() |
| .getUnambiguousCanonicalForm()) |
| .setRuleClassName(repoRule.getRuleClass().getName()) |
| .setAttributes( |
| AttributeValues.create( |
| Maps.transformEntries(kwargs, (k, v) -> ruleInstance.getAttr(k)))) |
| .build(); |
| generatedRepoSpecs.put(name, repoSpec); |
| } |
| return RunModuleExtensionResult.create( |
| ImmutableMap.of(), |
| generatedRepoSpecs.buildOrThrow(), |
| Optional.empty(), |
| ImmutableTable.of()); |
| } |
| } |
| |
| @Nullable |
| private RegularRunnableExtension loadRegularRunnableExtension( |
| ModuleExtensionId extensionId, |
| SingleExtensionUsagesValue usagesValue, |
| StarlarkSemantics starlarkSemantics, |
| Environment env) |
| throws InterruptedException, SingleExtensionEvalFunctionException { |
| 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> envVars = |
| RepositoryFunction.getEnvVarValues(env, ImmutableSet.copyOf(extension.getEnvVariables())); |
| if (envVars == null) { |
| return null; |
| } |
| return new RegularRunnableExtension( |
| BazelModuleContext.of(bzlLoadValue.getModule()), extension, envVars); |
| } |
| |
| private final class RegularRunnableExtension implements RunnableExtension { |
| private final BazelModuleContext bazelModuleContext; |
| private final ModuleExtension extension; |
| private final ImmutableMap<String, String> envVars; |
| |
| RegularRunnableExtension( |
| BazelModuleContext bazelModuleContext, |
| ModuleExtension extension, |
| ImmutableMap<String, String> envVars) { |
| this.bazelModuleContext = bazelModuleContext; |
| this.extension = extension; |
| this.envVars = envVars; |
| } |
| |
| @Override |
| public ModuleExtensionEvalFactors getEvalFactors() { |
| return ModuleExtensionEvalFactors.create( |
| extension.getOsDependent() ? OS.getCurrent().toString() : "", |
| extension.getArchDependent() ? OS_ARCH.value() : ""); |
| } |
| |
| @Override |
| public ImmutableMap<String, String> getEnvVars() { |
| return envVars; |
| } |
| |
| @Override |
| public byte[] getBzlTransitiveDigest() { |
| return bazelModuleContext.bzlTransitiveDigest(); |
| } |
| |
| @Nullable |
| @Override |
| public RunModuleExtensionResult run( |
| Environment env, |
| SingleExtensionUsagesValue usagesValue, |
| StarlarkSemantics starlarkSemantics, |
| ModuleExtensionId extensionId) |
| throws InterruptedException, SingleExtensionEvalFunctionException { |
| ModuleExtensionEvalStarlarkThreadContext threadContext = |
| new ModuleExtensionEvalStarlarkThreadContext( |
| usagesValue.getExtensionUniqueName() + "~", |
| extensionId.getBzlFileLabel().getPackageIdentifier(), |
| bazelModuleContext.repoMapping(), |
| directories, |
| env.getListener()); |
| ModuleExtensionContext moduleContext; |
| Optional<ModuleExtensionMetadata> moduleExtensionMetadata; |
| var repoMappingRecorder = new Label.RepoMappingRecorder(); |
| 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); |
| threadContext.storeInThread(thread); |
| // This is used by the `Label()` constructor in Starlark, to record any attempts to resolve |
| // apparent repo names to canonical repo names. See #20721 for why this is necessary. |
| thread.setThreadLocal(Label.RepoMappingRecorder.class, repoMappingRecorder); |
| 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, |
| repoMappingRecorder.recordedEntries()); |
| } |
| |
| private ModuleExtensionContext createContext( |
| Environment env, |
| SingleExtensionUsagesValue usagesValue, |
| StarlarkSemantics starlarkSemantics, |
| ModuleExtensionId extensionId) |
| 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(); |
| |
| abstract ImmutableTable<RepositoryName, String, RepositoryName> getRecordedRepoMappingEntries(); |
| |
| static RunModuleExtensionResult create( |
| ImmutableMap<Label, String> accumulatedFileDigests, |
| ImmutableMap<String, RepoSpec> generatedRepoSpecs, |
| Optional<ModuleExtensionMetadata> moduleExtensionMetadata, |
| ImmutableTable<RepositoryName, String, RepositoryName> recordedRepoMappingEntries) { |
| return new AutoValue_SingleExtensionEvalFunction_RunModuleExtensionResult( |
| accumulatedFileDigests, |
| generatedRepoSpecs, |
| moduleExtensionMetadata, |
| recordedRepoMappingEntries); |
| } |
| } |
| } |