Expose late-bound-attributes to Skylark.

RELNOTES: Late-bound attributes are exposed to skylark. This is a new API (`configuration_field()`) to depend on certain configuration-defined targets from skylark rules.
PiperOrigin-RevId: 174534104
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
index 541569c..ac85e07 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
 import com.google.devtools.build.lib.analysis.config.DefaultsPackage;
@@ -50,6 +51,7 @@
 import com.google.devtools.build.lib.packages.RuleErrorConsumer;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.syntax.Environment;
 import com.google.devtools.build.lib.syntax.Environment.Extension;
 import com.google.devtools.build.lib.syntax.Environment.Phase;
@@ -296,6 +298,9 @@
      * between option classes, factories, and fragments, such that the factory depends only on the
      * options class and creates the fragment. This method provides a convenient way of adding both
      * the options class and the factory in a single call.
+     *
+     * <p>Note that configuration fragments annotated with a Skylark name must have a unique
+     * name; no two different configuration fragments can share the same name.
      */
     public Builder addConfig(
         Class<? extends FragmentOptions> options, ConfigurationFragmentFactory factory) {
@@ -312,6 +317,12 @@
       return this;
     }
 
+    /**
+     * Adds a configuration fragment factory.
+     *
+     * <p>Note that configuration fragments annotated with a Skylark name must have a unique
+     * name; no two different configuration fragments can share the same name.
+     */
     public Builder addConfigurationFragment(ConfigurationFragmentFactory factory) {
       configurationFragmentFactories.add(factory);
       return this;
@@ -549,6 +560,8 @@
 
   private final Environment.Frame globals;
 
+  private final ImmutableMap<String, Class<?>> configurationFragmentMap;
+
   private ConfiguredRuleClassProvider(
       Label preludeLabel,
       String runfilesPrefix,
@@ -581,6 +594,7 @@
     this.universalFragment = universalFragment;
     this.prerequisiteValidator = prerequisiteValidator;
     this.globals = createGlobals(skylarkAccessibleJavaClasses, skylarkModules);
+    this.configurationFragmentMap = createFragmentMap(configurationFragmentFactories);
   }
 
   public PrerequisiteValidator getPrerequisiteValidator() {
@@ -700,6 +714,19 @@
     }
   }
 
+  private static ImmutableMap<String, Class<?>> createFragmentMap(
+      Iterable<ConfigurationFragmentFactory> configurationFragmentFactories) {
+    ImmutableMap.Builder<String, Class<?>> mapBuilder = ImmutableMap.builder();
+    for (ConfigurationFragmentFactory fragmentFactory : configurationFragmentFactories) {
+      Class<? extends Fragment> fragmentClass = fragmentFactory.creates();
+      String fragmentName = SkylarkModule.Resolver.resolveName(fragmentClass);
+      if (fragmentName != null) {
+        mapBuilder.put(fragmentName, fragmentClass);
+      }
+    }
+    return mapBuilder.build();
+  }
+
   private Environment createSkylarkRuleClassEnvironment(
       Mutability mutability,
       Environment.Frame globals,
@@ -717,6 +744,7 @@
             .setPhase(Phase.LOADING)
             .build();
     SkylarkUtils.setToolsRepository(env, toolsRepository);
+    SkylarkUtils.setFragmentMap(env, configurationFragmentMap);
     return env;
   }
 
@@ -747,6 +775,11 @@
     return defaultWorkspaceFileSuffix;
   }
 
+  @Override
+  public Map<String, Class<?>> getConfigurationFragmentMap() {
+    return configurationFragmentMap;
+  }
+
   /**
    * Returns all registered {@link BuildConfiguration.Fragment} classes.
    */
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java
index d406511..48113f2 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java
@@ -488,7 +488,7 @@
       LateBoundDefault<FragmentT, ValueT> lateBoundDefault,
       Rule rule,
       AttributeMap attributeMap,
