blob: 704654232e7ccf6b9601fb6790ad7b520651edd7 [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
*
* <ol>
* <li>Gathering a fixed set of top-level symbols that are present in all versions of Bazel. This
* is handled by {@link StarlarkGlobals}.
* <li>Gathering additional toplevels and rules registered on the {@link
* ConfiguredRuleClassProvider}.
* <li>Applying builtins injection (see {@link StarlarkBuiltinsFunction}), if applicable.
* </ol>
*
* <p>The end result of (1) and (2) is constant for any given Bazel binary and is cached by an
* instance of this class upon construction. The final environment, which takes into account
* builtins injection, is obtained by calling methods on this class during Skyframe evaluation; the
* result is cached in {@link StarlarkBuiltinsValue}.
*
* <p>There are a few exceptions where this class is not the final word on the environment:
*
* <ul>
* <li>The WORKSPACE file's environment is setup with the help of {@link
* WorkspaceFactory#getDefaultEnvironment}.
* <li>If a prelude file is in use, its bindings are added to the ones this class specifies for
* BUILD files. This happens in {@link PackageFunction}.
* </ul>
*/
public final class BazelStarlarkEnvironment {
// TODO(#11954): Eventually the BUILD and WORKSPACE bzl dialects should converge. Right now they
// only differ on the "native" object.
// All of the environments stored in these fields exclude the symbols in {@link
// Starlark#UNIVERSE}, which the interpreter adds automatically.
// Constructor param, used in this class but also re-exported to clients.
private final StarlarkGlobals starlarkGlobals;
// The following fields correspond to the constructor params of the same name. These include only
// the params that are needed by injection. See the constructor for javadoc.
private final ImmutableMap<String, ?> ruleFunctions;
private final ImmutableMap<String, Object> registeredBzlToplevels;
private final ImmutableMap<String, Object> workspaceBzlNativeBindings;
/**
* The top-level predeclared symbols, excluding {@code native}, for a .bzl file (regardless of who
* loads it), before injection.
*/
private final ImmutableMap<String, Object> bzlToplevelsWithoutNative;
/** The {@code native} module fields for a BUILD-loaded bzl module, before builtins injection. */
private final ImmutableMap<String, Object> uninjectedBuildBzlNativeBindings;
/**
* The top-level predeclared symbols (including {@code native}) 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, before builtins injection.
*/
private final ImmutableMap<String, Object> uninjectedWorkspaceBzlEnv;
/** 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 MODULE.bazel file. */
private final ImmutableMap<String, Object> moduleBazelEnv;
/** The top-level predeclared symbols for a REPO.bazel file. */
private final ImmutableMap<String, Object> repoBazelEnv;
/**
* Constructs a new {@code BazelStarlarkEnvironment} that will have complete knowledge of the
* proper Starlark symbols available in each context, with and without injection.
*
* @param ruleFunctions a map from a rule class name (e.g. "java_library") to the (uninjected)
* Starlark callable that instantiates it
* @param registeredBuildFileToplevels a map of additional (i.e., registered with the rule class
* provider) top-level symbols for BUILD files, prior to builtins injection. These symbols are
* also added to the {@code native} object. Does not include rules.
* @param registeredBzlToplevels a map of additional (i.e., registered with the rule class
* provider) top-level symbols for .bzl files, prior to builtins injection
* @param workspaceBzlNativeBindings entries available in the {@code native} object for
* WORKSPACE-loaded .bzl files
* @param builtinsInternals a set of symbols to be made available to {@code @_builtins} .bzls
* under the {@code _builtins.internal} object. These symbols are not exposed to user .bzl
* code and do not constitute a public or stable API if not exposed through another means.
*/
public BazelStarlarkEnvironment(
StarlarkGlobals starlarkGlobals,
ImmutableMap<String, ?> ruleFunctions,
ImmutableMap<String, Object> registeredBuildFileToplevels,
ImmutableMap<String, Object> registeredBzlToplevels,
ImmutableMap<String, Object> workspaceBzlNativeBindings,
ImmutableMap<String, Object> builtinsInternals) {
this.starlarkGlobals = starlarkGlobals;
this.ruleFunctions = ruleFunctions;
this.registeredBzlToplevels = registeredBzlToplevels;
this.workspaceBzlNativeBindings = workspaceBzlNativeBindings;
this.bzlToplevelsWithoutNative =
createBzlToplevelsWithoutNative(starlarkGlobals, registeredBzlToplevels);
this.uninjectedBuildBzlNativeBindings =
createUninjectedBuildBzlNativeBindings(
starlarkGlobals, ruleFunctions, registeredBuildFileToplevels);
this.uninjectedBuildBzlEnv =
createUninjectedBuildBzlEnv(bzlToplevelsWithoutNative, uninjectedBuildBzlNativeBindings);
this.uninjectedWorkspaceBzlEnv =
createWorkspaceBzlEnv(bzlToplevelsWithoutNative, workspaceBzlNativeBindings);
this.builtinsBzlEnv =
createBuiltinsBzlEnv(
starlarkGlobals,
builtinsInternals,
uninjectedBuildBzlNativeBindings,
uninjectedBuildBzlEnv);
this.uninjectedBuildEnv =
createUninjectedBuildEnv(starlarkGlobals, ruleFunctions, registeredBuildFileToplevels);
this.moduleBazelEnv = starlarkGlobals.getModuleToplevels();
this.repoBazelEnv = starlarkGlobals.getRepoToplevels();
}
/**
* Returns a {@link StarlarkGlobals} instance.
*
* <p>In practice, {@link StarlarkGlobals} is a singleton, so this accessor is really about
* retrieving {@link StarlarkGlobalsImpl#INSTANCE} without requiring a dependency on the
* lib/analysis/ package.
*/
public StarlarkGlobals getStarlarkGlobals() {
return starlarkGlobals;
}
/**
* 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. Excludes symbols in {@link Starlark#UNIVERSE}.
*
* <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. Excludes symbols in {@link Starlark#UNIVERSE}.
*
* <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 before builtins injection. Excludes
* symbols in {@link Starlark#UNIVERSE}.
*/
public ImmutableMap<String, Object> getUninjectedWorkspaceBzlEnv() {
return uninjectedWorkspaceBzlEnv;
}
/**
* Returns the environment for bzl files in the {@code @_builtins} pseudo-repository. Excludes
* symbols in {@link Starlark#UNIVERSE}.
*/
public ImmutableMap<String, Object> getBuiltinsBzlEnv() {
return builtinsBzlEnv;
}
/** Returns the environment for MODULE.bazel files. */
public ImmutableMap<String, Object> getModuleBazelEnv() {
return moduleBazelEnv;
}
/** Returns the environment for REPO.bazel files. */
public ImmutableMap<String, Object> getRepoBazelEnv() {
return repoBazelEnv;
}
private static ImmutableMap<String, Object> createBzlToplevelsWithoutNative(
StarlarkGlobals starlarkGlobals, Map<String, Object> registeredBzlToplevels) {
ImmutableMap.Builder<String, Object> env = new ImmutableMap.Builder<>();
env.putAll(starlarkGlobals.getFixedBzlToplevels());
env.putAll(registeredBzlToplevels);
return env.buildOrThrow();
}
/**
* Produces everything that would be in the {@code native} object for BUILD-loaded bzl files if
* builtins injection didn't happen.
*/
private static ImmutableMap<String, Object> createUninjectedBuildBzlNativeBindings(
StarlarkGlobals starlarkGlobals,
Map<String, ?> ruleFunctions,
Map<String, Object> registeredBuildFileToplevels) {
ImmutableMap.Builder<String, Object> env = new ImmutableMap.Builder<>();
env.putAll(starlarkGlobals.getFixedBuildFileToplevelsSharedWithNative());
env.putAll(ruleFunctions);
env.putAll(registeredBuildFileToplevels);
return env.buildOrThrow();
}
/** 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(
Map<String, Object> bzlToplevelsWithoutNative,
Map<String, Object> uninjectedBuildBzlNativeBindings) {
ImmutableMap.Builder<String, Object> env = new ImmutableMap.Builder<>();
env.putAll(bzlToplevelsWithoutNative);
// 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 env.buildOrThrow();
}
private static ImmutableMap<String, Object> createUninjectedBuildEnv(
StarlarkGlobals starlarkGlobals,
Map<String, ?> ruleFunctions,
Map<String, Object> registeredBuildFileToplevels) {
ImmutableMap.Builder<String, Object> env = ImmutableMap.builder();
env.putAll(starlarkGlobals.getFixedBuildFileToplevelsSharedWithNative());
env.putAll(starlarkGlobals.getFixedBuildFileToplevelsNotInNative());
env.putAll(ruleFunctions);
env.putAll(registeredBuildFileToplevels);
return env.buildOrThrow();
}
private static ImmutableMap<String, Object> createWorkspaceBzlEnv(
Map<String, Object> bzlToplevelsWithoutNative,
Map<String, Object> workspaceBzlNativeBindings) {
ImmutableMap.Builder<String, Object> env = new ImmutableMap.Builder<>();
env.putAll(bzlToplevelsWithoutNative);
// See above comments for native in BUILD bzls.
env.put("native", createNativeModule(workspaceBzlNativeBindings));
return env.buildOrThrow();
}
private static ImmutableMap<String, Object> createBuiltinsBzlEnv(
StarlarkGlobals starlarkGlobals,
Map<String, Object> builtinsInternals,
Map<String, Object> uninjectedBuildBzlNativeBindings,
Map<String, Object> uninjectedBuildBzlEnv) {
Map<String, Object> env = new HashMap<>(starlarkGlobals.getFixedBzlToplevels());
// For _builtins.toplevel, replace all GuardedValues 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(builtinsInternals));
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 one that was registered on the rule class provider (e.g., {@code CcInfo} or
* {@code cc_library}), not a fixed symbol that's always available (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 com.google.devtools.build.lib.skyframe.StarlarkBuiltinsFunction
*/
public ImmutableMap<String, Object> createBuildBzlEnvUsingInjection(
Map<String, Object> exportedToplevels,
Map<String, Object> exportedRules,
List<String> overridesList)
throws InjectionException {
return createBzlEnvUsingInjection(
exportedToplevels, exportedRules, overridesList, uninjectedBuildBzlNativeBindings);
}
/**
* Constructs an environment for a WORKSPACE-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}.
*
* @see com.google.devtools.build.lib.skyframe.StarlarkBuiltinsFunction
*/
public ImmutableMap<String, Object> createWorkspaceBzlEnvUsingInjection(
Map<String, Object> exportedToplevels,
Map<String, Object> exportedRules,
List<String> overridesList)
throws InjectionException {
return createBzlEnvUsingInjection(
exportedToplevels, exportedRules, overridesList, workspaceBzlNativeBindings);
}
private ImmutableMap<String, Object> createBzlEnvUsingInjection(
Map<String, Object> exportedToplevels,
Map<String, Object> exportedRules,
List<String> overridesList,
Map<String, Object> nativeBase)
throws InjectionException {
Map<String, Boolean> overridesMap = parseInjectionOverridesList(overridesList);
Map<String, Object> env = new HashMap<>(bzlToplevelsWithoutNative);
// Determine "native" bindings.
// TODO(#11954): See above comment in createUninjectedBuildBzlEnv.
Map<String, Object> nativeBindings = new HashMap<>(nativeBase);
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));
// Determine top-level symbols.
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()),
registeredBzlToplevels.keySet(),
"top-level symbol");
if (injectionApplies(key, overridesMap)) {
env.put(name, entry.getValue());
}
}
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);
}
}
}