blob: 645b423f02b254b6e298f7e9c9347aeb1d3b0296 [file] [log] [blame]
// Copyright 2018 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.analysis.starlark;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.packages.BazelStarlarkContext;
import com.google.devtools.build.lib.packages.BzlInitThreadContext;
import com.google.devtools.build.lib.packages.BzlVisibility;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkBuildApiGlobals;
import java.util.List;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkThread;
/**
* Bazel implementation of {@link StarlarkBuildApiGlobals}: a collection of global Starlark build
* API functions that belong in the global namespace.
*/
// TODO(brandjon): This should probably be refactored into a StarlarkLibrary#BZL field, analogous to
// StarlarkLibrary#COMMON and StarlarkLibrary#BUILD.
public class BazelBuildApiGlobals implements StarlarkBuildApiGlobals {
@Override
public void visibility(Object value, StarlarkThread thread) throws EvalException {
// Manually check the experimental flag because enableOnlyWithFlag doesn't work for top-level
// builtins.
if (!thread.getSemantics().getBool(BuildLanguageOptions.EXPERIMENTAL_BZL_VISIBILITY)) {
throw Starlark.errorf("Use of `visibility()` requires --experimental_bzl_visibility");
}
// Fail if we're not initializing a .bzl module, or if that .bzl module isn't on the
// experimental allowlist, or if visibility is already set.
BzlInitThreadContext context = BzlInitThreadContext.fromOrFailFunction(thread, "visibility");
PackageIdentifier pkgId = context.getBzlFile().getPackageIdentifier();
List<String> allowlist =
thread.getSemantics().get(BuildLanguageOptions.EXPERIMENTAL_BZL_VISIBILITY_ALLOWLIST);
checkVisibilityAllowlist(pkgId, allowlist);
if (context.getBzlVisibility() != null) {
throw Starlark.errorf(".bzl visibility may not be set more than once");
}
BzlVisibility bzlVisibility = null;
// `visibility("public")` and `visibility("private")`
if (value instanceof String) {
if (value.equals("public")) {
bzlVisibility = BzlVisibility.PUBLIC;
} else if (value.equals("private")) {
bzlVisibility = BzlVisibility.PRIVATE;
}
// `visibility(["//pkg1", "//pkg2", ...])`
} else if (value instanceof StarlarkList) {
List<String> packageStrings = Sequence.cast(value, String.class, "visibility list");
ImmutableList.Builder<PackageIdentifier> packages =
ImmutableList.builderWithExpectedSize(packageStrings.size());
for (String packageString : packageStrings) {
PackageIdentifier packageId;
// Disallow "@foo//pkg", or even "@//pkg" or "@the_current_repo//pkg".
if (packageString.startsWith("@")) {
throw Starlark.errorf("package specifiers cannot begin with '@'");
}
try {
packageId = PackageIdentifier.parse(packageString);
} catch (LabelSyntaxException ex) {
throw Starlark.errorf("Invalid package: %s", ex.getMessage());
}
// PackageIdentifier.parse() on a string without a repo qualifier returns an identifier in
// the main repo. Substitute it with our own repo.
Preconditions.checkState(packageId.getRepository().equals(RepositoryName.MAIN));
packages.add(
PackageIdentifier.create(
context.getBzlFile().getRepository(), packageId.getPackageFragment()));
}
bzlVisibility = new BzlVisibility.PackageListBzlVisibility(packages.build());
}
if (bzlVisibility == null) {
throw Starlark.errorf(
"Invalid bzl-visibility: got '%s', want \"public\", \"private\", or list of package path"
+ " strings",
Starlark.type(value));
}
context.setBzlVisibility(bzlVisibility);
}
private void checkVisibilityAllowlist(PackageIdentifier pkgId, List<String> allowlist)
throws EvalException {
// The allowlist is represented as a list of strings because BuildLanguageOptions isn't allowed
// to depend on Label, PackageIdentifier, etc. For simplicity we just convert the strings to
// PackageIdentifiers here, at linear cost and redundantly for each call to `visibility()`. This
// is ok because the allowlist is not intended to stay permanent, it is expected to remain
// small, and calls to visibility() are relatively infrequent.
boolean foundMatch = false;
for (String allowedPkgString : allowlist) {
// TODO(b/22193153): This seems incorrect since parse doesn't take into account any repository
// map. (This shouldn't matter within Google's monorepo, which doesn't use a repo map.)
try {
// Special constant to disable allowlisting. For migration to enable the feature globally.
if (allowedPkgString.equals("everyone")) {
foundMatch = true;
break;
}
PackageIdentifier allowedPkgId = PackageIdentifier.parse(allowedPkgString);
if (pkgId.equals(allowedPkgId)) {
foundMatch = true;
break;
}
} catch (LabelSyntaxException ex) {
throw new EvalException("Invalid bzl-visibility allowlist", ex);
}
}
if (!foundMatch) {
throw Starlark.errorf(
"`visibility() is not enabled for package %s; consider adding it to "
+ "--experimental_bzl_visibility_allowlist",
pkgId.getCanonicalForm());
}
}
@Override
public StarlarkLateBoundDefault<?> configurationField(
String fragment, String name, StarlarkThread thread) throws EvalException {
BazelStarlarkContext context = BazelStarlarkContext.from(thread);
Class<?> fragmentClass = context.getFragmentNameToClass().get(fragment);
if (fragmentClass == null) {
throw Starlark.errorf("invalid configuration fragment name '%s'", fragment);
}
try {
return StarlarkLateBoundDefault.forConfigurationField(
fragmentClass, name, context.getToolsRepository());
} catch (StarlarkLateBoundDefault.InvalidConfigurationFieldException exception) {
throw new EvalException(exception);
}
}
}