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.
+ }
+}