blob: 731401d0f5e238ccc2bacf6d88e46a6d165b113c [file] [log] [blame]
// Copyright 2015 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.skyframe;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.devtools.build.lib.analysis.config.StarlarkDefinedConfigTransition.COMMAND_LINE_OPTION_PREFIX;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.PlatformOptions;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.analysis.config.FragmentFactory;
import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
import com.google.devtools.build.lib.analysis.config.OptionInfo;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.packages.RuleClassProvider;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionMetadataTag;
import com.google.devtools.common.options.OptionsParsingException;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.Nullable;
import net.starlark.java.eval.StarlarkSemantics;
/** A builder for {@link BuildConfigurationValue} instances. */
public final class BuildConfigurationFunction implements SkyFunction {
// The length of the hash of the config tacked onto the end of the output path.
// Limited for ergonomics and MAX_PATH reasons.
private static final int HASH_LENGTH = 12;
private final BlazeDirectories directories;
private final ConfiguredRuleClassProvider ruleClassProvider;
private final FragmentFactory fragmentFactory = new FragmentFactory();
public BuildConfigurationFunction(
BlazeDirectories directories, RuleClassProvider ruleClassProvider) {
this.directories = directories;
this.ruleClassProvider = (ConfiguredRuleClassProvider) ruleClassProvider;
}
@Override
@Nullable
public SkyValue compute(SkyKey skyKey, Environment env)
throws InterruptedException, BuildConfigurationFunctionException {
WorkspaceNameValue workspaceNameValue = (WorkspaceNameValue) env
.getValue(WorkspaceNameValue.key());
if (workspaceNameValue == null) {
return null;
}
StarlarkSemantics starlarkSemantics = PrecomputedValue.STARLARK_SEMANTICS.get(env);
if (starlarkSemantics == null) {
return null;
}
BuildConfigurationKey key = (BuildConfigurationKey) skyKey.argument();
BuildOptions targetOptions = key.getOptions();
String transitionDirectoryNameFragment;
if (targetOptions
.get(CoreOptions.class)
.outputDirectoryNamingScheme
.equals(CoreOptions.OutputDirectoryNamingScheme.DIFF_AGAINST_BASELINE)) {
// Herein lies a hack to apply platform mappings to the baseline options.
// TODO(blaze-configurability-team): this should become unnecessary once --platforms is marked
// as EXPLICIT_IN_OUTPUT_PATH
PlatformMappingValue platformMappingValue =
(PlatformMappingValue)
env.getValue(
PlatformMappingValue.Key.create(
targetOptions.get(PlatformOptions.class).platformMappings));
if (platformMappingValue == null) {
return null;
}
BuildOptions baselineOptions = PrecomputedValue.BASELINE_CONFIGURATION.get(env);
try {
BuildOptions mappedBaselineOptions =
BuildConfigurationKey.withPlatformMapping(platformMappingValue, baselineOptions)
.getOptions();
transitionDirectoryNameFragment =
computeNameFragmentWithDiff(targetOptions, mappedBaselineOptions);
} catch (OptionsParsingException e) {
throw new BuildConfigurationFunctionException(e);
}
} else {
transitionDirectoryNameFragment =
computeNameFragmentWithAffectedByStarlarkTransition(targetOptions);
}
try {
return BuildConfigurationValue.create(
targetOptions,
RepositoryName.createUnvalidated(workspaceNameValue.getName()),
starlarkSemantics.getBool(BuildLanguageOptions.EXPERIMENTAL_SIBLING_REPOSITORY_LAYOUT),
transitionDirectoryNameFragment,
// Arguments below this are server-global.
directories,
ruleClassProvider,
fragmentFactory);
} catch (InvalidConfigurationException e) {
throw new BuildConfigurationFunctionException(e);
}
}
private static final class BuildConfigurationFunctionException extends SkyFunctionException {
BuildConfigurationFunctionException(Exception e) {
super(e, Transience.PERSISTENT);
}
}
/**
* Compute the hash for the new BuildOptions based on the names and values of all options (both
* native and Starlark) that are different from some supplied baseline configuration.
*/
private static String computeNameFragmentWithDiff(
BuildOptions toOptions, BuildOptions baselineOptions) {
// Quick short-circuit for trivial case.
if (toOptions.equals(baselineOptions)) {
return "";
}
// TODO(blaze-configurability-team): As a mild performance update, getFirst already includes
// details of the corresponding option. Could incorporate this instead of hashChosenOptions
// regenerating the OptionDefinitions and values.
BuildOptions.OptionsDiff diff = BuildOptions.diff(toOptions, baselineOptions);
// Note: getFirst only excludes options trimmed between baselineOptions to toOptions and this is
// considered OK as a given Rule should not be being built with options of different
// trimmings. See longform note in {@link ConfiguredTargetKey} for details.
ImmutableSet<String> chosenNativeOptions =
diff.getFirst().keySet().stream()
.filter(
optionDef ->
!optionDef.hasOptionMetadataTag(OptionMetadataTag.EXPLICIT_IN_OUTPUT_PATH))
.map(OptionDefinition::getOptionName)
.collect(toImmutableSet());
// Note: getChangedStarlarkOptions includes all changed options, added options and removed
// options between baselineOptions and toOptions. This is necessary since there is no current
// notion of trimming a Starlark option: 'null' or non-existent justs means set to default.
ImmutableSet<String> chosenStarlarkOptions =
diff.getChangedStarlarkOptions().stream().map(Label::toString).collect(toImmutableSet());
return hashChosenOptions(toOptions, chosenNativeOptions, chosenStarlarkOptions);
}
/**
* Compute the output directory name fragment corresponding to the new BuildOptions based on the
* names and values of all options (both native and Starlark) previously transitioned anywhere in
* the build by Starlark transitions. Options only set on command line are not affecting the
* computation.
*
* @param toOptions the {@link BuildOptions} to use to calculate which we need to compute {@code
* transitionDirectoryNameFragment}.
*/
private static String computeNameFragmentWithAffectedByStarlarkTransition(
BuildOptions toOptions) {
CoreOptions buildConfigOptions = toOptions.get(CoreOptions.class);
if (buildConfigOptions.affectedByStarlarkTransition.isEmpty()) {
return "";
}
ImmutableList.Builder<String> affectedNativeOptions = ImmutableList.builder();
ImmutableList.Builder<String> affectedStarlarkOptions = ImmutableList.builder();
for (String optionName : buildConfigOptions.affectedByStarlarkTransition) {
if (optionName.startsWith(COMMAND_LINE_OPTION_PREFIX)) {
String nativeOptionName = optionName.substring(COMMAND_LINE_OPTION_PREFIX.length());
affectedNativeOptions.add(nativeOptionName);
} else {
affectedStarlarkOptions.add(optionName);
}
}
return hashChosenOptions(
toOptions, affectedNativeOptions.build(), affectedStarlarkOptions.build());
}
/**
* Compute a hash of the given BuildOptions by hashing only the options referenced in both
* chosenNative and chosenStarlark. The order of the chosen order does not matter (as this
* function will effectively sort them into a canonical order) and the pre-hash for each option
* will be of the form (//command_line_option:[native option]|[Starlark option label])=[value].
*
* <p>If a supplied native option does not exist, it is skipped (as it is presumed non-existence
* is due to trimming).
*
* <p>If a supplied Starlark option does exist, the pre-hash will be [Starlark option label]@null
* (as it is presumed non-existence is due to being set to default value).
*/
private static String hashChosenOptions(
BuildOptions toOptions, Iterable<String> chosenNative, Iterable<String> chosenStarlark) {
// TODO(blaze-configurability-team): A mild performance optimization would have this be global.
ImmutableMap<String, OptionInfo> optionInfoMap = OptionInfo.buildMapFrom(toOptions);
// Note that the TreeMap guarantees a stable ordering of keys and thus
// it is okay if chosenNative or chosenStarlark do not have a stable iteration order
TreeMap<String, Object> toHash = new TreeMap<>();
for (String nativeOptionName : chosenNative) {
Object value;
try {
OptionInfo optionInfo = optionInfoMap.get(nativeOptionName);
if (optionInfo == null) {
// This can occur if toOptions has been trimmed but the supplied chosen native options
// includes that trimmed options.
// (e.g. legacy naming mode, using --trim_test_configuration and --test_arg transition).
continue;
}
value =
optionInfo
.getDefinition()
.getField()
.get(toOptions.get(optionInfoMap.get(nativeOptionName).getOptionClass()));
} catch (IllegalAccessException e) {
throw new VerifyException(
"IllegalAccess for option " + nativeOptionName + ": " + e.getMessage());
}
// TODO(blaze-configurability-team): The commandline option is legacy and can be removed
// after fixing up all the associated tests.
toHash.put("//command_line_option:" + nativeOptionName, value);
}
for (String starlarkOptionName : chosenStarlark) {
Object value =
toOptions.getStarlarkOptions().get(Label.parseAbsoluteUnchecked(starlarkOptionName));
toHash.put(starlarkOptionName, value);
}
if (toHash.isEmpty()) {
return "";
} else {
ImmutableList.Builder<String> hashStrs = ImmutableList.builderWithExpectedSize(toHash.size());
for (Map.Entry<String, Object> singleOptionAndValue : toHash.entrySet()) {
Object value = singleOptionAndValue.getValue();
if (value != null) {
hashStrs.add(singleOptionAndValue.getKey() + "=" + value);
} else {
// Avoid using =null to different from value being the non-null String "null"
hashStrs.add(singleOptionAndValue.getKey() + "@null");
}
}
return transitionDirectoryNameFragment(hashStrs.build());
}
}
@VisibleForTesting
public static String transitionDirectoryNameFragment(Iterable<String> opts) {
Fingerprint fp = new Fingerprint();
for (String opt : opts) {
fp.addString(opt);
}
// Shorten the hash to 48 bits. This should provide sufficient collision avoidance
// (that is, we don't expect anyone to experience a collision ever).
// Shortening the hash is important for Windows paths that tend to be short.
String suffix = fp.hexDigestAndReset().substring(0, HASH_LENGTH);
return "ST-" + suffix;
}
}