blob: 9e1ebabc634dcb9a764a79aaaf0bc500c2598352 [file] [log] [blame]
// 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.starlark;
import static com.google.devtools.build.lib.analysis.config.StarlarkDefinedConfigTransition.COMMAND_LINE_OPTION_PREFIX;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.RequiredConfigFragmentsProvider;
import com.google.devtools.build.lib.analysis.config.BuildOptionDetails;
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.StarlarkDefinedConfigTransition;
import com.google.devtools.build.lib.analysis.config.StarlarkDefinedConfigTransition.Settings;
import com.google.devtools.build.lib.analysis.config.transitions.ConfigurationTransition;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.packages.Type.ConversionException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/** A marker class for configuration transitions that are defined in Starlark. */
public abstract class StarlarkTransition implements ConfigurationTransition {
private final StarlarkDefinedConfigTransition starlarkDefinedConfigTransition;
protected StarlarkTransition(StarlarkDefinedConfigTransition starlarkDefinedConfigTransition) {
this.starlarkDefinedConfigTransition = starlarkDefinedConfigTransition;
}
@Override
public String getName() {
return "Starlark transition:" + starlarkDefinedConfigTransition.getLocation();
}
// Get the inputs of the starlark transition as a list of canonicalized labels strings.
private ImmutableList<String> getInputs() {
return starlarkDefinedConfigTransition.getInputsCanonicalizedToGiven().keySet().asList();
}
// Get the outputs of the starlark transition as a list of canonicalized labels strings.
private ImmutableList<String> getOutputs() {
return starlarkDefinedConfigTransition.getOutputsCanonicalizedToGiven().keySet().asList();
}
@Override
public void addRequiredFragments(
RequiredConfigFragmentsProvider.Builder requiredFragments, BuildOptionDetails optionDetails) {
for (String optionStarlarkName : Iterables.concat(getInputs(), getOutputs())) {
if (!optionStarlarkName.startsWith(COMMAND_LINE_OPTION_PREFIX)) {
requiredFragments.addStarlarkOption(Label.parseCanonicalUnchecked(optionStarlarkName));
} else {
String optionNativeName = optionStarlarkName.substring(COMMAND_LINE_OPTION_PREFIX.length());
// A null optionsClass means the flag is invalid. Starlark transitions independently catch
// and report that (search the code for "do not correspond to valid settings").
Class<? extends FragmentOptions> optionsClass =
optionDetails.getOptionClass(optionNativeName);
if (optionsClass != null) {
requiredFragments.addOptionsClass(optionsClass);
}
}
}
}
/** Exception class for exceptions thrown during application of a starlark-defined transition */
// TODO(blaze-configurability): add more information to this exception e.g. originating target of
// transition.
public static class TransitionException extends Exception {
public TransitionException(String message) {
super(message);
}
public TransitionException(Throwable cause) {
super(cause);
}
public TransitionException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Given a {@link ConfigurationTransition}, decompose (if possible) and find all referenced
* Starlark build settings.
*
* <p>If a transition references a build setting via an alias, this set includes the alias' label
* and *does not* include the actual label i.e. this method returns all referenced labels exactly
* as they are.
*/
public static ImmutableSet<Label> getAllStarlarkBuildSettings(ConfigurationTransition root) {
ImmutableSet.Builder<Label> keyBuilder = new ImmutableSet.Builder<>();
try {
root.visit(
(StarlarkTransitionVisitor)
transition ->
keyBuilder.addAll(
getRelevantStarlarkSettingsFromTransition(
transition, Settings.INPUTS_AND_OUTPUTS)));
} catch (TransitionException e) {
// Not actually thrown in the visitor, but declared.
}
return keyBuilder.build();
}
/**
* Method to be called after Starlark-transitions are applied. Checks outputs.
*
* <p>We only do validation on Starlark-defined build settings. Native options (designated with
* {@code COMMAND_LINE_OPTION_PREFIX}) already have their output values checked in {@link
* FunctionTransitionUtil#applyTransition}.
*
* <p>Remove build settings in {@code toOptions} that have been set to their default value. This
* is how we ensure that an unset build setting and a set-to-default build settings represent the
* same configuration.
*
* @param root transition that was applied. Likely a {@link
* com.google.devtools.build.lib.analysis.config.transitions.ComposingTransition} so we
* decompose and post-process all StarlarkTransitions out of whatever transition is passed
* here.
* @param details a StarlarkBuildSettingsDetailsValue whose corresponding key was all the input
* and output settings of root. Use {@link getAllStarlarkBuildSettings}.
* @param toOptions result of applying {@code root}
* @return validated toOptions with default values filtered out
* @throws TransitionException if an error occurred during Starlark transition application.
*/
// TODO(juliexxia): the current implementation masks certain bad transitions and only checks the
// final result. I.e. if a transition that writes a non int --//int-build-setting is composed
// with another transition that writes --//int-build-setting (without reading it first), then
// the bad output of transition 1 is masked.
public static Map<String, BuildOptions> validate(
ConfigurationTransition root,
StarlarkBuildSettingsDetailsValue details,
Map<String, BuildOptions> toOptions)
throws TransitionException {
// Collect settings that are inputs or outputs of the transition together with their types.
// Output setting values will be validated and removed if set to their default.
// Raw means these have not been unaliased.
ImmutableSet.Builder<Label> rawInputAndOutputSettingsBuilder = ImmutableSet.builder();
// Collect settings that were only used as inputs to the transition and thus possibly had their
// default values added to the fromOptions. They will be removed if set to ther default, but
// should not be validated.
ImmutableSet.Builder<Label> inputOnlySettingsBuilder = ImmutableSet.builder();
root.visit(
(StarlarkTransitionVisitor)
transition -> {
ImmutableSet<Label> inputAndOutputSettings =
getRelevantStarlarkSettingsFromTransition(
transition, Settings.INPUTS_AND_OUTPUTS);
ImmutableSet<Label> outputSettings =
getRelevantStarlarkSettingsFromTransition(transition, Settings.OUTPUTS);
for (Label setting : inputAndOutputSettings) {
rawInputAndOutputSettingsBuilder.add(setting);
if (!outputSettings.contains(setting)) {
inputOnlySettingsBuilder.add(setting);
}
}
});
ImmutableSet<Label> rawInputAndOutputSettings = rawInputAndOutputSettingsBuilder.build();
ImmutableSet<Label> inputOnlySettings = inputOnlySettingsBuilder.build();
// Return early if the transition has neither inputs nor outputs (rare).
if (rawInputAndOutputSettings.isEmpty()) {
return toOptions;
}
// Verify changed settings were changed to something reasonable for their type and filter out
// default values.
ImmutableMap.Builder<String, BuildOptions> cleanedOptionMap = ImmutableMap.builder();
for (Map.Entry<String, BuildOptions> entry : toOptions.entrySet()) {
// Lazily initialized to optimize for the common case where we don't modify anything.
BuildOptions.Builder cleanedOptions = null;
// Clean up aliased values.
// TODO(blaze-configurability-team): This is actually a quagmire of undefined behavior
// if a user asks for both an alias and the unaliased build setting.
BuildOptions options = unalias(entry.getValue(), details.aliasToActual());
for (Label maybeAliasSetting : rawInputAndOutputSettings) {
// Note that if the build setting may be referenced in the transition via an alias
Label setting = details.aliasToActual().getOrDefault(maybeAliasSetting, maybeAliasSetting);
// Input-only settings may have had their literal default value added to the BuildOptions
// so that the transition can read them. We have to remove these explicitly set value here
// to preserve the invariant that Starlark settings at default values are not explicitly set
// in the BuildOptions.
final boolean isInputOnlySettingAtDefault =
inputOnlySettings.contains(maybeAliasSetting)
&& details
.buildSettingToDefault()
.get(setting)
.equals(options.getStarlarkOptions().get(setting));
// For output settings, the raw value returned by the transition first has to be validated
// and converted to the proper type before it can be compared to the default value.
if (isInputOnlySettingAtDefault
|| validateAndCheckIfAtDefault(
details, options, maybeAliasSetting, setting, rawInputAndOutputSettings)) {
if (cleanedOptions == null) {
cleanedOptions = options.toBuilder();
}
cleanedOptions.removeStarlarkOption(setting);
}
}
// Keep the same instance if we didn't do anything to maintain reference equality later on.
options = cleanedOptions != null ? cleanedOptions.build() : options;
cleanedOptionMap.put(entry.getKey(), options);
}
return cleanedOptionMap.buildOrThrow();
}
/**
* Validate the value of a particular build setting after a transition has been applied.
*
* @param buildSettingRule the build setting to validate.
* @param options the {@link BuildOptions} reflecting the post-transition configuration.
* @param maybeAliasSetting the label used to refer to the build setting in the transition,
* possibly an alias. This is only used for error messages.
* @param inputAndOutputSettings the transition input and output settings. This is only used for
* error messages.
* @return {@code true} if and only if the setting is set to its default value after the
* transition.
* @throws TransitionException if the value returned by the transition for this setting has an
* invalid type.
*/
private static boolean validateAndCheckIfAtDefault(
StarlarkBuildSettingsDetailsValue details,
BuildOptions options,
Label maybeAliasSetting,
Label setting,
Set<Label> inputAndOutputSettings)
throws TransitionException {
Object newValue = options.getStarlarkOptions().get(setting);
// TODO(b/154132845): fix NPE occasionally observed here.
Preconditions.checkState(
newValue != null,
"Error while attempting to validate new values from starlark"
+ " transition(s) with the inputs and outputs %s. Post-transition configuration should"
+ " include '%s' but only includes starlark options: %s. If you run into this error"
+ " please ping b/154132845 or email blaze-configurability@google.com.",
inputAndOutputSettings,
setting,
options.getStarlarkOptions().keySet());
boolean allowsMultiple = details.buildSettingIsAllowsMultiple().contains(setting);
if (allowsMultiple) {
// if this setting allows multiple settings
if (!(newValue instanceof List)) {
throw new TransitionException(
String.format(
"'%s' allows multiple values and must be set"
+ " in transition using a starlark list instead of single value '%s'",
setting, newValue));
}
List<?> rawNewValueAsList = (List<?>) newValue;
List<Object> convertedValue = new ArrayList<>();
Type<?> type = details.buildSettingToType().get(setting);
for (Object value : rawNewValueAsList) {
try {
convertedValue.add(type.convert(value, maybeAliasSetting));
} catch (ConversionException e) {
throw new TransitionException(e);
}
}
return convertedValue.equals(ImmutableList.of(details.buildSettingToDefault().get(setting)));
} else {
// if this setting does not allow multiple settings
Object convertedValue;
try {
convertedValue =
details.buildSettingToType().get(setting).convert(newValue, maybeAliasSetting);
} catch (ConversionException e) {
throw new TransitionException(e);
}
return convertedValue.equals(details.buildSettingToDefault().get(setting));
}
}
/*
* Resolve aliased build setting issues
*
* <p>If a build setting is transitioned upon via an alias, the resulting {@link
* BuildOptions#getStarlarkOptions()} map will look like this:
*
* <entry1>alias-label -> new-value
* <entry2>actual-label -> old-value
*
* <p>we need to collapse this to the correct single entry: actual-label -> new-value.
* By the end of this method, the starlark options map in the returned {@link BuildOptions}
* contains only keys that are actual build settings, no aliases.
*/
private static BuildOptions unalias(
BuildOptions options, ImmutableMap<Label, Label> aliasToActual) {
if (aliasToActual.isEmpty()) {
return options;
}
Collection<Label> aliases = aliasToActual.keySet();
Collection<Label> actuals = aliasToActual.values();
BuildOptions.Builder toReturn = options.toBuilder();
for (Map.Entry<Label, Object> entry : options.getStarlarkOptions().entrySet()) {
Label setting = entry.getKey();
if (actuals.contains(setting)) {
// if entry is keyed by an actual (e.g. <entry2> in javadoc), don't care about its value
// it's stale
continue;
}
if (aliases.contains(setting)) {
// if an entry is keyed by an alias (e.g. <entry1> in javadoc), newly key (overwrite) its
// actual to its alias' value and remove the alias-keyed entry
toReturn.addStarlarkOption(
aliasToActual.get(setting), options.getStarlarkOptions().get(setting));
toReturn.removeStarlarkOption(setting);
} else {
// else - just copy over
toReturn.addStarlarkOption(entry.getKey(), entry.getValue());
}
}
return toReturn.build();
}
/** Adds the default values for a transition's input build settings to its input build options. */
public static BuildOptions addDefaultStarlarkOptions(
BuildOptions fromOptions,
ConfigurationTransition transition,
StarlarkBuildSettingsDetailsValue details)
throws TransitionException {
if (details.buildSettingToDefault().isEmpty()) {
// No need to traverse the transition to find its Starlark flag inputs. There are none.
return fromOptions;
}
BuildOptions.Builder optionsWithDefaults = null;
for (Label maybeAliasSetting : getAllStarlarkBuildSettings(transition)) {
// details will only have the defaults of the actual setting so must unalias
Label setting = details.aliasToActual().getOrDefault(maybeAliasSetting, maybeAliasSetting);
if (!fromOptions.getStarlarkOptions().containsKey(maybeAliasSetting)) {
if (optionsWithDefaults == null) {
optionsWithDefaults = fromOptions.toBuilder();
}
optionsWithDefaults.addStarlarkOption(
maybeAliasSetting, details.buildSettingToDefault().get(setting));
}
}
return optionsWithDefaults == null ? fromOptions : optionsWithDefaults.build();
}
private static ImmutableSet<Label> getRelevantStarlarkSettingsFromTransition(
StarlarkTransition transition, Settings settings) {
Set<String> toGet = new HashSet<>();
switch (settings) {
case INPUTS:
toGet.addAll(transition.getInputs());
break;
case OUTPUTS:
toGet.addAll(transition.getOutputs());
break;
case INPUTS_AND_OUTPUTS:
toGet.addAll(transition.getInputs());
toGet.addAll(transition.getOutputs());
break;
}
return ImmutableSet.copyOf(
toGet.stream()
.filter(setting -> !setting.startsWith(COMMAND_LINE_OPTION_PREFIX))
.map(absName -> Label.parseCanonicalUnchecked(absName))
.collect(Collectors.toSet()));
}
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (object instanceof StarlarkTransition) {
StarlarkDefinedConfigTransition starlarkDefinedConfigTransition =
((StarlarkTransition) object).starlarkDefinedConfigTransition;
return Objects.equals(starlarkDefinedConfigTransition, this.starlarkDefinedConfigTransition);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(starlarkDefinedConfigTransition);
}
/** Given a transition, figures out if it composes any Starlark transitions. */
public static boolean doesStarlarkTransition(ConfigurationTransition root)
throws TransitionException {
AtomicBoolean doesStarlarkTransition = new AtomicBoolean(false);
root.visit((StarlarkTransitionVisitor) transition -> doesStarlarkTransition.set(true));
return doesStarlarkTransition.get();
}
@FunctionalInterface
// This is only used in this class to handle the cast and the exception
@SuppressWarnings("FunctionalInterfaceMethodChanged")
private interface StarlarkTransitionVisitor
extends ConfigurationTransition.Visitor<TransitionException> {
@Override
default void accept(ConfigurationTransition transition) throws TransitionException {
if (transition instanceof StarlarkTransition) {
this.accept((StarlarkTransition) transition);
}
}
void accept(StarlarkTransition transition) throws TransitionException;
}
}