-      BuildConfiguration config) {
+      BuildConfiguration config) throws EvalException {
     Class<FragmentT> fragmentClass = lateBoundDefault.getFragmentClass();
     // TODO(b/65746853): remove this when nothing uses it anymore
     if (BuildConfiguration.class.equals(fragmentClass)) {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java
index ad927c4..9883e63 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java
@@ -49,6 +49,7 @@
 import com.google.devtools.build.lib.syntax.SkylarkList;
 import com.google.devtools.build.lib.syntax.SkylarkSignatureProcessor;
 import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.build.lib.syntax.SkylarkUtils;
 import com.google.devtools.build.lib.syntax.Type;
 import com.google.devtools.build.lib.syntax.Type.ConversionException;
 import com.google.devtools.build.lib.syntax.Type.LabelClass;
@@ -196,6 +197,8 @@
         builder.value(
             new SkylarkComputedDefaultTemplate(
                 type, callback.getParameterNames(), callback, ast.getLocation()));
+      } else if (defaultValue instanceof SkylarkLateBoundDefault) {
+        builder.value((SkylarkLateBoundDefault) defaultValue);
       } else {
         builder.defaultValue(defaultValue, env.getGlobals().getTransitiveLabel(), DEFAULT_ARG);
       }
@@ -519,6 +522,49 @@
       };
 
   @SkylarkSignature(
+    name = "configuration_field",
+    returnType = SkylarkLateBoundDefault.class,
+    // TODO(cparsons): Provide a link to documentation for available SkylarkConfigurationFields.
+    doc = "References a late-bound default value for an attribute of type "
+      + "<a href=\"attr.html#label\">label</a>. A value is 'late-bound' if it requires "
+      + "the configuration to be built before determining the value. Any attribute using this "
+      + "as a value must <a href=\"../rules.html#private-attributes\">be private</a>.",
+    parameters = {
+        @Param(
+            name = "fragment",
+            type = String.class,
+            doc = "The name of a configuration fragment which contains the late-bound value."
+        ),
+        @Param(
+            name = "name",
+            type = String.class,
+            doc = "The name of the value to obtain from the configuration fragment."),
+    },
+    useLocation = true,
+    useEnvironment = true
+  )
+  private static final BuiltinFunction configurationField =
+      new BuiltinFunction("configuration_field") {
+        public SkylarkLateBoundDefault<?> invoke(
+            String fragment, String name, Location loc, Environment env)
+            throws EvalException {
+          Class<?> fragmentClass = SkylarkUtils.getFragmentMap(env).get(fragment);
+
+          if (fragmentClass == null) {
+            throw new EvalException(
+                loc,
+                String.format("invalid configuration fragment name '%s'", fragment));
+          }
+          try {
+            return SkylarkLateBoundDefault.forConfigurationField(
+                fragmentClass, name, SkylarkUtils.getToolsRepository(env));
+          } catch (SkylarkLateBoundDefault.InvalidConfigurationFieldException exception) {
+            throw new EvalException(loc, exception);
+          }
+        }
+      };
+
+  @SkylarkSignature(
     name = "string",
     doc = "Creates an attribute of type <a href=\"string.html\">string</a>.",
     objectType = SkylarkAttr.class,
@@ -598,6 +644,7 @@
         allowedTypes = {
           @ParamType(type = Label.class),
           @ParamType(type = String.class),
+          @ParamType(type = SkylarkLateBoundDefault.class)
         },
         callbackEnabled = true,
         noneable = true,
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkConfigurationField.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkConfigurationField.java
new file mode 100644
index 0000000..153751c3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkConfigurationField.java
@@ -0,0 +1,71 @@
+// Copyright 2017 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.analysis.skylark;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A marker interface for Java methods of Skylark-exposed configuration fragments which denote
+ * Skylark "configuration fields": late-bound attribute defaults that depend on configuration.
+ *
+ * <p>Methods annotated with this annotation have a few constraints:
+ * <ul>
+ * <li>The annotated method must be on a configuration fragment exposed to skylark.</li>
+ * <li>The method must have return type Label.</li>
+ * <li>The method must be public.</li>
+ * <li>The method must have zero arguments.</li>
+ * <li>The method must not throw exceptions.</li>
+ * </ul>
+ */
+// TODO(b/68817606): Verify the above constraints using annotation processing.
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SkylarkConfigurationField {
+
+  /**
+   * Name of the configuration field, as exposed to Skylark.
+   */
+  String name();
+
+  /**
+   * The default label associated with this field, corresponding to the value of this configuration
+   * field with default command line flags.
+   *
+   * <p>If the default label is under the tools repository, omit the tools repository prefix
+   * from this default, but set {@link #defaultInToolRepository} to true.</p>
+   */
+  String defaultLabel();
+
+  /**
+   * Whether the default label as defined in {@link #defaultLabel} should be prefixed with
+   * the tools repository.
+   */
+  boolean defaultInToolRepository() default false;
+
+  /**
+   * The documentation text in Skylark. It can contain HTML tags for special formatting.
+   *
+   * <p>It is allowed to be empty only if {@link #documented()} is false.
+   */
+  String doc() default "";
+
+  /**
+   * If true, the function will appear in the Skylark documentation. Set this to false if the
+   * function is experimental or an overloading and doesn't need to be documented.
+   */
+  boolean documented() default true;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkLateBoundDefault.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkLateBoundDefault.java
new file mode 100644
index 0000000..51f694c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkLateBoundDefault.java
@@ -0,0 +1,224 @@
+// Copyright 2017 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.analysis.skylark;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundDefault;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * An implementation of {@link LateBoundDefault} which obtains a late-bound attribute value
+ * (of type 'label') specifically by skylark configuration fragment name and field name, as
+ * registered by {@link SkylarkConfigurationField}.
+ *
+ * <p>For example, a SkylarkLateBoundDefault on "java" and "toolchain" would
+ * require a valid configuration fragment named "java" with a method annotated with
+ * {@link SkylarkConfigurationField} of name "toolchain". This {@link LateBoundDefault} would
+ * provide a late-bound dependency (defined by the label returned by that configuration field)
+ * in the current target configuration.
+ */
+@Immutable
+public class SkylarkLateBoundDefault<FragmentT> extends LateBoundDefault<FragmentT, Label> {
+
+  private final Method method;
+  private final String fragmentName;
+  private final String fragmentFieldName;
+
+  @Override
+  public Label resolve(Rule rule, AttributeMap attributes, FragmentT config) {
+    Class<?> fragmentClass = config.getClass();
+    try {
+      Object result = method.invoke(config);
+      return (Label) result;
+    } catch (IllegalAccessException | InvocationTargetException e) {
+      // Configuration field methods should not throw either of these exceptions.
+      throw new AssertionError("Method invocation failed: " + e);
+    }
+  }
+
+  /**
+   * Returns the {@link SkylarkConfigurationField} annotation corresponding to this method.
+   */
+  private static Label getDefaultLabel(
+      SkylarkConfigurationField annotation, String toolsRepository) {
+    Label defaultLabel = annotation.defaultInToolRepository()
+        ? Label.parseAbsoluteUnchecked(toolsRepository + annotation.defaultLabel())
+        : Label.parseAbsoluteUnchecked(annotation.defaultLabel());
+    return defaultLabel;
+  }
+
+  private SkylarkLateBoundDefault(SkylarkConfigurationField annotation,
+      Class<FragmentT> fragmentClass, String fragmentName, Method method, String toolsRepository) {
+    super(false /* don't use host configuration */,
+        fragmentClass,
+        getDefaultLabel(annotation, toolsRepository));
+
+    this.method = method;
+    this.fragmentName = fragmentName;
+    this.fragmentFieldName = annotation.name();
+  }
+
+  /**
+   * Returns the skylark name of the configuration fragment that this late bound default requires.
+   */
+  public String getFragmentName() {
+    return fragmentName;
+  }
+
+  /**
+   * Returns the skylark name of the configuration field name, as registered by
+   * {@link SkylarkConfigurationField} annotation on the configuration fragment.
+   */
+  public String getFragmentFieldName() {
+    return fragmentFieldName;
+  }
+
+  /**
+   * An exception thrown if a user specifies an invalid configuration field identifier.
+   *
+   * @see SkylarkConfigurationField
+   **/
+  public static class InvalidConfigurationFieldException extends Exception {
+    public InvalidConfigurationFieldException(String message) {
+      super(message);
+    }
+  }
+
+
+  private static class CacheKey {
+    private final Class<?> fragmentClass;
+    private final String toolsRepository;
+
+    private CacheKey(Class<?> fragmentClass,
+        String toolsRepository) {
+      this.fragmentClass = fragmentClass;
+      this.toolsRepository = toolsRepository;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+      if (object == this) {
+        return true;
+      } else if (!(object instanceof CacheKey)) {
+        return false;
+      } else {
+        CacheKey cacheKey = (CacheKey) object;
+        return fragmentClass.equals(cacheKey.fragmentClass)
+            && toolsRepository.equals(cacheKey.toolsRepository);
+      }
+    }
+
+    @Override
+    public int hashCode() {
+      int result = fragmentClass.hashCode();
+      result = 31 * result + toolsRepository.hashCode();
+      return result;
+    }
+  }
+
+  /**
+   * A cache for efficient {@link SkylarkLateBoundDefault} loading by configuration fragment. Each
+   * configuration fragment class key is mapped to a {@link Map} where keys are configuration field
+   * skylark names, and values are the {@link SkylarkLateBoundDefault}s. Methods must be annotated
+   * with {@link SkylarkConfigurationField} to be considered.
+   */
+  private static final LoadingCache<CacheKey, Map<String, SkylarkLateBoundDefault<?>>> fieldCache =
+      CacheBuilder.newBuilder()
+          .initialCapacity(10)
+          .maximumSize(100)
+          .build(
+              new CacheLoader<CacheKey, Map<String, SkylarkLateBoundDefault<?>>>() {
+                @Override
+                public Map<String, SkylarkLateBoundDefault<?>> load(CacheKey key) throws Exception {
+                  ImmutableMap.Builder<String, SkylarkLateBoundDefault<?>> lateBoundDefaultMap =
+                      new ImmutableMap.Builder<>();
+                  Class<?> fragmentClass = key.fragmentClass;
+                  String fragmentName = SkylarkModule.Resolver.resolveName(fragmentClass);
+                  for (Method method : fragmentClass.getMethods()) {
+                    if (method.isAnnotationPresent(SkylarkConfigurationField.class)) {
+                      // TODO(b/68817606): Use annotation processors to verify these constraints.
+                      Preconditions.checkArgument(
+                          method.getReturnType() == Label.class,
+                          String.format("Method %s must have return type 'Label'", method));
+                      Preconditions.checkArgument(
+                          method.getParameterTypes().length == 0,
+                          String.format("Method %s must not accept arguments", method));
+
+                      SkylarkConfigurationField configField =
+                          method.getAnnotation(SkylarkConfigurationField.class);
+                      lateBoundDefaultMap.put(
+                          configField.name(),
+                          new SkylarkLateBoundDefault<>(
+                              configField,
+                              fragmentClass,
+                              fragmentName,
+                              method,
+                              key.toolsRepository));
+                    }
+                  }
+                  return lateBoundDefaultMap.build();
+                }
+              });
+
+  /**
+   * Returns a {@link LateBoundDefault} which obtains a late-bound attribute value
+   * (of type 'label') specifically by skylark configuration fragment name and field name, as
+   * registered by {@link SkylarkConfigurationField}.
+   *
+   * @param fragmentClass the configuration fragment class, which must have a valid skylark name
+   * @param fragmentFieldName the configuration field name, as registered by
+   *     {@link SkylarkConfigurationField} annotation
+   * @param toolsRepository the Bazel tools repository path fragment
+   *
+   * @throws InvalidConfigurationFieldException if there is no valid configuration field with the
+   *     given fragment class and field name
+   */
+  public static <FragmentT> SkylarkLateBoundDefault<FragmentT> forConfigurationField(
+      Class<FragmentT> fragmentClass,
+      String fragmentFieldName,
+      String toolsRepository) throws InvalidConfigurationFieldException {
+    try {
+      CacheKey cacheKey = new CacheKey(fragmentClass, toolsRepository);
+      SkylarkLateBoundDefault resolver =
+          fieldCache.get(cacheKey).get(fragmentFieldName);
+      if (resolver == null) {
+        String fragmentName = SkylarkModule.Resolver.resolveName(fragmentClass);
+        if (Strings.isNullOrEmpty(fragmentName)) {
+          throw new AssertionError("fragment class must have a valid skylark name");
+        }
+        throw new InvalidConfigurationFieldException(
+            String.format("invalid configuration field name '%s' on fragment '%s'",
+                fragmentFieldName, fragmentName));
+      }
+      return resolver;
+    } catch (ExecutionException e) {
+      throw new IllegalStateException("method invocation failed: " + e);
+    }
+  }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java
index 8b99d81..ec03962 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java
@@ -58,6 +58,8 @@
     Object value = attributes.getAttributeValue(index);
     if (value instanceof Attribute.ComputedDefault) {
       value = ((Attribute.ComputedDefault) value).getDefault(this);
+    } else if (value instanceof Attribute.LateBoundDefault) {
+      value = ((Attribute.LateBoundDefault) value).getDefault();
     }
     try {
       return type.cast(value);
@@ -87,6 +89,25 @@
     }
   }
 
+  /**
+   * Returns the given attribute if it's a {@link Attribute.LateBoundDefault}, null otherwise.
+   *
+   * @throws IllegalArgumentException if the given attribute doesn't exist with the specified
+   *         type. This happens whether or not it's a late bound default.
+   */
+  @Nullable
+  @SuppressWarnings("unchecked")
+  public <T> Attribute.LateBoundDefault<?, T> getLateBoundDefault(
+      String attributeName, Type<T> type) {
+    int index = getIndexWithTypeCheck(attributeName, type);
+    Object value = attributes.getAttributeValue(index);
+    if (value instanceof Attribute.LateBoundDefault) {
+      return (Attribute.LateBoundDefault<?, T>) value;
+    } else {
+      return null;
+    }
+  }
+
   @Override
   public Iterable<String> getAttributeNames() {
     ImmutableList.Builder<String> names = ImmutableList.builder();
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
index 54f6255..9b158e6 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
@@ -680,7 +680,6 @@
      */
     public Builder<TYPE> value(LateBoundDefault<?, ? extends TYPE> defaultValue) {
       Preconditions.checkState(!valueSet, "the default value is already set");
-      Preconditions.checkState(name.isEmpty() || isLateBound(name));
       value = defaultValue;
       valueSource = AttributeValueSource.LATE_BOUND;
       valueSet = true;
@@ -1082,6 +1081,9 @@
      */
     public Attribute build(String name) {
       Preconditions.checkState(!name.isEmpty(), "name has not been set");
+      if (valueSource == AttributeValueSource.LATE_BOUND) {
+        Preconditions.checkState(isLateBound(name));
+      }
       // TODO(bazel-team): Set the default to be no file type, then remove this check, and also
       // remove all allowedFileTypes() calls without parameters.
 
@@ -1715,7 +1717,7 @@
     private static final LateBoundDefault<Void, Void> ALWAYS_NULL =
         new SimpleLateBoundDefault<>(false, Void.class, null, (rule, attributes, unused) -> null);
 
-    private LateBoundDefault(
+    protected LateBoundDefault(
         boolean useHostConfiguration,
         Class<FragmentT> fragmentClass,
         ValueT defaultValue) {
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java b/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
index 7dd8acb..bf99b43 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
@@ -1601,6 +1601,8 @@
         // that depends on non-computed default attribute values, and that condition predicate is
         // evaluated by the call to Attribute#getDefaultValue.
         attrsWithComputedDefaults.add(attr);
+      } else if (attr.isLateBound()) {
+        rule.setAttributeValue(attr, attr.getLateBoundDefault(), /*explicit=*/ false);
       } else {
         Object defaultValue = getAttributeNoncomputedDefaultValue(attr, pkgBuilder);
         rule.setAttributeValue(attr, defaultValue, /*explicit=*/ false);
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/packages/RuleClassProvider.java
index 5235523..302b05b 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/RuleClassProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleClassProvider.java
@@ -93,4 +93,10 @@
    * Retrieves an aspect from the aspect factory map using the key provided
    */
   NativeAspectClass getNativeAspectClass(String key);
+
+  /**
+   * Retrieves a {@link Map} from skylark configuration fragment name to configuration fragment
+   * class.
+   */
+  Map<String, Class<?>> getConfigurationFragmentMap();
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/apple/AppleCommandLineOptions.java b/src/main/java/com/google/devtools/build/lib/rules/apple/AppleCommandLineOptions.java
index 1446cd0..14fb440 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/apple/AppleCommandLineOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/apple/AppleCommandLineOptions.java
@@ -331,7 +331,8 @@
    * changed from the default using the {@code xcode_version_config} build flag.
    */
   // TODO(cparsons): Update all callers to reference the actual xcode_version_config flag value.
-  static final String DEFAULT_XCODE_VERSION_CONFIG_LABEL = "//tools/objc:host_xcodes";
+  @VisibleForTesting
+  public static final String DEFAULT_XCODE_VERSION_CONFIG_LABEL = "//tools/objc:host_xcodes";
 
   /** Converter for --default_ios_provisioning_profile. */
   public static class DefaultProvisioningProfileConverter extends DefaultLabelConverter {
diff --git a/src/main/java/com/google/devtools/build/lib/rules/apple/AppleConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/apple/AppleConfiguration.java
index cdd22ee..127b0ac 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/apple/AppleConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/apple/AppleConfiguration.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
 import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
 import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.analysis.skylark.SkylarkConfigurationField;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.rules.apple.AppleCommandLineOptions.AppleBitcodeMode;
@@ -416,6 +417,12 @@
   /**
    * Returns the label of the xcode_config rule to use for resolving the host system xcode version.
    */
+  @SkylarkConfigurationField(
+      name = "xcode_config_label",
+      doc = "Returns the target denoted by the value of the --xcode_version_config flag",
+      defaultLabel = AppleCommandLineOptions.DEFAULT_XCODE_VERSION_CONFIG_LABEL,
+      defaultInToolRepository = true
+  )
   public Label getXcodeConfigLabel() {
     return xcodeConfigLabel;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkUtils.java
index 970543f..397768a 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkUtils.java
@@ -14,12 +14,15 @@
 
 package com.google.devtools.build.lib.syntax;
 
+import com.google.common.collect.ImmutableMap;
+
 /** This class contains Bazel-specific functions to extend or interoperate with Skylark. */
 public final class SkylarkUtils {
 
   /** Bazel-specific information that we store in the Environment. */
   private static class BazelInfo {
     String toolsRepository;
+    ImmutableMap<String, Class<?>> fragmentNameToClass;
   }
 
   private static final String BAZEL_INFO_KEY = "$bazel";
@@ -46,4 +49,21 @@
   public static String getToolsRepository(Environment env) {
     return getInfo(env).toolsRepository;
   }
+
+  /**
+   * Sets, on an {@link Environment}, a {@link Map} from configuration fragment name to
+   * configuration fragment class.
+   */
+  public static void setFragmentMap(Environment env,
+      ImmutableMap<String, Class<?>> fragmentNameToClass) {
+    getInfo(env).fragmentNameToClass = fragmentNameToClass;
+  }
+
+  /*
+   * Returns the {@link Map} from configuration fragment name to configuration fragment class, as
+   * set by {@link #setFragmentMap}.
+   */
+  public static ImmutableMap<String, Class<?>> getFragmentMap(Environment env) {
+    return getInfo(env).fragmentNameToClass;
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
index 516f5a6..bb588ec 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.expectThrows;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
@@ -2042,6 +2043,138 @@
     }
   }
 
+  @Test
+  public void testConfigurationField_invalidFragment() throws Exception {
+    scratch.file("test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return struct()",
+
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { '_myattr': attr.label(",
+        "        default = configuration_field(",
+        "        fragment = 'notarealfragment', name = 'method_name')),",
+        "    },",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        expectThrows(AssertionError.class,
+            () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected).hasMessageThat()
+        .contains("invalid configuration fragment name 'notarealfragment'");
+  }
+
+  @Test
+  public void testConfigurationField_doesNotChangeFragmentAccess() throws Exception {
+    scratch.file("test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return struct(platform = ctx.fragments.apple.single_arch_platform)",
+
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { '_myattr': attr.label(",
+        "        default = configuration_field(",
+        "        fragment = 'apple', name = 'xcode_config_label')),",
+        "    },",
+        "    fragments = [],",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        expectThrows(AssertionError.class,
+            () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected).hasMessageThat()
+        .contains("has to declare 'apple' as a required fragment in target configuration");
+  }
+
+  @Test
+  public void testConfigurationField_invalidFieldName() throws Exception {
+    scratch.file("test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return struct()",
+
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { '_myattr': attr.label(",
+        "        default = configuration_field(",
+        "        fragment = 'apple', name = 'notarealfield')),",
+        "    },",
+        "    fragments = ['apple'],",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        expectThrows(AssertionError.class,
+            () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected).hasMessageThat()
+        .contains("invalid configuration field name 'notarealfield' on fragment 'apple'");
+  }
+
+  // Verifies that configuration_field can only be used on 'private' attributes.
+  @Test
+  public void testConfigurationField_invalidVisibility() throws Exception {
+    scratch.file("test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return struct()",
+
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { 'myattr': attr.label(",
+        "        default = configuration_field(",
+        "        fragment = 'apple', name = 'xcode_config_label')),",
+        "    },",
+        "    fragments = ['apple'],",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        expectThrows(AssertionError.class,
+            () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected).hasMessageThat()
+        .contains("When an attribute value is a function, "
+            + "the attribute must be private (i.e. start with '_')");
+  }
+
+  // Verifies that configuration_field can only be used on 'label' attributes.
+  @Test
+  public void testConfigurationField_invalidAttributeType() throws Exception {
+    scratch.file("test/main_rule.bzl",
+        "def _impl(ctx):",
+        "  return struct()",
+
+        "main_rule = rule(implementation = _impl,",
+        "    attrs = { '_myattr': attr.int(",
+        "        default = configuration_field(",
+        "        fragment = 'apple', name = 'xcode_config_label')),",
+        "    },",
+        "    fragments = ['apple'],",
+        ")");
+
+    scratch.file("test/BUILD",
+        "load('//test:main_rule.bzl', 'main_rule')",
+        "main_rule(name='main')");
+
+    AssertionError expected =
+        expectThrows(AssertionError.class,
+            () -> getConfiguredTarget("//test:main"));
+
+    assertThat(expected).hasMessageThat()
+        .contains("argument 'default' has type 'SkylarkLateBoundDefault', but should be 'int'");
+  }
+
   private void setupThrowFunction(BuiltinFunction func) throws Exception {
     throwFunction = func;
     throwFunction.configure(