Add command to display config information given a checksum. Will also diff two configurations. The command is hidden and won't be listed in help. PiperOrigin-RevId: 250288453
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java index 860161d..4a40bee 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -14,6 +14,7 @@ package com.google.devtools.build.lib.analysis.config; +import static java.util.Comparator.comparing; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Suppliers; @@ -206,12 +207,11 @@ } public void describe(StringBuilder sb) { - for (Fragment fragment : fragments.values()) { - sb.append(fragment.getClass().getName()).append('\n'); - } - for (String s : buildOptions.toString().split(" ")) { - sb.append(s).append('\n'); - } + sb.append("BuildConfiguration ").append(checksum()).append(":\n"); + getOptions().getFragmentClasses().stream() + .sorted(comparing(Class::getName)) + .map(fragmentClass -> getOptions().get(fragmentClass)) + .forEach(fragment -> fragment.describe(sb)); } @Override
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java index 2ff277d..fb8027f 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java
@@ -14,6 +14,8 @@ package com.google.devtools.build.lib.analysis.config; +import static java.util.Map.Entry.comparingByKey; + import com.google.common.collect.ImmutableMap; import com.google.devtools.common.options.OptionDefinition; import com.google.devtools.common.options.Options; @@ -134,4 +136,14 @@ public Map<OptionDefinition, SelectRestriction> getSelectRestrictions() { return ImmutableMap.of(); } + + public void describe(StringBuilder sb) { + sb.append("Fragment ").append(getClass().getName()).append(" {\n"); + Map<String, Object> options = asMap(); + options.entrySet().stream() + .sorted(comparingByKey()) + .forEach( + e -> sb.append(" ").append(e.getKey()).append(": ").append(e.getValue()).append("\n")); + sb.append("}\n"); + } }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuiltinCommandModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BuiltinCommandModule.java index 48b4c0b..5c18a42 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/BuiltinCommandModule.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/BuiltinCommandModule.java
@@ -17,6 +17,7 @@ import com.google.devtools.build.lib.runtime.commands.BuildCommand; import com.google.devtools.build.lib.runtime.commands.CanonicalizeCommand; import com.google.devtools.build.lib.runtime.commands.CleanCommand; +import com.google.devtools.build.lib.runtime.commands.ConfigCommand; import com.google.devtools.build.lib.runtime.commands.CoverageCommand; import com.google.devtools.build.lib.runtime.commands.CqueryCommand; import com.google.devtools.build.lib.runtime.commands.DumpCommand; @@ -60,7 +61,8 @@ new TestCommand(), new VersionCommand(), new AqueryCommand(), - new CqueryCommand()); + new CqueryCommand(), + new ConfigCommand()); // Only enable the "license" command when this binary has an embedded LICENSE file. if (LicenseCommand.isSupported()) { builder.addCommands(new LicenseCommand());
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java new file mode 100644 index 0000000..8a6b08b --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java
@@ -0,0 +1,206 @@ +// Copyright 2019 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.runtime.commands; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Comparator.comparing; +import static java.util.Map.Entry.comparingByKey; + +import com.google.common.base.Functions; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.common.collect.Table; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.config.FragmentOptions; +import com.google.devtools.build.lib.events.Event; +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.skyframe.BuildConfigurationValue; +import com.google.devtools.build.lib.skyframe.SkyFunctions; +import com.google.devtools.build.lib.util.ExitCode; +import com.google.devtools.build.lib.util.Pair; +import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator; +import com.google.devtools.common.options.OptionDefinition; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.OptionsParsingResult; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nullable; + +/** Handles the 'config' command on the Blaze command line. */ +@Command( + name = "config", + builds = true, + inherits = {BuildCommand.class}, + usesConfigurationOptions = true, + shortDescription = "Displays details of configurations.", + allowResidue = true, + completion = "string", + hidden = true, + help = "resource:config.txt") +public class ConfigCommand implements BlazeCommand { + + @Override + public void editOptions(OptionsParser optionsParser) {} + + @Override + public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) { + if (options.getResidue().isEmpty()) { + env.getReporter().handle(Event.error("Missing config id.")); + return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); + } + ImmutableMap<String, BuildConfiguration> configurations = findConfigurations(env); + + try (PrintWriter writer = + new PrintWriter( + new OutputStreamWriter(env.getReporter().getOutErr().getOutputStream(), UTF_8))) { + if (options.getResidue().size() == 1) { + String configHash = options.getResidue().get(0); + env.getReporter() + .handle(Event.info(String.format("Displaying config with id %s", configHash))); + + BuildConfiguration config = configurations.get(configHash); + if (config == null) { + env.getReporter() + .handle(Event.error(String.format("No configuration found with id: %s", configHash))); + return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); + } + + StringBuilder sb = new StringBuilder(); + config.describe(sb); + writer.print(sb.toString()); + + return BlazeCommandResult.exitCode(ExitCode.SUCCESS); + } else if (options.getResidue().size() == 2) { + String configHash1 = options.getResidue().get(0); + String configHash2 = options.getResidue().get(1); + env.getReporter() + .handle( + Event.info( + String.format( + "Displaying diff between configs" + " %s and" + " %s", + configHash1, configHash2))); + + BuildConfiguration config1 = configurations.get(configHash1); + if (config1 == null) { + env.getReporter() + .handle( + Event.error(String.format("No configuration found with id: %s", configHash1))); + return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); + } + BuildConfiguration config2 = configurations.get(configHash2); + if (config2 == null) { + env.getReporter() + .handle( + Event.error(String.format("No configuration found with id: %s", configHash2))); + return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); + } + + writer.printf( + "Displaying diff between configs" + " %s and" + " %s\n", configHash1, configHash2); + Table<Class<? extends FragmentOptions>, String, Pair<Object, Object>> diffs = + diffConfigurations(config1, config2); + writer.print(describeConfigDiff(diffs)); + return BlazeCommandResult.exitCode(ExitCode.SUCCESS); + } else { + env.getReporter().handle(Event.error("Too many config ids.")); + return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); + } + } + } + + private ImmutableMap<String, BuildConfiguration> findConfigurations(CommandEnvironment env) { + InMemoryMemoizingEvaluator evaluator = + (InMemoryMemoizingEvaluator) + env.getRuntime().getWorkspace().getSkyframeExecutor().getEvaluatorForTesting(); + return evaluator.getDoneValues().entrySet().stream() + .filter(e -> SkyFunctions.BUILD_CONFIGURATION.equals(e.getKey().functionName())) + .map(Map.Entry::getValue) + .map(v -> (BuildConfigurationValue) v) + .map(BuildConfigurationValue::getConfiguration) + .distinct() + .collect(toImmutableMap(BuildConfiguration::checksum, Functions.identity())); + } + + private Table<Class<? extends FragmentOptions>, String, Pair<Object, Object>> diffConfigurations( + BuildConfiguration config1, BuildConfiguration config2) { + Table<Class<? extends FragmentOptions>, String, Pair<Object, Object>> diffs = + HashBasedTable.create(); + + for (Class<? extends FragmentOptions> fragment : + Sets.union( + config1.getOptions().getFragmentClasses(), config2.getOptions().getFragmentClasses())) { + FragmentOptions options1 = config1.getOptions().get(fragment); + FragmentOptions options2 = config2.getOptions().get(fragment); + diffs.row(fragment).putAll(diffOptions(fragment, options1, options2)); + } + + return diffs; + } + + private Map<String, Pair<Object, Object>> diffOptions( + Class<? extends FragmentOptions> fragment, + @Nullable FragmentOptions options1, + @Nullable FragmentOptions options2) { + Map<String, Pair<Object, Object>> diffs = new HashMap<>(); + + for (OptionDefinition option : OptionsParser.getOptionDefinitions(fragment)) { + Object value1 = options1 == null ? null : options1.getValueFromDefinition(option); + Object value2 = options2 == null ? null : options2.getValueFromDefinition(option); + + if (!Objects.equals(value1, value2)) { + diffs.put(option.getOptionName(), Pair.of(value1, value2)); + } + } + + return diffs; + } + + private String describeConfigDiff( + Table<Class<? extends FragmentOptions>, String, Pair<Object, Object>> diff) { + StringBuilder sb = new StringBuilder(); + + diff.rowKeySet().stream() + .sorted(comparing(Class::getName)) + .forEach(fragmentClass -> displayFragmentDiff(fragmentClass, diff.row(fragmentClass), sb)); + + return sb.toString(); + } + + private void displayFragmentDiff( + Class<? extends FragmentOptions> fragmentClass, + Map<String, Pair<Object, Object>> diff, + StringBuilder sb) { + sb.append("Fragment ").append(fragmentClass.getName()).append(" {\n"); + diff.entrySet().stream() + .sorted(comparingByKey()) + .forEach( + e -> + sb.append(" ") + .append(e.getKey()) + .append(": ") + .append(e.getValue().getFirst()) + .append(", ") + .append(e.getValue().getSecond()) + .append("\n")); + sb.append("}\n"); + } +}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/config.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/config.txt new file mode 100644 index 0000000..e79c5de --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/config.txt
@@ -0,0 +1,14 @@ + +Usage: %{product} %{command} <options> [<config_id> | <config_id> <config_id>] + +Displays the given configuration objects, if they can be found in the Skyframe cache. The best way +to use this command is to first run a build (or cquery), and then immediately run the config +command to view the configuration. Note that any flags passed to build (or cquery) must also be +passed to config, or else the Skyframe cache will be evicted and no configurations will be found. + +If two configuration ids are passed, the difference between them will be computed and displayed, +instead of the entire configurations. + +This command is experimental and unsupported. + +%{options}