// 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.rules.genrule; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.CompositeRunfilesSupplier; import com.google.devtools.build.lib.analysis.CommandHelper; import com.google.devtools.build.lib.analysis.ConfigurationMakeVariableContext; import com.google.devtools.build.lib.analysis.ConfiguredTarget; import com.google.devtools.build.lib.analysis.FileProvider; import com.google.devtools.build.lib.analysis.FilesToRunProvider; import com.google.devtools.build.lib.analysis.MakeVariableExpander.ExpansionException; import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.Runfiles; import com.google.devtools.build.lib.analysis.RunfilesProvider; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.packages.TargetUtils; import com.google.devtools.build.lib.rules.AliasProvider; import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory; import com.google.devtools.build.lib.rules.cpp.CppHelper; import com.google.devtools.build.lib.rules.java.JavaHelper; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.List; import java.util.Map; import java.util.regex.Pattern; /** * A base implementation of genrule, to be used by specific implementing rules which can change some * of the semantics around when the execution info and inputs are changed. */ public abstract class GenRuleBase implements RuleConfiguredTargetFactory { private static final Pattern CROSSTOOL_MAKE_VARIABLE = Pattern.compile("\\$\\((CC|AR|NM|OBJCOPY|STRIP|GCOVTOOL)\\)"); private static final Pattern JDK_MAKE_VARIABLE = Pattern.compile("\\$\\((JAVABASE|JAVA)\\)"); protected static boolean requiresCrosstool(String command) { return CROSSTOOL_MAKE_VARIABLE.matcher(command).find(); } protected boolean requiresJdk(String command) { return JDK_MAKE_VARIABLE.matcher(command).find(); } /** * Returns a {@link Map} of execution info, which will be used in later processing to construct * the actual command line that will be executed. * * <p>GenRule implementations can override this method to include additional specific information * needed. */ protected Map<String, String> getExtraExecutionInfo(RuleContext ruleContext, String command) { return ImmutableMap.of(); } /** * Returns an {@link Iterable} of {@link NestedSet}s, which will be added to the genrule's inputs * using the {@link NestedSetBuilder#addTransitive} method. * * <p>GenRule implementations can override this method to better control what inputs are needed * for specific command inputs. */ protected Iterable<NestedSet<Artifact>> getExtraInputArtifacts( RuleContext ruleContext, String command) { return ImmutableList.of(); } /** * Returns {@code true} if the rule should be stamped. * * <p>Genrule implementations can set this based on the rule context, including by defining their * own attributes over and above what is present in {@link GenRuleBaseRule}. */ protected abstract boolean isStampingEnabled(RuleContext ruleContext); /** * Updates the {@link RuleConfiguredTargetBuilder} that is used for this rule. * * <p>GenRule implementations can override this method to enhance and update the builder without * needing to entirely override the {@link #create} method. */ protected RuleConfiguredTargetBuilder updateBuilder( RuleConfiguredTargetBuilder builder, RuleContext ruleContext, NestedSet<Artifact> filesToBuild) { return builder; } @Override public ConfiguredTarget create(RuleContext ruleContext) throws RuleErrorException, InterruptedException { NestedSet<Artifact> filesToBuild = NestedSetBuilder.wrap(Order.STABLE_ORDER, ruleContext.getOutputArtifacts()); NestedSetBuilder<Artifact> resolvedSrcsBuilder = NestedSetBuilder.stableOrder(); if (filesToBuild.isEmpty()) { ruleContext.attributeError("outs", "Genrules without outputs don't make sense"); } if (ruleContext.attributes().get("executable", Type.BOOLEAN) && Iterables.size(filesToBuild) > 1) { ruleContext.attributeError( "executable", "if genrules produce executables, they are allowed only one output. " + "If you need the executable=1 argument, then you should split this genrule into " + "genrules producing single outputs"); } ImmutableMap.Builder<Label, NestedSet<Artifact>> labelMap = ImmutableMap.builder(); for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("srcs", Mode.TARGET)) { // This target provides specific types of files for genrules. GenRuleSourcesProvider provider = dep.getProvider(GenRuleSourcesProvider.class); NestedSet<Artifact> files = (provider != null) ? provider.getGenruleFiles() : dep.getProvider(FileProvider.class).getFilesToBuild(); resolvedSrcsBuilder.addTransitive(files); labelMap.put(AliasProvider.getDependencyLabel(dep), files); } NestedSet<Artifact> resolvedSrcs = resolvedSrcsBuilder.build(); CommandHelper commandHelper = new CommandHelper( ruleContext, ruleContext.getPrerequisites("tools", Mode.HOST), labelMap.build()); if (ruleContext.hasErrors()) { return null; } String baseCommand = commandHelper.resolveCommandAndExpandLabels( ruleContext.attributes().get("heuristic_label_expansion", Type.BOOLEAN), false); // Adds the genrule environment setup script before the actual shell command String command = String.format("source %s; %s", ruleContext.getPrerequisiteArtifact("$genrule_setup", Mode.HOST).getExecPath(), baseCommand); command = resolveCommand(command, ruleContext, resolvedSrcs, filesToBuild); String message = ruleContext.attributes().get("message", Type.STRING); if (message.isEmpty()) { message = "Executing genrule"; } ImmutableMap<String, String> env = ruleContext.getConfiguration().getLocalShellEnvironment(); ImmutableSet<String> clientEnvVars = ruleContext.getConfiguration().getVariableShellEnvironment(); Map<String, String> executionInfo = Maps.newLinkedHashMap(); executionInfo.putAll(TargetUtils.getExecutionInfo(ruleContext.getRule())); if (ruleContext.attributes().get("local", Type.BOOLEAN)) { executionInfo.put("local", ""); } executionInfo.putAll(getExtraExecutionInfo(ruleContext, baseCommand)); NestedSetBuilder<Artifact> inputs = NestedSetBuilder.stableOrder(); inputs.addTransitive(resolvedSrcs); inputs.addAll(commandHelper.getResolvedTools()); FilesToRunProvider genruleSetup = ruleContext.getPrerequisite("$genrule_setup", Mode.HOST, FilesToRunProvider.class); inputs.addAll(genruleSetup.getFilesToRun()); List<String> argv = commandHelper.buildCommandLine(command, inputs, ".genrule_script.sh", ImmutableMap.copyOf(executionInfo)); // TODO(bazel-team): Make the make variable expander pass back a list of these. if (requiresCrosstool(baseCommand)) { // If cc is used, silently throw in the crosstool filegroup as a dependency. inputs.addTransitive( CppHelper.getToolchain(ruleContext, ":cc_toolchain").getCrosstoolMiddleman()); } if (requiresJdk(baseCommand)) { // If javac is used, silently throw in the jdk filegroup as a dependency. // Note we expand Java-related variables with the *host* configuration. inputs.addTransitive(JavaHelper.getHostJavabaseInputs(ruleContext)); } for (NestedSet<Artifact> extraInputs : getExtraInputArtifacts(ruleContext, baseCommand)) { inputs.addTransitive(extraInputs); } if (isStampingEnabled(ruleContext)) { inputs.add(ruleContext.getAnalysisEnvironment().getStableWorkspaceStatusArtifact()); inputs.add(ruleContext.getAnalysisEnvironment().getVolatileWorkspaceStatusArtifact()); } ruleContext.registerAction( new GenRuleAction( ruleContext.getActionOwner(), ImmutableList.copyOf(commandHelper.getResolvedTools()), inputs.build(), filesToBuild, argv, env, clientEnvVars, ImmutableMap.copyOf(executionInfo), new CompositeRunfilesSupplier(commandHelper.getToolsRunfilesSuppliers()), message + ' ' + ruleContext.getLabel())); RunfilesProvider runfilesProvider = RunfilesProvider.withData( // No runfiles provided if not a data dependency. Runfiles.EMPTY, // We only need to consider the outputs of a genrule // No need to visit the dependencies of a genrule. They cross from the target into the host // configuration, because the dependencies of a genrule are always built for the host // configuration. new Runfiles.Builder( ruleContext.getWorkspaceName(), ruleContext.getConfiguration().legacyExternalRunfiles()) .addTransitiveArtifacts(filesToBuild) .build()); RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext) .setFilesToBuild(filesToBuild) .setRunfilesSupport(null, getExecutable(ruleContext, filesToBuild)) .addProvider(RunfilesProvider.class, runfilesProvider); builder = updateBuilder(builder, ruleContext, filesToBuild); return builder.build(); } /** * Returns the executable artifact, if the rule is marked as executable and there is only one * artifact. */ private static Artifact getExecutable(RuleContext ruleContext, NestedSet<Artifact> filesToBuild) { if (!ruleContext.attributes().get("executable", Type.BOOLEAN)) { return null; } if (Iterables.size(filesToBuild) == 1) { return Iterables.getOnlyElement(filesToBuild); } return null; } /** * Resolves any variables, including make and genrule-specific variables, in the command and * returns the expanded command. * * <p>GenRule implementations may override this method to perform additional expansions. */ protected String resolveCommand(String command, final RuleContext ruleContext, final NestedSet<Artifact> resolvedSrcs, final NestedSet<Artifact> filesToBuild) { return ruleContext.expandMakeVariables( "cmd", command, createCommandResolverContext(ruleContext, resolvedSrcs, filesToBuild)); } /** * Creates a new {@link CommandResolverContext} instance to use in {@link #resolveCommand}. */ protected CommandResolverContext createCommandResolverContext(RuleContext ruleContext, NestedSet<Artifact> resolvedSrcs, NestedSet<Artifact> filesToBuild) { return new CommandResolverContext(ruleContext, resolvedSrcs, filesToBuild); } /** * Implementation of {@link ConfigurationMakeVariableContext} used to expand variables in a * genrule command string. */ protected static class CommandResolverContext extends ConfigurationMakeVariableContext { private final RuleContext ruleContext; private final NestedSet<Artifact> resolvedSrcs; private final NestedSet<Artifact> filesToBuild; private static final ImmutableList<String> makeVariableAttributes = ImmutableList.of(":cc_toolchain", "toolchains"); public CommandResolverContext( RuleContext ruleContext, NestedSet<Artifact> resolvedSrcs, NestedSet<Artifact> filesToBuild) { super( ruleContext.getMakeVariables(makeVariableAttributes), ruleContext.getRule().getPackage(), ruleContext.getConfiguration()); this.ruleContext = ruleContext; this.resolvedSrcs = resolvedSrcs; this.filesToBuild = filesToBuild; } public RuleContext getRuleContext() { return ruleContext; } @Override public String lookupMakeVariable(String name) throws ExpansionException { if (name.equals("SRCS")) { return Artifact.joinExecPaths(" ", resolvedSrcs); } else if (name.equals("<")) { return expandSingletonArtifact(resolvedSrcs, "$<", "input file"); } else if (name.equals("OUTS")) { return Artifact.joinExecPaths(" ", filesToBuild); } else if (name.equals("@")) { return expandSingletonArtifact(filesToBuild, "$@", "output file"); } else if (name.equals("@D")) { // The output directory. If there is only one filename in outs, // this expands to the directory containing that file. If there are // multiple filenames, this variable instead expands to the // package's root directory in the genfiles tree, even if all the // generated files belong to the same subdirectory! if (Iterables.size(filesToBuild) == 1) { Artifact outputFile = Iterables.getOnlyElement(filesToBuild); PathFragment relativeOutputFile = outputFile.getExecPath(); if (relativeOutputFile.segmentCount() <= 1) { // This should never happen, since the path should contain at // least a package name and a file name. throw new IllegalStateException( "$(@D) for genrule " + ruleContext.getLabel() + " has less than one segment"); } return relativeOutputFile.getParentDirectory().getPathString(); } else { PathFragment dir; if (ruleContext.getRule().hasBinaryOutput()) { dir = ruleContext.getConfiguration().getBinFragment(); } else { dir = ruleContext.getConfiguration().getGenfilesFragment(); } PathFragment relPath = ruleContext.getRule().getLabel().getPackageIdentifier().getSourceRoot(); return dir.getRelative(relPath).getPathString(); } } else if (JDK_MAKE_VARIABLE.matcher("$(" + name + ")").find()) { return new ConfigurationMakeVariableContext( ruleContext.getMakeVariables(makeVariableAttributes), ruleContext.getTarget().getPackage(), ruleContext.getHostConfiguration()) .lookupMakeVariable(name); } else { return super.lookupMakeVariable(name); } } /** * Returns the path of the sole element "artifacts", generating an exception with an informative * error message iff the set is not a singleton. Used to expand "$<", "$@". */ private final String expandSingletonArtifact(Iterable<Artifact> artifacts, String variable, String artifactName) throws ExpansionException { if (Iterables.isEmpty(artifacts)) { throw new ExpansionException("variable '" + variable + "' : no " + artifactName); } else if (Iterables.size(artifacts) > 1) { throw new ExpansionException("variable '" + variable + "' : more than one " + artifactName); } return Iterables.getOnlyElement(artifacts).getExecPathString(); } } }