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