blob: bbffd24af102fa53ec88f16bfda76e7dfb5fb2f3 [file] [log] [blame]
// Copyright 2017 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.skylark;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.analysis.skylark.annotations.SkylarkConfigurationField;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.Attribute.AbstractLabelLateBoundDefault;
import com.google.devtools.build.lib.packages.Attribute.LateBoundDefault;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skylarkbuildapi.LateBoundDefaultApi;
import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.syntax.Printer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.annotation.concurrent.Immutable;
/**
* An implementation of {@link LateBoundDefault} which obtains a late-bound attribute value (of type
* 'label') specifically by skylark configuration fragment name and field name, as registered by
* {@link SkylarkConfigurationField}.
*
* <p>For example, a SkylarkLateBoundDefault on "java" and "toolchain" would require a valid
* configuration fragment named "java" with a method annotated with {@link
* SkylarkConfigurationField} of name "toolchain". This {@link LateBoundDefault} would provide a
* late-bound dependency (defined by the label returned by that configuration field) in the current
* target configuration.
*/
@Immutable
@AutoCodec
public class SkylarkLateBoundDefault<FragmentT> extends AbstractLabelLateBoundDefault<FragmentT>
implements LateBoundDefaultApi {
private final Method method;
private final String fragmentName;
private final String fragmentFieldName;
@Override
public Label resolve(Rule rule, AttributeMap attributes, FragmentT config) {
try {
Object result = method.invoke(config);
return (Label) result;
} catch (IllegalAccessException | InvocationTargetException e) {
// Configuration field methods should not throw either of these exceptions.
throw new AssertionError("Method invocation failed: " + e);
}
}
/**
* Returns the {@link SkylarkConfigurationField} annotation corresponding to this method.
*/
private static Label getDefaultLabel(
SkylarkConfigurationField annotation, String toolsRepository) {
if (annotation.defaultLabel().isEmpty()) {
return null;
}
Label defaultLabel = annotation.defaultInToolRepository()
? Label.parseAbsoluteUnchecked(toolsRepository + annotation.defaultLabel())
: Label.parseAbsoluteUnchecked(annotation.defaultLabel());
return defaultLabel;
}
private SkylarkLateBoundDefault(SkylarkConfigurationField annotation,
Class<FragmentT> fragmentClass, String fragmentName, Method method, String toolsRepository) {
this(
getDefaultLabel(annotation, toolsRepository),
fragmentClass,
method,
fragmentName,
annotation.name());
}
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
SkylarkLateBoundDefault(
Label defaultVal,
Class<FragmentT> fragmentClass,
Method method,
String fragmentName,
String fragmentFieldName) {
super(/*useHostConfiguration=*/ false, fragmentClass, defaultVal);
this.method = method;
this.fragmentName = fragmentName;
this.fragmentFieldName = fragmentFieldName;
}
/**
* Returns the skylark name of the configuration fragment that this late bound default requires.
*/
public String getFragmentName() {
return fragmentName;
}
/**
* Returns the skylark name of the configuration field name, as registered by
* {@link SkylarkConfigurationField} annotation on the configuration fragment.
*/
public String getFragmentFieldName() {
return fragmentFieldName;
}
@Override
public void repr(Printer printer) {
printer.format("<late-bound default>");
}
/** For use by @AutoCodec since the {@link #defaultValue} field is hard for it to process. */
@AutoCodec.VisibleForSerialization
Label getDefaultVal() {
return getDefault();
}
/**
* An exception thrown if a user specifies an invalid configuration field identifier.
*
* @see SkylarkConfigurationField
**/
public static class InvalidConfigurationFieldException extends Exception {
public InvalidConfigurationFieldException(String message) {
super(message);
}
}
private static class CacheKey {
private final Class<?> fragmentClass;
private final String toolsRepository;
private CacheKey(Class<?> fragmentClass,
String toolsRepository) {
this.fragmentClass = fragmentClass;
this.toolsRepository = toolsRepository;
}
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
} else if (!(object instanceof CacheKey)) {
return false;
} else {
CacheKey cacheKey = (CacheKey) object;
return fragmentClass.equals(cacheKey.fragmentClass)
&& toolsRepository.equals(cacheKey.toolsRepository);
}
}
@Override
public int hashCode() {
int result = fragmentClass.hashCode();
result = 31 * result + toolsRepository.hashCode();
return result;
}
}
/**
* A cache for efficient {@link SkylarkLateBoundDefault} loading by configuration fragment. Each
* configuration fragment class key is mapped to a {@link Map} where keys are configuration field
* skylark names, and values are the {@link SkylarkLateBoundDefault}s. Methods must be annotated
* with {@link SkylarkConfigurationField} to be considered.
*/
private static final LoadingCache<CacheKey, Map<String, SkylarkLateBoundDefault<?>>> fieldCache =
CacheBuilder.newBuilder()
.initialCapacity(10)
.maximumSize(100)
.build(
new CacheLoader<CacheKey, Map<String, SkylarkLateBoundDefault<?>>>() {
@Override
public Map<String, SkylarkLateBoundDefault<?>> load(CacheKey key) throws Exception {
ImmutableMap.Builder<String, SkylarkLateBoundDefault<?>> lateBoundDefaultMap =
new ImmutableMap.Builder<>();
Class<?> fragmentClass = key.fragmentClass;
SkylarkModule fragmentModule =
SkylarkInterfaceUtils.getSkylarkModule(fragmentClass);
if (fragmentModule != null) {
for (Method method : fragmentClass.getMethods()) {
if (method.isAnnotationPresent(SkylarkConfigurationField.class)) {
// TODO(b/68817606): Use annotation processors to verify these constraints.
Preconditions.checkArgument(
method.getReturnType() == Label.class,
String.format("Method %s must have return type 'Label'", method));
Preconditions.checkArgument(
method.getParameterTypes().length == 0,
String.format("Method %s must not accept arguments", method));
SkylarkConfigurationField configField =
method.getAnnotation(SkylarkConfigurationField.class);
lateBoundDefaultMap.put(
configField.name(),
new SkylarkLateBoundDefault<>(
configField,
fragmentClass,
fragmentModule.name(),
method,
key.toolsRepository));
}
}
}
return lateBoundDefaultMap.build();
}
});
/**
* Returns a {@link LateBoundDefault} which obtains a late-bound attribute value (of type 'label')
* specifically by skylark configuration fragment name and field name, as registered by {@link
* SkylarkConfigurationField}.
*
* @param fragmentClass the configuration fragment class, which must have a valid skylark name
* @param fragmentFieldName the configuration field name, as registered by {@link
* SkylarkConfigurationField} annotation
* @param toolsRepository the Bazel tools repository path fragment
* @throws InvalidConfigurationFieldException if there is no valid configuration field with the
* given fragment class and field name
*/
@SuppressWarnings("unchecked")
public static <FragmentT> SkylarkLateBoundDefault<FragmentT> forConfigurationField(
Class<FragmentT> fragmentClass, String fragmentFieldName, String toolsRepository)
throws InvalidConfigurationFieldException {
try {
CacheKey cacheKey = new CacheKey(fragmentClass, toolsRepository);
SkylarkLateBoundDefault<?> resolver = fieldCache.get(cacheKey).get(fragmentFieldName);
if (resolver == null) {
SkylarkModule moduleAnnotation = SkylarkInterfaceUtils.getSkylarkModule(fragmentClass);
if (moduleAnnotation == null) {
throw new AssertionError("fragment class must have a valid Starlark name");
}
throw new InvalidConfigurationFieldException(
String.format("invalid configuration field name '%s' on fragment '%s'",
fragmentFieldName, moduleAnnotation.name()));
}
return (SkylarkLateBoundDefault<FragmentT>) resolver; // unchecked cast
} catch (ExecutionException e) {
throw new IllegalStateException("method invocation failed: " + e);
}
}
}