blob: 34d3018638f0f4da3f20405b4270d178f79fc327 [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.rules;
import com.facebook.buck.model.BuildFileTree;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.parser.BuildTargetParser;
import com.facebook.buck.parser.BuildTargetPatternParser;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.parser.ParseContext;
import com.facebook.buck.util.BuckConstant;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.ProjectFilesystem;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* A set of parameters passed to a {@link BuildRuleFactory}.
*/
public final class BuildRuleFactoryParams {
static final String GENFILE_PREFIX = "BUCKGEN:";
private final Map<String, ?> instance;
private final ProjectFilesystem filesystem;
private final BuildFileTree buildFiles;
public final BuildTargetParser buildTargetParser;
public final BuildTargetPatternParser buildTargetPatternParser;
public final BuildTarget target;
private final ParseContext buildFileParseContext;
private final ParseContext visibilityParseContext;
private final boolean ignoreFileExistenceChecks;
private final AbstractBuildRuleBuilderParams abstractBuildRuleFactoryParams;
private final Function<String, String> resolveFilePathRelativeToBuildFileDirectoryTransform;
public BuildRuleFactoryParams(
Map<String, ?> instance,
ProjectFilesystem filesystem,
BuildFileTree buildFiles,
BuildTargetParser buildTargetParser,
BuildTarget target,
RuleKeyBuilderFactory ruleKeyBuilderFactory) {
this(instance,
filesystem,
buildFiles,
buildTargetParser,
target,
ruleKeyBuilderFactory,
false /* ignoreFileExistenceChecks */);
}
@VisibleForTesting
BuildRuleFactoryParams(
Map<String, ?> instance,
ProjectFilesystem filesystem,
BuildFileTree buildFiles,
BuildTargetParser buildTargetParser,
BuildTarget target,
RuleKeyBuilderFactory ruleKeyBuilderFactory,
boolean ignoreFileExistenceChecks) {
this.instance = instance;
this.filesystem = filesystem;
this.buildFiles = Preconditions.checkNotNull(buildFiles);
this.buildTargetParser = buildTargetParser;
this.buildTargetPatternParser = new BuildTargetPatternParser(filesystem);
this.target = Preconditions.checkNotNull(target);
this.visibilityParseContext = ParseContext.forVisibilityArgument();
this.buildFileParseContext = ParseContext.forBaseName(target.getBaseName());
this.ignoreFileExistenceChecks = ignoreFileExistenceChecks;
this.resolveFilePathRelativeToBuildFileDirectoryTransform = new Function<String, String>() {
@Override
public String apply(String input) {
return resolveFilePathRelativeToBuildFileDirectory(input);
}
};
this.abstractBuildRuleFactoryParams = new DefaultBuildRuleBuilderParams(filesystem,
ruleKeyBuilderFactory);
}
/** This is package-private so that only AbstractBuildRuleFactory can access it. */
AbstractBuildRuleBuilderParams getAbstractBuildRuleFactoryParams() {
return abstractBuildRuleFactoryParams;
}
public BuildTarget parseVisibilityTarget(String visibilityTarget)
throws NoSuchBuildTargetException {
return buildTargetParser.parse(visibilityTarget, visibilityParseContext);
}
/**
* Convenience method that combines the result of
* {@link #getRequiredStringAttribute(String)} with
* {@link #resolveFilePathRelativeToBuildFileDirectory(String)}.
*/
public Path getRequiredFileAsPathRelativeToProjectRoot(String attributeName) {
String localPath = getRequiredStringAttribute(attributeName);
return Paths.get(resolveFilePathRelativeToBuildFileDirectory(localPath));
}
/**
* For the specified string, return a corresponding file path that is relative to the project
* root. It is expected that {@code path} is a path relative to the directory containing the build
* file in which it was declared. This method will also assert that the file exists.
* <p>
* However, if {@code path} corresponds to a generated file, then {@code path} is treated as a
* path relative to the parallel build file directory in the generated files directory. In that
* case, its existence will not be verified.
*/
public String resolveFilePathRelativeToBuildFileDirectory(String path) {
if (path.startsWith(GENFILE_PREFIX)) {
path = path.substring(GENFILE_PREFIX.length());
return String.format("%s/%s",
BuckConstant.GEN_DIR,
resolvePathAgainstBuildTargetBase(path),
path);
} else {
String fullPath = resolvePathAgainstBuildTargetBase(path);
File file = filesystem.getFileForRelativePath(fullPath);
// TODO(mbolin): Eliminate this temporary exemption for symbolic links.
if (Files.isSymbolicLink(file.toPath())) {
return fullPath;
}
if (!ignoreFileExistenceChecks && !file.isFile()) {
throw new RuntimeException("Not an ordinary file: " + fullPath);
}
// First, verify that the path is a descendant of the directory containing the build file.
String basePath = target.getBasePath();
if (!basePath.isEmpty() && !fullPath.startsWith(basePath + '/')) {
throw new RuntimeException(file + " is not a descendant of " + target.getBasePath());
}
checkFullPath(fullPath);
return fullPath;
}
}
public String resolveAndCreateFilePathRelativeToBuildFileDirectory(String path) {
if (!path.startsWith(GENFILE_PREFIX)) {
String fullPath = resolvePathAgainstBuildTargetBase(path);
File file = filesystem.getFileForRelativePath(fullPath);
if (!file.isFile()) {
try {
file.createNewFile();
} catch (IOException e) {
throw new RuntimeException(String.format("Unable to create file '%s' at path '%s'",
path, target.getBasePath()));
}
}
}
return resolveFilePathRelativeToBuildFileDirectory(path);
}
private void checkFullPath(String fullPath) {
if (fullPath.contains("..")) {
throw new HumanReadableException(
"\"%s\" in target \"%s\" refers to a parent directory.",
fullPath,
target.getFullyQualifiedName());
}
String ancestor = buildFiles.getBasePathOfAncestorTarget(fullPath);
if (!target.getBasePath().equals(ancestor)) {
throw new HumanReadableException(
"\"%s\" in target \"%s\" crosses a buck package boundary. Find the nearest BUCK file " +
"in the directory containing this file and refer to the rule referencing the " +
"desired file.",
fullPath,
target);
}
}
public File resolveDirectoryRelativeToBuildFileDirectory(String path) {
return new File(resolvePathAgainstBuildTargetBase(path));
}
public String resolveDirectoryPathRelativeToBuildFileDirectory(String path) {
Preconditions.checkNotNull(path);
Preconditions.checkArgument(!path.startsWith(GENFILE_PREFIX));
String fullPath = resolvePathAgainstBuildTargetBase(path);
checkFullPath(fullPath);
File file = filesystem.getFileForRelativePath(fullPath);
if (!ignoreFileExistenceChecks && !file.isDirectory()) {
throw new RuntimeException("Not a directory: " + fullPath);
}
return fullPath;
}
private String resolvePathAgainstBuildTargetBase(String path) {
if (target.getBasePath().isEmpty()) {
return path;
} else {
return String.format("%s/%s", target.getBasePath(), path);
}
}
/**
* @param identifier for the argument in this params.
* @param builder If the value associated with {@code identifier} corresponds to a
* {@link BuildTarget}, that build target will be added to the {@code deps} of this builder.
* @return a source path that corresponds to the specified {@code identifier}.
*/
public SourcePath getRequiredSourcePath(String identifier, AbstractBuildRuleBuilder<?> builder) {
String resource = getRequiredStringAttribute(identifier);
return asSourcePath(resource, builder);
}
/**
* @param resource that identifies either a file path or a build target.
* @param builder If {@code resource} corresponds to a {@link BuildTarget}, that build target will
* be added to the {@code deps} of this builder.
* @return a source path that corresponds to the specified {@code resource}.
*/
public SourcePath asSourcePath(String resource, AbstractBuildRuleBuilder<?> builder) {
// TODO(simons): Don't hard code this check for built-target-ism
if (resource.startsWith(BuildTarget.BUILD_TARGET_PREFIX) || resource.charAt(0) == ':') {
BuildTarget buildTarget;
try {
buildTarget = resolveBuildTarget(resource);
} catch (NoSuchBuildTargetException e) {
throw new HumanReadableException(
"Unable to find build target '%s' while parsing definition of %s", resource, target);
}
builder.addDep(buildTarget);
return new BuildTargetSourcePath(buildTarget);
} else {
String relativePath = resolveFilePathRelativeToBuildFileDirectory(resource);
return new FileSourcePath(relativePath);
}
}
/**
* If there is a string value associated with the specified {@code identifier}, this returns a
* {@link SourcePath} that corresponds to that value.
* @param identifier for the argument in this params.
* @param builder If {@code resource} corresponds to a {@link BuildTarget}, that build target will
* be added to the {@code deps} of this builder.
* @return a source path that corresponds to the specified {@code identifier}.
*/
public Optional<SourcePath> getOptionalSourcePath(String identifier,
AbstractBuildRuleBuilder<?> builder) {
Optional<String> option = getOptionalStringAttribute(identifier);
if (option.isPresent()) {
SourcePath sourcePath = asSourcePath(option.get(), builder);
return Optional.of(sourcePath);
} else {
return Optional.absent();
}
}
public BuildTarget getRequiredBuildTarget(String attributeName) {
String buildTarget = getRequiredStringAttribute(attributeName);
return resolveBuildTargetWithHumanReadableException(buildTarget, attributeName);
}
public Optional<BuildTarget> getOptionalBuildTarget(String attributeName) {
Optional<String> value = getOptionalStringAttribute(attributeName);
if (value.isPresent()) {
BuildTarget target = resolveBuildTargetWithHumanReadableException(value.get(), attributeName);
return Optional.of(target);
} else {
return Optional.absent();
}
}
public BuildTarget resolveBuildTarget(String target) throws NoSuchBuildTargetException {
return buildTargetParser.parse(target, buildFileParseContext);
}
private BuildTarget resolveBuildTargetWithHumanReadableException(String buildTarget,
String attributeNameForContext) {
try {
return resolveBuildTarget(buildTarget);
} catch (NoSuchBuildTargetException e) {
throw new HumanReadableException("Couldn't resolve build target for %s=%s in %s.",
attributeNameForContext,
buildTarget,
this.target.getFullyQualifiedName());
}
}
public String getRequiredStringAttribute(String attributeName) {
Optional<String> value = getOptionalStringAttribute(attributeName);
if (value.isPresent()) {
return value.get();
} else {
throw new RuntimeException(
String.format("%s is missing required attribute: %s", target, attributeName));
}
}
public Optional<String> getOptionalStringAttribute(String attributeName) {
Object value = instance.get(attributeName);
if (value != null) {
if (value instanceof String) {
return Optional.of((String)value);
} else {
throw new RuntimeException(String.format("Expected a string for %s in %s but was %s",
attributeName,
target.getBuildFilePath(),
value));
}
} else {
return Optional.absent();
}
}
public boolean hasAttribute(String attributeName) {
return instance.containsKey(attributeName);
}
public Function<String, String> getResolveFilePathRelativeToBuildFileDirectoryTransform() {
return resolveFilePathRelativeToBuildFileDirectoryTransform;
}
public long getRequiredLongAttribute(String attributeName) {
Object value = instance.get(attributeName);
if (value != null && value instanceof Long) {
return (Long)value;
} else {
throw new RuntimeException(String.format("Expected a long for %s in %s but was %s",
attributeName,
target.getBuildFilePath(),
value));
}
}
/** If a boolean attribute has not been specified, then it always defaults to false. */
public boolean getBooleanAttribute(String attributeName) {
Object value = instance.get(attributeName);
if (value == null) {
return false;
} else if (value instanceof Boolean) {
return (Boolean)value;
} else {
throw new RuntimeException(String.format(
"Value for %s in %s must be either True or False",
attributeName,
target.getFullyQualifiedName()));
}
}
/** @return non-null list that may be empty */
@SuppressWarnings("unchecked")
public List<String> getOptionalListAttribute(String attributeName) {
Object value = instance.get(attributeName);
if (value != null) {
if (value instanceof List) {
return (List<String>)value;
} else {
throw new RuntimeException(String.format("Expected an array for %s in %s but was %s",
attributeName,
target.getBuildFilePath(),
value));
}
} else {
return ImmutableList.of();
}
}
/**
* This is similar to {@link #getOptionalListAttribute(String)}, except if the value associated
* with {@code attributeName} is {@code null}, then this method actually returns {@code null}
* rather than an empty list.
*/
@Nullable
public List<String> getNullableListAttribute(String attributeName) {
Object value = instance.get(attributeName);
if (value != null) {
return getOptionalListAttribute(attributeName);
} else {
return null;
}
}
}