Implement split transition based on skylark functions

This change provides split transitions in skylark, using a skylark
function to specify the transition.

PiperOrigin-RevId: 215639497
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 ac2f9a9..cb0c927 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
@@ -514,6 +514,23 @@
     )
     public String outputDirectoryName;
 
+    /**
+     * This option is used by skylark transitions to add a disginguishing element to the output
+     * directory name, in order to avoid name clashing.
+     */
+    @Option(
+      name = "transition directory name fragment",
+      defaultValue = "null",
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {
+          OptionEffectTag.LOSES_INCREMENTAL_STATE,
+          OptionEffectTag.AFFECTS_OUTPUTS,
+          OptionEffectTag.LOADING_AND_ANALYSIS
+      },
+      metadataTags = { OptionMetadataTag.INTERNAL }
+    )
+    public String transitionDirectoryNameFragment;
+
     @Option(
       name = "platform_suffix",
       defaultValue = "null",
@@ -1396,6 +1413,9 @@
       nameParts.add(fragment.getOutputDirectoryName());
     }
     nameParts.add(getCompilationMode() + platformSuffix);
+    if (options.transitionDirectoryNameFragment != null) {
+      nameParts.add(options.transitionDirectoryNameFragment);
+    }
     return Joiner.on('-').skipNulls().join(nameParts);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionSplitTransitionProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionSplitTransitionProvider.java
