blob: 2e6e9675b4586344cb03e32af16b9c6e476ecf9f [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.android.AndroidPlatformTarget;
import com.facebook.buck.android.NoAndroidSdkException;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.HasOutputName;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.shell.AbstractGenruleStep.CommandString;
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.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 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 = ':katana_manifest',
* deps = [
* # 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-out/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 AbstractBuildRule implements HasOutputName {
/**
* The order in which elements are specified in the {@code srcs} attribute of a genrule matters.
*/
protected final ImmutableList<SourcePath> srcs;
protected final Function<String, String> macroExpander;
protected final Optional<String> cmd;
protected final Optional<String> bash;
protected final Optional<String> cmdExe;
protected final Map<Path, Path> srcsToAbsolutePaths;
private final String out;
protected final Path pathToOutDirectory;
protected final Path pathToOutFile;
private final Path pathToTmpDirectory;
private final Path absolutePathToTmpDirectory;
private final Path pathToSrcDirectory;
private final Path absolutePathToSrcDirectory;
protected final Function<Path, Path> relativeToAbsolutePathFunction;
protected Genrule(
BuildRuleParams params,
SourcePathResolver resolver,
List<SourcePath> srcs,
Function<String, String> macroExpander,
Optional<String> cmd,
Optional<String> bash,
Optional<String> cmdExe,
String out,
final Function<Path, Path> relativeToAbsolutePathFunction) {
super(params, resolver);
this.srcs = ImmutableList.copyOf(srcs);
this.macroExpander = macroExpander;
this.cmd = cmd;
this.bash = bash;
this.cmdExe = cmdExe;
this.srcsToAbsolutePaths = FluentIterable
.from(srcs)
.transform(resolver.getPathFunction())
.toMap(new Function<Path, Path>() {
@Override
public Path apply(Path src) {
return relativeToAbsolutePathFunction.apply(src);
}
});
this.out = out;
BuildTarget target = params.getBuildTarget();
this.pathToOutDirectory = Paths.get(
BuckConstant.GEN_DIR,
target.getBasePathWithSlash());
this.pathToOutFile = this.pathToOutDirectory.resolve(out);
this.pathToTmpDirectory = Paths.get(
BuckConstant.GEN_DIR,
target.getBasePathWithSlash(),
String.format("%s__tmp", target.getShortNameAndFlavorPostfix()));
// TODO(simons): pathToTmpDirectory.toAbsolutePath() should be enough
this.absolutePathToTmpDirectory = relativeToAbsolutePathFunction.apply(pathToTmpDirectory);
this.pathToSrcDirectory = Paths.get(
BuckConstant.GEN_DIR,
target.getBasePathWithSlash(),
String.format("%s__srcs", target.getShortNameAndFlavorPostfix()));
// TODO(simons): And here.
this.absolutePathToSrcDirectory = relativeToAbsolutePathFunction.apply(pathToSrcDirectory);
this.relativeToAbsolutePathFunction = relativeToAbsolutePathFunction;
}
/** @return the absolute path to the output file */
public String getAbsoluteOutputFilePath() {
return relativeToAbsolutePathFunction.apply(getPathToOutputFile()).toString();
}
@Override
public ImmutableCollection<Path> getInputsToCompareToOutput() {
return getResolver().filterInputsToCompareToOutput(srcs);
}
@Override
public Path getPathToOutputFile() {
return pathToOutFile;
}
@Override
public RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) {
return builder
.setReflectively("cmd", cmd)
.setReflectively("bash", bash)
.setReflectively("cmd_exe", cmdExe)
.setReflectively("out", out);
}
protected void addEnvironmentVariables(ExecutionContext context,
ImmutableMap.Builder<String, String> environmentVariablesBuilder) {
environmentVariablesBuilder.put("SRCS", Joiner.on(' ').join(srcsToAbsolutePaths.values()));
environmentVariablesBuilder.put("OUT", getAbsoluteOutputFilePath());
final Set<String> depFiles = Sets.newHashSet();
final Set<BuildRule> processedBuildRules = Sets.newHashSet();
for (BuildRule dep : getDeps()) {
transformNames(processedBuildRules, depFiles, dep);
}
environmentVariablesBuilder.put(
"GEN_DIR", relativeToAbsolutePathFunction.apply(BuckConstant.GEN_PATH).toString());
environmentVariablesBuilder.put("DEPS", Joiner.on(' ').skipNulls().join(depFiles));
environmentVariablesBuilder.put("SRCDIR", absolutePathToSrcDirectory.toString());
environmentVariablesBuilder.put("TMP", absolutePathToTmpDirectory.toString());
// TODO(mbolin): This entire hack needs to be removed. The [tools] section of .buckconfig
// should be generalized to specify local paths to tools that can be used in genrules.
AndroidPlatformTarget android;
try {
android = context.getAndroidPlatformTarget();
} catch (NoAndroidSdkException e) {
android = null;
}
if (android != null) {
environmentVariablesBuilder.put("DX", android.getDxExecutable().toString());
environmentVariablesBuilder.put("ZIPALIGN", android.getZipalignExecutable().toString());
}
// TODO(user): This shouldn't be necessary. Speculatively disabling.
environmentVariablesBuilder.put("NO_BUCKD", "1");
}
private void transformNames(Set<BuildRule> processedBuildRules,
Set<String> appendTo,
BuildRule rule) {
if (processedBuildRules.contains(rule)) {
return;
}
processedBuildRules.add(rule);
Path output = rule.getPathToOutputFile();
if (output != null) {
// TODO(user): This is a giant hack and we should do away with $DEPS altogether.
// There can be a lot of paths here and the filesystem location can be arbitrarily long.
// We can easily hit the shell command character limit. What this does is find
// BuckConstant.GEN_DIR (which should be the same for every path) and replaces
// it with a shell variable. This way the character count is much lower when run
// from the shell but anyone reading the environment variable will get the
// full paths due to variable interpolation
if (output.startsWith(BuckConstant.GEN_PATH)) {
Path relativePath =
output.subpath(BuckConstant.GEN_PATH.getNameCount(), output.getNameCount());
appendTo.add("$GEN_DIR/" + relativePath);
} else {
appendTo.add(relativeToAbsolutePathFunction.apply(output).toString());
}
}
for (BuildRule dep : rule.getDeps()) {
transformNames(processedBuildRules, appendTo, dep);
}
}
public AbstractGenruleStep createGenruleStep() {
// The user's command (this.cmd) should be run from the directory that contains only the
// symlinked files. This ensures that the user can reference only the files that were declared
// as srcs. Without this, a genrule is not guaranteed to be hermetic.
File workingDirectory = new File(absolutePathToSrcDirectory.toString());
return new AbstractGenruleStep(
getBuildTarget(),
new CommandString(
cmd.transform(macroExpander),
bash.transform(macroExpander),
cmdExe.transform(macroExpander)),
workingDirectory) {
@Override
protected void addEnvironmentVariables(
ExecutionContext context,
ImmutableMap.Builder<String, String> environmentVariablesBuilder) {
Genrule.this.addEnvironmentVariables(context, environmentVariablesBuilder);
}
};
}
@Override
@VisibleForTesting
public ImmutableList<Step> getBuildSteps(
BuildContext context,
BuildableContext buildableContext) {
ImmutableList.Builder<Step> commands = ImmutableList.builder();
// Delete the old output for this rule, if it exists.
commands.add(new RmStep(getPathToOutputFile(), 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(pathToOutDirectory));
// Delete the old temp directory
commands.add(new MakeCleanDirectoryStep(pathToTmpDirectory));
// Create a directory to hold all the source files.
commands.add(new MakeCleanDirectoryStep(pathToSrcDirectory));
addSymlinkCommands(commands);
// Create a shell command that corresponds to this.cmd.
commands.add(createGenruleStep());
buildableContext.recordArtifact(pathToOutFile);
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<Path, Path> entry : srcsToAbsolutePaths.entrySet()) {
String localPath = entry.getKey().toString();
Path canonicalPath;
canonicalPath = MorePaths.absolutify(entry.getValue());
// 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 = canonicalPath.getFileName().toString();
}
}
Path destination = pathToSrcDirectory.resolve(localPath);
commands.add(new MkdirAndSymlinkFileStep(entry.getKey(), destination));
}
}
/**
* Get the output name of the generated file, as listed in the BUCK file.
*/
@Override
public String getOutputName() {
return out;
}
}