Skip to content

Commit 37d39a2

Browse files
committed
Do not validate input-only settings in transitions
Starlark transition logic temporarily explicitly sets all input build settings of a transition to their defaults. Since #13971, these values are cleared after the transition. However, since then they have also been subject to type validation, which is not only unnecessary, but also breaks in the special case of a string build setting with allow_multiple. With this commit, input-only build settings at their literal default values are removed from the post-transition BuildOptions without going through validation. This is made possible by a refactoring of `StarlarkTransition#validate` that extracts the validation logic into a separate function and also improves some variable names.
1 parent a9fb0fc commit 37d39a2

File tree

2 files changed

+239
-72
lines changed

2 files changed

+239
-72
lines changed

src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkTransition.java

+107-72
Original file line numberDiff line numberDiff line change
@@ -267,31 +267,39 @@ public static Map<String, BuildOptions> validate(
267267
Map<PackageValue.Key, PackageValue> buildSettingPackages,
268268
Map<String, BuildOptions> toOptions)
269269
throws TransitionException {
270-
// Collect settings changed during this transition and their types. This includes settings that
271-
// were only used as inputs as to the transition and thus had their default values added to the
272-
// fromOptions, which in case of a no-op transition directly end up in toOptions.
273-
Map<Label, Rule> changedSettingToRule = Maps.newHashMap();
270+
// Collect settings that are inputs or outputs of the transition together with their types.
271+
// Output setting values will be validated and removed if set to their default.
272+
Map<Label, Rule> inputAndOutputSettingsToRule = Maps.newHashMap();
273+
// Collect settings that were only used as inputs to the transition and thus possibly had their
274+
// default values added to the fromOptions. They will be removed if set to ther default, but
275+
// should not be validated.
276+
Set<Label> inputOnlySettings = Sets.newHashSet();
274277
root.visit(
275278
(StarlarkTransitionVisitor)
276279
transition -> {
277-
ImmutableSet<Label> changedSettings =
280+
ImmutableSet<Label> inputAndOutputSettings =
278281
getRelevantStarlarkSettingsFromTransition(
279282
transition, Settings.INPUTS_AND_OUTPUTS);
280-
for (Label setting : changedSettings) {
281-
changedSettingToRule.put(
283+
ImmutableSet<Label> outputSettings =
284+
getRelevantStarlarkSettingsFromTransition(transition, Settings.OUTPUTS);
285+
for (Label setting : inputAndOutputSettings) {
286+
inputAndOutputSettingsToRule.put(
282287
setting, getActual(buildSettingPackages, setting).getAssociatedRule());
288+
if (!outputSettings.contains(setting)) {
289+
inputOnlySettings.add(setting);
290+
}
283291
}
284292
});
285293

286-
// Return early if no starlark settings were affected.
287-
if (changedSettingToRule.isEmpty()) {
294+
// Return early if the transition has neither inputs nor outputs (rare).
295+
if (inputAndOutputSettingsToRule.isEmpty()) {
288296
return toOptions;
289297
}
290298

291299
ImmutableMap.Builder<Label, Label> aliasToActualBuilder = new ImmutableMap.Builder<>();
292-
for (Map.Entry<Label, Rule> changedSettingWithRule : changedSettingToRule.entrySet()) {
293-
Label setting = changedSettingWithRule.getKey();
294-
Rule rule = changedSettingWithRule.getValue();
300+
for (Map.Entry<Label, Rule> settingWithRule : inputAndOutputSettingsToRule.entrySet()) {
301+
Label setting = settingWithRule.getKey();
302+
Rule rule = settingWithRule.getValue();
295303
if (!rule.getLabel().equals(setting)) {
296304
aliasToActualBuilder.put(setting, rule.getLabel());
297305
}
@@ -307,69 +315,27 @@ public static Map<String, BuildOptions> validate(
307315
BuildOptions.Builder cleanedOptions = null;
308316
// Clean up aliased values.
309317
BuildOptions options = unalias(entry.getValue(), aliasToActual);
310-
for (Map.Entry<Label, Rule> changedSettingWithRule : changedSettingToRule.entrySet()) {
318+
for (Map.Entry<Label, Rule> settingWithRule : inputAndOutputSettingsToRule.entrySet()) {
311319
// If the build setting was referenced in the transition via an alias, this is that alias
312-
Label maybeAliasSetting = changedSettingWithRule.getKey();
313-
Rule rule = changedSettingWithRule.getValue();
314-
// If the build setting was *not* referenced in the transition by an alias, this is the same
315-
// value as {@code maybeAliasSetting} above.
320+
Label maybeAliasSetting = settingWithRule.getKey();
321+
Rule rule = settingWithRule.getValue();
316322
Label actualSetting = rule.getLabel();
317-
Object newValue = options.getStarlarkOptions().get(actualSetting);
318-
// TODO(b/154132845): fix NPE occasionally observed here.
319-
Preconditions.checkState(
320-
newValue != null,
321-
"Error while attempting to validate new values from starlark"
322-
+ " transition(s) with the outputs %s. Post-transition configuration should include"
323-
+ " '%s' but only includes starlark options: %s. If you run into this error"
324-
+ " please ping b/154132845 or email [email protected].",
325-
changedSettingToRule.keySet(),
326-
actualSetting,
327-
options.getStarlarkOptions().keySet());
328-
boolean allowsMultiple = rule.getRuleClassObject().getBuildSetting().allowsMultiple();
329-
if (allowsMultiple) {
330-
// if this setting allows multiple settings
331-
if (!(newValue instanceof List)) {
332-
throw new TransitionException(
333-
String.format(
334-
"'%s' allows multiple values and must be set"
335-
+ " in transition using a starlark list instead of single value '%s'",
336-
actualSetting, newValue));
337-
}
338-
List<?> rawNewValueAsList = (List<?>) newValue;
339-
List<Object> convertedValue = new ArrayList<>();
340-
Type<?> type = rule.getRuleClassObject().getBuildSetting().getType();
341-
for (Object value : rawNewValueAsList) {
342-
try {
343-
convertedValue.add(type.convert(value, maybeAliasSetting));
344-
} catch (ConversionException e) {
345-
throw new TransitionException(e);
346-
}
347-
}
348-
if (convertedValue.equals(
349-
ImmutableList.of(rule.getAttr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME)))) {
350-
if (cleanedOptions == null) {
351-
cleanedOptions = options.toBuilder();
352-
}
353-
cleanedOptions.removeStarlarkOption(rule.getLabel());
354-
}
355-
} else {
356-
// if this setting does not allow multiple settings
357-
Object convertedValue;
358-
try {
359-
convertedValue =
360-
rule.getRuleClassObject()
361-
.getBuildSetting()
362-
.getType()
363-
.convert(newValue, maybeAliasSetting);
364-
} catch (ConversionException e) {
365-
throw new TransitionException(e);
366-
}
367-
if (convertedValue.equals(rule.getAttr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME))) {
368-
if (cleanedOptions == null) {
369-
cleanedOptions = options.toBuilder();
370-
}
371-
cleanedOptions.removeStarlarkOption(rule.getLabel());
323+
// Input-only settings may have had their literal default value added to the BuildOptions
324+
// so that the transition can read them. We have to remove these explicitly set value here
325+
// to preserve the invariant that Starlark settings at default values are not explicitly set
326+
// in the BuildOptions.
327+
final boolean isInputOnlySettingAtDefault =
328+
inputOnlySettings.contains(maybeAliasSetting) && rule.getAttr(
329+
STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME)
330+
.equals(options.getStarlarkOptions().get(actualSetting));
331+
// For output settings, the raw value returned by the transition first has to be validated
332+
// and converted to the proper type before it can be compared to the default value.
333+
if (isInputOnlySettingAtDefault || validateAndCheckIfAtDefault(
334+
rule, options, maybeAliasSetting, inputAndOutputSettingsToRule.keySet())) {
335+
if (cleanedOptions == null) {
336+
cleanedOptions = options.toBuilder();
372337
}
338+
cleanedOptions.removeStarlarkOption(rule.getLabel());
373339
}
374340
}
375341
// Keep the same instance if we didn't do anything to maintain reference equality later on.
@@ -381,6 +347,75 @@ public static Map<String, BuildOptions> validate(
381347
return cleanedOptionMap.buildOrThrow();
382348
}
383349

350+
/**
351+
* Validate the value of a particular build setting after a transition has been applied.
352+
*
353+
* @param buildSettingRule the build setting to validate.
354+
* @param options the {@link BuildOptions} reflecting the post-transition configuration.
355+
* @param maybeAliasSetting the label used to refer to the build setting in the transition,
356+
* possibly an alias. This is only used for error messages.
357+
* @param inputAndOutputSettings the transition input and output settings. This is only used for
358+
* error messages.
359+
* @return {@code true} if and only if the setting is set to its default value after the
360+
* transition.
361+
* @throws TransitionException if the value returned by the transition for this setting has an
362+
* invalid type.
363+
*/
364+
private static boolean validateAndCheckIfAtDefault(Rule buildSettingRule, BuildOptions options,
365+
Label maybeAliasSetting, Set<Label> inputAndOutputSettings) throws TransitionException {
366+
// If the build setting was *not* referenced in the transition by an alias, this is the same
367+
// value as {@code maybeAliasSetting}.
368+
Label actualSetting = buildSettingRule.getLabel();
369+
Object newValue = options.getStarlarkOptions().get(actualSetting);
370+
// TODO(b/154132845): fix NPE occasionally observed here.
371+
Preconditions.checkState(
372+
newValue != null,
373+
"Error while attempting to validate new values from starlark"
374+
+ " transition(s) with the inputs and outputs %s. Post-transition configuration should"
375+
+ " include '%s' but only includes starlark options: %s. If you run into this error"
376+
+ " please ping b/154132845 or email [email protected].",
377+
inputAndOutputSettings,
378+
actualSetting,
379+
options.getStarlarkOptions().keySet());
380+
boolean allowsMultiple = buildSettingRule.getRuleClassObject().getBuildSetting()
381+
.allowsMultiple();
382+
if (allowsMultiple) {
383+
// if this setting allows multiple settings
384+
if (!(newValue instanceof List)) {
385+
throw new TransitionException(
386+
String.format(
387+
"'%s' allows multiple values and must be set"
388+
+ " in transition using a starlark list instead of single value '%s'",
389+
actualSetting, newValue));
390+
}
391+
List<?> rawNewValueAsList = (List<?>) newValue;
392+
List<Object> convertedValue = new ArrayList<>();
393+
Type<?> type = buildSettingRule.getRuleClassObject().getBuildSetting().getType();
394+
for (Object value : rawNewValueAsList) {
395+
try {
396+
convertedValue.add(type.convert(value, maybeAliasSetting));
397+
} catch (ConversionException e) {
398+
throw new TransitionException(e);
399+
}
400+
}
401+
return convertedValue.equals(
402+
ImmutableList.of(buildSettingRule.getAttr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME)));
403+
} else {
404+
// if this setting does not allow multiple settings
405+
Object convertedValue;
406+
try {
407+
convertedValue =
408+
buildSettingRule.getRuleClassObject()
409+
.getBuildSetting()
410+
.getType()
411+
.convert(newValue, maybeAliasSetting);
412+
} catch (ConversionException e) {
413+
throw new TransitionException(e);
414+
}
415+
return convertedValue.equals(buildSettingRule.getAttr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME));
416+
}
417+
}
418+
384419
/*
385420
* Resolve aliased build setting issues
386421
*

src/test/java/com/google/devtools/build/lib/analysis/StarlarkAttrTransitionProviderTest.java

+132
Original file line numberDiff line numberDiff line change
@@ -2549,4 +2549,136 @@ public void testExplicitNoopTransitionTrimsInputBuildSettings() throws Exception
25492549
new EventBus());
25502550
assertNoEvents();
25512551
}
2552+
2553+
@Test
2554+
public void testTransitionPreservesAllowMultipleDefault() throws Exception {
2555+
writeAllowlistFile();
2556+
scratch.file(
2557+
"test/starlark/rules.bzl",
2558+
"P = provider(fields = ['value'])",
2559+
"def _s_impl(ctx):",
2560+
" return [P(value = ctx.build_setting_value)]",
2561+
"def _t_impl(settings, attr):",
2562+
" if 'foo' in settings['//test/starlark:a']:",
2563+
" return {'//test/starlark:b': ['bar']}",
2564+
" else:",
2565+
" return {'//test/starlark:b': ['baz']}",
2566+
"def _r_impl(ctx):",
2567+
" pass",
2568+
"s = rule(",
2569+
" implementation = _s_impl,",
2570+
" build_setting = config.string(allow_multiple = True, flag = True),",
2571+
")",
2572+
"t = transition(",
2573+
" implementation = _t_impl,",
2574+
" inputs = ['//test/starlark:a'],",
2575+
" outputs = ['//test/starlark:b'],",
2576+
")",
2577+
"r = rule(",
2578+
" implementation = _r_impl,",
2579+
" attrs = {",
2580+
" 'setting': attr.label(cfg = t),",
2581+
" '_allowlist_function_transition': attr.label(",
2582+
" default = '//tools/allowlists/function_transition_allowlist',",
2583+
" ),",
2584+
" },",
2585+
")");
2586+
scratch.file(
2587+
"test/starlark/BUILD",
2588+
"load(':rules.bzl', 's', 'r')",
2589+
"s(",
2590+
" name = 'a',",
2591+
" build_setting_default = '',",
2592+
")",
2593+
"s(",
2594+
" name = 'b',",
2595+
" build_setting_default = '',",
2596+
")",
2597+
"r(",
2598+
" name = 'c',",
2599+
" setting = ':b',",
2600+
")");
2601+
update(
2602+
ImmutableList.of("//test/starlark:c"),
2603+
/*keepGoing=*/ false,
2604+
LOADING_PHASE_THREADS,
2605+
/*doAnalysis=*/ true,
2606+
new EventBus());
2607+
assertNoEvents();
2608+
}
2609+
2610+
@Test
2611+
public void testTransitionPreservesNonDefaultInputOnlySetting() throws Exception {
2612+
writeAllowlistFile();
2613+
scratch.file(
2614+
"test/starlark/rules.bzl",
2615+
"def _string_impl(ctx):",
2616+
" return []",
2617+
"string_flag = rule(",
2618+
" implementation = _string_impl,",
2619+
" build_setting = config.string(flag = True),",
2620+
")",
2621+
"def _transition_impl(settings, attr):",
2622+
" return {",
2623+
" '//test/starlark:output_only': settings['//test/starlark:input_only'],",
2624+
" }",
2625+
"_transition = transition(",
2626+
" implementation = _transition_impl,",
2627+
" inputs = [",
2628+
" '//test/starlark:input_only',",
2629+
" ],",
2630+
" outputs = [",
2631+
" '//test/starlark:output_only',",
2632+
" ],",
2633+
")",
2634+
"def _apply_transition_impl(ctx):",
2635+
" ctx.actions.symlink(",
2636+
" output = ctx.outputs.out,",
2637+
" target_file = ctx.file.target,",
2638+
" )",
2639+
" return [DefaultInfo(executable = ctx.outputs.out)]",
2640+
"apply_transition = rule(",
2641+
" implementation = _apply_transition_impl,",
2642+
" attrs = {",
2643+
" 'target': attr.label(",
2644+
" cfg = _transition,",
2645+
" allow_single_file = True,",
2646+
" ),",
2647+
" 'out': attr.output(),",
2648+
" '_allowlist_function_transition': attr.label(",
2649+
" default = '//tools/allowlists/function_transition_allowlist',",
2650+
" ),",
2651+
" },",
2652+
" executable = False,",
2653+
")");
2654+
scratch.file(
2655+
"test/starlark/BUILD",
2656+
"load('//test/starlark:rules.bzl', 'apply_transition', 'string_flag')",
2657+
"",
2658+
"string_flag(",
2659+
" name = 'input_only',",
2660+
" build_setting_default = 'input_only_default',",
2661+
")",
2662+
"string_flag(",
2663+
" name = 'output_only',",
2664+
" build_setting_default = 'output_only_default',",
2665+
")",
2666+
"cc_binary(",
2667+
" name = 'main',",
2668+
" srcs = ['main.cc'],",
2669+
")",
2670+
"apply_transition(",
2671+
" name = 'transitioned_main',",
2672+
" target = ':main',",
2673+
" out = 'main_out',",
2674+
")");
2675+
2676+
useConfiguration(ImmutableMap.of("//test/starlark:input_only", "not_the_default"));
2677+
Label inputOnlySetting = Label.parseAbsoluteUnchecked("//test/starlark:input_only");
2678+
ConfiguredTarget transitionedDep =
2679+
getDirectPrerequisite(getConfiguredTarget("//test/starlark:transitioned_main"),
2680+
"//test/starlark:main");
2681+
assertThat(getConfiguration(transitionedDep).getOptions().getStarlarkOptions()
2682+
.get(inputOnlySetting)).isEqualTo("not_the_default");
2683+
}
25522684
}

0 commit comments

Comments
 (0)