ModqueryCommand added - `ModqueryCommand` added for the external dependency inspection `modquery` bazel command with option parsing logic implemented. - `ModqueryOptions` added. - `ModqueryExecutor` empty skeleton added to separate query execution logic and print to the injected output stream. Dummy test implementations provided for `tree` and `deps` query types. https://github.com/bazelbuild/bazel/issues/15365 PiperOrigin-RevId: 454914018 Change-Id: Ic9cdd71748eafe63fbf67bb74c0a170a12f8647b
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/BUILD index 278b192..04c4235 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/BUILD +++ b/src/main/java/com/google/devtools/build/lib/bazel/BUILD
@@ -27,6 +27,7 @@ "//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories", "//src/main/java/com/google/devtools/build/lib/analysis:config/build_configuration", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common", + "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:inspection", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:registry", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:resolution", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:resolution_impl",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java index e87754a..8bf9411 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -25,6 +25,7 @@ import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.RuleDefinition; import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue; +import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorFunction; import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleResolutionFunction; import com.google.devtools.build.lib.bazel.bzlmod.LocalPathOverride; import com.google.devtools.build.lib.bazel.bzlmod.ModuleExtensionResolutionFunction; @@ -36,6 +37,7 @@ import com.google.devtools.build.lib.bazel.bzlmod.SingleExtensionEvalFunction; import com.google.devtools.build.lib.bazel.bzlmod.SingleExtensionUsagesFunction; import com.google.devtools.build.lib.bazel.commands.FetchCommand; +import com.google.devtools.build.lib.bazel.commands.ModqueryCommand; import com.google.devtools.build.lib.bazel.commands.SyncCommand; import com.google.devtools.build.lib.bazel.repository.LocalConfigPlatformFunction; import com.google.devtools.build.lib.bazel.repository.LocalConfigPlatformRule; @@ -200,6 +202,7 @@ @Override public void serverInit(OptionsParsingResult startupOptions, ServerBuilder builder) { builder.addCommands(new FetchCommand()); + builder.addCommands(new ModqueryCommand()); builder.addCommands(new SyncCommand()); builder.addInfoItems(new RepositoryCacheInfoItem(repositoryCache)); } @@ -256,6 +259,7 @@ SkyFunctions.MODULE_FILE, new ModuleFileFunction(registryFactory, directories.getWorkspace(), builtinModules)) .addSkyFunction(SkyFunctions.BAZEL_MODULE_RESOLUTION, new BazelModuleResolutionFunction()) + .addSkyFunction(SkyFunctions.BAZEL_MODULE_INSPECTION, new BazelModuleInspectorFunction()) .addSkyFunction(SkyFunctions.SINGLE_EXTENSION_EVAL, singleExtensionEvalFunction) .addSkyFunction(SkyFunctions.SINGLE_EXTENSION_USAGES, new SingleExtensionUsagesFunction()) .addSkyFunction(
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD index 897a1e1..a80c320 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
@@ -225,12 +225,14 @@ srcs = [ "BazelModuleInspectorFunction.java", "BazelModuleInspectorValue.java", + "ModqueryExecutor.java", ], deps = [ ":common", ":resolution", "//src/main/java/com/google/devtools/build/lib/skyframe:sky_functions", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec:serialization-constant", + "//src/main/java/com/google/devtools/build/lib/util/io", "//src/main/java/com/google/devtools/build/skyframe", "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects", "//third_party:auto_value",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModqueryExecutor.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModqueryExecutor.java new file mode 100644 index 0000000..278b748 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModqueryExecutor.java
@@ -0,0 +1,106 @@ +// 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 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.BazelModuleInspectorValue.AugmentedModule.ResolutionReason; +import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter; +import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter.Mode; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Executes inspection queries for {@link + * com.google.devtools.build.lib.bazel.commands.ModqueryCommand} and prints the resulted output. + */ +public class ModqueryExecutor { + private final ImmutableMap<ModuleKey, Module> resolvedDepGraph; + private final ImmutableMap<ModuleKey, AugmentedModule> depGraph; + private final ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex; + private final AnsiTerminalPrinter printer; + + public ModqueryExecutor( + ImmutableMap<ModuleKey, Module> resolvedDepGraph, + ImmutableMap<ModuleKey, AugmentedModule> depGraph, + ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex, + AnsiTerminalPrinter printer) { + this.resolvedDepGraph = resolvedDepGraph; + this.depGraph = depGraph; + this.modulesIndex = modulesIndex; + this.printer = printer; + } + + public void tree(ImmutableSet<ModuleKey> from) { + printer.printLn(Mode.WARNING + "DUMMY IMPLEMENTATION" + Mode.DEFAULT); + printer.printLn(""); + printer.printLn("All modules index:"); + printer.printLn(modulesIndex.toString()); + printer.printLn(""); + for (ModuleKey target : from) { + printer.printLn(Mode.INFO.toString() + target + Mode.DEFAULT); + Set<ModuleKey> seen = new HashSet<>(); + Deque<ModuleKey> toVisit = new ArrayDeque<>(); + toVisit.add(target); + seen.add(target); + while (!toVisit.isEmpty()) { + ModuleKey curr = toVisit.remove(); + AugmentedModule module = depGraph.get(curr); + for (Entry<ModuleKey, ResolutionReason> e : module.getDeps().entrySet()) { + ModuleKey child = e.getKey(); + if (!resolvedDepGraph.containsKey(child) || seen.contains(child)) { + continue; + } + seen.add(child); + toVisit.add(child); + printer.printLn(child + " " + e.getValue()); + } + } + printer.printLn(""); + } + } + + public void deps(ImmutableSet<ModuleKey> targets) { + printer.printLn(Mode.WARNING + "DUMMY IMPLEMENTATION" + Mode.DEFAULT); + printer.printLn(""); + for (ModuleKey target : targets) { + printer.printLn(Mode.INFO.toString() + target + Mode.DEFAULT); + for (Entry<ModuleKey, ResolutionReason> e : depGraph.get(target).getDeps().entrySet()) { + printer.printLn(e.getKey() + " " + e.getValue()); + } + printer.printLn(""); + } + } + + public void path(ImmutableSet<ModuleKey> from, ImmutableSet<ModuleKey> to) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + public void allPaths(ImmutableSet<ModuleKey> from, ImmutableSet<ModuleKey> to) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + public void explain(ImmutableSet<ModuleKey> targets) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + public void show(ImmutableSet<ModuleKey> targets) { + throw new UnsupportedOperationException("Not implemented yet"); + } +}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD index e04e817..e2c22f2 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD +++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/BUILD
@@ -15,6 +15,7 @@ srcs = glob(["*.java"]), resources = [ "fetch.txt", + "modquery.txt", "sync.txt", ], deps = [ @@ -25,6 +26,9 @@ "//src/main/java/com/google/devtools/build/lib/analysis:no_build_event", "//src/main/java/com/google/devtools/build/lib/analysis:no_build_request_finished_event", "//src/main/java/com/google/devtools/build/lib/bazel:resolved_event", + "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common", + "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:inspection", + "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:resolution", "//src/main/java/com/google/devtools/build/lib/bazel/repository", "//src/main/java/com/google/devtools/build/lib/bazel/repository/starlark", "//src/main/java/com/google/devtools/build/lib/cmdline", @@ -44,12 +48,15 @@ "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code", "//src/main/java/com/google/devtools/build/lib/util:exit_code", "//src/main/java/com/google/devtools/build/lib/util:interrupted_failure_details", + "//src/main/java/com/google/devtools/build/lib/util/io", "//src/main/java/com/google/devtools/build/lib/vfs", "//src/main/java/com/google/devtools/build/skyframe", "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects", "//src/main/java/com/google/devtools/common/options", "//src/main/java/net/starlark/java/eval", "//src/main/protobuf:failure_details_java_proto", + "//third_party:auto_value", "//third_party:guava", + "//third_party:jsr305", ], )
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommand.java b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommand.java new file mode 100644 index 0000000..c0d9438 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommand.java
@@ -0,0 +1,298 @@ +// 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 com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +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.BazelModuleResolutionValue; +import com.google.devtools.build.lib.bazel.bzlmod.ModqueryExecutor; +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.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.runtime.UiOptions; +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.lib.util.io.AnsiTerminalPrinter; +import com.google.devtools.build.skyframe.EvaluationContext; +import com.google.devtools.build.skyframe.EvaluationResult; +import com.google.devtools.build.skyframe.SkyValue; +import com.google.devtools.common.options.OptionsParsingException; +import com.google.devtools.common.options.OptionsParsingResult; +import java.util.List; +import java.util.Objects; + +/** Queries the Bzlmod external dependency graph. */ +@Command( + name = ModqueryCommand.NAME, + options = { + ModqueryOptions.class, + PackageOptions.class, + KeepGoingOption.class, + LoadingPhaseThreadsOption.class + }, + // TODO(andreisolo): figure out which extra options are really needed + 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) { + BazelModuleResolutionValue moduleResolution; + BazelModuleInspectorValue moduleInspector; + + try { + // Don't know exactly what it does, used in 'fetch' + env.syncPackageLoading(options); + + SkyframeExecutor skyframeExecutor = env.getSkyframeExecutor(); + LoadingPhaseThreadsOption threadsOption = options.getOptions(LoadingPhaseThreadsOption.class); + + EvaluationContext evaluationContext = + EvaluationContext.newBuilder() + .setNumThreads(threadsOption.threads) + .setEventHandler(env.getReporter()) + .build(); + + EvaluationResult<SkyValue> evaluationResult = + skyframeExecutor.prepareAndGet( + ImmutableSet.of(BazelModuleResolutionValue.KEY, 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); + } + + moduleResolution = + (BazelModuleResolutionValue) evaluationResult.get(BazelModuleResolutionValue.KEY); + + 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()); + } + + AnsiTerminalPrinter printer = + new AnsiTerminalPrinter( + env.getReporter().getOutErr().getOutputStream(), + options.getOptions(UiOptions.class).useColor()); + + ModqueryExecutor modqueryExecutor = + new ModqueryExecutor( + moduleResolution.getDepGraph(), + moduleInspector.getDepGraph(), + moduleInspector.getModulesIndex(), + printer); + + 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); + } + + 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()); + + ImmutableList<ImmutableSet<ModuleKey>> argsKeysList; + try { + argsKeysList = parseTargetArgs(args, query.getArgNumber(), moduleInspector.getModulesIndex()); + } 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; + try { + fromKeys = + targetListToModuleKeySet(modqueryOptions.modulesFrom, moduleInspector.getModulesIndex()); + } catch (InvalidArgumentException e) { + return reportAndCreateFailureResult(env, e.getMessage(), e.getCode()); + } + + switch (query) { + case TREE: + modqueryExecutor.tree(fromKeys); + break; + case DEPS: + modqueryExecutor.deps(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.explain(argsKeysList.get(0)); + break; + case SHOW: + modqueryExecutor.show(argsKeysList.get(0)); + break; + } + + return BlazeCommandResult.success(); + } + + @VisibleForTesting + public static ImmutableList<ImmutableSet<ModuleKey>> parseTargetArgs( + List<String> args, + int requiredArgNum, + ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex) + 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); + argsKeysListBuilder.add(argModuleKeys); + } + return argsKeysListBuilder.build(); + } + + private static ImmutableSet<ModuleKey> targetListToModuleKeySet( + ImmutableList<TargetModule> targetList, + ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex) + throws InvalidArgumentException { + ImmutableSet.Builder<ModuleKey> allTargetKeys = new ImmutableSet.Builder<>(); + for (TargetModule targetModule : targetList) { + allTargetKeys.addAll(targetToModuleKeySet(targetModule, modulesIndex)); + } + return allTargetKeys.build(); + } + + // Helper to check the module-version argument exists and retrieve its present version(s) + // (ModuleKey(s)) if not specified + private static ImmutableSet<ModuleKey> targetToModuleKeySet( + TargetModule target, ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex) + throws InvalidArgumentException { + if (target.getName().isEmpty() && Objects.equals(target.getVersion(), Version.EMPTY)) { + return ImmutableSet.of(ModuleKey.ROOT); + } + 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 */ + @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; + } + } +}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryOptions.java b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryOptions.java new file mode 100644 index 0000000..7019a9c6 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModqueryOptions.java
@@ -0,0 +1,283 @@ +// 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 java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Ascii; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.bazel.bzlmod.Version; +import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException; +import com.google.devtools.common.options.Converter; +import com.google.devtools.common.options.Converters.CommaSeparatedNonEmptyOptionListConverter; +import com.google.devtools.common.options.EnumConverter; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionDocumentationCategory; +import com.google.devtools.common.options.OptionEffectTag; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParsingException; +import java.util.List; +import javax.annotation.Nullable; + +/** Options for ModqueryCommand */ +public class ModqueryOptions extends OptionsBase { + + @Option( + name = "from", + defaultValue = "root", + converter = TargetModuleListConverter.class, + documentationCategory = OptionDocumentationCategory.MODQUERY, + effectTags = {OptionEffectTag.EXECUTION}, + help = + "The module(s) starting from which the dependency graph query will be displayed. Check" + + " each query’s description for the exact semantic. Defaults to root.\n") + public ImmutableList<TargetModule> modulesFrom; + + @Option( + name = "extra", + defaultValue = "false", + documentationCategory = OptionDocumentationCategory.MODQUERY, + effectTags = {OptionEffectTag.EXECUTION}, + help = + "The queries will also display the reason why modules were resolved to their current" + + " version (if changed). Defaults to true only for the explain query.") + public boolean extra; + + @Option( + name = "unused", + defaultValue = "false", + documentationCategory = OptionDocumentationCategory.MODQUERY, + effectTags = {OptionEffectTag.EXECUTION}, + help = + "The queries will also take into account and display the unused modules, which are not" + + " present in the module resolution graph after selection (due to the" + + " Minimal-Version Selection or override rules). This can have different effects for" + + " each of the query types i.e. include new paths in the all_paths command, or extra" + + " dependants in the explain command.\n") + public boolean unused; + + @Option( + name = "depth", + defaultValue = "2147483647", + documentationCategory = OptionDocumentationCategory.MODQUERY, + effectTags = {OptionEffectTag.EXECUTION}, + help = + "Maximum display depth of the dependency tree. A depth of 1 displays the direct" + + " dependencies, for example. For tree, path and all_paths it defaults to" + + " Integer.MAX_VALUE, while for explain it defaults to 1 (only displays direct deps" + + " of the root besides the leaves and their parents).\n") + public int depth; + + @Option( + name = "cycles", + defaultValue = "false", + documentationCategory = OptionDocumentationCategory.MODQUERY, + effectTags = {OptionEffectTag.EXECUTION}, + help = + "Points out dependency cycles inside the displayed tree, which are normally ignored by" + + " default.") + public boolean cycles; + + @Option( + name = "charset", + defaultValue = "utf8", + converter = CharsetConverter.class, + documentationCategory = OptionDocumentationCategory.MODQUERY, + effectTags = {OptionEffectTag.TERMINAL_OUTPUT}, + help = + "Chooses the character set to use for the tree. Only affects text output. Valid values" + + " are \"utf8\" or \"ascii\". Default is \"utf8\"") + public Charset charset; + + @Option( + name = "prefix", + defaultValue = "indent", + converter = PrefixConverter.class, + documentationCategory = OptionDocumentationCategory.MODQUERY, + effectTags = {OptionEffectTag.TERMINAL_OUTPUT}, + help = + "Sets how each line is displayed (only affects `text` output). The prefix value can be" + + " one of \"indent\", \"depth\" or \"none\". The default is \"indent\"") + public Prefix prefix; + + @Option( + name = "output", + defaultValue = "text", + converter = OutputFormatConverter.class, + documentationCategory = OptionDocumentationCategory.MODQUERY, + effectTags = {OptionEffectTag.TERMINAL_OUTPUT}, + help = + "The format in which the query results should be printed. Allowed values for query are: " + + "text, json, graph") + public OutputFormat outputFormat; + + enum QueryType { + DEPS(1), + TREE(0), + ALL_PATHS(1), + PATH(1), + EXPLAIN(1), + SHOW(1); + + /* Store the number of arguments that it accepts for easy pre-check */ + private final int argNumber; + + QueryType(int argNumber) { + this.argNumber = argNumber; + } + + @Override + public String toString() { + return Ascii.toLowerCase(this.name()); + } + + public int getArgNumber() { + return argNumber; + } + + public static String printValues() { + return "(" + stream(values()).map(QueryType::toString).collect(joining(", ")) + ")"; + } + } + + /** Converts a query type option string to a properly typed {@link QueryType} */ + public static class QueryTypeConverter extends EnumConverter<QueryType> { + public QueryTypeConverter() { + super(QueryType.class, "query type"); + } + } + + enum Charset { + UTF8, + ASCII + } + + /** Converts a charset option string to a properly typed {@link Charset} */ + public static class CharsetConverter extends EnumConverter<Charset> { + public CharsetConverter() { + super(Charset.class, "output charset"); + } + } + + enum Prefix { + INDENT, + DEPTH, + NONE + } + + /** Converts a prefix option string to a properly typed {@link Charset} */ + public static class PrefixConverter extends EnumConverter<Prefix> { + public PrefixConverter() { + super(Prefix.class, "output tree prefix"); + } + } + + enum OutputFormat { + TEXT, + JSON, + GRAPH + } + + /** Converts an output format option string to a properly typed {@link OutputFormat} */ + public static class OutputFormatConverter extends EnumConverter<OutputFormat> { + public OutputFormatConverter() { + super(OutputFormat.class, "output format"); + } + } + + /** Argument of a modquery converted from the form name@version or name. */ + @AutoValue + abstract static class TargetModule { + static TargetModule create(String name, Version version) { + return new AutoValue_ModqueryOptions_TargetModule(name, version); + } + + abstract String getName(); + + /** + * If it is null, it represents any (one or multiple) present versions of the module in the dep + * graph, which is different from the empty version + */ + @Nullable + abstract Version getVersion(); + } + + /** Converts a module target argument string to a properly typed {@link TargetModule} */ + static class TargetModuleConverter implements Converter<TargetModule> { + + @Override + public TargetModule convert(String input) throws OptionsParsingException { + String errorMessage = String.format("Cannot parse the given module argument: %s.", input); + Preconditions.checkArgument(input != null); + if (Ascii.equalsIgnoreCase(input, "root")) { + return TargetModule.create("", Version.EMPTY); + } else { + List<String> splits = Splitter.on('@').splitToList(input); + if (splits.isEmpty() || splits.get(0).isEmpty()) { + throw new OptionsParsingException(errorMessage); + } + + if (splits.size() == 2) { + if (splits.get(1).equals("_")) { + return TargetModule.create(splits.get(0), Version.EMPTY); + } + if (splits.get(1).isEmpty()) { + throw new OptionsParsingException(errorMessage); + } + try { + return TargetModule.create(splits.get(0), Version.parse(splits.get(1))); + } catch (ParseException e) { + throw new OptionsParsingException(errorMessage, e); + } + + } else if (splits.size() == 1) { + return TargetModule.create(splits.get(0), null); + } else { + throw new OptionsParsingException(errorMessage); + } + } + } + + @Override + public String getTypeDescription() { + return "root, <module>@<version> or <module>"; + } + } + + /** Converts a comma-separated module list argument (i.e. A@1.0,B@2.0) */ + public static class TargetModuleListConverter implements Converter<ImmutableList<TargetModule>> { + + @Override + public ImmutableList<TargetModule> convert(String input) throws OptionsParsingException { + CommaSeparatedNonEmptyOptionListConverter listConverter = + new CommaSeparatedNonEmptyOptionListConverter(); + TargetModuleConverter targetModuleConverter = new TargetModuleConverter(); + ImmutableList<String> targetStrings = listConverter.convert(input); + ImmutableList.Builder<TargetModule> targetModules = new ImmutableList.Builder<>(); + for (String targetInput : targetStrings) { + targetModules.add(targetModuleConverter.convert(targetInput)); + } + return targetModules.build(); + } + + @Override + public String getTypeDescription() { + return "a list of <module>s separated by comma"; + } + } +}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/modquery.txt b/src/main/java/com/google/devtools/build/lib/bazel/commands/modquery.txt new file mode 100644 index 0000000..84c1dc3 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/modquery.txt
@@ -0,0 +1,31 @@ + +Usage: %{product} %{command} [<option> ...] <query_type> [<args> ...] + +The command will display a dependency tree or parts of the dependency tree, structured to display different kinds of insights depending on the query type. +Calling the command with no argument will default to: +bazel modquery tree root + +<query_type> [<args> ...] can be one of the following: + + - tree: Displays the full dependency tree. Use the --from option to specify which module(s) you want the tree to start from (defaults to root which displays the whole dependency tree). + - deps <module(s)>: Displays the direct dependencies of the target module(s). + - path <module(s)_to>: Displays the shortest path found in the dependency graph from (any of) the --from module(s) to (any of) <module(s)_to>. + - all_paths <module(s)_to>: Display the dependency graph starting from (any of) the --from module(s) and containing any existing paths to (any of) the <module(s)_to>. + - explain <module(s)>: Prints all the places where the module is (or was) requested as a direct dependency, along with the reason why the respective final version was selected. It will display a pruned version of the all_paths root <module(s)> command which only contains the direct deps of the root, the <module(s)> leaves, along with their dependants (can be modified with --depth). + - show <module(s)>: Prints the rule that generated these modules’ repos (i.e. http_archive()). + + +<module> arguments must be of type: + + - root: The current (root) module you are inside of. + - <name>@<version>: A specific module version. + - <name>@_: Specifies the empty version of a module (for non-registry overridden) modules). + - <name>: Can be used as a placeholder for all the present versions of the module <name> + +<modules> means: + - <module>,<module>,... : A list of comma separated modules, where each <module> has the form of one of the above. + +NOTE: This command is still very experimental and the precise semantics +will change in the near future. + +%{options} \ No newline at end of file
diff --git a/src/main/java/com/google/devtools/common/options/OptionDocumentationCategory.java b/src/main/java/com/google/devtools/common/options/OptionDocumentationCategory.java index 34a2b02..83da7c3 100644 --- a/src/main/java/com/google/devtools/common/options/OptionDocumentationCategory.java +++ b/src/main/java/com/google/devtools/common/options/OptionDocumentationCategory.java
@@ -55,8 +55,8 @@ LOGGING, /** - * This option affects how strictly Bazel enforces valid build inputs (rule definitions, - * flag combinations, etc). + * This option affects how strictly Bazel enforces valid build inputs (rule definitions, flag + * combinations, etc). */ INPUT_STRICTNESS, @@ -79,8 +79,8 @@ OUTPUT_PARAMETERS, /** - * This option provides information about signing outputs of the build. (For example, signing - * an iOS application with a certificate.) + * This option provides information about signing outputs of the build. (For example, signing an + * iOS application with a certificate.) */ SIGNING, @@ -90,9 +90,7 @@ */ STARLARK_SEMANTICS, - /** - * This option dictates information about the test environment or test runner. - */ + /** This option dictates information about the test environment or test runner. */ TESTING, /** @@ -106,6 +104,9 @@ /** This option relates to query output and semantics. */ QUERY, + /** This option relates to modquery (external dependencies) output and semantics. */ + MODQUERY, + /** * This option specifies or alters a generic input to a Bazel command. This category should only * be used if the input is generic and does not fall into other categories, such as toolchain-
diff --git a/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java b/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java index d85a8c4..1e66ed6 100644 --- a/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java +++ b/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java
@@ -32,6 +32,7 @@ OptionDocumentationCategory.STARLARK_SEMANTICS, OptionDocumentationCategory.TESTING, OptionDocumentationCategory.QUERY, + OptionDocumentationCategory.MODQUERY, OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION, OptionDocumentationCategory.LOGGING, OptionDocumentationCategory.GENERIC_INPUTS, @@ -86,6 +87,9 @@ "Options that configure the toolchain used for action execution") .put(OptionDocumentationCategory.QUERY, "Options relating to query output and semantics") .put( + OptionDocumentationCategory.MODQUERY, + "Options relating to modquery output and semantics") + .put( OptionDocumentationCategory.GENERIC_INPUTS, "Options specifying or altering a generic input to a Bazel command that does not fall " + "into other categories.")
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto index c8a8721..54091ef 100644 --- a/src/main/protobuf/failure_details.proto +++ b/src/main/protobuf/failure_details.proto
@@ -147,6 +147,7 @@ StarlarkLoading starlark_loading = 179; ExternalDeps external_deps = 181; DiffAwareness diff_awareness = 182; + ModqueryCommand modquery_command = 183; } reserved 102; // For internal use @@ -1274,3 +1275,14 @@ Code code = 1; } + +message ModqueryCommand { + enum Code { + MODQUERY_COMMAND_UNKNOWN = 0 [(metadata) = { exit_code: 37 }]; + MISSING_ARGUMENTS = 1 [(metadata) = { exit_code: 2 }]; + TOO_MANY_ARGUMENTS = 2 [(metadata) = { exit_code: 2 }]; + INVALID_ARGUMENTS = 3 [(metadata) = { exit_code: 2 }]; + } + + Code code = 1; +}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/BUILD index 8b826e0..2579b51 100644 --- a/src/test/java/com/google/devtools/build/lib/bazel/BUILD +++ b/src/test/java/com/google/devtools/build/lib/bazel/BUILD
@@ -8,6 +8,7 @@ testonly = 0, srcs = glob(["*"]) + [ "//src/test/java/com/google/devtools/build/lib/bazel/bzlmod:srcs", + "//src/test/java/com/google/devtools/build/lib/bazel/commands:srcs", "//src/test/java/com/google/devtools/build/lib/bazel/debug:srcs", "//src/test/java/com/google/devtools/build/lib/bazel/execlog:srcs", "//src/test/java/com/google/devtools/build/lib/bazel/repository:srcs",
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/commands/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/commands/BUILD new file mode 100644 index 0000000..847dd62 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/bazel/commands/BUILD
@@ -0,0 +1,40 @@ +load("@rules_java//java:defs.bzl", "java_test") + +package( + default_testonly = 1, + default_visibility = ["//src:__subpackages__"], +) + +licenses(["notice"]) + +filegroup( + name = "srcs", + testonly = 0, + srcs = glob(["*"]), + visibility = ["//src:__subpackages__"], +) + +java_library( + name = "BzlmodCommandsTests_lib", + srcs = glob( + ["*.java"], + ), + deps = [ + "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common", + "//src/main/java/com/google/devtools/build/lib/bazel/commands", + "//src/main/java/com/google/devtools/common/options", + "//src/main/protobuf:failure_details_java_proto", + "//third_party:guava", + "//third_party:junit4", + "//third_party:truth", + ], +) + +java_test( + name = "BzlmodCommandsTests", + test_class = "com.google.devtools.build.lib.AllTests", + runtime_deps = [ + ":BzlmodCommandsTests_lib", + "//src/test/java/com/google/devtools/build/lib:test_runner", + ], +)
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommandTest.java b/src/test/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommandTest.java new file mode 100644 index 0000000..c3d0bee --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/bazel/commands/ModqueryCommandTest.java
@@ -0,0 +1,122 @@ +// 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.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +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.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.bazel.commands.ModqueryCommand.InvalidArgumentException; +import com.google.devtools.build.lib.bazel.commands.ModqueryOptions.QueryType; +import com.google.devtools.build.lib.server.FailureDetails.ModqueryCommand.Code; +import com.google.devtools.common.options.OptionsParsingException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ModqueryCommand}. */ +@RunWith(JUnit4.class) +public class ModqueryCommandTest { + + public final ImmutableMap<String, ImmutableSet<ModuleKey>> modulesIndex = + ImmutableMap.of( + "A", + ImmutableSet.of(ModuleKey.ROOT), + "B", + ImmutableSet.of( + ModuleKey.create("B", Version.parse("1.0")), + ModuleKey.create("B", Version.parse("2.0"))), + "C", + ImmutableSet.of(ModuleKey.create("C", Version.EMPTY))); + + public ModqueryCommandTest() throws ParseException {} + + @Test + public void testAllPathsNoArgsThrowsMissingArguments() { + InvalidArgumentException e = + assertThrows( + InvalidArgumentException.class, + () -> + ModqueryCommand.parseTargetArgs( + ImmutableList.of(), QueryType.ALL_PATHS.getArgNumber(), modulesIndex)); + assertThat(e.getCode()).isEqualTo(Code.MISSING_ARGUMENTS); + } + + @Test + public void testTreeNoArgs() throws InvalidArgumentException, OptionsParsingException { + ModqueryCommand.parseTargetArgs( + ImmutableList.of(), QueryType.TREE.getArgNumber(), modulesIndex); + } + + @Test + public void testTreeWithArgsThrowsTooManyArguments() { + ImmutableList<String> args = ImmutableList.of("A"); + InvalidArgumentException e = + assertThrows( + InvalidArgumentException.class, + () -> + ModqueryCommand.parseTargetArgs(args, QueryType.TREE.getArgNumber(), modulesIndex)); + assertThat(e.getCode()).isEqualTo(Code.TOO_MANY_ARGUMENTS); + } + + @Test + public void testDepsArgWrongFormat_noVersion() { + ImmutableList<String> args = ImmutableList.of("A@"); + assertThrows( + OptionsParsingException.class, + () -> ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex)); + } + + @Test + public void testDepsArgInvalid_missingModule() { + ImmutableList<String> args = ImmutableList.of("D"); + InvalidArgumentException e = + assertThrows( + InvalidArgumentException.class, + () -> + ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex)); + assertThat(e.getCode()).isEqualTo(Code.INVALID_ARGUMENTS); + } + + @Test + public void testDepsArgInvalid_missingModuleVersion() { + ImmutableList<String> args = ImmutableList.of("B@3.0"); + InvalidArgumentException e = + assertThrows( + InvalidArgumentException.class, + () -> + ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex)); + assertThat(e.getCode()).isEqualTo(Code.INVALID_ARGUMENTS); + } + + @Test + public void testDepsArgInvalid_invalidListFormat() { + ImmutableList<String> args = ImmutableList.of("B@1.0;B@2.0"); + assertThrows( + OptionsParsingException.class, + () -> ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex)); + } + + @Test + public void testDepsListArg_ok() throws InvalidArgumentException, OptionsParsingException { + ImmutableList<String> args = ImmutableList.of("A,B@1.0,B@2.0,C@_"); + ModqueryCommand.parseTargetArgs(args, QueryType.DEPS.getArgNumber(), modulesIndex); + } +}