blob: 11ece1c6b6b9db2ee2775e30f39e40aa835488ee [file] [log] [blame]
// 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.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
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.StarlarkDefinedConfigTransition;
import com.google.devtools.build.lib.analysis.config.transitions.SplitTransition;
import com.google.devtools.build.lib.packages.Attribute.SplitTransitionProvider;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.Runtime.NoneType;
import com.google.devtools.build.lib.syntax.SkylarkDict;
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.LinkedHashSet;
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 static final String COMMAND_LINE_OPTION_PREFIX = "//command_line_option:";
private final StarlarkDefinedConfigTransition starlarkDefinedConfigTransition;
public FunctionSplitTransitionProvider(
StarlarkDefinedConfigTransition starlarkDefinedConfigTransition) {
this.starlarkDefinedConfigTransition = starlarkDefinedConfigTransition;
}
@Override
public SplitTransition apply(AttributeMap attributeMap) {
return new FunctionSplitTransition(starlarkDefinedConfigTransition);
}
private static class FunctionSplitTransition implements SplitTransition {
private final StarlarkDefinedConfigTransition starlarkDefinedConfigTransition;
public FunctionSplitTransition(
StarlarkDefinedConfigTransition starlarkDefinedConfigTransition) {
this.starlarkDefinedConfigTransition = starlarkDefinedConfigTransition;
}
@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.
try {
Map<String, OptionInfo> optionInfoMap = buildOptionInfo(buildOptions);
SkylarkDict<String, Object> settings =
buildSettings(buildOptions, optionInfoMap, starlarkDefinedConfigTransition.getInputs());
ImmutableList.Builder<BuildOptions> splitBuildOptions = ImmutableList.builder();
ImmutableList<Map<String, Object>> transitions =
starlarkDefinedConfigTransition.getChangedSettings(settings);
// TODO(juliexxia): Validate that the output values correctly match the output types.
validateFunctionOutputs(transitions, starlarkDefinedConfigTransition.getOutputs());
for (Map<String, Object> transition : transitions) {
BuildOptions options = buildOptions.clone();
applyTransition(options, transition, optionInfoMap);
splitBuildOptions.add(options);
}
return splitBuildOptions.build();
} catch (InterruptedException | EvalException e) {
// TODO(juliexxia): Throw an exception better than RuntimeException.
throw new RuntimeException(e);
}
}
private void validateFunctionOutputs(
ImmutableList<Map<String, Object>> transitions,
List<String> expectedOutputs) throws EvalException {
for (Map<String, Object> transition : transitions) {
LinkedHashSet<String> remainingOutputs = Sets.newLinkedHashSet(expectedOutputs);
for (String outputKey : transition.keySet()) {
if (!remainingOutputs.remove(outputKey)) {
throw new EvalException(
starlarkDefinedConfigTransition.getLocationForErrorReporting(),
String.format("transition function returned undeclared output '%s'", outputKey));
}
}
if (!remainingOutputs.isEmpty()) {
throw new EvalException(
starlarkDefinedConfigTransition.getLocationForErrorReporting(),
String.format(
"transition outputs [%s] were not defined by transition function",
Joiner.on(", ").join(remainingOutputs)));
}
}
}
/**
* Given a label-like string representing a command line option, returns the command line
* option string that it represents. This is a temporary measure to support command line
* options with strings that look "label-like", so that migrating users using this
* experimental syntax is easier later.
*
* @throws EvalException if the given string is not a valid format to represent to
* a command line option
*/
private String commandLineOptionLabelToOption(String label) throws EvalException {
if (label.startsWith(COMMAND_LINE_OPTION_PREFIX)) {
return label.substring(COMMAND_LINE_OPTION_PREFIX.length());
} else {
throw new EvalException(
starlarkDefinedConfigTransition.getLocationForErrorReporting(),
String.format(
"Option key '%s' is of invalid form. "
+ "Expected command line option to begin with %s",
label, COMMAND_LINE_OPTION_PREFIX));
}
}
/**
* 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.
* @throws EvalException if any of the specified transition inputs do not correspond to a valid
* build setting
*/
private SkylarkDict<String, Object> buildSettings(BuildOptions buildOptions,
Map<String, OptionInfo> optionInfoMap, List<String> inputs) throws EvalException {
LinkedHashSet<String> remainingInputs = Sets.newLinkedHashSet(inputs);
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();
String optionKey = COMMAND_LINE_OPTION_PREFIX + optionName;
if (!remainingInputs.remove(optionKey)) {
// This option was not present in inputs. Skip it.
continue;
}
OptionInfo optionInfo = entry.getValue();
try {
Field field = optionInfo.getDefinition().getField();
FragmentOptions options = buildOptions.get(optionInfo.getOptionClass());
Object optionValue = field.get(options);
dict.put(optionKey, optionValue, null, mutability);
} catch (IllegalAccessException e) {
// These exceptions should not happen, but if they do, throw a RuntimeException.
throw new RuntimeException(e);
}
}
if (!remainingInputs.isEmpty()) {
throw new EvalException(
starlarkDefinedConfigTransition.getLocationForErrorReporting(),
String.format(
"transition inputs [%s] do not correspond to valid settings",
Joiner.on(", ").join(remainingInputs)));
}
return 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 optionKey = entry.getKey();
// TODO(juliexxia): Handle keys which correspond to build_setting target labels instead
// of assuming every key is for a command line option.
String optionName = commandLineOptionLabelToOption(optionKey);
Object optionValue = entry.getValue();
// Convert NoneType to null.
if (optionValue instanceof NoneType) {
optionValue = null;
}
try {
if (!optionInfoMap.containsKey(optionName)) {
throw new EvalException(
starlarkDefinedConfigTransition.getLocationForErrorReporting(),
String.format(
"transition output '%s' does not correspond to a valid setting", optionKey));
}
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(
starlarkDefinedConfigTransition.getLocationForErrorReporting(),
"Invalid value type for option '" + optionName + "'");
}
} catch (IllegalAccessException e) {
throw new RuntimeException(
"IllegalAccess for option " + optionName + ": " + e.getMessage());
} catch (OptionsParsingException e) {
throw new EvalException(
starlarkDefinedConfigTransition.getLocationForErrorReporting(),
"OptionsParsingError for option '" + optionName + "': " + e.getMessage());
}
}
BuildConfiguration.Options buildConfigOptions;
buildConfigOptions = buildOptions.get(BuildConfiguration.Options.class);
if (starlarkDefinedConfigTransition.isForAnalysisTesting()) {
buildConfigOptions.evaluatingForAnalysisTest = true;
}
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;
}
}
}
}