Give --define a new Starlark friendly converter.
This allow us to read --define in starlark transitions.
RELNOTES: None.
PiperOrigin-RevId: 243342166
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 73fbc53..cbc7447 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
@@ -37,6 +37,7 @@
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.analysis.skylark.StarlarkConverter;
import com.google.devtools.build.lib.buildeventstream.BuildEventId;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
import com.google.devtools.build.lib.cmdline.Label;
@@ -50,6 +51,7 @@
import com.google.devtools.build.lib.skylarkbuildapi.BuildConfigurationApi;
import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
+import com.google.devtools.build.lib.syntax.SkylarkDict;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.RegexFilter;
import com.google.devtools.build.lib.vfs.Path;
@@ -321,15 +323,28 @@
help = "If true, the genfiles directory is folded into the bin directory.")
public boolean mergeGenfilesDirectory;
+ /**
+ * AssignmentConverter that also supports Starlarkization. For use with options that use {@link
+ * com.google.devtools.common.options.Converters.AssignmentConverter} and
+ * {@link @Option#allowMultiple}.
+ */
+ public static class StarlarkAssignmentConverter extends Converters.AssignmentConverter
+ implements StarlarkConverter<List<Map.Entry<String, String>>, Map.Entry<String, String>> {
+ @Override
+ public SkylarkDict<String, String> convertToStarlark(
+ List<Map.Entry<String, String>> entries) {
+ return SkylarkDict.copyOf(null, ImmutableMap.copyOf(entries));
+ }
+ }
+
@Option(
- name = "define",
- converter = Converters.AssignmentConverter.class,
- defaultValue = "",
- allowMultiple = true,
- documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
- effectTags = {OptionEffectTag.CHANGES_INPUTS, OptionEffectTag.AFFECTS_OUTPUTS},
- help = "Each --define option specifies an assignment for a build variable."
- )
+ name = "define",
+ converter = StarlarkAssignmentConverter.class,
+ defaultValue = "",
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
+ effectTags = {OptionEffectTag.CHANGES_INPUTS, OptionEffectTag.AFFECTS_OUTPUTS},
+ help = "Each --define option specifies an assignment for a build variable.")
public List<Map.Entry<String, String>> commandLineBuildVariables;
@Option(
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
index c014b8a6..d074579 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
@@ -17,6 +17,7 @@
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -34,6 +35,8 @@
import com.google.devtools.build.lib.syntax.Runtime.NoneType;
import com.google.devtools.build.lib.syntax.SkylarkDict;
import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
@@ -153,7 +156,7 @@
* @throws EvalException if any of the specified transition inputs do not correspond to a valid
* build setting
*/
- static SkylarkDict<String, Object> buildSettings(
+ static <S, T> SkylarkDict<String, Object> buildSettings(
BuildOptions buildOptions,
Map<String, OptionInfo> optionInfoMap,
StarlarkDefinedConfigTransition starlarkTransition)
@@ -178,10 +181,29 @@
Field field = optionInfo.getDefinition().getField();
FragmentOptions options = buildOptions.get(optionInfo.getOptionClass());
Object optionValue = field.get(options);
-
- dict.put(optionKey, optionValue == null ? Runtime.NONE : optionValue, null, mutability);
+ Converter<?> converter = optionInfo.getDefinition().getConverter();
+ if (converter instanceof StarlarkConverter) {
+ @SuppressWarnings("unchecked") // Type erasure
+ Object convertedValue =
+ ((StarlarkConverter<S, T>) converter).convertToStarlark((S) optionValue);
+ Preconditions.checkNotNull(convertedValue);
+ optionValue = convertedValue;
+ } else if (optionValue == null) {
+ optionValue = Runtime.NONE;
+ } else {
+ optionValue = SkylarkType.convertToSkylark(optionValue, mutability);
+ if (optionValue != null && !SkylarkType.isSkylarkAcceptable(optionValue.getClass())) {
+ throw new EvalException(
+ starlarkTransition.getLocationForErrorReporting(),
+ String.format(
+ "Unable to read option '%s' - option '%s' is Starlark incompatible.",
+ optionKey, optionKey));
+ }
+ }
+ dict.put(optionKey, optionValue, null, mutability);
} catch (IllegalAccessException e) {
- // These exceptions should not happen, but if they do, throw a RuntimeException.
+ // These exceptions should not happen since all options should be public, but if they
+ // do, throw a RuntimeException.
throw new RuntimeException(e);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkConverter.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkConverter.java
new file mode 100644
index 0000000..6798fab
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkConverter.java
@@ -0,0 +1,44 @@
+// Copyright 2019 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.devtools.build.lib.skylarkinterface.SkylarkValue;
+import com.google.devtools.common.options.Converter;
+
+/**
+ * A converter that also knows how to turn a native object into a Starlark object
+ *
+ * <p>This interface declares/inherits two converting methods:
+ *
+ * <p>For converting from command line to Java (inherited)
+ *
+ * <p>{@code convert} String -> Object<T>
+ *
+ * <p>For converting from Java to Starlark
+ *
+ * <p>{@code convertToStarlark} Object<S> -> SkylarkValue
+ *
+ * <p>In practice, <T> and <S> will almost always be the same type. An example of an exception to
+ * this rule is for options with {@code allowMultiple = true} for which <S> is a list of <T>
+ * objects.
+ */
+// TODO(juliexxia): Add Starlark -> Java converter method to this interface.
+public interface StarlarkConverter<S, T> extends Converter<T> {
+
+ /** Convert a java object of type S into a Starlark friendly value. */
+ // TODO(bazel-team): exapnd this interface to allow "primitive" types to also be returned here.
+ // Example case of where this would be useful is for {@code enum}s.
+ SkylarkValue convertToStarlark(S input);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
index 38e7710..d8583b4 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
@@ -25,7 +25,6 @@
import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
-import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -147,21 +146,6 @@
|| c.equals(Boolean.class);
}
- /**
- * Returns true if the type is acceptable to be returned to the Skylark language.
- */
- public static boolean isSkylarkAcceptable(Class<?> c) {
- return SkylarkValue.class.isAssignableFrom(c) // implements SkylarkValue
- || c.equals(String.class) // basic values
- || c.equals(Integer.class)
- || c.equals(Boolean.class)
- // there is a registered Skylark ancestor class (useful e.g. when using AutoValue)
- || SkylarkInterfaceUtils.getSkylarkModule(c) != null
- || ImmutableMap.class.isAssignableFrom(c) // will be converted to SkylarkDict
- || NestedSet.class.isAssignableFrom(c) // will be converted to SkylarkNestedSet
- || PathFragment.class.isAssignableFrom(c); // other known class
- }
-
// TODO(bazel-team): move the following few type-related functions to SkylarkType
/**
* Return the Skylark-type of {@code c}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java b/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
index b322d23..4bf6281 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
@@ -148,7 +148,7 @@
}
// TODO(bazel-team): get rid of this, by having everyone use the Skylark data structures
result = SkylarkType.convertToSkylark(result, method, env);
- if (result != null && !EvalUtils.isSkylarkAcceptable(result.getClass())) {
+ if (result != null && !SkylarkType.isSkylarkAcceptable(result.getClass())) {
throw new EvalException(
loc,
Printer.format(
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java
index a714782..84afa81 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java
@@ -26,9 +26,11 @@
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
+import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
@@ -663,6 +665,19 @@
return true;
}
+ /** Returns true if the type is acceptable to be returned to the Skylark language. */
+ public static boolean isSkylarkAcceptable(Class<?> c) {
+ return SkylarkValue.class.isAssignableFrom(c) // implements SkylarkValue
+ || c.equals(String.class) // basic values
+ || c.equals(Integer.class)
+ || c.equals(Boolean.class)
+ // there is a registered Skylark ancestor class (useful e.g. when using AutoValue)
+ || SkylarkInterfaceUtils.getSkylarkModule(c) != null
+ || ImmutableMap.class.isAssignableFrom(c) // will be converted to SkylarkDict
+ || NestedSet.class.isAssignableFrom(c) // will be converted to SkylarkNestedSet
+ || PathFragment.class.isAssignableFrom(c); // other known class
+ }
+
/**
* Throws EvalException if the type of the object is not allowed to be present in Skylark.
*/
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
index c56d4eb..03ef28f 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
@@ -21,6 +21,7 @@
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration.EmptyToNullLabelConverter;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Options.StarlarkAssignmentConverter;
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.FragmentOptions;
@@ -29,10 +30,11 @@
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
-import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsParsingException;
import java.util.List;
import java.util.Map;
import org.junit.Test;
@@ -61,13 +63,46 @@
@Option(
name = "allow_multiple",
- converter = Converters.AssignmentConverter.class,
+ converter = StarlarkAssignmentConverter.class,
defaultValue = "",
allowMultiple = true,
- documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
- effectTags = {OptionEffectTag.CHANGES_INPUTS, OptionEffectTag.AFFECTS_OUTPUTS},
- help = "Each --define option specifies an assignment for a build variable.")
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.NO_OP},
+ help = "An option that mimics the behavior of --define (allowMultiple=true + converter).")
public List<Map.Entry<String, String>> allowMultiple;
+
+ @Option(
+ name = "foo",
+ defaultValue = "",
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.NO_OP},
+ help = "A simple option for basic testing.")
+ public String foo;
+
+ /** TestObject to expose to Starlark. */
+ static class FooObject {}
+
+ /** A converter from String -> FooObject for foo_object option */
+ public static class FooConverter implements Converter<FooObject> {
+ @Override
+ public FooObject convert(String input) throws OptionsParsingException {
+ return new FooObject();
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "";
+ }
+ }
+
+ @Option(
+ name = "foo_object",
+ converter = FooConverter.class,
+ defaultValue = "",
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.NO_OP},
+ help = "An option with a non-Starlark friendly java form.")
+ public FooObject fooObject;
}
/** Loads a new {link @DummyTestFragment} instance. */
@@ -94,6 +129,7 @@
ConfiguredRuleClassProvider.Builder builder = new ConfiguredRuleClassProvider.Builder();
TestRuleClassProvider.addStandardRules(builder);
builder.addConfigurationFragment(new DummyTestLoader());
+ builder.addConfigurationOptions(BuildConfiguration.Options.class);
return builder.build();
}
@@ -927,4 +963,123 @@
.containsExactly(
Maps.immutableEntry("APRIL", "SHOWERS"), Maps.immutableEntry("MAY", "FLOWERS"));
}
+
+ @Test
+ public void testReadNativeOption_allowMultipleOptions() throws Exception {
+ writeWhitelistFile();
+ scratch.file(
+ "test/transitions.bzl",
+ "def _impl(settings, attr):",
+ " if settings['//command_line_option:allow_multiple']['APRIL'] == 'FOOLS':",
+ " return {",
+ " '//command_line_option:foo': 'post-transition'",
+ " }",
+ " else:",
+ " return {",
+ " '//command_line_option:foo': ''",
+ " }",
+ "my_transition = transition(",
+ " implementation = _impl,",
+ " inputs = ['//command_line_option:allow_multiple'],",
+ " outputs = ['//command_line_option:foo'])");
+ scratch.file(
+ "test/rules.bzl",
+ "load('//test:transitions.bzl', 'my_transition')",
+ "def _impl(ctx):",
+ " return []",
+ "my_rule = rule(",
+ " implementation = _impl,",
+ " cfg = my_transition,",
+ " attrs = {",
+ " '_whitelist_function_transition': attr.label(",
+ " default = '//tools/whitelists/function_transition_whitelist',",
+ " ),",
+ " })");
+ scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'test')");
+
+ useConfiguration("--allow_multiple=APRIL=FOOLS");
+
+ BuildConfiguration configuration = getConfiguration(getConfiguredTarget("//test"));
+ assertThat(configuration.getOptions().get(DummyTestOptions.class).foo)
+ .isEqualTo("post-transition");
+ }
+
+ @Test
+ public void testReadNativeOption_testDefine() throws Exception {
+ writeWhitelistFile();
+ scratch.file(
+ "test/transitions.bzl",
+ "def _impl(settings, attr):",
+ " if settings['//command_line_option:define']['APRIL'] == 'SHOWERS' and "
+ + "settings['//command_line_option:define']['MAY'] == 'FLOWERS':",
+ " return {'//command_line_option:foo': 'post-transition'}",
+ " else:",
+ " return {",
+ " '//command_line_option:foo': ''",
+ " }",
+ "my_transition = transition(",
+ " implementation = _impl,",
+ " inputs = ['//command_line_option:define'],",
+ " outputs = ['//command_line_option:foo'])");
+ scratch.file(
+ "test/rules.bzl",
+ "load('//test:transitions.bzl', 'my_transition')",
+ "def _impl(ctx):",
+ " return []",
+ "my_rule = rule(",
+ " implementation = _impl,",
+ " cfg = my_transition,",
+ " attrs = {",
+ " '_whitelist_function_transition': attr.label(",
+ " default = '//tools/whitelists/function_transition_whitelist',",
+ " ),",
+ " })");
+ scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'test')");
+
+ useConfiguration("--define=APRIL=SHOWERS", "--define=MAY=MORE_SHOWERS");
+
+ BuildConfiguration configuration = getConfiguration(getConfiguredTarget("//test"));
+ assertThat(configuration.getOptions().get(DummyTestOptions.class).foo).isEmpty();
+
+ useConfiguration("--define=APRIL=SHOWERS", "--define=MAY=FLOWERS");
+
+ configuration = getConfiguration(getConfiguredTarget("//test"));
+ assertThat(configuration.getOptions().get(DummyTestOptions.class).foo)
+ .isEqualTo("post-transition");
+ }
+
+ @Test
+ public void testReadNativeOption_noStarlarkConverter() throws Exception {
+ writeWhitelistFile();
+ scratch.file(
+ "test/transitions.bzl",
+ "def _impl(settings, attr):",
+ " return {",
+ " '//command_line_option:foo_object': settings['//command_line_option:foo_object'] ",
+ " }",
+ "my_transition = transition(",
+ " implementation = _impl,",
+ " inputs = ['//command_line_option:foo_object'],",
+ " outputs = ['//command_line_option:foo_object'])");
+ scratch.file(
+ "test/rules.bzl",
+ "load('//test:transitions.bzl', 'my_transition')",
+ "def _impl(ctx):",
+ " return []",
+ "my_rule = rule(",
+ " implementation = _impl,",
+ " cfg = my_transition,",
+ " attrs = {",
+ " '_whitelist_function_transition': attr.label(",
+ " default = '//tools/whitelists/function_transition_whitelist',",
+ " ),",
+ " })");
+ scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'test')");
+
+ reporter.removeHandler(failFastHandler);
+ getConfiguredTarget("//test");
+ assertContainsEvent(
+ "Unable to read option '//command_line_option:foo_object' - option"
+ + " '//command_line_option:foo_object' is Starlark incompatible");
+ }
}