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}