| // 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.commands; |
| |
| import static com.google.common.collect.ImmutableMap.toImmutableMap; |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| import static java.nio.charset.StandardCharsets.US_ASCII; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| 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; |
| import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule; |
| import com.google.devtools.build.lib.bazel.bzlmod.BzlmodRepoRuleValue; |
| 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.commands.ModqueryOptions.Charset; |
| import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.QueryType; |
| import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.QueryTypeConverter; |
| import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.TargetModule; |
| import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.TargetModuleListConverter; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.pkgcache.PackageOptions; |
| import com.google.devtools.build.lib.runtime.BlazeCommand; |
| import com.google.devtools.build.lib.runtime.BlazeCommandResult; |
| import com.google.devtools.build.lib.runtime.Command; |
| import com.google.devtools.build.lib.runtime.CommandEnvironment; |
| import com.google.devtools.build.lib.runtime.KeepGoingOption; |
| import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption; |
| import com.google.devtools.build.lib.server.FailureDetails; |
| import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; |
| import com.google.devtools.build.lib.server.FailureDetails.ModqueryCommand.Code; |
| import com.google.devtools.build.lib.skyframe.SkyframeExecutor; |
| import com.google.devtools.build.lib.util.AbruptExitException; |
| import com.google.devtools.build.lib.util.DetailedExitCode; |
| import com.google.devtools.build.lib.util.InterruptedFailureDetails; |
| import com.google.devtools.build.skyframe.EvaluationContext; |
| import com.google.devtools.build.skyframe.EvaluationResult; |
| import com.google.devtools.build.skyframe.SkyKey; |
| import com.google.devtools.build.skyframe.SkyValue; |
| import com.google.devtools.common.options.OptionsParsingException; |
| import com.google.devtools.common.options.OptionsParsingResult; |
| import java.io.OutputStreamWriter; |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** Queries the Bzlmod external dependency graph. */ |
| @Command( |
| name = ModqueryCommand.NAME, |
| // TODO(andreisolo): figure out which extra options are really needed |
| options = { |
| ModqueryOptions.class, |
| // Don't know what these do but were used in fetch |
| PackageOptions.class, |
| KeepGoingOption.class, |
| LoadingPhaseThreadsOption.class |
| }, |
| help = "resource:modquery.txt", |
| shortDescription = "Queries the Bzlmod external dependency graph", |
| allowResidue = true) |
| public final class ModqueryCommand implements BlazeCommand { |
| |
| public static final String NAME = "modquery"; |
| |
| @Override |
| public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) { |
| BazelModuleInspectorValue moduleInspector; |
| |
| SkyframeExecutor skyframeExecutor = env.getSkyframeExecutor(); |
| LoadingPhaseThreadsOption threadsOption = options.getOptions(LoadingPhaseThreadsOption.class); |
| |
| EvaluationContext evaluationContext = |
| EvaluationContext.newBuilder() |
| .setNumThreads(threadsOption.threads) |
| .setEventHandler(env.getReporter()) |
| .build(); |
| |
| try { |
| env.syncPackageLoading(options); |
| |
| EvaluationResult<SkyValue> evaluationResult = |
| skyframeExecutor.prepareAndGet( |
| ImmutableSet.of(BazelModuleInspectorValue.KEY), evaluationContext); |
| |
| if (evaluationResult.hasError()) { |
| Exception e = evaluationResult.getError().getException(); |
| String message = "Unexpected error during repository rule evaluation."; |
| if (e != null) { |
| message = e.getMessage(); |
| } |
| return reportAndCreateFailureResult(env, message, Code.INVALID_ARGUMENTS); |
| } |
| |
| moduleInspector = |
| (BazelModuleInspectorValue) evaluationResult.get(BazelModuleInspectorValue.KEY); |
| |
| } catch (InterruptedException e) { |
| String errorMessage = "Modquery interrupted: " + e.getMessage(); |
| env.getReporter().handle(Event.error(errorMessage)); |
| return BlazeCommandResult.detailedExitCode( |
| InterruptedFailureDetails.detailedExitCode(errorMessage)); |
| } catch (AbruptExitException e) { |
| env.getReporter().handle(Event.error(null, "Unknown error: " + e.getMessage())); |
| return BlazeCommandResult.detailedExitCode(e.getDetailedExitCode()); |
| } |
| |
| ModqueryOptions modqueryOptions = options.getOptions(ModqueryOptions.class); |
| Preconditions.checkArgument(modqueryOptions != null); |
| |
| if (options.getResidue().isEmpty()) { |
| String errorMessage = |
| String.format("No query type specified, choose one from : %s.", QueryType.printValues()); |
| return reportAndCreateFailureResult(env, errorMessage, Code.MODQUERY_COMMAND_UNKNOWN); |
| } |
| |
| // The first keyword in the residue must be the QueryType, and then comes a list of "arguments". |
| String queryInput = options.getResidue().get(0); |
| QueryType query; |
| try { |
| query = new QueryTypeConverter().convert(queryInput); |
| } catch (OptionsParsingException e) { |
| String errorMessage = |
| String.format("Invalid query type, choose one from : %s.", QueryType.printValues()); |
| return reportAndCreateFailureResult(env, errorMessage, Code.MODQUERY_COMMAND_UNKNOWN); |
| } |
| List<String> args = options.getResidue().subList(1, options.getResidue().size()); |
| |
| // Arguments are structured as a list of comma-separated target lists for generality, |
| // even though there can only be 0 or 1 args so far. |
| ImmutableList<ImmutableSet<ModuleKey>> argsKeysList; |
| AugmentedModule rootModule = moduleInspector.getDepGraph().get(ModuleKey.ROOT); |
| try { |
| argsKeysList = |
| parseTargetArgs( |
| query.getArgNumber(), |
| moduleInspector.getModulesIndex(), |
| args, |
| rootModule.getDeps(), |
| rootModule.getUnusedDeps(), |
| modqueryOptions.includeUnused); |
| } catch (InvalidArgumentException e) { |
| return reportAndCreateFailureResult(env, e.getMessage(), e.getCode()); |
| } catch (OptionsParsingException e) { |
| return reportAndCreateFailureResult(env, e.getMessage(), Code.INVALID_ARGUMENTS); |
| } |
| /* Extract and check the --from argument */ |
| ImmutableSet<ModuleKey> fromKeys; |
| if (modqueryOptions.modulesFrom == null) { |
| fromKeys = ImmutableSet.of(ModuleKey.ROOT); |
| } else { |
| try { |
| fromKeys = |
| targetListToModuleKeySet( |
| modqueryOptions.modulesFrom, |
| moduleInspector.getModulesIndex(), |
| rootModule.getDeps(), |
| rootModule.getUnusedDeps(), |
| modqueryOptions.includeUnused); |
| } catch (InvalidArgumentException e) { |
| return reportAndCreateFailureResult(env, e.getMessage(), e.getCode()); |
| } |
| } |
| |
| ImmutableMap<ModuleKey, BzlmodRepoRuleValue> repoRuleValues = null; |
| // If the query is a SHOW, also request the BzlmodRepoRuleValues from SkyFrame. |
| // Unused modules do not have a BzlmodRepoRuleValue, so they are filtered out. |
| if (query == QueryType.SHOW) { |
| try { |
| ImmutableSet<ModuleKey> keys = |
| argsKeysList.get(0).stream() |
| .filter( |
| k -> |
| ModqueryExecutor.filterUnused( |
| k, modqueryOptions.includeUnused, false, moduleInspector.getDepGraph())) |
| .collect(toImmutableSet()); |
| ImmutableSet<SkyKey> skyKeys = |
| keys.stream() |
| .map(k -> BzlmodRepoRuleValue.key(k.getCanonicalRepoName())) |
| .collect(toImmutableSet()); |
| EvaluationResult<SkyValue> result = |
| env.getSkyframeExecutor().prepareAndGet(skyKeys, evaluationContext); |
| repoRuleValues = |
| keys.stream() |
| .collect( |
| toImmutableMap( |
| k -> k, |
| k -> |
| (BzlmodRepoRuleValue) |
| result.get(BzlmodRepoRuleValue.key(k.getCanonicalRepoName())))); |
| } catch (InterruptedException e) { |
| String errorMessage = "Modquery interrupted: " + e.getMessage(); |
| env.getReporter().handle(Event.error(errorMessage)); |
| return BlazeCommandResult.detailedExitCode( |
| InterruptedFailureDetails.detailedExitCode(errorMessage)); |
| } |
| } |
| |
| // Workaround to allow different default value for DEPS and EXPLAIN, and also use |
| // Integer.MAX_VALUE instead of the exact number string. |
| if (modqueryOptions.depth < 1) { |
| if (query == QueryType.EXPLAIN || query == QueryType.DEPS) { |
| modqueryOptions.depth = 1; |
| } else { |
| modqueryOptions.depth = Integer.MAX_VALUE; |
| } |
| } |
| |
| ModqueryExecutor modqueryExecutor = |
| new ModqueryExecutor( |
| moduleInspector.getDepGraph(), |
| modqueryOptions, |
| new OutputStreamWriter( |
| env.getReporter().getOutErr().getOutputStream(), |
| modqueryOptions.charset == Charset.UTF8 ? UTF_8 : US_ASCII)); |
| |
| switch (query) { |
| case TREE: |
| modqueryExecutor.tree(fromKeys); |
| break; |
| case DEPS: |
| modqueryExecutor.tree(argsKeysList.get(0)); |
| break; |
| case PATH: |
| modqueryExecutor.path(fromKeys, argsKeysList.get(0)); |
| break; |
| case ALL_PATHS: |
| modqueryExecutor.allPaths(fromKeys, argsKeysList.get(0)); |
| break; |
| case EXPLAIN: |
| modqueryExecutor.allPaths(fromKeys, argsKeysList.get(0)); |
| break; |
| case SHOW: |
| Preconditions.checkArgument(repoRuleValues != null); |
| modqueryExecutor.show(repoRuleValues); |
| break; |
| } |
| |
| return BlazeCommandResult.success(); |
| } |
| |
| /** |
| * A general parser for an undefined number of arguments. The arguments are comma-separated lists |
| * of {@link TargetModule}s. Each target {@link TargetModule} can represent a {@code repo_name}, |
| * as defined in the {@code MODULE.bazel} file of the root project, a specific version of a |
| * module, or all present versions of a module. The root module can only be specified by the |
| * {@code root} keyword, which takes precedence over the other above (in case of modules named |
| * root). |
| */ |
| @VisibleForTesting |
| public static ImmutableList<ImmutableSet<ModuleKey>> parseTargetArgs( |
| int requiredArgNum, |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| List<String> args, |
| ImmutableBiMap<String, ModuleKey> rootDeps, |
| ImmutableBiMap<String, ModuleKey> rootUnusedDeps, |
| boolean includeUnused) |
| throws OptionsParsingException, InvalidArgumentException { |
| if (requiredArgNum != args.size()) { |
| throw new InvalidArgumentException( |
| String.format( |
| "Invalid number of arguments (provided %d, required %d).", |
| args.size(), requiredArgNum), |
| requiredArgNum > args.size() ? Code.MISSING_ARGUMENTS : Code.TOO_MANY_ARGUMENTS); |
| } |
| |
| TargetModuleListConverter converter = new TargetModuleListConverter(); |
| ImmutableList.Builder<ImmutableSet<ModuleKey>> argsKeysListBuilder = |
| new ImmutableList.Builder<>(); |
| |
| for (String arg : args) { |
| ImmutableList<TargetModule> targetList = converter.convert(arg); |
| ImmutableSet<ModuleKey> argModuleKeys = |
| targetListToModuleKeySet( |
| targetList, modulesIndex, rootDeps, rootUnusedDeps, includeUnused); |
| argsKeysListBuilder.add(argModuleKeys); |
| } |
| return argsKeysListBuilder.build(); |
| } |
| |
| /** Collects a list of {@link TargetModule} into a set of {@link ModuleKey}s. */ |
| private static ImmutableSet<ModuleKey> targetListToModuleKeySet( |
| ImmutableList<TargetModule> targetList, |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableBiMap<String, ModuleKey> rootDeps, |
| ImmutableBiMap<String, ModuleKey> rootUnusedDeps, |
| boolean includeUnused) |
| throws InvalidArgumentException { |
| ImmutableSet.Builder<ModuleKey> allTargetKeys = new ImmutableSet.Builder<>(); |
| for (TargetModule targetModule : targetList) { |
| allTargetKeys.addAll( |
| targetToModuleKeySet( |
| targetModule, modulesIndex, rootDeps, rootUnusedDeps, includeUnused)); |
| } |
| return allTargetKeys.build(); |
| } |
| |
| /** |
| * Helper to check the module (and version) of the given {@link TargetModule} exists and retrieve |
| * it, (or retrieve all present versions if it's semantic specifies it, i.e. when <code> |
| * Version == null</code>). |
| */ |
| private static ImmutableSet<ModuleKey> targetToModuleKeySet( |
| TargetModule target, |
| ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, |
| ImmutableBiMap<String, ModuleKey> rootDeps, |
| ImmutableBiMap<String, ModuleKey> rootUnusedDeps, |
| boolean includeUnused) |
| throws InvalidArgumentException { |
| if (target.getName().isEmpty() && Objects.equals(target.getVersion(), Version.EMPTY)) { |
| return ImmutableSet.of(ModuleKey.ROOT); |
| } |
| if (rootDeps.containsKey(target.getName())) { |
| if (includeUnused && rootUnusedDeps.containsKey(target.getName())) { |
| return ImmutableSet.of( |
| rootDeps.get(target.getName()), rootUnusedDeps.get(target.getName())); |
| } |
| return ImmutableSet.of(rootDeps.get(target.getName())); |
| } |
| ImmutableSet<ModuleKey> existingKeys = modulesIndex.get(target.getName()); |
| |
| if (existingKeys == null) { |
| throw new InvalidArgumentException( |
| String.format("Module %s does not exist in the dependency graph.", target.getName()), |
| Code.INVALID_ARGUMENTS); |
| } |
| |
| if (target.getVersion() == null) { |
| return existingKeys; |
| } |
| ModuleKey key = ModuleKey.create(target.getName(), target.getVersion()); |
| if (!existingKeys.contains(key)) { |
| throw new InvalidArgumentException( |
| String.format( |
| "Module version %s@%s does not exist, available versions: %s.", |
| target.getName(), key, existingKeys), |
| Code.INVALID_ARGUMENTS); |
| } |
| return ImmutableSet.of(key); |
| } |
| |
| private static BlazeCommandResult reportAndCreateFailureResult( |
| CommandEnvironment env, String message, Code detailedCode) { |
| if (message.charAt(message.length() - 1) != '.') { |
| message = message.concat("."); |
| } |
| String fullMessage = |
| String.format( |
| message.concat(" Type '%s help modquery' for syntax and help."), |
| env.getRuntime().getProductName()); |
| env.getReporter().handle(Event.error(fullMessage)); |
| return createFailureResult(fullMessage, detailedCode); |
| } |
| |
| private static BlazeCommandResult createFailureResult(String message, Code detailedCode) { |
| return BlazeCommandResult.detailedExitCode( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setModqueryCommand( |
| FailureDetails.ModqueryCommand.newBuilder().setCode(detailedCode).build()) |
| .setMessage(message) |
| .build())); |
| } |
| |
| /** |
| * Exception thrown when a user-input argument is invalid (wrong number of arguments or the |
| * specified modules do not exist). |
| */ |
| @VisibleForTesting |
| public static class InvalidArgumentException extends Exception { |
| private final Code code; |
| |
| private InvalidArgumentException(String message, Code code) { |
| super(message); |
| this.code = code; |
| } |
| |
| public Code getCode() { |
| return code; |
| } |
| } |
| } |