| // Copyright 2018 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.actions; | |
| import static com.google.common.base.Preconditions.checkArgument; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.devtools.build.lib.analysis.config.CoreOptions; | |
| import com.google.devtools.build.lib.util.Fingerprint; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.PathStrippable; | |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.UUID; | |
| import javax.annotation.Nullable; | |
| /** | |
| * A class that keeps a list of command lines and optional associated parameter file info. | |
| * | |
| * <p>This class is used by {@link com.google.devtools.build.lib.exec.SpawnRunner} implementations | |
| * to expand the command lines into a master argument list + any param files needed to be written. | |
| */ | |
| public abstract sealed class CommandLines { | |
| // A (hopefully) conservative estimate of how much long each param file arg would be | |
| // eg. the length of '@path/to/param_file'. | |
| private static final int PARAM_FILE_ARG_LENGTH_ESTIMATE = 512; | |
| private static final UUID PARAM_FILE_UUID = | |
| UUID.fromString("106c1389-88d7-4cc1-8f05-f8a61fd8f7b1"); | |
| /** A simple tuple of a {@link CommandLine} and a {@link ParamFileInfo}. */ | |
| public static class CommandLineAndParamFileInfo { | |
| public final CommandLine commandLine; | |
| @Nullable public final ParamFileInfo paramFileInfo; | |
| public CommandLineAndParamFileInfo( | |
| CommandLine commandLine, @Nullable ParamFileInfo paramFileInfo) { | |
| this.commandLine = commandLine; | |
| this.paramFileInfo = paramFileInfo; | |
| } | |
| } | |
| private CommandLines() {} | |
| /** | |
| * Expands this object into a single primary command line and (0-N) param files. The spawn runner | |
| * is expected to write these param files prior to execution of an action. | |
| * | |
| * @param inputMetadataProvider the metadata provider to expand composite artifacts | |
| * @param paramFileBasePath Used to derive param file names. Often the first output of an action | |
| * @param pathMapper function to map configuration prefixes in output paths to more cache-friendly | |
| * identifiers | |
| * @param limits The command line limits the host OS can support. | |
| * @return The expanded command line and its param files (if any). | |
| */ | |
| public ExpandedCommandLines expand( | |
| InputMetadataProvider inputMetadataProvider, | |
| PathFragment paramFileBasePath, | |
| PathMapper pathMapper, | |
| CommandLineLimits limits) | |
| throws CommandLineExpansionException, InterruptedException { | |
| return expand( | |
| inputMetadataProvider, | |
| paramFileBasePath, | |
| limits, | |
| pathMapper, | |
| PARAM_FILE_ARG_LENGTH_ESTIMATE); | |
| } | |
| @VisibleForTesting | |
| ExpandedCommandLines expand( | |
| InputMetadataProvider inputMetadataProvider, | |
| PathFragment paramFileBasePath, | |
| CommandLineLimits limits, | |
| PathMapper pathMapper, | |
| int paramFileArgLengthEstimate) | |
| throws CommandLineExpansionException, InterruptedException { | |
| ImmutableList<CommandLineAndParamFileInfo> commandLines = unpack(); | |
| ImmutableList.Builder<String> arguments = ImmutableList.builder(); | |
| ArrayList<ParamFileActionInput> paramFiles = new ArrayList<>(commandLines.size()); | |
| int conservativeMaxLength = limits.maxLength - commandLines.size() * paramFileArgLengthEstimate; | |
| int cmdLineLength = 0; | |
| // We name based on the output, starting at <output>-0.params and then incrementing | |
| int paramFileNameSuffix = 0; | |
| for (CommandLineAndParamFileInfo pair : commandLines) { | |
| CommandLine commandLine = pair.commandLine; | |
| ParamFileInfo paramFileInfo = pair.paramFileInfo; | |
| ArgChunk chunk = commandLine.expand(inputMetadataProvider, pathMapper); | |
| if (paramFileInfo == null) { | |
| arguments.addAll(chunk.arguments(pathMapper)); | |
| cmdLineLength += chunk.totalArgLength(pathMapper); | |
| } else { | |
| boolean useParamFile = true; | |
| if (!paramFileInfo.always()) { | |
| int tentativeCmdLineLength = cmdLineLength + chunk.totalArgLength(pathMapper); | |
| if (tentativeCmdLineLength <= conservativeMaxLength) { | |
| arguments.addAll(chunk.arguments(pathMapper)); | |
| cmdLineLength = tentativeCmdLineLength; | |
| useParamFile = false; | |
| } | |
| } | |
| if (useParamFile) { | |
| PathFragment paramFileExecPath; | |
| String paramFileName = paramFileInfo.getParamFileName(); | |
| if (paramFileName != null) { | |
| paramFileExecPath = paramFileBasePath.replaceName(paramFileName); | |
| } else { | |
| paramFileExecPath = | |
| ParameterFile.derivePath(paramFileBasePath, Integer.toString(paramFileNameSuffix)); | |
| ++paramFileNameSuffix; | |
| } | |
| String paramArg = | |
| SingleStringArgFormatter.format( | |
| paramFileInfo.getFlagFormatString(), | |
| pathMapper.map(paramFileExecPath).getPathString()); | |
| arguments.add(paramArg); | |
| cmdLineLength += paramArg.length() + 1; | |
| if (paramFileInfo.flagsOnly()) { | |
| // Move just the flags into the file, and keep the positional parameters on the command | |
| // line. | |
| paramFiles.add( | |
| new ParamFileActionInput( | |
| paramFileExecPath, | |
| paramArg, | |
| ParameterFile.flagsOnly(chunk.arguments(pathMapper)), | |
| paramFileInfo.getFileType())); | |
| for (String positionalArg : ParameterFile.nonFlags(chunk.arguments(pathMapper))) { | |
| arguments.add(positionalArg); | |
| cmdLineLength += positionalArg.length() + 1; | |
| } | |
| } else { | |
| paramFiles.add( | |
| new ParamFileActionInput( | |
| paramFileExecPath, | |
| paramArg, | |
| chunk.arguments(pathMapper), | |
| paramFileInfo.getFileType())); | |
| } | |
| } | |
| } | |
| } | |
| return new ExpandedCommandLines(arguments.build(), paramFiles); | |
| } | |
| /** | |
| * Returns all arguments, including ones inside of param files. | |
| * | |
| * <p>Suitable for debugging and printing messages to users. This expands all command lines, so it | |
| * is potentially expensive. | |
| */ | |
| public ImmutableList<String> allArguments() | |
| throws CommandLineExpansionException, InterruptedException { | |
| return allArguments(PathMapper.NOOP); | |
| } | |
| /** Variation of {@link #allArguments()} that supports output path stripping. */ | |
| public ImmutableList<String> allArguments(PathMapper pathMapper) | |
| throws CommandLineExpansionException, InterruptedException { | |
| ImmutableList.Builder<String> arguments = ImmutableList.builder(); | |
| for (CommandLineAndParamFileInfo pair : unpack()) { | |
| arguments.addAll(pair.commandLine.arguments(/* inputMetadataProvider= */ null, pathMapper)); | |
| } | |
| return arguments.build(); | |
| } | |
| public void addToFingerprint( | |
| ActionKeyContext actionKeyContext, | |
| @Nullable InputMetadataProvider inputMetadataProvider, | |
| CoreOptions.OutputPathsMode effectiveOutputPathsMode, | |
| Fingerprint fingerprint) | |
| throws CommandLineExpansionException, InterruptedException { | |
| ImmutableList<CommandLineAndParamFileInfo> commandLines = unpack(); | |
| for (CommandLineAndParamFileInfo pair : commandLines) { | |
| CommandLine commandLine = pair.commandLine; | |
| ParamFileInfo paramFileInfo = pair.paramFileInfo; | |
| commandLine.addToFingerprint( | |
| actionKeyContext, inputMetadataProvider, effectiveOutputPathsMode, fingerprint); | |
| if (paramFileInfo != null) { | |
| addParamFileInfoToFingerprint(paramFileInfo, fingerprint); | |
| } | |
| } | |
| } | |
| /** | |
| * Expanded command lines. | |
| * | |
| * <p>The spawn runner implementation is expected to ensure the param files are available once the | |
| * spawn is executed. | |
| */ | |
| public static class ExpandedCommandLines { | |
| private final ImmutableList<String> arguments; | |
| private final List<ParamFileActionInput> paramFiles; | |
| ExpandedCommandLines(ImmutableList<String> arguments, List<ParamFileActionInput> paramFiles) { | |
| this.arguments = arguments; | |
| this.paramFiles = paramFiles; | |
| } | |
| /** Returns the primary command line of the command. */ | |
| public ImmutableList<String> arguments() { | |
| return arguments; | |
| } | |
| /** Returns the param file action inputs needed to execute the command. */ | |
| public List<ParamFileActionInput> getParamFiles() { | |
| return paramFiles; | |
| } | |
| } | |
| /** | |
| * Unpacks the optimized storage format into a list of {@link CommandLineAndParamFileInfo}. | |
| * | |
| * <p>The returned {@link ImmutableList} and its {@link CommandLineAndParamFileInfo} elements are | |
| * not part of the optimized storage representation. Retaining them in an action would defeat the | |
| * memory optimizations made by {@link CommandLines}. | |
| */ | |
| public abstract ImmutableList<CommandLineAndParamFileInfo> unpack(); | |
| private static void addParamFileInfoToFingerprint( | |
| ParamFileInfo paramFileInfo, Fingerprint fingerprint) { | |
| fingerprint.addUUID(PARAM_FILE_UUID); | |
| fingerprint.addString(paramFileInfo.getFlagFormatString()); | |
| fingerprint.addString(paramFileInfo.getFileType().toString()); | |
| } | |
| public static Builder builder() { | |
| return new Builder(); | |
| } | |
| /** Returns an instance with a single command line. */ | |
| public static CommandLines of(CommandLine commandLine) { | |
| return new OnePartCommandLines(commandLine); | |
| } | |
| /** Returns an instance with a single trivial command line. */ | |
| public static CommandLines of(ImmutableList<String> args) { | |
| return new OnePartCommandLines(CommandLine.of(args)); | |
| } | |
| public static CommandLines concat(CommandLine commandLine, CommandLines commandLines) { | |
| Builder builder = builder(); | |
| builder.addCommandLine(commandLine); | |
| for (CommandLineAndParamFileInfo pair : commandLines.unpack()) { | |
| builder.addCommandLine(pair); | |
| } | |
| return builder.build(); | |
| } | |
| /** | |
| * Builder for {@link CommandLines}. | |
| * | |
| * <p>Attempts to build the most memory-efficient {@link CommandLines} instance possible. Most | |
| * command lines are composed of 1-3 parts. Additionally, the first part is typically just an | |
| * executable or shell command and does not have an associated params file. If both of these | |
| * criteria are met, memory is saved by using one of the array-free subclasses. Otherwise, uses | |
| * {@link NPartCommandLines} which handles any arbitrary case. | |
| */ | |
| public static class Builder { | |
| private Object part1; // Set to null when we need to use NPartCommandLines. | |
| private Object part2; | |
| private ParamFileInfo part2ParamFileInfo; | |
| private Object part3; | |
| private ParamFileInfo part3ParamFileInfo; | |
| private int parts = 0; | |
| private final List<Object> commandLines = new ArrayList<>(); | |
| @CanIgnoreReturnValue | |
| public Builder addSingleArgument(Object argument) { | |
| checkArgument( | |
| !(argument instanceof ParamFileInfo) | |
| && !(argument instanceof CommandLineAndParamFileInfo), | |
| argument); | |
| return addInternal(argument, null); | |
| } | |
| @CanIgnoreReturnValue | |
| public Builder addCommandLine(CommandLine commandLine) { | |
| return addInternal(commandLine, null); | |
| } | |
| @CanIgnoreReturnValue | |
| public Builder addCommandLine(CommandLine commandLine, @Nullable ParamFileInfo paramFileInfo) { | |
| return addInternal(commandLine, paramFileInfo); | |
| } | |
| @CanIgnoreReturnValue | |
| public Builder addCommandLine(CommandLineAndParamFileInfo pair) { | |
| return addInternal(pair.commandLine, pair.paramFileInfo); | |
| } | |
| private Builder addInternal(Object part, @Nullable ParamFileInfo paramFileInfo) { | |
| parts++; | |
| if (parts == 1) { | |
| if (paramFileInfo == null) { | |
| part1 = part; | |
| } | |
| } else if (parts == 2) { | |
| part2 = part; | |
| part2ParamFileInfo = paramFileInfo; | |
| } else if (parts == 3) { | |
| part3 = part; | |
| part3ParamFileInfo = paramFileInfo; | |
| } else if (parts == 4) { | |
| part1 = null; // Destined to build an NPartCommandLines. | |
| } | |
| commandLines.add(part); | |
| if (paramFileInfo != null) { | |
| commandLines.add(paramFileInfo); | |
| } | |
| return this; | |
| } | |
| public CommandLines build() { | |
| if (part1 == null) { | |
| return new NPartCommandLines(commandLines.toArray()); | |
| } | |
| if (parts == 1) { | |
| return new OnePartCommandLines(part1); | |
| } | |
| if (parts == 2) { | |
| return new TwoPartCommandLines(part1, part2, part2ParamFileInfo); | |
| } | |
| if (part2ParamFileInfo == null && part3ParamFileInfo == null) { | |
| return new ThreePartCommandLinesWithoutParamsFiles(part1, part2, part3); | |
| } | |
| return new ThreePartCommandLines(part1, part2, part2ParamFileInfo, part3, part3ParamFileInfo); | |
| } | |
| } | |
| private static CommandLine toExecutableCommandLine(Object obj) { | |
| return toCommandLine(obj, /* hasExecutablePath= */ true); | |
| } | |
| private static CommandLine toNonExecutableCommandLine(Object obj) { | |
| return toCommandLine(obj, /* hasExecutablePath= */ false); | |
| } | |
| private static CommandLine toCommandLine(Object obj, boolean hasExecutablePath) { | |
| return obj instanceof CommandLine commandLine | |
| ? commandLine | |
| : new SingletonCommandLine(obj, hasExecutablePath); | |
| } | |
| private static final class OnePartCommandLines extends CommandLines { | |
| private final Object part1; | |
| OnePartCommandLines(Object part1) { | |
| this.part1 = part1; | |
| } | |
| @Override | |
| public ImmutableList<CommandLineAndParamFileInfo> unpack() { | |
| return ImmutableList.of( | |
| new CommandLineAndParamFileInfo(toExecutableCommandLine(part1), null)); | |
| } | |
| } | |
| private static final class TwoPartCommandLines extends CommandLines { | |
| private final Object part1; | |
| private final Object part2; | |
| @Nullable private final ParamFileInfo part2ParamFileInfo; | |
| TwoPartCommandLines(Object part1, Object part2, @Nullable ParamFileInfo part2ParamFileInfo) { | |
| this.part1 = part1; | |
| this.part2 = part2; | |
| this.part2ParamFileInfo = part2ParamFileInfo; | |
| } | |
| @Override | |
| public ImmutableList<CommandLineAndParamFileInfo> unpack() { | |
| return ImmutableList.of( | |
| new CommandLineAndParamFileInfo(toExecutableCommandLine(part1), null), | |
| new CommandLineAndParamFileInfo(toNonExecutableCommandLine(part2), part2ParamFileInfo)); | |
| } | |
| } | |
| private static final class ThreePartCommandLinesWithoutParamsFiles extends CommandLines { | |
| private final Object part1; | |
| private final Object part2; | |
| private final Object part3; | |
| ThreePartCommandLinesWithoutParamsFiles(Object part1, Object part2, Object part3) { | |
| this.part1 = part1; | |
| this.part2 = part2; | |
| this.part3 = part3; | |
| } | |
| @Override | |
| public ImmutableList<CommandLineAndParamFileInfo> unpack() { | |
| return ImmutableList.of( | |
| new CommandLineAndParamFileInfo(toExecutableCommandLine(part1), null), | |
| new CommandLineAndParamFileInfo(toNonExecutableCommandLine(part2), null), | |
| new CommandLineAndParamFileInfo(toNonExecutableCommandLine(part3), null)); | |
| } | |
| } | |
| private static final class ThreePartCommandLines extends CommandLines { | |
| private final Object part1; | |
| private final Object part2; | |
| @Nullable private final ParamFileInfo part2ParamFileInfo; | |
| private final Object part3; | |
| @Nullable private final ParamFileInfo part3ParamFileInfo; | |
| ThreePartCommandLines( | |
| Object part1, | |
| Object part2, | |
| @Nullable ParamFileInfo part2ParamFileInfo, | |
| Object part3, | |
| @Nullable ParamFileInfo part3ParamFileInfo) { | |
| this.part1 = part1; | |
| this.part2 = part2; | |
| this.part2ParamFileInfo = part2ParamFileInfo; | |
| this.part3 = part3; | |
| this.part3ParamFileInfo = part3ParamFileInfo; | |
| } | |
| @Override | |
| public ImmutableList<CommandLineAndParamFileInfo> unpack() { | |
| return ImmutableList.of( | |
| new CommandLineAndParamFileInfo(toExecutableCommandLine(part1), null), | |
| new CommandLineAndParamFileInfo(toNonExecutableCommandLine(part2), part2ParamFileInfo), | |
| new CommandLineAndParamFileInfo(toNonExecutableCommandLine(part3), part3ParamFileInfo)); | |
| } | |
| } | |
| private static final class NPartCommandLines extends CommandLines { | |
| /** | |
| * Stored as an {@code Object[]} to save memory. Elements in this array are either: | |
| * | |
| * <ul> | |
| * <li>A {@link CommandLine}, optionally followed by a {@link ParamFileInfo}. | |
| * <li>An arbitrary {@link Object} to be wrapped in a {@link SingletonCommandLine}. | |
| * </ul> | |
| */ | |
| private final Object[] commandLines; | |
| NPartCommandLines(Object[] commandLines) { | |
| this.commandLines = commandLines; | |
| } | |
| @Override | |
| public ImmutableList<CommandLineAndParamFileInfo> unpack() { | |
| ImmutableList.Builder<CommandLineAndParamFileInfo> result = ImmutableList.builder(); | |
| for (int i = 0; i < commandLines.length; i++) { | |
| Object obj = commandLines[i]; | |
| CommandLine commandLine; | |
| ParamFileInfo paramFileInfo = null; | |
| if (obj instanceof CommandLine c) { | |
| commandLine = c; | |
| if (i + 1 < commandLines.length && commandLines[i + 1] instanceof ParamFileInfo) { | |
| paramFileInfo = (ParamFileInfo) commandLines[++i]; | |
| } | |
| } else { | |
| commandLine = new SingletonCommandLine(obj, /* hasExecutablePath= */ i == 0); | |
| } | |
| result.add(new CommandLineAndParamFileInfo(commandLine, paramFileInfo)); | |
| } | |
| return result.build(); | |
| } | |
| } | |
| private static class SingletonCommandLine extends AbstractCommandLine { | |
| private final Object arg; | |
| private final boolean hasExecutablePath; | |
| SingletonCommandLine(Object arg, boolean hasExecutablePath) { | |
| this.arg = arg; | |
| this.hasExecutablePath = hasExecutablePath; | |
| } | |
| @Override | |
| public Iterable<String> arguments() { | |
| return arguments(null, PathMapper.NOOP); | |
| } | |
| @Override | |
| public Iterable<String> arguments( | |
| @Nullable InputMetadataProvider inputMetadataProvider, PathMapper pathMapper) { | |
| return ImmutableList.of( | |
| switch (arg) { | |
| case PathStrippable ps -> ps.expand(pathMapper::map); | |
| // StarlarkAction stores the executable path as a string to save memory, but it should | |
| // still be mapped just like a PathFragment. In this case, the string always represents | |
| // a normalized path, so if it isn't (e.g. because it is an absolute path, possibly for | |
| // another OS), don't normalize or map it. | |
| case String s when hasExecutablePath -> { | |
| PathFragment pathFragment = PathFragment.create(s); | |
| if (!pathFragment.getPathString().equals(s)) { | |
| yield s; | |
| } | |
| yield pathMapper.map(pathFragment).getPathString(); | |
| } | |
| default -> CommandLineItem.expandToCommandLine(arg); | |
| }); | |
| } | |
| } | |
| } |