Java tutorial
/* * 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.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.model.BuildTarget; import com.facebook.buck.step.Step; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.shell.ShellStep; 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.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 ImmutableList<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(BuildRuleParams buildRuleParams, List<String> srcs, String cmd, String out, Function<String, String> relativeToAbsolutePathFunction) { super(buildRuleParams); this.srcs = ImmutableList.copyOf(srcs); this.cmd = Preconditions.checkNotNull(cmd); this.srcsToAbsolutePaths = Maps.toMap(srcs, relativeToAbsolutePathFunction); Preconditions.checkNotNull(out); this.outDirectory = String.format("%s/%s", BuckConstant.GEN_DIR, buildRuleParams.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, buildRuleParams.getBuildTarget().getBasePath(), getBuildTarget().getShortName()); this.tmpDirectory = relativeToAbsolutePathFunction.apply(temp); String srcdir = String.format("%s/%s/%s__srcs", BuckConstant.GEN_DIR, buildRuleParams.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 @VisibleForTesting public ImmutableList<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", "-c", 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; // 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) + binaryBuildRule.getExecutableCommand(); matcher.appendReplacement(buffer, replacement); } matcher.appendTail(buffer); return buffer.toString(); } public static Builder newGenruleBuilder() { return new Builder(); } public static class Builder extends AbstractBuildRuleBuilder 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(createBuildRuleParams(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; } 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; } } }