blob: f93f6f3eda2c43d24f6e752017a8f49d02343003 [file] [log] [blame]
// Copyright 2014 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.packages.PackageFactory.getContext;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
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.PackageIdentifier;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.packages.Globber.BadGlobException;
import com.google.devtools.build.lib.packages.PackageFactory.NotRepresentableException;
import com.google.devtools.build.lib.packages.PackageFactory.PackageContext;
import com.google.devtools.build.lib.packages.RuleClass.Builder.ThirdPartyLicenseExistencePolicy;
import com.google.devtools.build.lib.packages.Type.ConversionException;
import com.google.devtools.build.lib.skylarkbuildapi.SkylarkNativeModuleApi;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
import com.google.devtools.build.lib.syntax.CallUtils;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.EvalUtils;
import com.google.devtools.build.lib.syntax.Runtime;
import com.google.devtools.build.lib.syntax.SkylarkDict;
import com.google.devtools.build.lib.syntax.SkylarkList;
import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
import com.google.devtools.build.lib.syntax.SkylarkUtils;
import com.google.devtools.build.lib.syntax.StarlarkThread;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.Nullable;
/** The Skylark native module. */
// TODO(cparsons): Move the definition of native.package() to this class.
public class SkylarkNativeModule implements SkylarkNativeModuleApi {
/**
* This map contains all the (non-rule) functions of the native module (keyed by their symbol
* name). These native module bindings should be added (without the 'native' module namespace) to
* the global Starlark environment for BUILD files.
*
* <p>For example, the function "glob" is available under both a global symbol name {@code glob()}
* as well as under the native module namepsace {@code native.glob()}. An entry of this map is
* thus ("glob" : glob function).
*/
public static final ImmutableMap<String, Object> BINDINGS_FOR_BUILD_FILES = initializeBindings();
private static ImmutableMap<String, Object> initializeBindings() {
SkylarkNativeModule nativeModule = new SkylarkNativeModule();
ImmutableMap.Builder<String, Object> bindings = ImmutableMap.builder();
for (String methodName : CallUtils.getMethodNames(SkylarkNativeModule.class)) {
bindings.put(methodName, CallUtils.getBuiltinCallable(nativeModule, methodName));
}
return bindings.build();
}
@Override
public SkylarkList<?> glob(
SkylarkList<?> include,
SkylarkList<?> exclude,
Integer excludeDirs,
Object allowEmptyArgument,
Location loc,
StarlarkThread thread)
throws EvalException, ConversionException, InterruptedException {
SkylarkUtils.checkLoadingPhase(thread, "native.glob", loc);
PackageContext context = getContext(thread, loc);
List<String> includes = Type.STRING_LIST.convert(include, "'glob' argument");
List<String> excludes = Type.STRING_LIST.convert(exclude, "'glob' argument");
List<String> matches;
boolean allowEmpty;
if (allowEmptyArgument == Runtime.UNBOUND) {
allowEmpty = !thread.getSemantics().incompatibleDisallowEmptyGlob();
} else if (allowEmptyArgument instanceof Boolean) {
allowEmpty = (Boolean) allowEmptyArgument;
} else {
throw new EvalException(
loc, "expected boolean for argument `allow_empty`, got `" + allowEmptyArgument + "`");
}
try {
Globber.Token globToken =
context.globber.runAsync(includes, excludes, excludeDirs != 0, allowEmpty);
matches = context.globber.fetch(globToken);
} catch (IOException e) {
String errorMessage =
String.format(
"error globbing [%s]%s: %s",
Joiner.on(", ").join(includes),
excludes.isEmpty() ? "" : " - [" + Joiner.on(", ").join(excludes) + "]",
e.getMessage());
context.eventHandler.handle(Event.error(loc, errorMessage));
context.pkgBuilder.setIOExceptionAndMessage(e, errorMessage);
matches = ImmutableList.of();
} catch (BadGlobException e) {
throw new EvalException(loc, e.getMessage());
} catch (IllegalArgumentException e) {
throw new EvalException(loc, "illegal argument in call to glob", e);
}
return MutableList.copyOf(thread, matches);
}
@Override
public Object existingRule(String name, Location loc, StarlarkThread thread)
throws EvalException, InterruptedException {
SkylarkUtils.checkLoadingOrWorkspacePhase(thread, "native.existing_rule", loc);
PackageContext context = getContext(thread, loc);
Target target = context.pkgBuilder.getTarget(name);
SkylarkDict<String, Object> rule = targetDict(target, loc, thread);
return rule != null ? rule : Runtime.NONE;
}
/*
If necessary, we could allow filtering by tag (anytag, alltags), name (regexp?), kind ?
For now, we ignore this, since users can implement it in Skylark.
*/
@Override
public SkylarkDict<String, SkylarkDict<String, Object>> existingRules(
Location loc, StarlarkThread thread) throws EvalException, InterruptedException {
SkylarkUtils.checkLoadingOrWorkspacePhase(thread, "native.existing_rules", loc);
PackageContext context = getContext(thread, loc);
Collection<Target> targets = context.pkgBuilder.getTargets();
SkylarkDict<String, SkylarkDict<String, Object>> rules = SkylarkDict.of(thread);
for (Target t : targets) {
if (t instanceof Rule) {
SkylarkDict<String, Object> rule = targetDict(t, loc, thread);
Preconditions.checkNotNull(rule);
rules.put(t.getName(), rule, loc, thread);
}
}
return rules;
}
@Override
public Runtime.NoneType packageGroup(
String name,
SkylarkList<?> packagesO,
SkylarkList<?> includesO,
Location loc,
StarlarkThread thread)
throws EvalException {
SkylarkUtils.checkLoadingPhase(thread, "native.package_group", loc);
PackageContext context = getContext(thread, loc);
List<String> packages =
Type.STRING_LIST.convert(packagesO, "'package_group.packages argument'");
List<Label> includes =
BuildType.LABEL_LIST.convert(
includesO, "'package_group.includes argument'", context.pkgBuilder.getBuildFileLabel());
try {
context.pkgBuilder.addPackageGroup(name, packages, includes, context.eventHandler, loc);
return Runtime.NONE;
} catch (LabelSyntaxException e) {
throw new EvalException(
loc, "package group has invalid name: " + name + ": " + e.getMessage());
} catch (Package.NameConflictException e) {
throw new EvalException(loc, e.getMessage());
}
}
@Override
public Runtime.NoneType exportsFiles(
SkylarkList<?> srcs,
Object visibilityO,
Object licensesO,
Location loc,
StarlarkThread thread)
throws EvalException {
SkylarkUtils.checkLoadingPhase(thread, "native.exports_files", loc);
Package.Builder pkgBuilder = getContext(thread, loc).pkgBuilder;
List<String> files = Type.STRING_LIST.convert(srcs, "'exports_files' operand");
RuleVisibility visibility;
try {
visibility =
EvalUtils.isNullOrNone(visibilityO)
? ConstantRuleVisibility.PUBLIC
: PackageFactory.getVisibility(
pkgBuilder.getBuildFileLabel(),
BuildType.LABEL_LIST.convert(
visibilityO, "'exports_files' operand", pkgBuilder.getBuildFileLabel()));
} catch (EvalException e) {
throw new EvalException(loc, e.getMessage());
}
// TODO(bazel-team): is licenses plural or singular?
License license = BuildType.LICENSE.convertOptional(licensesO, "'exports_files' operand");
for (String file : files) {
String errorMessage = LabelValidator.validateTargetName(file);
if (errorMessage != null) {
throw new EvalException(loc, errorMessage);
}
try {
InputFile inputFile = pkgBuilder.createInputFile(file, loc);
if (inputFile.isVisibilitySpecified() && inputFile.getVisibility() != visibility) {
throw new EvalException(
loc,
String.format(
"visibility for exported file '%s' declared twice", inputFile.getName()));
}
if (license != null && inputFile.isLicenseSpecified()) {
throw new EvalException(
loc,
String.format("licenses for exported file '%s' declared twice", inputFile.getName()));
}
// See if we should check third-party licenses: first checking for any hard-coded policy,
// then falling back to user-settable flags.
boolean checkLicenses;
if (pkgBuilder.getThirdPartyLicenseExistencePolicy()
== ThirdPartyLicenseExistencePolicy.ALWAYS_CHECK) {
checkLicenses = true;
} else if (pkgBuilder.getThirdPartyLicenseExistencePolicy()
== ThirdPartyLicenseExistencePolicy.NEVER_CHECK) {
checkLicenses = false;
} else {
checkLicenses = !thread.getSemantics().incompatibleDisableThirdPartyLicenseChecking();
}
if (checkLicenses
&& license == null
&& !pkgBuilder.getDefaultLicense().isSpecified()
&& RuleClass.isThirdPartyPackage(pkgBuilder.getPackageIdentifier())) {
throw new EvalException(
loc,
"third-party file '"
+ inputFile.getName()
+ "' lacks a license declaration "
+ "with one of the following types: notice, reciprocal, permissive, "
+ "restricted, unencumbered, by_exception_only");
}
pkgBuilder.setVisibilityAndLicense(inputFile, visibility, license);
} catch (Package.Builder.GeneratedLabelConflict e) {
throw new EvalException(loc, e.getMessage());
}
}
return Runtime.NONE;
}
@Override
public String packageName(Location loc, StarlarkThread thread) throws EvalException {
SkylarkUtils.checkLoadingPhase(thread, "native.package_name", loc);
PackageIdentifier packageId =
PackageFactory.getContext(thread, loc).getBuilder().getPackageIdentifier();
return packageId.getPackageFragment().getPathString();
}
@Override
public String repositoryName(Location location, StarlarkThread thread) throws EvalException {
SkylarkUtils.checkLoadingPhase(thread, "native.repository_name", location);
PackageIdentifier packageId =
PackageFactory.getContext(thread, location).getBuilder().getPackageIdentifier();
return packageId.getRepository().toString();
}
@Nullable
private static SkylarkDict<String, Object> targetDict(
Target target, Location loc, StarlarkThread thread) throws EvalException {
if (!(target instanceof Rule)) {
return null;
}
SkylarkDict<String, Object> values = SkylarkDict.<String, Object>of(thread);
Rule rule = (Rule) target;
AttributeContainer cont = rule.getAttributeContainer();
for (Attribute attr : rule.getAttributes()) {
if (!Character.isAlphabetic(attr.getName().charAt(0))) {
continue;
}
if (attr.getName().equals("distribs")) {
// attribute distribs: cannot represent type class java.util.Collections$SingletonSet
// in Skylark: [INTERNAL].
continue;
}
try {
Object val = skylarkifyValue(cont.getAttr(attr.getName()), target.getPackage());
if (val == null) {
continue;
}
values.put(attr.getName(), val, loc, thread);
} catch (NotRepresentableException e) {
throw new NotRepresentableException(
String.format(
"target %s, attribute %s: %s", target.getName(), attr.getName(), e.getMessage()));
}
}
values.put("name", rule.getName(), loc, thread);
values.put("kind", rule.getRuleClass(), loc, thread);
return values;
}
/**
* Converts back to type that will work in BUILD and skylark, such as string instead of label,
* SkylarkList instead of List, Returns null if we don't want to export the value.
*
* <p>All of the types returned are immutable. If we want, we can change this to immutable in the
* future, but this is the safe choice for now.
*/
@Nullable
private static Object skylarkifyValue(Object val, Package pkg) throws NotRepresentableException {
// TODO(bazel-team): the location of this function is ad-hoc. Arguably, the conversion
// from Java native types to Skylark types should be part of the Type class hierarchy,
if (val == null) {
return null;
}
if (val instanceof Boolean) {
return val;
}
if (val instanceof Integer) {
return val;
}
if (val instanceof String) {
return val;
}
if (val instanceof TriState) {
switch ((TriState) val) {
case AUTO:
return -1;
case YES:
return 1;
case NO:
return 0;
}
}
if (val instanceof Label) {
Label l = (Label) val;
if (l.getPackageName().equals(pkg.getName())) {
return ":" + l.getName();
}
return l.getCanonicalForm();
}
if (val instanceof List) {
List<Object> l = new ArrayList<>();
for (Object o : (List) val) {
Object elt = skylarkifyValue(o, pkg);
if (elt == null) {
continue;
}
l.add(elt);
}
return SkylarkList.Tuple.copyOf(l);
}
if (val instanceof Map) {
Map<Object, Object> m = new TreeMap<>();
for (Map.Entry<?, ?> e : ((Map<?, ?>) val).entrySet()) {
Object key = skylarkifyValue(e.getKey(), pkg);
Object mapVal = skylarkifyValue(e.getValue(), pkg);
if (key == null || mapVal == null) {
continue;
}
m.put(key, mapVal);
}
return m;
}
if (val.getClass().isAnonymousClass()) {
// Computed defaults. They will be represented as
// "deprecation": com.google.devtools.build.lib.analysis.BaseRuleClasses$2@6960884a,
// Filter them until we invent something more clever.
return null;
}
if (val instanceof License) {
// License is deprecated as a Starlark type, so omit this type from Starlark values
// to avoid exposing these objects, even though they are technically SkylarkValue.
return null;
}
if (val instanceof SkylarkValue) {
return val;
}
if (val instanceof BuildType.SelectorList) {
// This is terrible:
// 1) this value is opaque, and not a BUILD value, so it cannot be used in rule arguments
// 2) its representation has a pointer address, so it breaks hermeticity.
//
// Even though this is clearly imperfect, we return this value because otherwise
// native.rules() fails if there is any rule using a select() in the BUILD file.
//
// To remedy this, we should return a syntax.SelectorList. To do so, we have to
// 1) recurse into the Selector contents of SelectorList, so those values are skylarkified too
// 2) get the right Class<?> value. We could probably get at that by looking at
// ((SelectorList)val).getSelectors().first().getEntries().first().getClass().
return val;
}
// We are explicit about types we don't understand so we minimize changes to existing callers
// if we add more types that we can represent.
throw new NotRepresentableException(
String.format("cannot represent %s (%s) in Starlark", val, val.getClass()));
}
}