blob: 29f1a4162199176d14facea9e9ba230e8cbb8640 [file] [log] [blame]
// Copyright 2022 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.Strings.nullToEmpty;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableTable;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue;
import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.packages.LabelConverter;
import com.google.devtools.build.lib.server.FailureDetails.ExternalDeps.Code;
import com.google.devtools.build.lib.skyframe.ClientEnvironmentFunction;
import com.google.devtools.build.lib.skyframe.ClientEnvironmentValue;
import com.google.devtools.build.lib.vfs.PathFragment;
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.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;
/**
* This function runs Bazel module resolution, extracts the dependency graph from it and creates a
* value containing all Bazel modules, along with a few lookup maps that help with further usage. By
* this stage, module extensions are not evaluated yet.
*/
public class BazelDepGraphFunction implements SkyFunction {
public BazelDepGraphFunction() {}
@Override
@Nullable
public SkyValue compute(SkyKey skyKey, Environment env)
throws BazelDepGraphFunctionException, InterruptedException {
RootModuleFileValue root =
(RootModuleFileValue) env.getValue(ModuleFileValue.KEY_FOR_ROOT_MODULE);
if (root == null) {
return null;
}
LockfileMode lockfileMode = BazelLockFileFunction.LOCKFILE_MODE.get(env);
ImmutableMap<String, String> localOverrideHashes = null;
ImmutableMap<ModuleKey, Module> depGraph = null;
BzlmodFlagsAndEnvVars flags = null;
BazelLockFileValue lockfile = null;
// If the module has not changed (has the same contents and flags as the lockfile),
// read the dependency graph from the lock file, else run resolution and update lockfile
if (!lockfileMode.equals(LockfileMode.OFF)) {
lockfile = (BazelLockFileValue) env.getValue(BazelLockFileValue.KEY);
if (lockfile == null) {
return null;
}
flags = getFlagsAndEnvVars(env);
if (flags == null) { // unable to read environment variables
return null;
}
localOverrideHashes = getLocalOverridesHashes(root.getOverrides(), env);
if (localOverrideHashes == null) { // still reading an override "module"
return null;
}
if (root.getModuleFileHash().equals(lockfile.getModuleFileHash())
&& flags.equals(lockfile.getFlags())
&& localOverrideHashes.equals(lockfile.getLocalOverrideHashes())) {
depGraph = lockfile.getModuleDepGraph();
} else if (lockfileMode.equals(LockfileMode.ERROR)) {
ImmutableList<String> diffLockfile =
lockfile.getModuleAndFlagsDiff(root.getModuleFileHash(), localOverrideHashes, flags);
throw new BazelDepGraphFunctionException(
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.",
String.join(", ", diffLockfile)),
Transience.PERSISTENT);
}
}
if (depGraph == null) {
BazelModuleResolutionValue selectionResult =
(BazelModuleResolutionValue) env.getValue(BazelModuleResolutionValue.KEY);
if (env.valuesMissing()) {
return null;
}
depGraph = selectionResult.getResolvedDepGraph();
}
ImmutableMap<RepositoryName, ModuleKey> canonicalRepoNameLookup =
depGraph.keySet().stream()
.collect(toImmutableMap(ModuleKey::getCanonicalRepoName, key -> key));
ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsagesById;
try {
extensionUsagesById = getExtensionUsagesById(depGraph);
} catch (ExternalDepsException e) {
throw new BazelDepGraphFunctionException(e, Transience.PERSISTENT);
}
ImmutableBiMap<String, ModuleExtensionId> extensionUniqueNames =
calculateUniqueNameForUsedExtensionId(extensionUsagesById);
if (lockfileMode.equals(LockfileMode.UPDATE)) {
BazelLockFileValue resolutionOnlyLockfile =
BazelLockFileValue.builder()
.setModuleFileHash(root.getModuleFileHash())
.setFlags(flags)
.setLocalOverrideHashes(localOverrideHashes)
.setModuleDepGraph(depGraph)
.build();
env.getListener()
.post(
BazelModuleResolutionEvent.create(
lockfile, resolutionOnlyLockfile, extensionUsagesById));
}
return BazelDepGraphValue.create(
depGraph,
canonicalRepoNameLookup,
depGraph.values().stream().map(AbridgedModule::from).collect(toImmutableList()),
extensionUsagesById,
extensionUniqueNames.inverse());
}
@Nullable
@VisibleForTesting
static ImmutableMap<String, String> getLocalOverridesHashes(
Map<String, ModuleOverride> overrides, Environment env) throws InterruptedException {
ImmutableMap.Builder<String, String> localOverrideHashes = new ImmutableMap.Builder<>();
for (Entry<String, ModuleOverride> entry : overrides.entrySet()) {
if (entry.getValue() instanceof LocalPathOverride) {
ModuleFileValue moduleValue =
(ModuleFileValue)
env.getValue(
ModuleFileValue.key(
ModuleKey.create(entry.getKey(), Version.EMPTY), entry.getValue()));
if (moduleValue == null) {
return null;
}
localOverrideHashes.put(entry.getKey(), moduleValue.getModuleFileHash());
}
}
return localOverrideHashes.buildOrThrow();
}
@VisibleForTesting
@Nullable
static BzlmodFlagsAndEnvVars getFlagsAndEnvVars(Environment env) throws InterruptedException {
ClientEnvironmentValue allowedYankedVersionsFromEnv =
(ClientEnvironmentValue)
env.getValue(
ClientEnvironmentFunction.key(
YankedVersionsUtil.BZLMOD_ALLOWED_YANKED_VERSIONS_ENV));
if (allowedYankedVersionsFromEnv == null) {
return null;
}
ImmutableList<String> registries = ImmutableList.copyOf(ModuleFileFunction.REGISTRIES.get(env));
ImmutableMap<String, String> moduleOverrides =
ModuleFileFunction.MODULE_OVERRIDES.get(env).entrySet().stream()
.collect(
toImmutableMap(Entry::getKey, e -> ((LocalPathOverride) e.getValue()).getPath()));
ImmutableList<String> yankedVersions =
ImmutableList.copyOf(YankedVersionsUtil.ALLOWED_YANKED_VERSIONS.get(env));
Boolean ignoreDevDeps = ModuleFileFunction.IGNORE_DEV_DEPS.get(env);
String compatabilityMode =
BazelModuleResolutionFunction.BAZEL_COMPATIBILITY_MODE.get(env).name();
String directDepsMode = BazelModuleResolutionFunction.CHECK_DIRECT_DEPENDENCIES.get(env).name();
String envYanked = allowedYankedVersionsFromEnv.getValue();
return BzlmodFlagsAndEnvVars.create(
registries,
moduleOverrides,
yankedVersions,
nullToEmpty(envYanked),
ignoreDevDeps,
directDepsMode,
compatabilityMode);
}
/**
* For each extension usage, we resolve (i.e. canonicalize) its bzl file label. Then we can group
* all usages by the label + name (the ModuleExtensionId).
*/
public static ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage>
getExtensionUsagesById(ImmutableMap<ModuleKey, Module> depGraph)
throws ExternalDepsException {
ImmutableTable.Builder<ModuleExtensionId, ModuleKey, ModuleExtensionUsage>
extensionUsagesTableBuilder = ImmutableTable.builder();
for (Module module : depGraph.values()) {
LabelConverter labelConverter =
new LabelConverter(
PackageIdentifier.create(module.getCanonicalRepoName(), PathFragment.EMPTY_FRAGMENT),
module.getRepoMappingWithBazelDepsOnly());
for (ModuleExtensionUsage usage : module.getExtensionUsages()) {
ModuleExtensionId moduleExtensionId;
try {
moduleExtensionId =
ModuleExtensionId.create(
labelConverter.convert(usage.getExtensionBzlFile()),
usage.getExtensionName(),
usage.getIsolationKey());
} catch (LabelSyntaxException e) {
throw ExternalDepsException.withCauseAndMessage(
Code.BAD_MODULE,
e,
"invalid label for module extension found at %s",
usage.getLocation());
}
if (!moduleExtensionId.getBzlFileLabel().getRepository().isVisible()) {
throw ExternalDepsException.withMessage(
Code.BAD_MODULE,
"invalid label for module extension found at %s: no repo visible as '@%s' here",
usage.getLocation(),
moduleExtensionId.getBzlFileLabel().getRepository().getName());
}
extensionUsagesTableBuilder.put(moduleExtensionId, module.getKey(), usage);
}
}
return extensionUsagesTableBuilder.buildOrThrow();
}
private ImmutableBiMap<String, ModuleExtensionId> calculateUniqueNameForUsedExtensionId(
ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsagesById) {
// Calculate a unique name for each used extension id with the following property that is
// required for BzlmodRepoRuleFunction to unambiguously identify the extension that generates a
// given repo:
// After appending a single `~` to each such name, none of the resulting strings is a prefix of
// any other such string.
BiMap<String, ModuleExtensionId> extensionUniqueNames = HashBiMap.create();
for (ModuleExtensionId id : extensionUsagesById.rowKeySet()) {
int attempt = 1;
while (extensionUniqueNames.putIfAbsent(makeUniqueNameCandidate(id, attempt), id) != null) {
attempt++;
}
}
return ImmutableBiMap.copyOf(extensionUniqueNames);
}
private static String makeUniqueNameCandidate(ModuleExtensionId id, int attempt) {
// Ensure that the resulting extension name (and thus the repository names derived from it) do
// not start with a tilde.
RepositoryName repository = id.getBzlFileLabel().getRepository();
String nonEmptyRepoPart = repository.isMain() ? "_main" : repository.getName();
// When using a namespace, prefix the extension name with "_" to distinguish the prefix from
// those generated by non-namespaced extension usages. Extension names are identified by their
// Starlark identifier, which in the case of an exported symbol cannot start with "_".
Preconditions.checkArgument(attempt >= 1);
String extensionNameDisambiguator = attempt == 1 ? "" : String.valueOf(attempt);
return id.getIsolationKey()
.map(
namespace ->
String.format(
"%s~_%s%s~%s~%s~%s",
nonEmptyRepoPart,
id.getExtensionName(),
extensionNameDisambiguator,
namespace.getModule().getName(),
namespace.getModule().getVersion(),
namespace.getUsageExportedName()))
.orElse(nonEmptyRepoPart + "~" + id.getExtensionName() + extensionNameDisambiguator);
}
static class BazelDepGraphFunctionException extends SkyFunctionException {
BazelDepGraphFunctionException(ExternalDepsException e, Transience transience) {
super(e, transience);
}
}
}