blob: 9ab4c0cb44296d2302dc41060c1cac48a7d9ecf5 [file] [log] [blame]
// Copyright 2020 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.packages;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import net.starlark.java.eval.GuardedValue;
import net.starlark.java.eval.Starlark;
// TODO(adonovan): move skyframe.PackageFunction into lib.packages so we needn't expose this and
// the other env-building functions.
/**
* This class encapsulates knowledge of how to set up the Starlark environment for BUILD, WORKSPACE,
* and bzl file evaluation, including the top-level predeclared symbols, the {@code native} module,
* and the special environment for {@code @_builtins} bzl evaluation.
*
* <p>The set of available symbols is determined by 1) gathering registered toplevels, rules,
* extensions, etc., from the {@link ConfiguredRuleClassProvider} and {@link PackageFactory}, and
* then 2) applying builtins injection (see {@link StarlarkBuiltinsFunction}, if applicable. The
* result of (1) is cached by an instance of this class. (2) is obtained using helper methods on
* this class, and cached in {@link StarlarkBuiltinsValue}.
*/
public final class BazelStarlarkEnvironment {
// TODO(#11954): Eventually the BUILD and WORKSPACE bzl dialects should converge. Right now they
// only differ on the "native" object.
private final RuleClassProvider ruleClassProvider;
private final ImmutableMap<String, ?> ruleFunctions;
/** The "native" module fields for a BUILD-loaded bzl module, before builtins injection. */
private final ImmutableMap<String, Object> uninjectedBuildBzlNativeBindings;
/** The "native" module fields for a WORKSPACE-loaded bzl module. */
private final ImmutableMap<String, Object> workspaceBzlNativeBindings;
/** The top-level predeclared symbols for a BUILD-loaded bzl module, before builtins injection. */
private final ImmutableMap<String, Object> uninjectedBuildBzlEnv;
/** The top-level predeclared symbols for BUILD files, before builtins injection and prelude. */
private final ImmutableMap<String, Object> uninjectedBuildEnv;
/** The top-level predeclared symbols for a WORKSPACE-loaded bzl module. */
private final ImmutableMap<String, Object> workspaceBzlEnv;
/** The top-level predeclared symbols for a bzl module in the {@code @_builtins} pseudo-repo. */
private final ImmutableMap<String, Object> builtinsBzlEnv;
/** The top-level predeclared symbols for a bzl module in the Bzlmod system. */
private final ImmutableMap<String, Object> bzlmodBzlEnv;
BazelStarlarkEnvironment(
RuleClassProvider ruleClassProvider,
ImmutableMap<String, ?> ruleFunctions,
List<PackageFactory.EnvironmentExtension> environmentExtensions,
Object packageFunction,
String version) {
this.ruleClassProvider = ruleClassProvider;
this.ruleFunctions = ruleFunctions;
this.uninjectedBuildBzlNativeBindings =
createUninjectedBuildBzlNativeBindings(
ruleFunctions, packageFunction, environmentExtensions);
this.workspaceBzlNativeBindings = createWorkspaceBzlNativeBindings(ruleClassProvider, version);
this.uninjectedBuildBzlEnv =
createUninjectedBuildBzlEnv(ruleClassProvider, uninjectedBuildBzlNativeBindings);
this.workspaceBzlEnv = createWorkspaceBzlEnv(ruleClassProvider, workspaceBzlNativeBindings);
// TODO(pcloudy): this should be a bzlmod specific environment, but keep using the workspace
// envirnment until we implement module rules.
this.bzlmodBzlEnv = createWorkspaceBzlEnv(ruleClassProvider, workspaceBzlNativeBindings);
this.builtinsBzlEnv =
createBuiltinsBzlEnv(
ruleClassProvider, uninjectedBuildBzlNativeBindings, uninjectedBuildBzlEnv);
this.uninjectedBuildEnv =
createUninjectedBuildEnv(ruleFunctions, packageFunction, environmentExtensions);
}
/**
* Returns the contents of the "native" object for BUILD-loaded bzls, not accounting for builtins
* injection.
*/
public ImmutableMap<String, Object> getUninjectedBuildBzlNativeBindings() {
return uninjectedBuildBzlNativeBindings;
}
/** Returns the contents of the "native" object for WORKSPACE-loaded bzls. */
public ImmutableMap<String, Object> getWorkspaceBzlNativeBindings() {
return workspaceBzlNativeBindings;
}
/**
* Returns the original environment for BUILD-loaded bzl files, not accounting for builtins
* injection.
*
* <p>The post-injection environment may differ from this one by what symbols a name is bound to,
* but the set of symbols remains the same.
*/
public ImmutableMap<String, Object> getUninjectedBuildBzlEnv() {
return uninjectedBuildBzlEnv;
}
/**
* Returns the original environment for BUILD files, not accounting for builtins injection or
* application of the prelude.
*
* <p>Applying builtins injection may update name bindings, but not add or remove them. I.e. some
* names may refer to different symbols but the static set of names remains the same. Applying the
* prelude file may update and add name bindings but not remove them.
*/
public ImmutableMap<String, Object> getUninjectedBuildEnv() {
return uninjectedBuildEnv;
}
/** Returns the environment for WORKSPACE-loaded bzl files. */
public ImmutableMap<String, Object> getWorkspaceBzlEnv() {
return workspaceBzlEnv;
}
/** Returns the environment for bzl files in the {@code @_builtins} pseudo-repository. */
public ImmutableMap<String, Object> getBuiltinsBzlEnv() {
return builtinsBzlEnv;
}
/** Returns the environment for Bzlmod-loaded bzl files. */
public ImmutableMap<String, Object> getBzlmodBzlEnv() {
return bzlmodBzlEnv;
}
/**
* Produces everything that would be in the "native" object for BUILD-loaded bzl files if builtins
* injection didn't happen.
*/
private static ImmutableMap<String, Object> createUninjectedBuildBzlNativeBindings(
Map<String, ?> ruleFunctions,
Object packageFunction,
List<PackageFactory.EnvironmentExtension> environmentExtensions) {
ImmutableMap.Builder<String, Object> env = new ImmutableMap.Builder<>();
env.putAll(StarlarkNativeModule.BINDINGS_FOR_BUILD_FILES);
env.putAll(ruleFunctions);
env.put("package", packageFunction);
for (PackageFactory.EnvironmentExtension ext : environmentExtensions) {
ext.updateNative(env);
}
return env.buildOrThrow();
}
/** Produces everything in the "native" object for WORKSPACE-loaded bzl files. */
private static ImmutableMap<String, Object> createWorkspaceBzlNativeBindings(
RuleClassProvider ruleClassProvider, String version) {
return WorkspaceFactory.createNativeModuleBindings(ruleClassProvider, version);
}
/** Constructs a "native" module object with the given contents. */
private static Object createNativeModule(Map<String, Object> bindings) {
return StructProvider.STRUCT.create(bindings, "no native function or rule '%s'");
}
private static ImmutableMap<String, Object> createUninjectedBuildBzlEnv(
RuleClassProvider ruleClassProvider, Map<String, Object> uninjectedBuildBzlNativeBindings) {
Map<String, Object> env = new HashMap<>(ruleClassProvider.getEnvironment());
// Determine the "native" module.
// TODO(#11954): Use the same "native" object for both BUILD- and WORKSPACE-loaded .bzls, and
// just have it be a dynamic error to call the wrong thing at the wrong time. This is a breaking
// change.
env.put("native", createNativeModule(uninjectedBuildBzlNativeBindings));
return ImmutableMap.copyOf(env);
}
private static ImmutableMap<String, Object> createUninjectedBuildEnv(
Map<String, ?> ruleFunctions,
Object packageFunction,
List<PackageFactory.EnvironmentExtension> environmentExtensions) {
ImmutableMap.Builder<String, Object> env = ImmutableMap.builder();
env.putAll(StarlarkLibrary.BUILD); // e.g. rule, select, depset
env.putAll(StarlarkNativeModule.BINDINGS_FOR_BUILD_FILES);
env.put("package", packageFunction);
env.putAll(ruleFunctions);
for (PackageFactory.EnvironmentExtension ext : environmentExtensions) {
ext.update(env);
}
return env.buildOrThrow();
}
private static ImmutableMap<String, Object> createWorkspaceBzlEnv(
RuleClassProvider ruleClassProvider, Map<String, Object> workspaceBzlNativeBindings) {
Map<String, Object> env = new HashMap<>(ruleClassProvider.getEnvironment());
// See above comments for native in BUILD bzls.
env.put("native", createNativeModule(workspaceBzlNativeBindings));
return ImmutableMap.copyOf(env);
}
private static ImmutableMap<String, Object> createBuiltinsBzlEnv(
RuleClassProvider ruleClassProvider,
ImmutableMap<String, Object> uninjectedBuildBzlNativeBindings,
ImmutableMap<String, Object> uninjectedBuildBzlEnv) {
Map<String, Object> env = new HashMap<>(ruleClassProvider.getEnvironment());
// Clear out rule-specific symbols like CcInfo.
env.keySet().removeAll(ruleClassProvider.getNativeRuleSpecificBindings().keySet());
// For _builtins.toplevel, replace all FlagGuardedValues with the underlying value;
// StarlarkSemantics flags do not affect @_builtins.
//
// We do this because otherwise we'd need to differentiate the _builtins.toplevel object (and
// therefore the @_builtins environment) based on StarlarkSemantics. That seems unnecessary.
// Instead we trust @_builtins to not misuse flag-guarded features, same as native code.
//
// If foo is flag-guarded (either experimental or incompatible), it is unconditionally visible
// as _builtins.toplevel.foo. It is legal to list it in exported_toplevels unconditionally, but
// the flag still controls whether the symbol is actually visible to user code.
Map<String, Object> unwrappedBuildBzlSymbols = new HashMap<>();
for (Map.Entry<String, Object> entry : uninjectedBuildBzlEnv.entrySet()) {
Object symbol = entry.getValue();
if (symbol instanceof GuardedValue) {
symbol = ((GuardedValue) symbol).getObject();
}
unwrappedBuildBzlSymbols.put(entry.getKey(), symbol);
}
Object builtinsModule =
new BuiltinsInternalModule(
createNativeModule(uninjectedBuildBzlNativeBindings),
// createNativeModule() is good enough for the "toplevel" and "internal" objects too.
createNativeModule(unwrappedBuildBzlSymbols),
createNativeModule(ruleClassProvider.getStarlarkBuiltinsInternals()));
Object conflictingValue = env.put("_builtins", builtinsModule);
Preconditions.checkState(
conflictingValue == null, "'_builtins' name is reserved for builtins injection");
return ImmutableMap.copyOf(env);
}
/**
* Throws {@link InjectionException} with an appropriate error message if the given {@code symbol}
* is not in both {@code existingSymbols} and {@code injectableSymbols}. {@code kind} is a string
* describing the domain of {@code symbol}.
*/
private static void validateSymbolIsInjectable(
String symbol, Set<String> existingSymbols, Set<String> injectableSymbols, String kind)
throws InjectionException {
if (!existingSymbols.contains(symbol)) {
throw new InjectionException(
String.format(
"Injected %s '%s' must override an existing one by that name", kind, symbol));
} else if (!injectableSymbols.contains(symbol)) {
throw new InjectionException(
String.format("Cannot override '%s' with an injected %s", symbol, kind));
}
}
/** Given a string prefixed with + or -, returns that prefix character, or null otherwise. */
@Nullable
private static Character getKeyPrefix(String key) {
if (key.isEmpty()) {
return null;
}
char prefix = key.charAt(0);
if (!(prefix == '+' || prefix == '-')) {
return null;
}
return prefix;
}
/**
* Given a string prefixed with + or -, returns the remainder of the string, or the whole string
* otherwise.
*/
private static String getKeySuffix(String key) {
return getKeyPrefix(key) == null ? key : key.substring(1);
}
/**
* Given a list of strings representing the +/- prefixed items in {@code
* --experimental_builtins_injection_override}, returns a map from each item to a Boolean
* indicating whether it last appeared with the + suffix (True) or - suffix (False).
*
* @throws InjectionException if an item is not prefixed with either "+" or "-"
*/
private static Map<String, Boolean> parseInjectionOverridesList(List<String> overrides)
throws InjectionException {
HashMap<String, Boolean> result = new HashMap<>();
for (String prefixedItem : overrides) {
Character prefix = getKeyPrefix(prefixedItem);
if (prefix == null) {
throw new InjectionException(
String.format("Invalid injection override item: '%s'", prefixedItem));
}
result.put(prefixedItem.substring(1), prefix == '+');
}
return result;
}
/**
* Given an exports dict key, and an override map, return whether injection should be applied for
* that key.
*/
private static boolean injectionApplies(String key, Map<String, Boolean> overrides) {
Character prefix = getKeyPrefix(key);
if (prefix == null) {
// Unprefixed; overrides don't get a say in the matter.
return true;
}
Boolean override = overrides.get(key.substring(1));
if (override == null) {
return prefix == '+';
} else {
return override;
}
}
/**
* Constructs an environment for a BUILD-loaded bzl file based on the default environment, the
* maps corresponding to the {@code exported_toplevels} and {@code exported_rules} dicts, and the
* value of {@code --experimental_builtins_injection_override}.
*
* <p>Injected symbols must override an existing symbol of that name. Furthermore, the overridden
* symbol must be a rule or a piece of a specific ruleset's logic (e.g., {@code CcInfo} or {@code
* cc_library}), not a generic built-in (e.g., {@code provider} or {@code glob}). Throws
* InjectionException if these conditions are not met.
*
* <p>Whether or not injection actually occurs for a given map key depends on its prefix (if any)
* and the prefix of its appearance (if it appears at all) in the override list; see the
* documentation for {@code --experimental_builtins_injection_override}. Non-injected symbols must
* still obey the above constraints.
*
* @see StarlarkBuiltinsFunction
*/
public ImmutableMap<String, Object> createBuildBzlEnvUsingInjection(
Map<String, Object> exportedToplevels,
Map<String, Object> exportedRules,
List<String> overridesList)
throws InjectionException {
Map<String, Boolean> overridesMap = parseInjectionOverridesList(overridesList);
// Determine top-level symbols.
Map<String, Object> env = new HashMap<>(uninjectedBuildBzlEnv);
for (Map.Entry<String, Object> entry : exportedToplevels.entrySet()) {
String key = entry.getKey();
String name = getKeySuffix(key);
validateSymbolIsInjectable(
name,
Sets.union(env.keySet(), Starlark.UNIVERSE.keySet()),
ruleClassProvider.getNativeRuleSpecificBindings().keySet(),
"top-level symbol");
if (injectionApplies(key, overridesMap)) {
env.put(name, entry.getValue());
}
}
// Determine "native" bindings.
// TODO(#11954): See above comment in createUninjectedBuildBzlEnv.
Map<String, Object> nativeBindings = new HashMap<>(uninjectedBuildBzlNativeBindings);
for (Map.Entry<String, Object> entry : exportedRules.entrySet()) {
String key = entry.getKey();
String name = getKeySuffix(key);
validateSymbolIsInjectable(name, nativeBindings.keySet(), ruleFunctions.keySet(), "rule");
if (injectionApplies(key, overridesMap)) {
nativeBindings.put(name, entry.getValue());
}
}
env.put("native", createNativeModule(nativeBindings));
return ImmutableMap.copyOf(env);
}
/**
* Constructs an environment for a BUILD file based on the default environment, the map
* corresponding to the {@code exported_rules} dict, and the value of {@code
* --experimental_builtins_injection_override}.
*
* <p>Injected rule symbols must override an existing native rule of that name. Only rules may be
* overridden in this manner, not generic built-ins such as {@code package} or {@code glob}.
* Throws InjectionException if these conditions are not met.
*
* <p>Whether or not injection actually occurs for a given map key depends on its prefix (if any)
* and the prefix of its appearance (if it appears at all) in the override list; see the
* documentation for {@code --experimental_builtins_injection_override}. Non-injected symbols must
* still obey the above constraints.
*/
public ImmutableMap<String, Object> createBuildEnvUsingInjection(
Map<String, Object> exportedRules, List<String> overridesList) throws InjectionException {
Map<String, Boolean> overridesMap = parseInjectionOverridesList(overridesList);
HashMap<String, Object> env = new HashMap<>(uninjectedBuildEnv);
for (Map.Entry<String, Object> entry : exportedRules.entrySet()) {
String key = entry.getKey();
String name = getKeySuffix(key);
validateSymbolIsInjectable(
name,
Sets.union(env.keySet(), Starlark.UNIVERSE.keySet()),
ruleFunctions.keySet(),
"rule");
if (injectionApplies(key, overridesMap)) {
env.put(name, entry.getValue());
}
}
return ImmutableMap.copyOf(env);
}
/** Indicates a problem performing builtins injection. */
public static final class InjectionException extends Exception {
InjectionException(String message) {
super(message);
}
}
}