| // 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.packages; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.devtools.build.lib.cmdline.LabelConstants; |
| import com.google.devtools.build.lib.cmdline.LabelSyntaxException; |
| import com.google.devtools.build.lib.cmdline.RepositoryName; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.NullEventHandler; |
| import com.google.devtools.build.lib.events.StoredEventHandler; |
| import com.google.devtools.build.lib.packages.Package.NameConflictException; |
| import com.google.devtools.build.lib.packages.PackageFactory.EnvironmentExtension; |
| import com.google.devtools.build.lib.syntax.Dict; |
| import com.google.devtools.build.lib.syntax.EvalException; |
| import com.google.devtools.build.lib.syntax.EvalUtils; |
| import com.google.devtools.build.lib.syntax.Module; |
| import com.google.devtools.build.lib.syntax.Mutability; |
| import com.google.devtools.build.lib.syntax.ParserInput; |
| import com.google.devtools.build.lib.syntax.Printer; |
| import com.google.devtools.build.lib.syntax.Resolver; |
| import com.google.devtools.build.lib.syntax.Starlark; |
| import com.google.devtools.build.lib.syntax.StarlarkCallable; |
| import com.google.devtools.build.lib.syntax.StarlarkFile; |
| import com.google.devtools.build.lib.syntax.StarlarkSemantics; |
| import com.google.devtools.build.lib.syntax.StarlarkThread; |
| import com.google.devtools.build.lib.syntax.Tuple; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.lib.vfs.Root; |
| import com.google.devtools.build.lib.vfs.RootedPath; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import javax.annotation.Nullable; |
| |
| /** Parser for WORKSPACE files. Fills in an ExternalPackage.Builder */ |
| // TODO(adonovan): make a simpler API around a single static function of this form: |
| // nextState = Workspace.executeChunk(environment, previousState). |
| public class WorkspaceFactory { |
| |
| private final Package.Builder builder; |
| |
| private final Path installDir; |
| private final Path workspaceDir; |
| private final Path defaultSystemJavabaseDir; |
| private final Mutability mutability; |
| |
| private final RuleFactory ruleFactory; |
| |
| private final WorkspaceGlobals workspaceGlobals; |
| private final StarlarkSemantics starlarkSemantics; |
| private final ImmutableMap<String, Object> workspaceFunctions; |
| private final ImmutableList<EnvironmentExtension> environmentExtensions; |
| |
| // Values accumulated from all previous WORKSPACE file parts. |
| private final Map<String, Module> loadedModules = new HashMap<>(); |
| private final Map<String, Object> bindings = new HashMap<>(); |
| |
| // TODO(bazel-team): document installDir |
| /** |
| * @param builder a builder for the Workspace |
| * @param ruleClassProvider a provider for known rule classes |
| * @param environmentExtensions the Starlark environment extensions |
| * @param mutability the Mutability for the current evaluation context |
| * @param installDir the install directory |
| * @param workspaceDir the workspace directory |
| * @param defaultSystemJavabaseDir the local JDK directory |
| */ |
| public WorkspaceFactory( |
| Package.Builder builder, |
| RuleClassProvider ruleClassProvider, |
| ImmutableList<EnvironmentExtension> environmentExtensions, |
| Mutability mutability, |
| boolean allowOverride, |
| @Nullable Path installDir, |
| @Nullable Path workspaceDir, |
| @Nullable Path defaultSystemJavabaseDir, |
| StarlarkSemantics starlarkSemantics) { |
| this.builder = builder; |
| this.mutability = mutability; |
| this.installDir = installDir; |
| this.workspaceDir = workspaceDir; |
| this.defaultSystemJavabaseDir = defaultSystemJavabaseDir; |
| this.environmentExtensions = environmentExtensions; |
| this.ruleFactory = new RuleFactory(ruleClassProvider); |
| this.workspaceGlobals = new WorkspaceGlobals(allowOverride, ruleFactory); |
| this.starlarkSemantics = starlarkSemantics; |
| this.workspaceFunctions = |
| createWorkspaceFunctions( |
| allowOverride, ruleFactory, this.workspaceGlobals, starlarkSemantics); |
| } |
| |
| // TODO(adonovan): move this into the test. It doesn't need privileged access, |
| // and it's called exactly once (!). |
| @VisibleForTesting |
| void parseForTesting(ParserInput source, @Nullable StoredEventHandler localReporter) |
| throws BuildFileContainsErrorsException, InterruptedException { |
| if (localReporter == null) { |
| localReporter = new StoredEventHandler(); |
| } |
| |
| StarlarkFile file = StarlarkFile.parse(source); // use default options in tests |
| if (!file.ok()) { |
| Event.replayEventsOn(localReporter, file.errors()); |
| throw new BuildFileContainsErrorsException( |
| LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER, "Failed to parse " + source.getFile()); |
| } |
| execute( |
| file, |
| /* additionalLoadedModules= */ ImmutableMap.of(), |
| localReporter, |
| WorkspaceFileValue.key( |
| RootedPath.toRootedPath( |
| Root.fromPath(workspaceDir), PathFragment.create(source.getFile())))); |
| } |
| |
| /** |
| * Actually runs through the AST, calling the functions in the WORKSPACE file and adding rules to |
| * the //external package. |
| */ |
| public void execute( |
| StarlarkFile file, |
| Map<String, Module> loadedModules, |
| WorkspaceFileValue.WorkspaceFileKey workspaceFileKey) |
| throws InterruptedException { |
| Preconditions.checkNotNull(file); |
| Preconditions.checkNotNull(loadedModules); |
| // TODO(adonovan): What's up with the transient StoredEventHandler? |
| // Doesn't this discard events, including print statements? |
| execute(file, loadedModules, new StoredEventHandler(), workspaceFileKey); |
| } |
| |
| private void execute( |
| StarlarkFile file, |
| Map<String, Module> additionalLoadedModules, |
| StoredEventHandler localReporter, |
| WorkspaceFileValue.WorkspaceFileKey workspaceFileKey) |
| throws InterruptedException { |
| loadedModules.putAll(additionalLoadedModules); |
| |
| // set up predeclared environment |
| HashMap<String, Object> predeclared = new HashMap<>(); |
| predeclared.putAll(getDefaultEnvironment()); |
| predeclared.putAll(bindings); // (may shadow bindings in default environment) |
| Module module = Module.withPredeclared(starlarkSemantics, predeclared); |
| |
| // resolve |
| Resolver.resolveFile(file, module); |
| |
| // create thread |
| StarlarkThread thread = new StarlarkThread(mutability, starlarkSemantics); |
| thread.setLoader(loadedModules::get); |
| thread.setPrintHandler(Event.makeDebugPrintHandler(localReporter)); |
| thread.setThreadLocal( |
| PackageFactory.PackageContext.class, |
| new PackageFactory.PackageContext(builder, null, localReporter)); |
| |
| // The workspace environment doesn't need the tools repository or the fragment map |
| // because executing workspace rules happens before analysis and it doesn't need a |
| // repository mapping because calls to the Label constructor in the WORKSPACE file |
| // are, by definition, not in an external repository and so they don't need the mapping |
| new BazelStarlarkContext( |
| BazelStarlarkContext.Phase.WORKSPACE, |
| /*toolsRepository=*/ null, |
| /*fragmentNameToClass=*/ null, |
| /*repoMapping=*/ ImmutableMap.of(), |
| new SymbolGenerator<>(workspaceFileKey), |
| /*analysisRuleLabel=*/ null) |
| .storeInThread(thread); |
| |
| List<String> globs = new ArrayList<>(); // unused |
| if (!file.ok()) { |
| Event.replayEventsOn(localReporter, file.errors()); |
| } else if (PackageFactory.checkBuildSyntax( |
| file, globs, globs, new HashMap<>(), localReporter)) { |
| try { |
| EvalUtils.exec(file, module, thread); |
| } catch (EvalException ex) { |
| localReporter.handle(Event.error(ex.getLocation(), ex.getMessage())); |
| } |
| } |
| |
| // Accumulate the global bindings created by this chunk of the WORKSPACE file, |
| // for use in the next chunk. This set does not include the bindings |
| // added by getDefaultEnvironment; but it does include bindings created by load, |
| // so we will need to set the legacy load-binds-globally flag for this file in due course. |
| this.bindings.putAll(module.getGlobals()); |
| |
| builder.addPosts(localReporter.getPosts()); |
| builder.addEvents(localReporter.getEvents()); |
| if (localReporter.hasErrors()) { |
| builder.setContainsErrors(); |
| } |
| localReporter.clear(); |
| } |
| |
| /** |
| * Adds the various values returned by the parsing of the previous workspace file parts. {@code |
| * aPackage} is the package returned by the parent WorkspaceFileFunction, {@code loadedModules} is |
| * the set of modules loaded by load statements in the parent WorkspaceFileFunction and {@code |
| * variableBindings} the list of top level variable bindings of that same call. |
| */ |
| public void setParent( |
| Package aPackage, Map<String, Module> loadedModules, Map<String, Object> bindings) |
| throws NameConflictException, InterruptedException { |
| this.bindings.putAll(bindings); |
| this.loadedModules.putAll(loadedModules); |
| builder.setWorkspaceName(aPackage.getWorkspaceName()); |
| // Transmit the content of the parent package to the new package builder. |
| builder.addPosts(aPackage.getPosts()); |
| builder.addEvents(aPackage.getEvents()); |
| if (aPackage.containsErrors()) { |
| builder.setContainsErrors(); |
| } |
| builder.addRegisteredExecutionPlatforms(aPackage.getRegisteredExecutionPlatforms()); |
| builder.addRegisteredToolchains(aPackage.getRegisteredToolchains()); |
| builder.addRepositoryMappings(aPackage); |
| for (Rule rule : aPackage.getTargets(Rule.class)) { |
| try { |
| // The old rule references another Package instance and we wan't to keep the invariant that |
| // every Rule references the Package it is contained within |
| Rule newRule = |
| builder.createRule( |
| rule.getLabel(), |
| rule.getRuleClassObject(), |
| rule.getLocation(), |
| rule.getCallStack().toList(), |
| rule.getAttributeContainer()); |
| newRule.populateOutputFiles(NullEventHandler.INSTANCE, builder); |
| if (rule.containsErrors()) { |
| newRule.setContainsErrors(); |
| } |
| builder.addRule(newRule); |
| } catch (LabelSyntaxException e) { |
| // This rule has already been created once, so it should have worked the second time, too |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| |
| /** |
| * Returns a callable Starlark value that implements the build or workspace rule "ruleClass" (e.g. |
| * cc_library) in the specified package context. |
| */ |
| private static StarlarkCallable newRuleFunction( |
| final RuleFactory ruleFactory, final String ruleClassName, final boolean allowOverride) { |
| return new StarlarkCallable() { |
| @Override |
| public String getName() { |
| return ruleClassName; |
| } |
| |
| @Override |
| public String toString() { |
| return getName() + "(...)"; |
| } |
| |
| @Override |
| public boolean isImmutable() { |
| return true; |
| } |
| |
| @Override |
| public void repr(Printer printer) { |
| printer.append("<built-in function " + getName() + ">"); |
| } |
| |
| @Override |
| public Object call(StarlarkThread thread, Tuple<Object> args, Dict<String, Object> kwargs) |
| throws EvalException, InterruptedException { |
| if (!args.isEmpty()) { |
| throw new EvalException(null, "unexpected positional arguments"); |
| } |
| try { |
| Package.Builder builder = PackageFactory.getContext(thread).pkgBuilder; |
| // TODO(adonovan): this doesn't look safe! |
| String externalRepoName = (String) kwargs.get("name"); |
| if (!allowOverride |
| && externalRepoName != null |
| && builder.getTarget(externalRepoName) != null) { |
| throw Starlark.errorf( |
| "Cannot redefine repository after any load statement in the WORKSPACE file (for" |
| + " repository '%s')", |
| externalRepoName); |
| } |
| // Add an entry in every repository from @<mainRepoName> to "@" to avoid treating |
| // @<mainRepoName> as a separate repository. This will be overridden if the main |
| // repository has a repo_mapping entry from <mainRepoName> to something. |
| WorkspaceFactoryHelper.addMainRepoEntry(builder, externalRepoName, thread.getSemantics()); |
| WorkspaceFactoryHelper.addRepoMappings(builder, kwargs, externalRepoName); |
| RuleClass ruleClass = ruleFactory.getRuleClass(ruleClassName); |
| RuleClass bindRuleClass = ruleFactory.getRuleClass("bind"); |
| Rule rule = |
| WorkspaceFactoryHelper.createAndAddRepositoryRule( |
| builder, |
| ruleClass, |
| bindRuleClass, |
| WorkspaceFactoryHelper.getFinalKwargs(kwargs), |
| thread.getSemantics(), |
| thread.getCallStack()); |
| if (!WorkspaceGlobals.isLegalWorkspaceName(rule.getName())) { |
| throw new EvalException( |
| null, |
| rule |
| + "'s name field must be a legal workspace name;" |
| + " workspace names may contain only A-Z, a-z, 0-9, '-', '_' and '.'"); |
| } |
| } catch (RuleFactory.InvalidRuleException |
| | Package.NameConflictException |
| | LabelSyntaxException e) { |
| throw new EvalException(null, e.getMessage()); |
| } |
| return Starlark.NONE; |
| } |
| }; |
| } |
| |
| private static ImmutableMap<String, Object> createWorkspaceFunctions( |
| boolean allowOverride, |
| RuleFactory ruleFactory, |
| WorkspaceGlobals workspaceGlobals, |
| StarlarkSemantics starlarkSemantics) { |
| ImmutableMap.Builder<String, Object> env = ImmutableMap.builder(); |
| Starlark.addMethods(env, workspaceGlobals, starlarkSemantics); |
| |
| for (String ruleClass : ruleFactory.getRuleClassNames()) { |
| // There is both a "bind" WORKSPACE function and a "bind" rule. In workspace files, |
| // the non-rule function takes precedence. |
| // TODO(cparsons): Rule functions should not be added to WORKSPACE files. |
| if (!ruleClass.equals("bind")) { |
| StarlarkCallable ruleFunction = newRuleFunction(ruleFactory, ruleClass, allowOverride); |
| env.put(ruleClass, ruleFunction); |
| } |
| } |
| |
| return env.build(); |
| } |
| |
| private ImmutableMap<String, Object> getDefaultEnvironment() { |
| ImmutableMap.Builder<String, Object> env = ImmutableMap.builder(); |
| env.putAll(StarlarkLibrary.COMMON); // e.g. select, depset |
| env.putAll(workspaceFunctions); |
| if (installDir != null) { |
| env.put("__embedded_dir__", installDir.getPathString()); |
| } |
| if (workspaceDir != null) { |
| env.put("__workspace_dir__", workspaceDir.getPathString()); |
| } |
| env.put("DEFAULT_SYSTEM_JAVABASE", getDefaultSystemJavabase()); |
| for (EnvironmentExtension ext : environmentExtensions) { |
| ext.updateWorkspace(env); |
| } |
| return env.build(); |
| } |
| |
| private String getDefaultSystemJavabase() { |
| // --javabase is empty if there's no locally installed JDK |
| return defaultSystemJavabaseDir != null ? defaultSystemJavabaseDir.toString() : ""; |
| } |
| |
| /** Returns the entries to populate the "native" module with, for WORKSPACE-loaded .bzl files. */ |
| static ImmutableMap<String, Object> createNativeModuleBindings( |
| RuleClassProvider ruleClassProvider, String version) { |
| // Machinery to build the collection of workspace functions. |
| RuleFactory ruleFactory = new RuleFactory(ruleClassProvider); |
| WorkspaceGlobals workspaceGlobals = new WorkspaceGlobals(/*allowOverride=*/ false, ruleFactory); |
| // TODO(bazel-team): StarlarkSemantics should be a parameter here, as native module can be |
| // configured by flags. [brandjon: This should be possible now that we create the native module |
| // in StarlarkBuiltinsFunction. We could defer creation until the StarlarkSemantics are known. |
| // But mind that some code may depend on being able to enumerate all possible entries regardless |
| // of the particular semantics.] |
| ImmutableMap<String, Object> workspaceFunctions = |
| createWorkspaceFunctions( |
| /*allowOverride=*/ false, ruleFactory, workspaceGlobals, StarlarkSemantics.DEFAULT); |
| |
| // Determine the contents for native. |
| ImmutableMap.Builder<String, Object> bindings = new ImmutableMap.Builder<>(); |
| Starlark.addMethods(bindings, new StarlarkNativeModule()); |
| for (Map.Entry<String, Object> entry : workspaceFunctions.entrySet()) { |
| String name = entry.getKey(); |
| if (name.startsWith("$")) { |
| // Skip "abstract" rules like "$go_rule". |
| continue; |
| } |
| // "workspace" is explicitly omitted from the native module, |
| // as it must only occur at the top of a WORKSPACE file. |
| // TODO(cparsons): Clean up separation between environments. |
| if (name.equals("workspace")) { |
| continue; |
| } |
| bindings.put(entry); |
| } |
| bindings.put("bazel_version", version); |
| |
| return bindings.build(); |
| } |
| |
| public Map<String, Module> getLoadedModules() { |
| return loadedModules; |
| } |
| |
| public Map<String, Object> getVariableBindings() { |
| return ImmutableMap.copyOf(bindings); |
| } |
| |
| public Map<PathFragment, RepositoryName> getManagedDirectories() { |
| return workspaceGlobals.getManagedDirectories(); |
| } |
| |
| public ImmutableSortedSet<String> getDoNotSymlinkInExecrootPaths() { |
| return workspaceGlobals.getDoNotSymlinkInExecrootPaths(); |
| } |
| } |