| // Copyright 2023 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.modcommand; |
| |
| import static com.google.common.collect.ImmutableMap.toImmutableMap; |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.base.Ascii; |
| import com.google.common.collect.ImmutableBiMap; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule; |
| import com.google.devtools.build.lib.bazel.bzlmod.ModuleKey; |
| import com.google.devtools.build.lib.bazel.bzlmod.Version; |
| import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException; |
| 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.server.FailureDetails.ModCommand.Code; |
| import com.google.devtools.common.options.Converter; |
| import com.google.devtools.common.options.Converters.CommaSeparatedNonEmptyOptionListConverter; |
| import com.google.devtools.common.options.OptionsParsingException; |
| import java.util.Optional; |
| import net.starlark.java.eval.EvalException; |
| |
| /** |
| * Represents a reference to one or more modules in the external dependency graph, used for |
| * modquery. This is parsed from a command-line argument (either as the value of a flag, or just as |
| * a bare argument), and can take one of various forms (see implementations). |
| */ |
| public interface ModuleArg { |
| |
| /** Resolves this module argument to a set of module keys. */ |
| ImmutableSet<ModuleKey> resolveToModuleKeys( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| ImmutableBiMap<String, ModuleKey> baseModuleDeps, |
| ImmutableBiMap<String, ModuleKey> baseModuleUnusedDeps, |
| boolean includeUnused, |
| boolean warnUnused) |
| throws InvalidArgumentException; |
| |
| /** Resolves this module argument to a set of repo names. */ |
| ImmutableMap<String, RepositoryName> resolveToRepoNames( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| RepositoryMapping mapping) |
| throws InvalidArgumentException; |
| |
| /** |
| * Refers to a specific version of a module. Parsed from {@code <module>@<version>}. {@code |
| * <version>} can be the special string {@code _} to signify the empty version (for non-registry |
| * overrides). |
| */ |
| @AutoValue |
| abstract class SpecificVersionOfModule implements ModuleArg { |
| static SpecificVersionOfModule create(ModuleKey key) { |
| return new AutoValue_ModuleArg_SpecificVersionOfModule(key); |
| } |
| |
| public abstract ModuleKey moduleKey(); |
| |
| private void throwIfNonexistent( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| boolean includeUnused, |
| boolean warnUnused) |
| throws InvalidArgumentException { |
| AugmentedModule mod = depGraph.get(moduleKey()); |
| if (mod != null && !includeUnused && warnUnused && !mod.isUsed()) { |
| // Warn the user when unused modules are allowed and the specified version exists, but the |
| // --include_unused flag was not set. |
| throw new InvalidArgumentException( |
| String.format( |
| "Module version %s is unused as a result of module resolution. Use the" |
| + " --include_unused flag to include it.", |
| moduleKey()), |
| Code.INVALID_ARGUMENTS); |
| } |
| if (mod == null || (!includeUnused && !mod.isUsed())) { |
| ImmutableSet<ModuleKey> existingKeys = modulesIndex.get(moduleKey().getName()); |
| if (existingKeys == null) { |
| throw new InvalidArgumentException( |
| String.format( |
| "Module %s does not exist in the dependency graph.", moduleKey().getName()), |
| Code.INVALID_ARGUMENTS); |
| } |
| // If --include_unused is not true, unused modules will be considered non-existent and an |
| // error will be thrown. |
| ImmutableSet<ModuleKey> filteredKeys = |
| existingKeys.stream() |
| .filter(k -> includeUnused || depGraph.get(k).isUsed()) |
| .collect(toImmutableSet()); |
| throw new InvalidArgumentException( |
| String.format( |
| "Module version %s does not exist, available versions: %s.", |
| moduleKey(), filteredKeys), |
| Code.INVALID_ARGUMENTS); |
| } |
| } |
| |
| @Override |
| public final ImmutableSet<ModuleKey> resolveToModuleKeys( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| ImmutableBiMap<String, ModuleKey> baseModuleDeps, |
| ImmutableBiMap<String, ModuleKey> baseModuleUnusedDeps, |
| boolean includeUnused, |
| boolean warnUnused) |
| throws InvalidArgumentException { |
| throwIfNonexistent(modulesIndex, depGraph, includeUnused, warnUnused); |
| return ImmutableSet.of(moduleKey()); |
| } |
| |
| @Override |
| public ImmutableMap<String, RepositoryName> resolveToRepoNames( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| RepositoryMapping mapping) |
| throws InvalidArgumentException { |
| throwIfNonexistent( |
| modulesIndex, depGraph, /* includeUnused= */ false, /* warnUnused= */ false); |
| return ImmutableMap.of(moduleKey().toString(), moduleKey().getCanonicalRepoName()); |
| } |
| |
| @Override |
| public final String toString() { |
| return moduleKey().toString(); |
| } |
| } |
| |
| /** Refers to all versions of a module. Parsed from {@code <module>}. */ |
| @AutoValue |
| abstract class AllVersionsOfModule implements ModuleArg { |
| static AllVersionsOfModule create(String moduleName) { |
| return new AutoValue_ModuleArg_AllVersionsOfModule(moduleName); |
| } |
| |
| public abstract String moduleName(); |
| |
| private ImmutableSet<ModuleKey> resolveInternal( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| boolean includeUnused, |
| boolean warnUnused) |
| throws InvalidArgumentException { |
| ImmutableSet<ModuleKey> existingKeys = modulesIndex.get(moduleName()); |
| if (existingKeys == null) { |
| throw new InvalidArgumentException( |
| String.format("Module %s does not exist in the dependency graph.", moduleName()), |
| Code.INVALID_ARGUMENTS); |
| } |
| ImmutableSet<ModuleKey> filteredKeys = |
| existingKeys.stream() |
| .filter(k -> includeUnused || depGraph.get(k).isUsed()) |
| .collect(toImmutableSet()); |
| if (filteredKeys.isEmpty()) { |
| if (warnUnused) { |
| throw new InvalidArgumentException( |
| String.format( |
| "Module %s is unused as a result of module resolution. Use the --include_unused" |
| + " flag to include it.", |
| moduleName()), |
| Code.INVALID_ARGUMENTS); |
| } |
| throw new InvalidArgumentException( |
| String.format("Module %s does not exist in the dependency graph.", moduleName()), |
| Code.INVALID_ARGUMENTS); |
| } |
| return filteredKeys; |
| } |
| |
| @Override |
| public ImmutableSet<ModuleKey> resolveToModuleKeys( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| ImmutableBiMap<String, ModuleKey> baseModuleDeps, |
| ImmutableBiMap<String, ModuleKey> baseModuleUnusedDeps, |
| boolean includeUnused, |
| boolean warnUnused) |
| throws InvalidArgumentException { |
| return resolveInternal(modulesIndex, depGraph, includeUnused, warnUnused); |
| } |
| |
| @Override |
| public ImmutableMap<String, RepositoryName> resolveToRepoNames( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| RepositoryMapping mapping) |
| throws InvalidArgumentException { |
| return resolveInternal( |
| modulesIndex, depGraph, /* includeUnused= */ false, /* warnUnused= */ false) |
| .stream() |
| .collect(toImmutableMap(ModuleKey::toString, ModuleKey::getCanonicalRepoName)); |
| } |
| |
| @Override |
| public final String toString() { |
| return moduleName(); |
| } |
| } |
| |
| /** |
| * Refers to a module with the given apparent repo name, in the context of {@code --base_module} |
| * (or when parsing that flag itself, in the context of the root module). Parsed from |
| * {@code @<name>}. |
| */ |
| @AutoValue |
| abstract class ApparentRepoName implements ModuleArg { |
| static ApparentRepoName create(String name) { |
| return new AutoValue_ModuleArg_ApparentRepoName(name); |
| } |
| |
| public abstract String name(); |
| |
| @Override |
| public ImmutableSet<ModuleKey> resolveToModuleKeys( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| ImmutableBiMap<String, ModuleKey> baseModuleDeps, |
| ImmutableBiMap<String, ModuleKey> baseModuleUnusedDeps, |
| boolean includeUnused, |
| boolean warnUnused) |
| throws InvalidArgumentException { |
| ImmutableSet.Builder<ModuleKey> builder = new ImmutableSet.Builder<>(); |
| ModuleKey dep = baseModuleDeps.get(name()); |
| if (dep != null) { |
| builder.add(dep); |
| } |
| ModuleKey unusedDep = baseModuleUnusedDeps.get(name()); |
| if (includeUnused && unusedDep != null) { |
| builder.add(unusedDep); |
| } |
| var result = builder.build(); |
| if (result.isEmpty()) { |
| throw new InvalidArgumentException( |
| String.format( |
| "No module with the apparent repo name @%s exists in the dependency graph", name()), |
| Code.INVALID_ARGUMENTS); |
| } |
| return result; |
| } |
| |
| @Override |
| public ImmutableMap<String, RepositoryName> resolveToRepoNames( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| RepositoryMapping mapping) |
| throws InvalidArgumentException { |
| RepositoryName repoName = mapping.get(name()); |
| if (!repoName.isVisible()) { |
| throw new InvalidArgumentException( |
| String.format( |
| "No repo visible as %s from @%s", name(), repoName.getOwnerRepoDisplayString()), |
| Code.INVALID_ARGUMENTS); |
| } |
| return ImmutableMap.of(toString(), repoName); |
| } |
| |
| @Override |
| public final String toString() { |
| return "@" + name(); |
| } |
| } |
| |
| /** Refers to a module with the given canonical repo name. Parsed from {@code @@<name>}. */ |
| @AutoValue |
| abstract class CanonicalRepoName implements ModuleArg { |
| static CanonicalRepoName create(RepositoryName repoName) { |
| return new AutoValue_ModuleArg_CanonicalRepoName(repoName); |
| } |
| |
| public abstract RepositoryName repoName(); |
| |
| @Override |
| public ImmutableSet<ModuleKey> resolveToModuleKeys( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| ImmutableBiMap<String, ModuleKey> baseModuleDeps, |
| ImmutableBiMap<String, ModuleKey> baseModuleUnusedDeps, |
| boolean includeUnused, |
| boolean warnUnused) |
| throws InvalidArgumentException { |
| Optional<AugmentedModule> mod = |
| depGraph.values().stream() |
| .filter(m -> m.getKey().getCanonicalRepoName().equals(repoName())) |
| .findAny(); |
| if (mod.isPresent() && !includeUnused && warnUnused && !mod.get().isUsed()) { |
| // Warn the user when unused modules are allowed and the specified version exists, but the |
| // --include_unused flag was not set. |
| throw new InvalidArgumentException( |
| String.format( |
| "Module version %s is unused as a result of module resolution. Use the" |
| + " --include_unused flag to include it.", |
| mod.get().getKey()), |
| Code.INVALID_ARGUMENTS); |
| } |
| if (mod.isEmpty() || (!includeUnused && !mod.get().isUsed())) { |
| // If --include_unused is not true, unused modules will be considered non-existent and an |
| // error will be thrown. |
| throw new InvalidArgumentException( |
| String.format( |
| "No module with the canonical repo name @@%s exists in the dependency graph", |
| repoName().getName()), |
| Code.INVALID_ARGUMENTS); |
| } |
| return ImmutableSet.of(mod.get().getKey()); |
| } |
| |
| @Override |
| public ImmutableMap<String, RepositoryName> resolveToRepoNames( |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| RepositoryMapping mapping) |
| throws InvalidArgumentException { |
| if (depGraph.values().stream() |
| .filter(m -> m.getKey().getCanonicalRepoName().equals(repoName()) && m.isUsed()) |
| .findAny() |
| .isEmpty()) { |
| throw new InvalidArgumentException( |
| String.format( |
| "No module with the canonical repo name @@%s exists in the dependency graph", |
| repoName().getName()), |
| Code.INVALID_ARGUMENTS); |
| } |
| return ImmutableMap.of(toString(), repoName()); |
| } |
| |
| @Override |
| public final String toString() { |
| return "@@" + repoName().getName(); |
| } |
| } |
| |
| /** Converter for {@link ModuleArg}. */ |
| final class ModuleArgConverter extends Converter.Contextless<ModuleArg> { |
| public static final ModuleArgConverter INSTANCE = new ModuleArgConverter(); |
| |
| @Override |
| public ModuleArg convert(String input) throws OptionsParsingException { |
| if (Ascii.equalsIgnoreCase(input, "<root>")) { |
| return SpecificVersionOfModule.create(ModuleKey.ROOT); |
| } |
| if (input.startsWith("@@")) { |
| try { |
| return CanonicalRepoName.create(RepositoryName.create(input.substring(2))); |
| } catch (LabelSyntaxException e) { |
| throw new OptionsParsingException("invalid argument '" + input + "': " + e.getMessage()); |
| } |
| } |
| if (input.startsWith("@")) { |
| String apparentRepoName = input.substring(1); |
| try { |
| RepositoryName.validateUserProvidedRepoName(apparentRepoName); |
| } catch (EvalException e) { |
| throw new OptionsParsingException("invalid argument '" + input + "': " + e.getMessage()); |
| } |
| return ApparentRepoName.create(apparentRepoName); |
| } |
| int atIdx = input.indexOf('@'); |
| if (atIdx >= 0) { |
| String moduleName = input.substring(0, atIdx); |
| String versionStr = input.substring(atIdx + 1); |
| if (versionStr.isEmpty()) { |
| throw new OptionsParsingException( |
| "invalid argument '" + input + "': use _ for the empty version"); |
| } |
| try { |
| Version version = versionStr.equals("_") ? Version.EMPTY : Version.parse(versionStr); |
| return SpecificVersionOfModule.create(ModuleKey.create(moduleName, version)); |
| } catch (ParseException e) { |
| throw new OptionsParsingException("invalid argument '" + input + "': " + e.getMessage()); |
| } |
| } |
| return AllVersionsOfModule.create(input); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "\"<root>\" for the root module; <module>@<version> for a specific version of a" |
| + " module; <module> for all versions of a module; @<name> for a repo with the" |
| + " given apparent name; or @@<name> for a repo with the given canonical name"; |
| } |
| } |
| |
| /** Converter for a comma-separated list of {@link ModuleArg}s. */ |
| class CommaSeparatedModuleArgListConverter |
| extends Converter.Contextless<ImmutableList<ModuleArg>> { |
| |
| @Override |
| public ImmutableList<ModuleArg> convert(String input) throws OptionsParsingException { |
| ImmutableList<String> args = new CommaSeparatedNonEmptyOptionListConverter().convert(input); |
| ImmutableList.Builder<ModuleArg> moduleArgs = new ImmutableList.Builder<>(); |
| for (String arg : args) { |
| moduleArgs.add(ModuleArgConverter.INSTANCE.convert(arg)); |
| } |
| return moduleArgs.build(); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "a comma-separated list of <module>s"; |
| } |
| } |
| } |