blob: 47e344d7c1a1cca521f61c7e10262e1b7c727b94 [file] [log] [blame]
// Copyright 2019 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 static com.google.devtools.build.lib.syntax.Starlark.NONE;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.LabelValidator;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.cmdline.TargetPattern;
import com.google.devtools.build.lib.packages.Package.NameConflictException;
import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException;
import com.google.devtools.build.lib.skylarkbuildapi.WorkspaceGlobalsApi;
import com.google.devtools.build.lib.syntax.Dict;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Module;
import com.google.devtools.build.lib.syntax.NoneType;
import com.google.devtools.build.lib.syntax.Sequence;
import com.google.devtools.build.lib.syntax.Starlark;
import com.google.devtools.build.lib.syntax.StarlarkThread;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/** A collection of global Starlark build API functions that apply to WORKSPACE files. */
public class WorkspaceGlobals implements WorkspaceGlobalsApi {
// Must start with a letter and can contain letters, numbers, and underscores
private static final Pattern LEGAL_WORKSPACE_NAME = Pattern.compile("^\\p{Alpha}\\w*$");
private final boolean allowOverride;
private final RuleFactory ruleFactory;
// Mapping of the relative paths of the incrementally updated managed directories
// to the managing external repositories
private final TreeMap<PathFragment, RepositoryName> managedDirectoriesMap;
// Directories to be excluded from symlinking to the execroot.
private ImmutableSortedSet<String> doNotSymlinkInExecrootPaths;
public WorkspaceGlobals(boolean allowOverride, RuleFactory ruleFactory) {
this.allowOverride = allowOverride;
this.ruleFactory = ruleFactory;
this.managedDirectoriesMap = Maps.newTreeMap();
}
@Override
public NoneType workspace(
String name,
Dict<?, ?> managedDirectories, // <String, Object>
StarlarkThread thread)
throws EvalException, InterruptedException {
if (!allowOverride) {
throw Starlark.errorf(
"workspace() function should be used only at the top of the WORKSPACE file");
}
if (!isLegalWorkspaceName(name)) {
throw Starlark.errorf("%s is not a legal workspace name", name);
}
String errorMessage = LabelValidator.validateTargetName(name);
if (errorMessage != null) {
throw Starlark.errorf("%s", errorMessage);
}
PackageFactory.getContext(thread).pkgBuilder.setWorkspaceName(name);
Package.Builder builder = PackageFactory.getContext(thread).pkgBuilder;
RuleClass localRepositoryRuleClass = ruleFactory.getRuleClass("local_repository");
RuleClass bindRuleClass = ruleFactory.getRuleClass("bind");
Map<String, Object> kwargs = ImmutableMap.<String, Object>of("name", name, "path", ".");
try {
// This effectively adds a "local_repository(name = "<ws>", path = ".")"
// definition to the WORKSPACE file.
WorkspaceFactoryHelper.createAndAddRepositoryRule(
builder,
localRepositoryRuleClass,
bindRuleClass,
kwargs,
thread.getSemantics(),
thread.getCallStack());
} catch (InvalidRuleException | NameConflictException | LabelSyntaxException e) {
throw Starlark.errorf("%s", e.getMessage());
}
// Add entry in repository map from "@name" --> "@" to avoid issue where bazel
// treats references to @name as a separate external repo
builder.addRepositoryMappingEntry(
RepositoryName.MAIN, RepositoryName.createFromValidStrippedName(name), RepositoryName.MAIN);
parseManagedDirectories(
Dict.cast(managedDirectories, String.class, Object.class, "managed_directories"));
return NONE;
}
@Override
public NoneType dontSymlinkDirectoriesInExecroot(Sequence<?> paths, StarlarkThread thread)
throws EvalException, InterruptedException {
List<String> pathsList = Sequence.cast(paths, String.class, "paths");
Set<String> set = Sets.newHashSet();
for (String path : pathsList) {
PathFragment pathFragment = PathFragment.create(path);
if (pathFragment.isEmpty()) {
throw Starlark.errorf("Empty path can not be passed to toplevel_output_directories.");
}
if (pathFragment.containsUplevelReferences() || pathFragment.segmentCount() > 1) {
throw Starlark.errorf(
"toplevel_output_directories can only accept top level directories under"
+ " workspace, \"%s\" can not be specified as an attribute.",
path);
}
if (pathFragment.isAbsolute()) {
throw Starlark.errorf(
"toplevel_output_directories can only accept top level directories under"
+ " workspace, absolute path \"%s\" can not be specified as an attribute.",
path);
}
if (!set.add(pathFragment.getBaseName())) {
throw Starlark.errorf(
"toplevel_output_directories should not contain duplicate values: \"%s\" is"
+ " specified more then once.",
path);
}
}
doNotSymlinkInExecrootPaths = ImmutableSortedSet.copyOf(set);
return NONE;
}
private void parseManagedDirectories(
Map<String, ?> managedDirectories) // <String, Sequence<String>>
throws EvalException {
Map<PathFragment, String> nonNormalizedPathsMap = Maps.newHashMap();
for (Map.Entry<String, ?> entry : managedDirectories.entrySet()) {
RepositoryName repositoryName = createRepositoryName(entry.getKey());
List<PathFragment> paths =
getManagedDirectoriesPaths(entry.getValue(), nonNormalizedPathsMap);
for (PathFragment dir : paths) {
PathFragment floorKey = managedDirectoriesMap.floorKey(dir);
if (dir.equals(floorKey)) {
throw Starlark.errorf(
"managed_directories attribute should not contain multiple"
+ " (or duplicate) repository mappings for the same directory ('%s').",
nonNormalizedPathsMap.get(dir));
}
PathFragment ceilingKey = managedDirectoriesMap.ceilingKey(dir);
boolean isDescendant = floorKey != null && dir.startsWith(floorKey);
if (isDescendant || (ceilingKey != null && ceilingKey.startsWith(dir))) {
throw Starlark.errorf(
"managed_directories attribute value can not contain nested mappings."
+ " '%s' is a descendant of '%s'.",
nonNormalizedPathsMap.get(isDescendant ? dir : ceilingKey),
nonNormalizedPathsMap.get(isDescendant ? floorKey : dir));
}
managedDirectoriesMap.put(dir, repositoryName);
}
}
}
private static RepositoryName createRepositoryName(String key) throws EvalException {
if (!key.startsWith("@")) {
throw Starlark.errorf(
"Cannot parse repository name '%s'. Repository name should start with '@'.", key);
}
try {
return RepositoryName.create(key);
} catch (LabelSyntaxException e) {
throw Starlark.errorf("%s", e);
}
}
private static List<PathFragment> getManagedDirectoriesPaths(
Object directoriesList, Map<PathFragment, String> nonNormalizedPathsMap)
throws EvalException {
if (!(directoriesList instanceof Sequence)) {
throw Starlark.errorf(
"managed_directories attribute value should be of the type attr.string_list_dict(),"
+ " mapping repository name to the list of managed directories.");
}
List<PathFragment> result = Lists.newArrayList();
for (Object obj : (Sequence) directoriesList) {
if (!(obj instanceof String)) {
throw Starlark.errorf("Expected managed directory path (as string), but got '%s'.", obj);
}
String path = ((String) obj).trim();
if (path.isEmpty()) {
throw Starlark.errorf("Expected managed directory path to be non-empty string.");
}
PathFragment pathFragment = PathFragment.create(path);
if (pathFragment.isAbsolute()) {
throw Starlark.errorf(
"Expected managed directory path ('%s') to be relative to the workspace root.", path);
}
if (pathFragment.containsUplevelReferences()) {
throw Starlark.errorf(
"Expected managed directory path ('%s') to be under the workspace root.", path);
}
nonNormalizedPathsMap.put(pathFragment, path);
result.add(pathFragment);
}
return result;
}
public Map<PathFragment, RepositoryName> getManagedDirectories() {
return managedDirectoriesMap;
}
public ImmutableSortedSet<String> getDoNotSymlinkInExecrootPaths() {
return doNotSymlinkInExecrootPaths;
}
private static RepositoryName getRepositoryName(@Nullable Label label) {
if (label == null) {
// registration happened directly in the main WORKSPACE
return RepositoryName.MAIN;
}
// registeration happened in a loaded bzl file
return label.getPackageIdentifier().getRepository();
}
private static ImmutableList<String> renamePatterns(
List<String> patterns, Package.Builder builder, StarlarkThread thread) {
BazelModuleContext bzlModule =
(BazelModuleContext) Module.ofInnermostEnclosingStarlarkFunction(thread).getClientData();
RepositoryName myName = getRepositoryName((bzlModule != null ? bzlModule.label() : null));
Map<RepositoryName, RepositoryName> renaming = builder.getRepositoryMappingFor(myName);
return patterns.stream()
.map(patternEntry -> TargetPattern.renameRepository(patternEntry, renaming))
.collect(ImmutableList.toImmutableList());
}
@Override
public NoneType registerExecutionPlatforms(Sequence<?> platformLabels, StarlarkThread thread)
throws EvalException, InterruptedException {
// Add to the package definition for later.
Package.Builder builder = PackageFactory.getContext(thread).pkgBuilder;
List<String> patterns = Sequence.cast(platformLabels, String.class, "platform_labels");
builder.addRegisteredExecutionPlatforms(renamePatterns(patterns, builder, thread));
return NONE;
}
@Override
public NoneType registerToolchains(Sequence<?> toolchainLabels, StarlarkThread thread)
throws EvalException, InterruptedException {
// Add to the package definition for later.
Package.Builder builder = PackageFactory.getContext(thread).pkgBuilder;
List<String> patterns = Sequence.cast(toolchainLabels, String.class, "toolchain_labels");
builder.addRegisteredToolchains(renamePatterns(patterns, builder, thread));
return NONE;
}
@Override
public NoneType bind(String name, Object actual, StarlarkThread thread)
throws EvalException, InterruptedException {
Label nameLabel;
try {
nameLabel = Label.parseAbsolute("//external:" + name, ImmutableMap.of());
} catch (LabelSyntaxException e) {
throw Starlark.errorf("%s", e.getMessage());
}
try {
Package.Builder builder = PackageFactory.getContext(thread).pkgBuilder;
RuleClass ruleClass = ruleFactory.getRuleClass("bind");
WorkspaceFactoryHelper.addBindRule(
builder,
ruleClass,
nameLabel,
actual == NONE ? null : Label.parseAbsolute((String) actual, ImmutableMap.of()),
thread.getSemantics(),
thread.getCallStack());
} catch (InvalidRuleException | Package.NameConflictException | LabelSyntaxException e) {
throw Starlark.errorf("%s", e.getMessage());
}
return NONE;
}
/**
* Returns true if the given name is a valid workspace name.
*/
public static boolean isLegalWorkspaceName(String name) {
Matcher matcher = LEGAL_WORKSPACE_NAME.matcher(name);
return matcher.matches();
}
}