Parse exec properties attribute into exec group properties for RuleContexts.

Exec property keys can take two forms: "property" and "exec_group_name.property"

Note - this does not do equivalent parsing for SkylarkRepositoryContext's inherited exec_properties attribute or PlatformInfo's declared exec_properties attribute. Since neither of those have the ability to declare exec_groups, it shouldn't be necessary. This could potentially lead to user confusion, so something to document well.

PiperOrigin-RevId: 312156395
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
index 8beffa9..50a0fb7 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
@@ -90,6 +90,7 @@
 import com.google.devtools.build.lib.packages.Type;
 import com.google.devtools.build.lib.packages.Type.LabelClass;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
+import com.google.devtools.build.lib.skyframe.SaneAnalysisException;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Location;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics;
@@ -203,6 +204,7 @@
   private final ConstraintSemantics<RuleContext> constraintSemantics;
   private final ImmutableSet<String> requiredConfigFragments;
   private final List<Expander> makeVariableExpanders = new ArrayList<>();
+  private final ImmutableMap<String, ImmutableMap<String, String>> execProperties;
 
   /** Map of exec group names to ActionOwners. */
   private final Map<String, ActionOwner> actionOwners = new HashMap<>();
@@ -224,7 +226,8 @@
       ImmutableMap<String, Attribute> aspectAttributes,
       @Nullable ToolchainCollection<ResolvedToolchainContext> toolchainContexts,
       ConstraintSemantics<RuleContext> constraintSemantics,
-      ImmutableSet<String> requiredConfigFragments) {
+      ImmutableSet<String> requiredConfigFragments)
+      throws InvalidExecGroupException {
     super(
         builder.env,
         builder.target.getAssociatedRule(),
@@ -255,6 +258,7 @@
     this.actionOwnerSymbolGenerator = new SymbolGenerator<>(actionLookupKey);
     reporter = builder.reporter;
     this.toolchainContexts = toolchainContexts;
+    this.execProperties = parseExecProperties();
     this.constraintSemantics = constraintSemantics;
     this.requiredConfigFragments = requiredConfigFragments;
   }
@@ -444,7 +448,7 @@
             rule,
             aspectDescriptors,
             getConfiguration(),
-            getTargetExecProperties(),
+            getExecProperties(execGroup, execProperties),
             getExecutionPlatform(execGroup));
     actionOwners.put(execGroup, actionOwner);
     return actionOwner;
@@ -1280,31 +1284,92 @@
     return ans.build();
   }
 