new file mode 100644
index 0000000..bfe0f7f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionSplitTransitionProvider.java
@@ -0,0 +1,337 @@
+// Copyright 2018 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 static java.nio.charset.StandardCharsets.US_ASCII;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.analysis.config.transitions.SplitTransition;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Attribute.SplitTransitionProvider;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.syntax.BaseFunction;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Mutability;
+import com.google.devtools.build.lib.syntax.SkylarkDict;
+import com.google.devtools.build.lib.syntax.SkylarkSemantics;
+import com.google.devtools.common.options.OptionDefinition;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import java.lang.reflect.Field;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class implements a split transition provider that takes a Skylark transition function as
+ * input.  The transition function takes a settings argument, which is a dictionary containing the
+ * current option values.  It either returns a dictionary mapping option name to new option value
+ * (for a patch transition), or a dictionary of such dictionaries (for a split transition).
+ *
+ * Currently the implementation ignores the attributes provided by the containing function.
+ */
+public class FunctionSplitTransitionProvider implements SplitTransitionProvider {
+  private final BaseFunction transitionFunction;
+  private final SkylarkSemantics semantics;
+  private final EventHandler eventHandler;
+
+  public FunctionSplitTransitionProvider(BaseFunction transitionFunction,
+      SkylarkSemantics semantics, EventHandler eventHandler) {
+    this.transitionFunction = transitionFunction;
+    this.semantics = semantics;
+    this.eventHandler = eventHandler;
+  }
+
+  @Override
+  public SplitTransition apply(AttributeMap attributeMap) {
+    return new FunctionSplitTransition(transitionFunction, semantics, eventHandler);
+  }
+
+  private static class FunctionSplitTransition implements SplitTransition {
+    private final BaseFunction transitionFunction;
+    private final SkylarkSemantics semantics;
+    private final EventHandler eventHandler;
+
+    public FunctionSplitTransition(BaseFunction transitionFunction, SkylarkSemantics semantics,
+        EventHandler eventHandler) {
+      this.transitionFunction = transitionFunction;
+      this.semantics = semantics;
+      this.eventHandler = eventHandler;
+    }
+
+    @Override
+    public final List<BuildOptions> split(BuildOptions buildOptions) {
+      // TODO(waltl): we should be able to build this once and use it across different split
+      // transitions.
+      Map<String, OptionInfo> optionInfoMap = buildOptionInfo(buildOptions);
+      SkylarkDict<String, Object> settings = buildSettings(buildOptions, optionInfoMap);
+
+      ImmutableList.Builder<BuildOptions> splitBuildOptions = ImmutableList.builder();
+
+      try {
+        ImmutableList<Map<String, Object>> transitions =
+            evalTransitionFunction(transitionFunction, settings);
+
+        for (Map<String, Object> transition : transitions) {
+          BuildOptions options = buildOptions.clone();
+          applyTransition(options, transition, optionInfoMap);
+          splitBuildOptions.add(options);
+        }
+      } catch (InterruptedException e) {
+        throw new RuntimeException(e);
+      } catch (EvalException e) {
+        throw new RuntimeException(e.print());
+      }
+
+      return splitBuildOptions.build();
+    }
+
+    /**
+     * For all the options in the BuildOptions, build a map from option name to its information.
+     */
+    private Map<String, OptionInfo> buildOptionInfo(BuildOptions buildOptions) {
+      ImmutableMap.Builder<String, OptionInfo> builder = new ImmutableMap.Builder<>();
+
+      ImmutableSet<Class<? extends FragmentOptions>> optionClasses =
+          buildOptions
+          .getNativeOptions()
+          .stream()
+          .map(FragmentOptions::getClass)
+          .collect(ImmutableSet.toImmutableSet());
+
+      for (Class<? extends FragmentOptions> optionClass : optionClasses) {
+        ImmutableList<OptionDefinition> optionDefinitions =
+            OptionsParser.getOptionDefinitions(optionClass);
+        for (OptionDefinition def : optionDefinitions) {
+          String optionName = def.getOptionName();
+          builder.put(optionName, new OptionInfo(optionClass, def));
+        }
+      }
+
+      return builder.build();
+    }
+
+    /**
+     * Enter the options in buildOptions into a skylark dictionary, and return the dictionary.
+     *
+     * @throws IllegalArgumentException If the method is unable to look up the value in buildOptions
+     *     corresponding to an entry in optionInfoMap.
+     * @throws RuntimeException If the field corresponding to an option value in buildOptions is
+     *     inaccessible due to Java language access control, or if an option name is an invalid key
+     *     to the Skylark dictionary.
+     */
+    private SkylarkDict<String, Object> buildSettings(BuildOptions buildOptions,
+        Map<String, OptionInfo> optionInfoMap) {
+      try (Mutability mutability = Mutability.create("build_settings")) {
+        SkylarkDict<String, Object> dict = SkylarkDict.withMutability(mutability);
+
+        for (Map.Entry<String, OptionInfo> entry : optionInfoMap.entrySet()) {
+          String optionName = entry.getKey();
+          OptionInfo optionInfo = entry.getValue();
+
+          try {
+            Field field = optionInfo.getDefinition().getField();
+            FragmentOptions options = buildOptions.get(optionInfo.getOptionClass());
+            Object optionValue = field.get(options);
+
+            dict.put(optionName, optionValue, null, mutability);
+          } catch (IllegalAccessException | EvalException e) {
+            // These exceptions should not happen, but if they do, throw a RuntimeException.
+            throw new RuntimeException(e);
+          }
+        }
+
+        return dict;
+      }
+    }
+
+    /**
+     * Evaluate the input function with the given argument, and return the return value.
+     */
+    private Object evalFunction(BaseFunction function, Object arg)
+        throws InterruptedException, EvalException {
+      try (Mutability mutability = Mutability.create("eval_transition_function")) {
+        Environment env =
+            Environment.builder(mutability)
+            .setSemantics(semantics)
+            .setEventHandler(eventHandler)
+            .build();
+
+        return function.call(ImmutableList.of(arg), ImmutableMap.of(), null, env);
+      }
+    }
+
+    /**
+     * Evaluate the transition function, and convert the result into a list of optionName ->
+     * optionValue dictionaries.
+     */
+    private ImmutableList<Map<String, Object>> evalTransitionFunction(BaseFunction function,
+        SkylarkDict<String, Object> settings)
+        throws InterruptedException, EvalException {
+      Object result;
+      try {
+        result = evalFunction(function, settings);
+      } catch (EvalException e) {
+        throw new EvalException(function.getLocation(), e.getMessage());
+      }
+
+      if (!(result instanceof SkylarkDict<?, ?>)) {
+        throw new EvalException(function.getLocation(),
+            "Transition function must return a dictionary.");
+      }
+
+      // The result is either:
+      // 1. a dictionary mapping option name to new option value (for a single transition), or
+      // 2. a dictionary of such dictionaries (for a split transition).
+      //
+      // First try to parse the result as a dictionary of option dictionaries; then try it as an
+      // option dictionary.
+      SkylarkDict<?, ?> dictOrDictOfDict = (SkylarkDict<?, ?>) result;
+
+      try {
+        Map<String, SkylarkDict> dictOfDict = dictOrDictOfDict.getContents(String.class,
+            SkylarkDict.class, "dictionary of option dictionaries");
+
+        ImmutableList.Builder<Map<String, Object>> builder = ImmutableList.builder();
+        for (Map.Entry<String, SkylarkDict> entry : dictOfDict.entrySet()) {
+          Map<String, Object> dict =
+              entry.getValue().getContents(String.class, Object.class, "an option dictionary");
+          builder.add(dict);
+        }
+        return builder.build();
+      } catch (EvalException e) {
+        // Fall through.
+      }
+
+      Map<String, Object> dict;
+      try {
+        dict = dictOrDictOfDict.getContents(String.class, Object.class, "an option dictionary");
+      } catch (EvalException e) {
+        throw new EvalException(function.getLocation(), e.getMessage());
+      }
+
+      return ImmutableList.of(dict);
+    }
+
+    /**
+     * Apply the transition dictionary to the build option, using optionInfoMap to look up the
+     * option info.
+     *
+     * @throws RuntimeException If a requested option field is inaccessible.
+     */
+    private void applyTransition(BuildOptions buildOptions, Map<String, Object> transition,
+        Map<String, OptionInfo> optionInfoMap)
+        throws EvalException {
+      for (Map.Entry<String, Object> entry : transition.entrySet()) {
+        String optionName = entry.getKey();
+        Object optionValue = entry.getValue();
+
+        try {
+          if (!optionInfoMap.containsKey(optionName)) {
+            throw new EvalException(transitionFunction.getLocation(),
+                "Unknown option '" + optionName + "'");
+          }
+
+          OptionInfo optionInfo = optionInfoMap.get(optionName);
+          OptionDefinition def = optionInfo.getDefinition();
+          Field field = def.getField();
+          FragmentOptions options = buildOptions.get(optionInfo.getOptionClass());
+          if (optionValue == null || def.getType().isInstance(optionValue)) {
+            field.set(options, optionValue);
+          } else if (optionValue instanceof String) {
+            field.set(options, def.getConverter().convert((String) optionValue));
+          } else {
+            throw new EvalException(transitionFunction.getLocation(),
+                "Invalid value type for option '" + optionName + "'");
+          }
+        } catch (IllegalAccessException e) {
+          throw new RuntimeException(
+              "IllegalAccess for option " + optionName + ": " + e.getMessage());
+        } catch (OptionsParsingException e) {
+          throw new EvalException(transitionFunction.getLocation(),
+              "OptionsParsingError for option '" + optionName + "': " + e.getMessage());
+        }
+      }
+
+      BuildConfiguration.Options buildConfigOptions;
+      buildConfigOptions = buildOptions.get(BuildConfiguration.Options.class);
+      updateOutputDirectoryNameFragment(buildConfigOptions, transition);
+    }
+
+    /**
+     * Compute the output directory name fragment corresponding to the transition, and append it to
+     * the existing name fragment in buildConfigOptions.
+     *
+     * @throws IllegalStateException If MD5 support is not available.
+     */
+    private void updateOutputDirectoryNameFragment(BuildConfiguration.Options buildConfigOptions,
+        Map<String, Object> transition) {
+      String transitionString = new String();
+      for (Map.Entry<String, Object> entry : transition.entrySet()) {
+        transitionString += entry.getKey() + ":";
+        if (entry.getValue() != null) {
+          transitionString += entry.getValue().toString() + "@";
+        }
+      }
+
+      // TODO(waltl): for transitions that don't read settings, it is possible to precompute and
+      // reuse the MD5 digest and even the transition itself.
+      try {
+        byte[] bytes = transitionString.getBytes(US_ASCII);
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        byte[] digest = md.digest(bytes);
+        String hexDigest = BaseEncoding.base16().lowerCase().encode(digest);
+
+        if (buildConfigOptions.transitionDirectoryNameFragment == null) {
+          buildConfigOptions.transitionDirectoryNameFragment = hexDigest;
+        } else {
+          buildConfigOptions.transitionDirectoryNameFragment += "-" + hexDigest;
+        }
+      } catch (NoSuchAlgorithmException e) {
+        throw new IllegalStateException("MD5 not available", e);
+      }
+    }
+
+    /**
+     * Stores option info useful to a FunctionSplitTransition.
+     */
+    private static class OptionInfo {
+      private final Class<? extends FragmentOptions> optionClass;
+      private final OptionDefinition definition;
+
+      public OptionInfo(Class<? extends FragmentOptions> optionClass,
+          OptionDefinition definition) {
+        this.optionClass = optionClass;
+        this.definition = definition;
+      }
+
+      Class<? extends FragmentOptions> getOptionClass() {
+        return optionClass;
+      }
+
+      OptionDefinition getDefinition() {
+        return definition;
+      }
+    }
+  }
+}
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 2ffa0e7..13cce13 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
@@ -35,6 +35,7 @@
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAttrApi;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+import com.google.devtools.build.lib.syntax.BaseFunction;
 import com.google.devtools.build.lib.syntax.Environment;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.EvalUtils;
