// Copyright 2014 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.analysis; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.RunfilesSupplier; import com.google.devtools.build.lib.analysis.actions.ExecutionRequirements; import com.google.devtools.build.lib.analysis.actions.FileWriteAction; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.rules.AliasProvider; import com.google.devtools.build.lib.syntax.SkylarkDict; import com.google.devtools.build.lib.syntax.SkylarkList; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.Pair; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; /** * Provides shared functionality for parameterized command-line launching. * Also used by {@link com.google.devtools.build.lib.rules.extra.ExtraActionFactory}. * * Two largely independent separate sets of functionality are provided: * 1- string interpolation for {@code $(location[s] ...)} and {@code $(MakeVariable)} * 2- a utility to build potentially large command lines (presumably made of multiple commands), * that if presumed too large for the kernel's taste can be dumped into a shell script * that will contain the same commands, * at which point the shell script is added to the list of inputs. */ public final class CommandHelper { /** * Maximum total command-line length, in bytes, not counting "/bin/bash -c ". * If the command is very long, then we write the command to a script file, * to avoid overflowing any limits on command-line length. * For short commands, we just use /bin/bash -c command. * * Maximum command line length on Windows is 32767[1], but for cmd.exe it is 8192[2]. * [1] https://msdn.microsoft.com/en-us/library/ms682425(VS.85).aspx * [2] https://support.microsoft.com/en-us/kb/830473. */ @VisibleForTesting public static int maxCommandLength = OS.getCurrent() == OS.WINDOWS ? 8000 : 64000; /** {@link RunfilesSupplier}s for tools used by this rule. */ private final SkylarkList<RunfilesSupplier> toolsRunfilesSuppliers; /** * Use labelMap for heuristically expanding labels (does not include "outs") * This is similar to heuristic location expansion in LocationExpander * and should be kept in sync. */ private final SkylarkDict<Label, ImmutableCollection<Artifact>> labelMap; /** * The ruleContext this helper works on */ private final RuleContext ruleContext; /** * Output executable files from the 'tools' attribute. */ private final SkylarkList<Artifact> resolvedTools; /** * Creates a {@link CommandHelper}. * * @param tools resolves set of tools into set of executable binaries. Populates manifests, * remoteRunfiles and label map where required. * @param labelMap adds files to set of known files of label. Used for resolving $(location) * variables. */ public CommandHelper( RuleContext ruleContext, Iterable<? extends TransitiveInfoCollection> tools, ImmutableMap<Label, ? extends Iterable<Artifact>> labelMap) { this.ruleContext = ruleContext; ImmutableList.Builder<Artifact> resolvedToolsBuilder = ImmutableList.builder(); ImmutableList.Builder<RunfilesSupplier> toolsRunfilesBuilder = ImmutableList.builder(); Map<Label, Collection<Artifact>> tempLabelMap = new HashMap<>(); for (Map.Entry<Label, ? extends Iterable<Artifact>> entry : labelMap.entrySet()) { Iterables.addAll(mapGet(tempLabelMap, entry.getKey()), entry.getValue()); } for (TransitiveInfoCollection dep : tools) { // (Note: host configuration) Label label = AliasProvider.getDependencyLabel(dep); FilesToRunProvider tool = dep.getProvider(FilesToRunProvider.class); if (tool == null) { continue; } Collection<Artifact> files = tool.getFilesToRun(); resolvedToolsBuilder.addAll(files); Artifact executableArtifact = tool.getExecutable(); // If the label has an executable artifact add that to the multimaps. if (executableArtifact != null) { mapGet(tempLabelMap, label).add(executableArtifact); // Also send the runfiles when running remotely. toolsRunfilesBuilder.add(tool.getRunfilesSupplier()); } else { // Map all depArtifacts to the respective label using the multimaps. mapGet(tempLabelMap, label).addAll(files); } } this.resolvedTools = SkylarkList.createImmutable(resolvedToolsBuilder.build()); this.toolsRunfilesSuppliers = SkylarkList.createImmutable(toolsRunfilesBuilder.build()); ImmutableMap.Builder<Label, ImmutableCollection<Artifact>> labelMapBuilder = ImmutableMap.builder(); for (Entry<Label, Collection<Artifact>> entry : tempLabelMap.entrySet()) { labelMapBuilder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue())); } this.labelMap = SkylarkDict.copyOf(null, labelMapBuilder.build()); } public SkylarkList<Artifact> getResolvedTools() { return resolvedTools; } public SkylarkList<RunfilesSupplier> getToolsRunfilesSuppliers() { return toolsRunfilesSuppliers; } // Returns the value in the specified corresponding to 'key', creating and // inserting an empty container if absent. We use Map not Multimap because // we need to distinguish the cases of "empty value" and "absent key". private static Collection<Artifact> mapGet(Map<Label, Collection<Artifact>> map, Label key) { Collection<Artifact> values = map.get(key); if (values == null) { // We use sets not lists, because it's conceivable that the same artifact // could appear twice, e.g. in "srcs" and "deps". values = Sets.newHashSet(); map.put(key, values); } return values; } /** * Resolves a command, and expands known locations for $(location) * variables. */ public String resolveCommandAndExpandLabels( String command, @Nullable String attribute, Boolean supportLegacyExpansion, Boolean allowDataInLabel) { LocationExpander expander = new LocationExpander( ruleContext, ImmutableMap.copyOf(labelMap), allowDataInLabel); if (attribute != null) { command = expander.expandAttribute(attribute, command); } else { command = expander.expand(command); } if (supportLegacyExpansion) { command = expandLabels(command, labelMap); } return command; } /** * Resolves the 'cmd' attribute, and expands known locations for $(location) * variables. */ public String resolveCommandAndExpandLabels( Boolean supportLegacyExpansion, Boolean allowDataInLabel) { return resolveCommandAndExpandLabels( ruleContext.attributes().get("cmd", Type.STRING), "cmd", supportLegacyExpansion, allowDataInLabel); } /** * Expands labels occurring in the string "expr" in the rule 'cmd'. * Each label must be valid, be a declared prerequisite, and expand to a * unique path. * * <p>If the expansion fails, an attribute error is reported and the original * expression is returned. */ private <T extends Iterable<Artifact>> String expandLabels(String expr, Map<Label, T> labelMap) { try { return LabelExpander.expand(expr, labelMap, ruleContext.getLabel()); } catch (LabelExpander.NotUniqueExpansionException nuee) { ruleContext.attributeError("cmd", nuee.getMessage()); return expr; } } private static Pair<List<String>, Artifact> buildCommandLineMaybeWithScriptFile( RuleContext ruleContext, String command, String scriptPostFix, PathFragment shellPath) { List<String> argv; Artifact scriptFileArtifact = null; if (command.length() <= maxCommandLength) { argv = buildCommandLineSimpleArgv(command, shellPath); } else { // Use script file. scriptFileArtifact = buildCommandLineArtifact(ruleContext, command, scriptPostFix); argv = buildCommandLineArgvWithArtifact(scriptFileArtifact, shellPath); } return Pair.of(argv, scriptFileArtifact); } private static ImmutableList<String> buildCommandLineArgvWithArtifact(Artifact scriptFileArtifact, PathFragment shellPath) { return ImmutableList.of(shellPath.getPathString(), scriptFileArtifact.getExecPathString()); } private static Artifact buildCommandLineArtifact(RuleContext ruleContext, String command, String scriptPostFix) { String scriptFileName = ruleContext.getTarget().getName() + scriptPostFix; String scriptFileContents = "#!/bin/bash\n" + command; Artifact scriptFileArtifact = FileWriteAction.createFile( ruleContext, scriptFileName, scriptFileContents, /*executable=*/true); return scriptFileArtifact; } private static ImmutableList<String> buildCommandLineSimpleArgv(String command, PathFragment shellPath) { return ImmutableList.of(shellPath.getPathString(), "-c", command); } /** * Builds the set of command-line arguments. Creates a bash script if the * command line is longer than the allowed maximum {@link #maxCommandLength}. * Fixes up the input artifact list with the created bash script when required. */ public List<String> buildCommandLine( String command, NestedSetBuilder<Artifact> inputs, String scriptPostFix) { return buildCommandLine(command, inputs, scriptPostFix, ImmutableMap.<String, String>of()); } /** * Builds the set of command-line arguments using the specified shell path. Creates a bash script * if the command line is longer than the allowed maximum {@link #maxCommandLength}. * Fixes up the input artifact list with the created bash script when required. * * @param executionInfo an execution info map of the action associated with the command line to be * built. */ public List<String> buildCommandLine( String command, NestedSetBuilder<Artifact> inputs, String scriptPostFix, Map<String, String> executionInfo) { Pair<List<String>, Artifact> argvAndScriptFile = buildCommandLineMaybeWithScriptFile(ruleContext, command, scriptPostFix, shellPath(executionInfo)); if (argvAndScriptFile.second != null) { inputs.add(argvAndScriptFile.second); } return argvAndScriptFile.first; } /** * Builds the set of command-line arguments. Creates a bash script if the * command line is longer than the allowed maximum {@link #maxCommandLength}. * Fixes up the input artifact list with the created bash script when required. */ public List<String> buildCommandLine( String command, List<Artifact> inputs, String scriptPostFix, Map<String, String> executionInfo) { Pair<List<String>, Artifact> argvAndScriptFile = buildCommandLineMaybeWithScriptFile( ruleContext, command, scriptPostFix, shellPath(executionInfo)); if (argvAndScriptFile.second != null) { inputs.add(argvAndScriptFile.second); } return argvAndScriptFile.first; } /** * Returns the path to the shell for an action with the given execution requirements. */ private PathFragment shellPath(Map<String, String> executionInfo) { // Use vanilla /bin/bash for actions running on mac machines. return executionInfo.containsKey(ExecutionRequirements.REQUIRES_DARWIN) ? PathFragment.create("/bin/bash") : ruleContext.getConfiguration().getShellExecutable(); } }