config doesn't error on duplicate `--define` values (#15473)

When `--define` flags are interpreted by other commands, the last one
wins. Currently `bazel config` causes a server crash when interpreting
duplicate `--define` values.

This change makes `config` consistent with the other commands, and
re-uses the same deduplication code across all call-sites.

Closes #14760.

PiperOrigin-RevId: 427266039

Co-authored-by: Daniel Wagner-Hall <dwagnerhall@apple.com>
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 af8bc01..34be910 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
@@ -46,7 +46,6 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.TreeMap;
 import java.util.function.Supplier;
 import net.starlark.java.annot.StarlarkAnnotations;
 import net.starlark.java.annot.StarlarkBuiltin;
@@ -215,11 +214,8 @@
     // We can't use an ImmutableMap.Builder here; we need the ability to add entries with keys that
     // are already in the map so that the same define can be specified on the command line twice,
     // and ImmutableMap.Builder does not support that.
-    Map<String, String> commandLineDefinesBuilder = new TreeMap<>();
-    for (Map.Entry<String, String> define : options.commandLineBuildVariables) {
-      commandLineDefinesBuilder.put(define.getKey(), define.getValue());
-    }
-    commandLineBuildVariables = ImmutableMap.copyOf(commandLineDefinesBuilder);
+    commandLineBuildVariables =
+        ImmutableMap.copyOf(options.getNormalizedCommandLineBuildVariables());
 
     this.actionEnv = actionEnvironment;
     this.testEnv = setupTestEnvironment();
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/CoreOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/CoreOptions.java
index 5203a88..c2d17a4 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/CoreOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/CoreOptions.java
@@ -87,6 +87,9 @@
       help = "If true, constraint settings from @bazel_tools are removed.")
   public boolean usePlatformsRepoForConstraints;
 
+  // Note: This value may contain conflicting duplicate values for the same define.
+  // Use `getNormalizedCommandLineBuildVariables` if you wish for these to be deduplicated
+  // (last-wins).
   @Option(
       name = "define",
       converter = Converters.AssignmentConverter.class,
@@ -878,16 +881,22 @@
     return host;
   }
 
+  /// Normalizes --define flags, preserving the last one to appear in the event of conflicts.
+  public LinkedHashMap<String, String> getNormalizedCommandLineBuildVariables() {
+    LinkedHashMap<String, String> flagValueByName = new LinkedHashMap<>();
+    for (Map.Entry<String, String> entry : commandLineBuildVariables) {
+      // If the same --define flag is passed multiple times we keep the last value.
+      flagValueByName.put(entry.getKey(), entry.getValue());
+    }
+    return flagValueByName;
+  }
+
   @Override
   public CoreOptions getNormalized() {
     CoreOptions result = (CoreOptions) clone();
 
     if (collapseDuplicateDefines) {
-      LinkedHashMap<String, String> flagValueByName = new LinkedHashMap<>();
-      for (Map.Entry<String, String> entry : result.commandLineBuildVariables) {
-        // If the same --define flag is passed multiple times we keep the last value.
-        flagValueByName.put(entry.getKey(), entry.getValue());
-      }
+      LinkedHashMap<String, String> flagValueByName = getNormalizedCommandLineBuildVariables();
 
       // This check is an optimization to avoid creating a new list if the normalization was a
       // no-op.
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
index 38f7361..3e848d2 100644
--- 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
@@ -557,7 +557,11 @@
 
     // --define:
     for (Map.Entry<String, String> entry :
-        config.getOptions().get(CoreOptions.class).commandLineBuildVariables) {
+        config
+            .getOptions()
+            .get(CoreOptions.class)
+            .getNormalizedCommandLineBuildVariables()
+            .entrySet()) {
       ans.put("--define:" + entry.getKey(), Verify.verifyNotNull(entry.getValue()));
     }
     return ans.build();
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandTest.java b/src/test/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandTest.java
index bc7e069..2c4a0b6 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandTest.java
@@ -427,4 +427,29 @@
                 .collect(Collectors.toList()))
         .isEmpty();
   }
+
+  @Test
+  public void conflictingDefinesLastWins() throws Exception {
+    analyzeTarget("--define", "a=1", "--define", "a=2");
+
+    ConfigurationForOutput targetConfig = null;
+    for (JsonElement configJson :
+        JsonParser.parseString(callConfigCommand("--dump_all").outAsLatin1()).getAsJsonArray()) {
+      ConfigurationForOutput config = new Gson().fromJson(configJson, ConfigurationForOutput.class);
+      if (isTargetConfig(config)) {
+        targetConfig = config;
+        break;
+      }
+    }
+
+    assertThat(targetConfig).isNotNull();
+    assertThat(getOptionValue(targetConfig, "user-defined", "--define:a")).isEqualTo("2");
+    assertThat(
+            targetConfig.fragmentOptions.stream()
+                .filter(fragment -> fragment.name.endsWith("CoreOptions"))
+                .flatMap(fragment -> fragment.options.keySet().stream())
+                .filter(name -> name.equals("define"))
+                .collect(Collectors.toList()))
+        .isEmpty();
+  }
 }