@@ -254,6 +255,10 @@
         builder.cfg((SplitTransition) trans);
       } else if (trans instanceof SplitTransitionProvider) {
         builder.cfg((SplitTransitionProvider) trans);
+      } else if (trans instanceof BaseFunction) {
+        builder.hasFunctionTransition();
+        builder.cfg(new FunctionSplitTransitionProvider((BaseFunction) trans,
+                env.getSemantics(), env.getEventHandler()));
       } else if (!trans.equals("target")) {
         throw new EvalException(ast.getLocation(),
             "cfg must be either 'data', 'host', or 'target'.");
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelPrerequisiteValidator.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelPrerequisiteValidator.java
index bb37e43..b07186a 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelPrerequisiteValidator.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelPrerequisiteValidator.java
@@ -19,6 +19,7 @@
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
 import com.google.devtools.build.lib.analysis.RuleContext;
 import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.FunctionSplitTransitionWhitelist;
 import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
 import com.google.devtools.build.lib.packages.PackageGroup;
 import com.google.devtools.build.lib.packages.RawAttributeMapper;
@@ -85,7 +86,9 @@
       boolean containsPackageSpecificationProvider =
           requiredProviders.getDescription().contains("PackageSpecificationProvider");
       // TODO(plf): Add the PackageSpecificationProvider to the 'visibility' attribute.
-      if (!attrName.equals("visibility") && !containsPackageSpecificationProvider) {
+      if (!attrName.equals("visibility")
+          && !attrName.equals(FunctionSplitTransitionWhitelist.WHITELIST_ATTRIBUTE_NAME)
+          && !containsPackageSpecificationProvider) {
         context.attributeError(
             attrName,
             "in "
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkDict.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkDict.java
index 9157de4..af09903 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkDict.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkDict.java
@@ -454,7 +454,9 @@
         ? null : Printer.formattable("'%s' value", description);
     for (Map.Entry<?, ?> e : this.entrySet()) {
       SkylarkType.checkType(e.getKey(), keyType, keyDescription);
-      SkylarkType.checkType(e.getValue(), valueType, valueDescription);
+      if (e.getValue() != null) {
+        SkylarkType.checkType(e.getValue(), valueType, valueDescription);
+      }
     }
     return Collections.unmodifiableMap((SkylarkDict<X, Y>) this);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index 8e93fb6..3658258 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -823,6 +823,7 @@
         "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
         "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
+        "//src/test/java/com/google/devtools/build/lib:packages_testutil",
         "//src/test/java/com/google/devtools/build/lib/rules/platform:testutil",
         "//src/test/java/com/google/devtools/build/lib/skyframe:testutil",
         "//third_party:auto_value",
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/FunctionSplitTransitionProviderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/FunctionSplitTransitionProviderTest.java
new file mode 100644
index 0000000..10b13f8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/FunctionSplitTransitionProviderTest.java
@@ -0,0 +1,284 @@
+// Copyright 2018 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.packages.util.BazelMockAndroidSupport;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for FunctionSplitTransitionProvider.
+ */
+@RunWith(JUnit4.class)
+public class FunctionSplitTransitionProviderTest extends BuildViewTestCase {
+  private void writeWhitelistFile() throws Exception {
+    scratch.file(
+        "tools/whitelists/function_transition_whitelist/BUILD",
+        "package_group(",
+        "    name = 'function_transition_whitelist',",
+        "    packages = [",
+        "        '//test/skylark/...',",
+        "    ],",
+        ")");
+  }
+
+  private void writeBasicTestFiles() throws Exception {
+    writeWhitelistFile();
+
+    scratch.file(
+        "test/skylark/my_rule.bzl",
+        "def transition_func(settings):",
+        "  return {",
+        "      't0': {'cpu': 'k8'},",
+        "      't1': {'cpu': 'armeabi-v7a'},",
+        "  }",
+        "def impl(ctx): ",
+        "  return struct(",
+        "    split_attr_deps = ctx.split_attr.deps,",
+        "    split_attr_dep = ctx.split_attr.dep,",
+        "    k8_deps = ctx.split_attr.deps.get('k8', None),",
+        "    attr_deps = ctx.attr.deps,",
+        "    attr_dep = ctx.attr.dep)",
+        "my_rule = rule(",
+        "  implementation = impl,",
+        "  attrs = {",
+        "    'deps': attr.label_list(cfg = transition_func),",
+        "    'dep':  attr.label(cfg = transition_func),",
+        "    '_whitelist_function_transition': attr.label(",
+        "        default = '//tools/whitelists/function_transition_whitelist',",
+        "    ),",
+        "  })");
+
+    scratch.file(
+        "test/skylark/BUILD",
+        "load('//test/skylark:my_rule.bzl', 'my_rule')",
+        "my_rule(name = 'test', deps = [':main1', ':main2'], dep = ':main1')",
+        "cc_binary(name = 'main1', srcs = ['main1.c'])",
+        "cc_binary(name = 'main2', srcs = ['main2.c'])");
+  }
+
+  @Test
+  public void testFunctionSplitTransitionCheckSplitAttrDeps() throws Exception {
+    writeBasicTestFiles();
+    testSplitTransitionCheckSplitAttrDeps(getConfiguredTarget("//test/skylark:test"));
+  }
+
+  @Test
+  public void testFunctionSplitTransitionCheckSplitAttrDep() throws Exception {
+    writeBasicTestFiles();
+    testSplitTransitionCheckSplitAttrDep(getConfiguredTarget("//test/skylark:test"));
+  }
+
+  @Test
+  public void testFunctionSplitTransitionCheckAttrDeps() throws Exception {
+    writeBasicTestFiles();
+    testSplitTransitionCheckAttrDeps(getConfiguredTarget("//test/skylark:test"));
+  }
+
+  @Test
+  public void testFunctionSplitTransitionCheckAttrDep() throws Exception {
+    writeBasicTestFiles();
+    testSplitTransitionCheckAttrDep(getConfiguredTarget("//test/skylark:test"));
+  }
+
+  @Test
+  public void testFunctionSplitTransitionCheckK8Deps() throws Exception {
+    writeBasicTestFiles();
+    testSplitTransitionCheckK8Deps(getConfiguredTarget("//test/skylark:test"));
+  }
+
+  private void testSplitTransitionCheckSplitAttrDeps(ConfiguredTarget target) throws Exception {
+    // Check that ctx.split_attr.deps has this structure:
+    // {
+    //   "k8": [ConfiguredTarget],
+    //   "armeabi-v7a": [ConfiguredTarget],
+    // }
+    @SuppressWarnings("unchecked")
+    Map<String, List<ConfiguredTarget>> splitDeps =
+        (Map<String, List<ConfiguredTarget>>) target.get("split_attr_deps");
+    assertThat(splitDeps).containsKey("k8");
+    assertThat(splitDeps).containsKey("armeabi-v7a");
+    assertThat(splitDeps.get("k8")).hasSize(2);
+    assertThat(splitDeps.get("armeabi-v7a")).hasSize(2);
+    assertThat(getConfiguration(splitDeps.get("k8").get(0)).getCpu()).isEqualTo("k8");
+    assertThat(getConfiguration(splitDeps.get("k8").get(1)).getCpu()).isEqualTo("k8");
+    assertThat(getConfiguration(splitDeps.get("armeabi-v7a").get(0)).getCpu()).isEqualTo("armeabi-v7a");
+    assertThat(getConfiguration(splitDeps.get("armeabi-v7a").get(1)).getCpu()).isEqualTo("armeabi-v7a");
+  }
+
+  private void testSplitTransitionCheckSplitAttrDep(ConfiguredTarget target) throws Exception {
+    // Check that ctx.split_attr.dep has this structure (that is, that the values are not lists):
+    // {
+    //   "k8": ConfiguredTarget,
+    //   "armeabi-v7a": ConfiguredTarget,
+    // }
+    @SuppressWarnings("unchecked")
+    Map<String, ConfiguredTarget> splitDep =
+        (Map<String, ConfiguredTarget>) target.get("split_attr_dep");
+    assertThat(splitDep).containsKey("k8");
+    assertThat(splitDep).containsKey("armeabi-v7a");
+    assertThat(getConfiguration(splitDep.get("k8")).getCpu()).isEqualTo("k8");
+    assertThat(getConfiguration(splitDep.get("armeabi-v7a")).getCpu()).isEqualTo("armeabi-v7a");
+  }
+
+  private void testSplitTransitionCheckAttrDeps(ConfiguredTarget target) throws Exception {
+    // The regular ctx.attr.deps should be a single list with all the branches of the split merged
+    // together (i.e. for aspects).
+    @SuppressWarnings("unchecked")
+    List<ConfiguredTarget> attrDeps = (List<ConfiguredTarget>) target.get("attr_deps");
+    assertThat(attrDeps).hasSize(4);
+    ListMultimap<String, Object> attrDepsMap = ArrayListMultimap.create();
+    for (ConfiguredTarget ct : attrDeps) {
+      attrDepsMap.put(getConfiguration(ct).getCpu(), target);
+    }
+    assertThat(attrDepsMap).valuesForKey("k8").hasSize(2);
+    assertThat(attrDepsMap).valuesForKey("armeabi-v7a").hasSize(2);
+  }
+
+  private void testSplitTransitionCheckAttrDep(ConfiguredTarget target) throws Exception {
+    // Check that even though my_rule.dep is defined as a single label, ctx.attr.dep is still a list
+    // with multiple ConfiguredTarget objects because of the two different CPUs.
+    @SuppressWarnings("unchecked")
+    List<ConfiguredTarget> attrDep = (List<ConfiguredTarget>) target.get("attr_dep");
+    assertThat(attrDep).hasSize(2);
+    ListMultimap<String, Object> attrDepMap = ArrayListMultimap.create();
+    for (ConfiguredTarget ct : attrDep) {
+      attrDepMap.put(getConfiguration(ct).getCpu(), target);
+    }
+    assertThat(attrDepMap).valuesForKey("k8").hasSize(1);
+    assertThat(attrDepMap).valuesForKey("armeabi-v7a").hasSize(1);
+  }
+
+  private void testSplitTransitionCheckK8Deps(ConfiguredTarget target) throws Exception {
+    // Check that the deps were correctly accessed from within Skylark.
+    @SuppressWarnings("unchecked")
+    List<ConfiguredTarget> k8Deps = (List<ConfiguredTarget>) target.get("k8_deps");
+    assertThat(k8Deps).hasSize(2);
+    assertThat(getConfiguration(k8Deps.get(0)).getCpu()).isEqualTo("k8");
+    assertThat(getConfiguration(k8Deps.get(1)).getCpu()).isEqualTo("k8");
+  }
+
+  private void writeReadSettingsTestFiles() throws Exception {
+    writeWhitelistFile();
+
+    scratch.file(
+        "test/skylark/my_rule.bzl",
+        "def transition_func(settings):",
+        "  transitions = {}",
+        "  for cpu in settings['fat_apk_cpu']:",
+        "    transitions[cpu] = {",
+        "      'cpu': cpu,",
+        "    }",
+        "  return transitions",
+        "def impl(ctx): ",
+        "  return struct(split_attr_dep = ctx.split_attr.dep)",
+        "my_rule = rule(",
+        "  implementation = impl,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = transition_func),",
+        "    '_whitelist_function_transition': attr.label(",
+        "        default = '//tools/whitelists/function_transition_whitelist',",
+        "    ),",
+        "  })");
+
+    scratch.file(
+        "test/skylark/BUILD",
+        "load('//test/skylark:my_rule.bzl', 'my_rule')",
+        "my_rule(name = 'test', dep = ':main')",
+        "cc_binary(name = 'main', srcs = ['main.c'])");
+  }
+
+  @Test
+  public void testReadSettingsSplitDepAttrDep() throws Exception {
+    // Check that ctx.split_attr.dep has this structure:
+    // {
+    //   "k8": ConfiguredTarget,
+    //   "armeabi-v7a": ConfiguredTarget,
+    // }
+    writeReadSettingsTestFiles();
+
+    useConfiguration("--fat_apk_cpu=k8,armeabi-v7a");
+    ConfiguredTarget target = getConfiguredTarget("//test/skylark:test");
+
+    @SuppressWarnings("unchecked")
+    Map<String, ConfiguredTarget> splitDep =
+        (Map<String, ConfiguredTarget>) target.get("split_attr_dep");
+    assertThat(splitDep).containsKey("k8");
+    assertThat(splitDep).containsKey("armeabi-v7a");
+    assertThat(getConfiguration(splitDep.get("k8")).getCpu()).isEqualTo("k8");
+    assertThat(getConfiguration(splitDep.get("armeabi-v7a")).getCpu()).isEqualTo("armeabi-v7a");
+  }
+
+  private void writeOptionConversionTestFiles() throws Exception {
+    writeWhitelistFile();
+
+    scratch.file(
+        "test/skylark/my_rule.bzl",
+        "def transition_func(settings):",
+        "  return {",
+        "    'cpu': 'armeabi-v7a',",
+        "    'dynamic_mode': 'off',",
+        "    'crosstool_top': '//android/crosstool:everything',",
+        "  }",
+        "def impl(ctx): ",
+        "  return struct(split_attr_dep = ctx.split_attr.dep)",
+        "my_rule = rule(",
+        "  implementation = impl,",
+        "  attrs = {",
+        "    'dep':  attr.label(cfg = transition_func),",
+        "    '_whitelist_function_transition': attr.label(",
+        "        default = '//tools/whitelists/function_transition_whitelist',",
+        "    ),",
+        "  })");
+
+    scratch.file(
+        "test/skylark/BUILD",
+        "load('//test/skylark:my_rule.bzl', 'my_rule')",
+        "my_rule(name = 'test', dep = ':main')",
+        "cc_binary(name = 'main', srcs = ['main.c'])");
+  }
+
+  @Test
+  public void testOptionConversionCpu() throws Exception {
+    writeOptionConversionTestFiles();
+    BazelMockAndroidSupport.setupNdk(mockToolsConfig);
+
+    ConfiguredTarget target = getConfiguredTarget("//test/skylark:test");
+
+    @SuppressWarnings("unchecked")
+    Map<String, ConfiguredTarget> splitDep =
+        (Map<String, ConfiguredTarget>) target.get("split_attr_dep");
+    assertThat(splitDep).containsKey("armeabi-v7a");
+    assertThat(getConfiguration(splitDep.get("armeabi-v7a")).getCpu()).isEqualTo("armeabi-v7a");
+  }
+
+  @Test
+  public void testOptionConversionDynamicMode() throws Exception {
+    // TODO(waltl): check that dynamic_mode is parsed properly.
+  }
+
+  @Test
+  public void testOptionConversionCrosstoolTop() throws Exception {
+    // TODO(waltl): check that crosstool_top is parsed properly.
+  }
+}