-  public Map<String, String> getTargetExecProperties() {
-    if (isAttrDefined(RuleClass.EXEC_PROPERTIES, Type.STRING_DICT)) {
-      return attributes.get(RuleClass.EXEC_PROPERTIES, Type.STRING_DICT);
+  private ImmutableMap<String, ImmutableMap<String, String>> parseExecProperties()
+      throws InvalidExecGroupException {
+    if (!isAttrDefined(RuleClass.EXEC_PROPERTIES, Type.STRING_DICT)) {
+      return ImmutableMap.of(DEFAULT_EXEC_GROUP_NAME, ImmutableMap.of());
     } else {
-      return ImmutableMap.of();
+      return parseExecProperties(
+          attributes.get(RuleClass.EXEC_PROPERTIES, Type.STRING_DICT),
+          toolchainContexts == null ? ImmutableSet.of() : toolchainContexts.getExecGroups());
     }
   }
 
-  @Override
-  @Nullable
-  public PlatformInfo getExecutionPlatform() {
-    if (getToolchainContext() == null) {
-      return null;
+  /**
+   * Parse raw exec properties attribute value into a map of exec group names to their properties.
+   * The raw map can have keys of two forms: (1) 'property' and (2) 'exec_group_name.property'. The
+   * former get parsed into the target's default exec group, the latter get parsed into their
+   * relevant exec groups.
+   */
+  private static ImmutableMap<String, ImmutableMap<String, String>> parseExecProperties(
+      Map<String, String> rawExecProperties, Set<String> execGroups)
+      throws InvalidExecGroupException {
+    Map<String, Map<String, String>> consolidatedProperties = new HashMap<>();
+    consolidatedProperties.put(DEFAULT_EXEC_GROUP_NAME, new HashMap<>());
+    for (Map.Entry<String, String> execProperty : rawExecProperties.entrySet()) {
+      String rawProperty = execProperty.getKey();
+      int delimiterIndex = rawProperty.indexOf('.');
+      if (delimiterIndex == -1) {
+        consolidatedProperties
+            .get(DEFAULT_EXEC_GROUP_NAME)
+            .put(rawProperty, execProperty.getValue());
+      } else {
+        String execGroup = rawProperty.substring(0, delimiterIndex);
+        String property = rawProperty.substring(delimiterIndex + 1);
+        if (!execGroups.contains(execGroup)) {
+          throw new InvalidExecGroupException(
+              String.format(
+                  "Tried to set exec property '%s' for non-existent exec group '%s'.",
+                  property, execGroup));
+        }
+        consolidatedProperties.putIfAbsent(execGroup, new HashMap<>());
+        consolidatedProperties.get(execGroup).put(property, execProperty.getValue());
+      }
     }
-    return getToolchainContext().executionPlatform();
+
+    // Copy everything to immutable maps.
+    ImmutableMap.Builder<String, ImmutableMap<String, String>> execProperties =
+        new ImmutableMap.Builder<>();
+    for (Map.Entry<String, Map<String, String>> execGroupMap : consolidatedProperties.entrySet()) {
+      execProperties.put(execGroupMap.getKey(), ImmutableMap.copyOf(execGroupMap.getValue()));
+    }
+
+    return execProperties.build();
   }
 
-  @Override
-  @Nullable
-  public PlatformInfo getExecutionPlatform(String execGroup) {
-    if (getToolchainContexts() == null) {
-      return null;
+  /**
+   * Gets the combined exec properties of the given exec group and the target's exec properties. If
+   * a property is set in both, the exec group properties take precedence. If a non-existent exec
+   * group is passed in, just returns the target's exec properties.
+   *
+   * @param execGroup group whose properties to retrieve
+   * @param execProperties Map of exec group name to map of properties and values
+   */
+  private static ImmutableMap<String, String> getExecProperties(
+      String execGroup, Map<String, ImmutableMap<String, String>> execProperties) {
+    if (!execProperties.containsKey(execGroup) || execGroup.equals(DEFAULT_EXEC_GROUP_NAME)) {
+      return execProperties.get(DEFAULT_EXEC_GROUP_NAME);
     }
-    ResolvedToolchainContext toolchainContext = getToolchainContext(execGroup);
-    return toolchainContext == null ? null : toolchainContext.executionPlatform();
+
+    // Use a HashMap to build here because we expect duplicate keys to happen
+    // (and rewrite previous entries).
+    Map<String, String> targetAndGroupProperties =
+        new HashMap<>(execProperties.get(DEFAULT_EXEC_GROUP_NAME));
+    targetAndGroupProperties.putAll(execProperties.get(execGroup));
+    return ImmutableMap.copyOf(targetAndGroupProperties);
+  }
+
+  /** An error for when the user tries to access an non-existent exec group */
+  public static final class InvalidExecGroupException extends Exception
+      implements SaneAnalysisException {
+    InvalidExecGroupException(String message) {
+      super(message);
+    }
+  }
+
+  @VisibleForTesting
+  public ImmutableMap<String, ImmutableMap<String, String>> getExecPropertiesForTesting() {
+    return execProperties;
   }
 
   private void checkAttribute(String attributeName, TransitionMode mode) {
@@ -1344,6 +1409,25 @@
     }
   }
 
+  @Override
+  @Nullable
+  public PlatformInfo getExecutionPlatform() {
+    if (getToolchainContext() == null) {
+      return null;
+    }
+    return getToolchainContext().executionPlatform();
+  }
+
+  @Override
+  @Nullable
+  public PlatformInfo getExecutionPlatform(String execGroup) {
+    if (getToolchainContexts() == null) {
+      return null;
+    }
+    ResolvedToolchainContext toolchainContext = getToolchainContext(execGroup);
+    return toolchainContext == null ? null : toolchainContext.executionPlatform();
+  }
+
   /**
    * For the specified attribute "attributeName" (which must be of type list(label)), resolve all
    * the labels into ConfiguredTargets (for the configuration appropriate to the attribute) and
@@ -1690,7 +1774,7 @@
     }
 
     @VisibleForTesting
-    public RuleContext build() {
+    public RuleContext build() throws InvalidExecGroupException {
       Preconditions.checkNotNull(prerequisiteMap);
       Preconditions.checkNotNull(configConditions);
       Preconditions.checkNotNull(visibility);