blob: 5f6192b45ef5de503d9b4fdbba4ca527e99e455f [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.shell;
import com.facebook.buck.java.JavaBinaryRule;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.AbstractCachingBuildRule;
import com.facebook.buck.rules.AbstractCachingBuildRuleBuilder;
import com.facebook.buck.rules.ArtifactCache;
import com.facebook.buck.rules.BinaryBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleType;
import com.facebook.buck.rules.CachingBuildRuleParams;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.SrcsAttributeBuilder;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirAndSymlinkFileStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.step.fs.RmStep;
import com.facebook.buck.util.BuckConstant;
import com.facebook.buck.util.Functions;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
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.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Build rule for generating a file via a shell command. For example, to generate the katana
* AndroidManifest.xml from the wakizashi AndroidManifest.xml, such a rule could be defined as:
* <pre>
* genrule(
* name = 'katana_manifest',
* srcs = [
* 'wakizashi_to_katana_manifest.py',
* 'AndroidManifest.xml',
* ],
* cmd = 'python wakizashi_to_katana_manifest.py ${SRCDIR}/AndroidManfiest.xml > $OUT',
* out = 'AndroidManifest.xml',
* )
* </pre>
* The output of this rule would likely be used as follows:
* <pre>
* android_binary(
* name = 'katana',
* manifest = genfile('AndroidManifest.xml'),
* deps = [
* ':katana_manifest',
* # Additional dependent android_library rules would be listed here, as well.
* ],
* )
* </pre>
* A <code>genrule</code> is evaluated by running the shell command specified by {@code cmd} with
* the following environment variable substitutions:
* <ul>
* <li><code>SRCS</code> will be a space-delimited string expansion of the <code>srcs</code>
* attribute where each element of <code>srcs</code> will be translated into an absolute path.
* <li><code>SRCDIR</code> will be a directory containing all files mentioned in the srcs.</li>
* <li><code>OUT</code> is the output file for the <code>genrule()</code>. The file specified by
* this variable must always be written by this command. If not, the execution of this rule
* will be considered a failure, halting the build process.
* </ul>
* In the above example, if the {@code katana_manifest} rule were defined in the
* {@code src/com/facebook/wakizashi} directory, then the command that would be executed would be:
* <pre>
* python convert_to_katana.py src/com/facebook/wakizashi/AndroidManifest.xml > \
* buck-gen/src/com/facebook/wakizashi/AndroidManifest.xml
* </pre>
* Note that {@code cmd} could be run on either Mac or Linux, so it should contain logic that works
* on either platform. If this becomes an issue in the future (or we want to support building on
* different platforms), then we could introduce a new attribute that is a map of target platforms
* to the appropriate build command for that platform.
* <p>
* Note that the <code>SRCDIR</code> is populated by symlinking the sources.
*/
public class Genrule extends AbstractCachingBuildRule {
/**
* The order in which elements are specified in the {@code srcs} attribute of a genrule matters.
*/
protected final ImmutableSortedSet<String> srcs;
protected final String cmd;
protected final Map<String, String> srcsToAbsolutePaths;
protected final String outDirectory;
protected final String outAsAbsolutePath;
protected final String tmpDirectory;
private final String srcDirectory;
protected final Function<String, String> relativeToAbsolutePathFunction;
protected Genrule(CachingBuildRuleParams cachingBuildRuleParams,
List<String> srcs,
String cmd,
String out,
Function<String, String> relativeToAbsolutePathFunction) {
super(cachingBuildRuleParams);
this.srcs = ImmutableSortedSet.<String>naturalOrder().addAll(srcs).build();
this.cmd = Preconditions.checkNotNull(cmd);
this.srcsToAbsolutePaths = Maps.toMap(srcs, relativeToAbsolutePathFunction);
Preconditions.checkNotNull(out);
this.outDirectory = String.format("%s/%s",
BuckConstant.GEN_DIR,
cachingBuildRuleParams.getBuildTarget().getBasePathWithSlash());
String outWithGenDirPrefix = String.format("%s%s", outDirectory, out);
this.outAsAbsolutePath = relativeToAbsolutePathFunction.apply(outWithGenDirPrefix);
String temp = String.format("%s/%s/%s__tmp",
BuckConstant.GEN_DIR,
cachingBuildRuleParams.getBuildTarget().getBasePath(),
getBuildTarget().getShortName()
);
this.tmpDirectory = relativeToAbsolutePathFunction.apply(temp);
String srcdir = String.format("%s/%s/%s__srcs",
BuckConstant.GEN_DIR,
cachingBuildRuleParams.getBuildTarget().getBasePath(),
getBuildTarget().getShortName()
);
this.srcDirectory = relativeToAbsolutePathFunction.apply(srcdir);
this.relativeToAbsolutePathFunction = relativeToAbsolutePathFunction;
}
@Override
public BuildRuleType getType() {
return BuildRuleType.GENRULE;
}
/** @return the absolute path to the output file */
public String getOutputFilePath() {
return outAsAbsolutePath;
}
@Override
protected ImmutableSortedSet<String> getInputsToCompareToOutput(BuildContext context) {
return srcs;
}
@Override
public File getOutput() {
return new File(getOutputFilePath());
}
@Override
protected RuleKey.Builder ruleKeyBuilder() {
return super.ruleKeyBuilder()
.set("srcs", srcs)
.set("cmd", cmd);
}
protected void addEnvironmentVariables(
ImmutableMap.Builder<String, String> environmentVariablesBuilder) {
environmentVariablesBuilder.put("SRCS", Joiner.on(' ').join(srcsToAbsolutePaths.values()));
environmentVariablesBuilder.put("OUT", getOutputFilePath());
final Set<String> depFiles = Sets.newHashSet();
final Set<BuildRule> processedBuildRules = Sets.newHashSet();
for (BuildRule dep : getDeps()) {
transformNames(processedBuildRules, depFiles, dep);
}
environmentVariablesBuilder.put("DEPS", Joiner.on(' ').skipNulls().join(depFiles));
environmentVariablesBuilder.put("SRCDIR", srcDirectory);
environmentVariablesBuilder.put("TMP", tmpDirectory);
}
private void transformNames(Set<BuildRule> processedBuildRules,
Set<String> appendTo,
BuildRule rule) {
if (processedBuildRules.contains(rule)) {
return;
}
processedBuildRules.add(rule);
File output = rule.getOutput();
if (output != null) {
appendTo.add(relativeToAbsolutePathFunction.apply(output.getPath()));
}
for (BuildRule dep : rule.getDeps()) {
transformNames(processedBuildRules, appendTo, dep);
}
}
@Override
@VisibleForTesting
public List<Step> buildInternal(BuildContext context) throws IOException {
ImmutableList.Builder<Step> commands = ImmutableList.builder();
// Delete the old output for this rule, if it exists.
commands.add(new RmStep(getOutputFilePath(), true /* shouldForceDeletion */));
// Make sure that the directory to contain the output file exists. Rules get output to a
// directory named after the base path, so we don't want to nuke the entire directory.
commands.add(new MkdirStep(outDirectory));
// Delete the old temp directory
commands.add(new MakeCleanDirectoryStep(tmpDirectory));
// Create a directory to hold all the source files.
// TODO(simons): Actually execute the command from here.
commands.add(new MakeCleanDirectoryStep(srcDirectory));
addSymlinkCommands(commands);
// Create a shell command that corresponds to this.cmd.
final String cmd = replaceBinaryBuildRuleRefsInCmd();
final ImmutableList<String> commandArgs = ImmutableList.of("/bin/bash", "-ec", cmd);
ImmutableMap.Builder<String, String> environmentVariablesBuilder = ImmutableMap.builder();
addEnvironmentVariables(environmentVariablesBuilder);
final ImmutableMap<String, String> environmentVariables = environmentVariablesBuilder.build();
commands.add(new ShellStep() {
@Override
public String getShortName(ExecutionContext context) {
return String.format("genrule: %s", cmd);
}
@Override
protected ImmutableList<String> getShellCommandInternal(ExecutionContext context) {
return commandArgs;
}
@Override
public ImmutableMap<String, String> getEnvironmentVariables() {
return environmentVariables;
}
@Override
protected boolean shouldPrintStdErr(ExecutionContext context) {
return true;
}
});
return commands.build();
}
@VisibleForTesting
void addSymlinkCommands(ImmutableList.Builder<Step> commands) {
String basePath = getBuildTarget().getBasePathWithSlash();
int basePathLength = basePath.length();
// Symlink all sources into the temp directory so that they can be used in the genrule.
for (Map.Entry<String, String> entry : srcsToAbsolutePaths.entrySet()) {
String localPath = entry.getKey();
String canonicalPath;
try {
canonicalPath = new File(entry.getValue()).getCanonicalPath();
} catch (IOException e) {
throw new HumanReadableException(
"Unable to determine the canonical path for: %s. Does the file exist?", localPath);
}
// By the time we get this far, all source paths (the keys in the map) have been converted
// to paths relative to the project root. We want the path relative to the build target, so
// strip the base path.
if (entry.getValue().equals(canonicalPath)) {
if (localPath.startsWith(basePath)) {
localPath = localPath.substring(basePathLength);
} else {
localPath = new File(canonicalPath).getName();
}
}
File destination = new File(srcDirectory, localPath);
commands.add(new MkdirAndSymlinkFileStep(entry.getValue(), destination.getAbsolutePath()));
}
}
/**
* Matches either a relative or fully-qualified build target wrapped in <tt>${}</tt>, unless the
* <code>$</code> is preceded by a backslash.
*/
@VisibleForTesting
static final Pattern BUILD_TARGET_PATTERN = Pattern.compile(
"([^\\\\]?)(\\$\\{((\\/\\/|:)[^\\}]+)\\})");
/**
* @return the cmd with binary build targets interpolated as executable commands
*/
@VisibleForTesting
String replaceBinaryBuildRuleRefsInCmd() {
Matcher matcher = BUILD_TARGET_PATTERN.matcher(cmd);
StringBuffer buffer = new StringBuffer();
Map<String, BuildRule> fullyQualifiedNameToBuildRule = null;
while (matcher.find()) {
if (fullyQualifiedNameToBuildRule == null) {
fullyQualifiedNameToBuildRule = Maps.newHashMap();
for (BuildRule dep : getDeps()) {
fullyQualifiedNameToBuildRule.put(dep.getFullyQualifiedName(), dep);
}
}
String buildTarget = matcher.group(3);
String prefix = matcher.group(4);
if (":".equals(prefix)) {
// This is a relative build target, so make it fully qualified.
buildTarget = String.format("//%s%s", this.getBuildTarget().getBasePath(), buildTarget);
}
BuildRule matchingRule = fullyQualifiedNameToBuildRule.get(buildTarget);
if (matchingRule == null) {
throw new HumanReadableException("No dep named %s for %s %s, cmd was %s",
buildTarget, getType().getDisplayName(), getFullyQualifiedName(), cmd);
}
if (!(matchingRule instanceof BinaryBuildRule)) {
throw new HumanReadableException("%s must correspond to a binary rule in %s for %s %s",
buildTarget, cmd, getType().getDisplayName(), getFullyQualifiedName());
}
BinaryBuildRule binaryBuildRule = (BinaryBuildRule)matchingRule;
String bincmd;
if (binaryBuildRule instanceof JavaBinaryRule) {
List<String> jvmArgs = Lists.newArrayListWithCapacity(4);
jvmArgs.add(String.format("-Djava.io.tmpdir=%s", tmpDirectory));
bincmd = ((JavaBinaryRule)binaryBuildRule).getExecutableCommand(jvmArgs);
} else {
bincmd = binaryBuildRule.getExecutableCommand();
}
// Note that matcher.group(1) is the non-backslash character that did not escape the dollar
// sign, so we make sure that it does not get lost during the regex replacement.
String replacement = matcher.group(1) + bincmd;
matcher.appendReplacement(buffer, replacement);
}
matcher.appendTail(buffer);
return buffer.toString();
}
public static Builder newGenruleBuilder() {
return new Builder();
}
public static class Builder extends AbstractCachingBuildRuleBuilder
implements SrcsAttributeBuilder {
protected List<String> srcs = Lists.newArrayList();
protected String cmd;
protected String out;
protected Function<String, String> relativeToAbsolutePathFunction =
Functions.RELATIVE_TO_ABSOLUTE_PATH;
@Override
public Genrule build(Map<String, BuildRule> buildRuleIndex) {
return new Genrule(createCachingBuildRuleParams(buildRuleIndex),
srcs,
cmd,
out,
relativeToAbsolutePathFunction);
}
@Override
public Builder addSrc(String src) {
srcs.add(src);
return this;
}
@Override
public Builder addDep(String dep) {
deps.add(dep);
return this;
}
@Override
public Builder setBuildTarget(BuildTarget buildTarget) {
this.buildTarget = buildTarget;
return this;
}
@Override
public Builder setArtifactCache(ArtifactCache artifactCache) {
this.artifactCache = artifactCache;
return this;
}
public Builder setCmd(String cmd) {
this.cmd = cmd;
return this;
}
public Builder setOut(String out) {
this.out = out;
return this;
}
@VisibleForTesting
public Builder setRelativeToAbsolutePathFunction(
Function<String, String> relativeToAbsolutePathFunction) {
this.relativeToAbsolutePathFunction = relativeToAbsolutePathFunction;
return this;
}
}
}