| // 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; |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| 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); |
| } |
| } |
| } |