| // 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.analysis.starlark; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Interner; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Iterators; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.UnmodifiableIterator; |
| import com.google.devtools.build.lib.actions.ActionKeyContext; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; |
| import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact; |
| import com.google.devtools.build.lib.actions.Artifact.MissingExpansionException; |
| import com.google.devtools.build.lib.actions.CommandLine; |
| import com.google.devtools.build.lib.actions.CommandLineExpansionException; |
| import com.google.devtools.build.lib.actions.CommandLineItem; |
| import com.google.devtools.build.lib.actions.CommandLineLimits; |
| import com.google.devtools.build.lib.actions.CommandLines; |
| import com.google.devtools.build.lib.actions.CommandLines.ParamFileActionInput; |
| import com.google.devtools.build.lib.actions.FilesetManifest; |
| import com.google.devtools.build.lib.actions.FilesetManifest.RelativeSymlinkBehaviorWithoutError; |
| import com.google.devtools.build.lib.actions.FilesetOutputSymlink; |
| import com.google.devtools.build.lib.actions.PathMapper; |
| import com.google.devtools.build.lib.actions.SingleStringArgFormatter; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.cmdline.RepositoryMapping; |
| import com.google.devtools.build.lib.cmdline.RepositoryName; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| import com.google.devtools.build.lib.concurrent.BlazeInterners; |
| import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; |
| import com.google.devtools.build.lib.starlarkbuildapi.DirectoryExpander; |
| import com.google.devtools.build.lib.starlarkbuildapi.FileApi; |
| import com.google.devtools.build.lib.starlarkbuildapi.FileRootApi; |
| import com.google.devtools.build.lib.util.Fingerprint; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.NoSuchElementException; |
| import java.util.UUID; |
| import java.util.function.Consumer; |
| import javax.annotation.Nullable; |
| import net.starlark.java.eval.EvalException; |
| import net.starlark.java.eval.Mutability; |
| import net.starlark.java.eval.Printer; |
| import net.starlark.java.eval.Sequence; |
| import net.starlark.java.eval.Starlark; |
| import net.starlark.java.eval.StarlarkCallable; |
| import net.starlark.java.eval.StarlarkFunction; |
| import net.starlark.java.eval.StarlarkSemantics; |
| import net.starlark.java.eval.StarlarkThread; |
| import net.starlark.java.syntax.Location; |
| |
| /** |
| * Supports {@code ctx.actions.args()} from Starlark. |
| * |
| * <p>To be as memory-friendly as possible, expansion happens in three stages. First, when a |
| * Starlark rule is analyzed, its {@code Args} are built into a {@code StarlarkCustomCommandLine}. |
| * This is retained in Skyframe, so care is taken to be as compact as possible. At this point, the |
| * {@linkplain #arguments representation} is just a "recipe" to compute the full command line later |
| * on. Additionally, {@link #addToFingerprint} supports computing a fingerprint without actually |
| * constructing the expanded command line. |
| * |
| * <p>Second, right before an action executes, {@link #expand(ArtifactExpander, PathMapper)} is |
| * called to "preprocess" the recipe into a {@link PreprocessedCommandLine}. This step includes |
| * flattening nested sets and applying any operations that can throw an exception, such as expanding |
| * directories and invoking {@code map_each} functions. At this point, the representation stores a |
| * string for each individual argument, but string formatting (including {@code format}, {@code |
| * format_each}, {@code before_each}, {@code join_with}, {@code format_joined}, and {@code |
| * flag_per_line}), is not yet applied. This means that in the common case of an {@link Artifact} |
| * with no {@code map_each} function, the string representation is still its {@link |
| * Artifact#getExecPathString}, which is not a novel string instance - it is already stored in the |
| * {@link Artifact}. This is crucial because for param files (the longest command lines), the |
| * preprocessed representation is retained throughout the action's execution. |
| * |
| * <p>Finally, string formatting is applied lazily during iteration over a {@link |
| * PreprocessedCommandLine}. When there is no param file, this happens up front during {@link |
| * CommandLines#expand(ArtifactExpander, PathFragment, PathMapper, CommandLineLimits)}. When a param |
| * file is used, the lazy {@link PreprocessedCommandLine#arguments} is stored in a {@link |
| * ParamFileActionInput}, which is processed by the action execution strategy. Strategies should |
| * respect the laziness of {@link ParamFileActionInput#getArguments} by iterating as few times as |
| * possible and not retaining elements longer than necessary. |
| * |
| * <p>As an example, consider this common usage pattern, where {@code inputs} is a {@code depset} of |
| * artifacts: |
| * |
| * <pre>{@code |
| * args = ctx.actions.args() |
| * args.use_param_file("--flagfile=%s") |
| * args.add_all(inputs, format_each = "--input=%s") |
| * }</pre> |
| * |
| * During analysis, the nested set is stored without flattening. During preprocessing, the nested |
| * set is flattened and {@link Artifact#expandToCommandLine} is called for each element, but this |
| * returns an exec path string instance already stored inside the artifact. {@code format_each} is |
| * not yet applied, so no new strings are created. {@link SingleStringArgFormatter#format} is only |
| * called during iteration over the {@link PreprocessedCommandLine#arguments}. |
| */ |
| // TODO: b/327187486 - PathMapper is currently invoked during the preprocessing step. If path |
| // stripping is enabled, this means that the lazy approach to string formatted described above is |
| // defeated. Ideally, PathMapper should be invoked lazily during iteration over a |
| // PreprocessedCommandLine. |
| public class StarlarkCustomCommandLine extends CommandLine { |
| |
| private static final Joiner LINE_JOINER = Joiner.on("\n").skipNulls(); |
| private static final Joiner FIELD_JOINER = Joiner.on(": ").skipNulls(); |
| |
| // Used to distinguish command line arguments that are potentially subject to special default |
| // stringification (such as Artifacts when path mapped or Labels when not main repo labels) from |
| // strings that happen to be identical to their string representations. |
| private enum StringificationType { |
| DEFAULT, |
| FILE, |
| LABEL |
| } |
| |
| /** |
| * Representation of a sequence of arguments originating from {@code Args.add_all} or {@code |
| * Args.add_joined}. |
| */ |
| @AutoCodec |
| static final class VectorArg { |
| private static final Interner<VectorArg> interner = BlazeInterners.newStrongInterner(); |
| |
| private static final int HAS_MAP_EACH = 1; |
| private static final int IS_NESTED_SET = 1 << 1; |
| private static final int EXPAND_DIRECTORIES = 1 << 2; |
| private static final int UNIQUIFY = 1 << 3; |
| private static final int OMIT_IF_EMPTY = 1 << 4; |
| private static final int HAS_ARG_NAME = 1 << 5; |
| private static final int HAS_FORMAT_EACH = 1 << 6; |
| private static final int HAS_BEFORE_EACH = 1 << 7; |
| private static final int HAS_JOIN_WITH = 1 << 8; |
| private static final int HAS_FORMAT_JOINED = 1 << 9; |
| private static final int HAS_TERMINATE_WITH = 1 << 10; |
| |
| private static final UUID EXPAND_DIRECTORIES_UUID = |
| UUID.fromString("9d7520d2-a187-11e8-98d0-529269fb1459"); |
| private static final UUID UNIQUIFY_UUID = |
| UUID.fromString("7f494c3e-faea-4498-a521-5d3bc6ee19eb"); |
| private static final UUID OMIT_IF_EMPTY_UUID = |
| UUID.fromString("923206f1-6474-4a8f-b30f-4dd3143622e6"); |
| private static final UUID ARG_NAME_UUID = |
| UUID.fromString("2bc00382-7199-46ec-ad52-1556577cde1a"); |
| private static final UUID FORMAT_EACH_UUID = |
| UUID.fromString("8e974aec-df07-4a51-9418-f4c1172b4045"); |
| private static final UUID BEFORE_EACH_UUID = |
| UUID.fromString("f7e101bc-644d-4277-8562-6515ad55a988"); |
| private static final UUID JOIN_WITH_UUID = |
| UUID.fromString("c227dbd3-edad-454e-bc8a-c9b5ba1c38a3"); |
| private static final UUID FORMAT_JOINED_UUID = |
| UUID.fromString("528af376-4233-4c27-be4d-b0ff24ed68db"); |
| private static final UUID TERMINATE_WITH_UUID = |
| UUID.fromString("a4e5e090-0dbd-4d41-899a-77cfbba58655"); |
| |
| private final int features; |
| private final StringificationType stringificationType; |
| |
| private VectorArg(int features, StringificationType stringificationType) { |
| this.features = features; |
| this.stringificationType = stringificationType; |
| } |
| |
| private static VectorArg create(int features, StringificationType stringificationType) { |
| return interner.intern(new VectorArg(features, stringificationType)); |
| } |
| |
| @VisibleForSerialization |
| @AutoCodec.Interner |
| static VectorArg intern(VectorArg vectorArg) { |
| return interner.intern(vectorArg); |
| } |
| |
| private static void push( |
| List<Object> arguments, Builder arg, StarlarkSemantics starlarkSemantics) { |
| // The location is really only needed if map_each is present, but it's easy enough to require |
| // it unconditionally. |
| checkNotNull(arg.location); |
| |
| int features = 0; |
| features |= arg.mapEach != null ? HAS_MAP_EACH : 0; |
| features |= arg.nestedSet != null ? IS_NESTED_SET : 0; |
| features |= arg.expandDirectories ? EXPAND_DIRECTORIES : 0; |
| features |= arg.uniquify ? UNIQUIFY : 0; |
| features |= arg.omitIfEmpty ? OMIT_IF_EMPTY : 0; |
| features |= arg.argName != null ? HAS_ARG_NAME : 0; |
| features |= arg.formatEach != null ? HAS_FORMAT_EACH : 0; |
| features |= arg.beforeEach != null ? HAS_BEFORE_EACH : 0; |
| features |= arg.joinWith != null ? HAS_JOIN_WITH : 0; |
| features |= arg.formatJoined != null ? HAS_FORMAT_JOINED : 0; |
| features |= arg.terminateWith != null ? HAS_TERMINATE_WITH : 0; |
| arguments.add(VectorArg.create(features, arg.nestedSetStringificationType)); |
| if (arg.mapEach != null) { |
| arguments.add(arg.mapEach); |
| arguments.add(arg.location); |
| arguments.add(starlarkSemantics); |
| } |
| if (arg.nestedSet != null) { |
| arguments.add(arg.nestedSet); |
| } else { |
| List<?> list = arg.list; |
| int count = list.size(); |
| arguments.add(count); |
| for (int i = 0; i < count; ++i) { |
| arguments.add(list.get(i)); |
| } |
| } |
| if (arg.argName != null) { |
| arguments.add(arg.argName); |
| } |
| if (arg.formatEach != null) { |
| arguments.add(arg.formatEach); |
| } |
| if (arg.beforeEach != null) { |
| checkState(arg.joinWith == null, "before_each and join_with are mutually exclusive"); |
| checkState( |
| arg.formatJoined == null, "before_each and format_joined are mutually exclusive"); |
| arguments.add(arg.beforeEach); |
| } |
| if (arg.joinWith != null) { |
| arguments.add(arg.joinWith); |
| } |
| if (arg.formatJoined != null) { |
| checkNotNull(arg.joinWith, "format_joined requires join_with"); |
| arguments.add(arg.formatJoined); |
| } |
| if (arg.terminateWith != null) { |
| arguments.add(arg.terminateWith); |
| } |
| } |
| |
| /** |
| * Adds this {@link VectorArg} to the given {@link PreprocessedCommandLine.Builder}. |
| * |
| * @param arguments result of {@link #rawArgsAsList} |
| * @param argi index in {@code arguments} at which this {@link VectorArg} begins; should be |
| * directly preceded by {@code this} |
| * @param builder the {@link PreprocessedCommandLine.Builder} in which to add a preprocessed |
| * representation of this arg |
| * @param pathMapper mapper for exec paths |
| * @return index in {@code arguments} where the next arg begins, or {@code arguments.size()} if |
| * this is the last argument |
| */ |
| private int preprocess( |
| List<Object> arguments, |
| int argi, |
| PreprocessedCommandLine.Builder builder, |
| @Nullable ArtifactExpander artifactExpander, |
| PathMapper pathMapper, |
| @Nullable RepositoryMapping mainRepoMapping) |
| throws CommandLineExpansionException, InterruptedException { |
| StarlarkCallable mapEach = null; |
| StarlarkSemantics starlarkSemantics = null; |
| Location location = null; |
| if ((features & HAS_MAP_EACH) != 0) { |
| mapEach = (StarlarkCallable) arguments.get(argi++); |
| location = (Location) arguments.get(argi++); |
| starlarkSemantics = (StarlarkSemantics) arguments.get(argi++); |
| } |
| |
| List<Object> originalValues; |
| if ((features & IS_NESTED_SET) != 0) { |
| @SuppressWarnings("unchecked") |
| NestedSet<Object> nestedSet = (NestedSet<Object>) arguments.get(argi++); |
| originalValues = nestedSet.toListNoMemoUpdate(); |
| } else { |
| int count = (Integer) arguments.get(argi++); |
| originalValues = arguments.subList(argi, argi + count); |
| argi += count; |
| } |
| List<Object> expandedValues = |
| maybeExpandDirectories(artifactExpander, originalValues, pathMapper); |
| List<String> stringValues; |
| if (mapEach != null) { |
| stringValues = new ArrayList<>(expandedValues.size()); |
| applyMapEach( |
| mapEach, |
| expandedValues, |
| stringValues::add, |
| location, |
| artifactExpander, |
| starlarkSemantics); |
| } else { |
| int count = expandedValues.size(); |
| stringValues = new ArrayList<>(expandedValues.size()); |
| for (int i = 0; i < count; ++i) { |
| stringValues.add(expandToCommandLine(expandedValues.get(i), pathMapper, mainRepoMapping)); |
| } |
| } |
| // It's safe to uniquify at this stage, any transformations after this |
| // will ensure continued uniqueness of the values |
| if ((features & UNIQUIFY) != 0) { |
| int count = stringValues.size(); |
| HashSet<String> seen = Sets.newHashSetWithExpectedSize(count); |
| int addIndex = 0; |
| for (int i = 0; i < count; ++i) { |
| String val = stringValues.get(i); |
| if (seen.add(val)) { |
| stringValues.set(addIndex++, val); |
| } |
| } |
| stringValues = stringValues.subList(0, addIndex); |
| } |
| boolean omitIfEmpty = (features & OMIT_IF_EMPTY) != 0; |
| boolean isEmptyAndShouldOmit = omitIfEmpty && stringValues.isEmpty(); |
| if ((features & HAS_ARG_NAME) != 0) { |
| String argName = (String) arguments.get(argi++); |
| if (!isEmptyAndShouldOmit) { |
| builder.addString(argName); |
| } |
| } |
| |
| String formatEach = null; |
| String beforeEach = null; |
| String joinWith = null; |
| String formatJoined = null; |
| if ((features & HAS_FORMAT_EACH) != 0) { |
| formatEach = (String) arguments.get(argi++); |
| } |
| if ((features & HAS_BEFORE_EACH) != 0) { |
| beforeEach = (String) arguments.get(argi++); |
| } else if ((features & HAS_JOIN_WITH) != 0) { |
| joinWith = (String) arguments.get(argi++); |
| if ((features & HAS_FORMAT_JOINED) != 0) { |
| formatJoined = (String) arguments.get(argi++); |
| } |
| } |
| |
| // If !omitIfEmpty, joining yields a single argument even if stringValues is empty. Note that |
| // the argument may still be non-empty if format_joined is used. |
| if (!stringValues.isEmpty() || (!omitIfEmpty && joinWith != null)) { |
| PreprocessedArg arg = |
| joinWith != null |
| ? new JoinedPreprocessedVectorArg(stringValues, formatEach, joinWith, formatJoined) |
| : new UnjoinedPreprocessedVectorArg(stringValues, formatEach, beforeEach); |
| builder.addPreprocessedArg(arg); |
| } |
| |
| if ((features & HAS_TERMINATE_WITH) != 0) { |
| String terminateWith = (String) arguments.get(argi++); |
| if (!isEmptyAndShouldOmit) { |
| builder.addString(terminateWith); |
| } |
| } |
| return argi; |
| } |
| |
| /** |
| * Expands the directories if {@code expand_directories} feature is enabled and a |
| * ArtifactExpander is available. |
| * |
| * <p>Technically, we should always expand the directories if the feature is requested, however |
| * we cannot do that in the absence of the {@link ArtifactExpander}. |
| */ |
| private List<Object> maybeExpandDirectories( |
| @Nullable ArtifactExpander artifactExpander, |
| List<Object> originalValues, |
| PathMapper pathMapper) |
| throws CommandLineExpansionException { |
| if ((features & EXPAND_DIRECTORIES) == 0 |
| || artifactExpander == null |
| || !hasDirectory(originalValues)) { |
| return originalValues; |
| } |
| |
| return expandDirectories(artifactExpander, originalValues, pathMapper); |
| } |
| |
| private static boolean hasDirectory(List<Object> originalValues) { |
| int n = originalValues.size(); |
| for (int i = 0; i < n; ++i) { |
| Object object = originalValues.get(i); |
| if (isDirectory(object)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isDirectory(Object object) { |
| return object instanceof Artifact && ((Artifact) object).isDirectory(); |
| } |
| |
| private static List<Object> expandDirectories( |
| Artifact.ArtifactExpander artifactExpander, |
| List<Object> originalValues, |
| PathMapper pathMapper) |
| throws CommandLineExpansionException { |
| List<Object> expandedValues = new ArrayList<>(originalValues.size()); |
| for (Object object : originalValues) { |
| if (isDirectory(object)) { |
| Artifact artifact = (Artifact) object; |
| if (artifact.isTreeArtifact()) { |
| expandedValues.addAll(artifactExpander.expandTreeArtifact(artifact)); |
| } else if (artifact.isFileset()) { |
| expandFileset(artifactExpander, artifact, expandedValues, pathMapper); |
| } else { |
| throw new AssertionError("Unknown artifact type."); |
| } |
| } else { |
| expandedValues.add(object); |
| } |
| } |
| return expandedValues; |
| } |
| |
| private static void expandFileset( |
| Artifact.ArtifactExpander artifactExpander, |
| Artifact fileset, |
| List<Object> expandedValues, |
| PathMapper pathMapper) |
| throws CommandLineExpansionException { |
| ImmutableList<FilesetOutputSymlink> expandedFileSet; |
| try { |
| expandedFileSet = artifactExpander.expandFileset(fileset); |
| } catch (MissingExpansionException e) { |
| throw new CommandLineExpansionException( |
| String.format( |
| "Could not expand fileset: %s. Did you forget to add it as an input of the" |
| + " action?", |
| fileset), |
| e); |
| } |
| FilesetManifest filesetManifest = |
| FilesetManifest.constructFilesetManifestWithoutError( |
| expandedFileSet, fileset.getExecPath(), RelativeSymlinkBehaviorWithoutError.IGNORE); |
| for (PathFragment relativePath : filesetManifest.getEntries().keySet()) { |
| PathFragment mappedRelativePath = pathMapper.map(relativePath); |
| expandedValues.add(new FilesetSymlinkFile(fileset, mappedRelativePath)); |
| } |
| } |
| |
| private int addToFingerprint( |
| List<Object> arguments, |
| int argi, |
| ActionKeyContext actionKeyContext, |
| Fingerprint fingerprint, |
| @Nullable ArtifactExpander artifactExpander) |
| throws CommandLineExpansionException, InterruptedException { |
| StarlarkCallable mapEach = null; |
| Location location = null; |
| StarlarkSemantics starlarkSemantics = null; |
| if ((features & HAS_MAP_EACH) != 0) { |
| mapEach = (StarlarkCallable) arguments.get(argi++); |
| location = (Location) arguments.get(argi++); |
| starlarkSemantics = (StarlarkSemantics) arguments.get(argi++); |
| } |
| |
| // NestedSets and lists never result in the same fingerprint as the |
| // ActionKeyContext#addNestedSetToFingerprint call below always adds the order of the |
| // NestedSet to the fingerprint. |
| // |
| // Path mapping may affect the default stringification of Artifact instances at execution |
| // time, but the effect of path mapping on an individual command line element is a pure |
| // function of: |
| // * whether the element is of type Artifact (FileApi in Starlark), which is fingerprinted |
| // via the elementType UUID below; |
| // * the path of the artifact, which is fingerprinted via its default string representation |
| // below; |
| // * the paths and possibly the digests of all input artifacts as well as the path mapping |
| // mode, which are fingerprinted by SpawnAction. |
| // It is thus safe to use PathMapper.NOOP below for anything that relies on the default |
| // stringification behavior (which excludes custom mapEach functions, but those do not |
| // support path mapping yet). |
| if ((features & IS_NESTED_SET) != 0) { |
| NestedSet<?> values = (NestedSet<?>) arguments.get(argi++); |
| if (mapEach != null) { |
| // mapEach functions do not rely on default stringification behavior, so we can omit |
| // fingerprinting stringificationType here. |
| CommandLineItemMapEachAdaptor commandLineItemMapFn = |
| new CommandLineItemMapEachAdaptor( |
| mapEach, |
| location, |
| starlarkSemantics, |
| (features & EXPAND_DIRECTORIES) != 0 ? artifactExpander : null); |
| try { |
| actionKeyContext.addNestedSetToFingerprint(commandLineItemMapFn, fingerprint, values); |
| } finally { |
| // The cache holds an entry for a NestedSet for every (map_fn, hasArtifactExpanderBit). |
| // Clearing the artifactExpander itself saves us from storing the contents of it in the |
| // cache keys (it is no longer needed after we evaluate the value). |
| // NestedSet cache is cleared after every build, which means that the artifactExpander |
| // for a given action, if present, cannot change within the lifetime of the fingerprint |
| // cache (we call getKey with artifactExpander to check action key, when we are ready to |
| // execute the action in case of a cache miss). |
| commandLineItemMapFn.clearArtifactExpander(); |
| } |
| } else { |
| fingerprint.addInt(stringificationType.ordinal()); |
| actionKeyContext.addNestedSetToFingerprint(fingerprint, values); |
| } |
| } else { |
| int count = (Integer) arguments.get(argi++); |
| List<Object> maybeExpandedValues = |
| maybeExpandDirectories( |
| artifactExpander, arguments.subList(argi, argi + count), PathMapper.NOOP); |
| argi += count; |
| if (mapEach != null) { |
| // TODO(b/160181927): If artifactExpander == null (which happens in the analysis phase) |
| // but expandDirectories is true, we run the map_each function on directory values without |
| // actually expanding them. This differs from the real evaluation behavior. This means |
| // that we can erroneously produce the same digest for two command lines that differ only |
| // in their directory expansion. Fortunately, this is only a problem for shared action |
| // conflict checking/aquery result, since at execution time we have an artifactExpander. |
| applyMapEach( |
| mapEach, |
| maybeExpandedValues, |
| fingerprint::addString, |
| location, |
| artifactExpander, |
| starlarkSemantics); |
| } else { |
| for (Object value : maybeExpandedValues) { |
| addSingleObjectToFingerprint(fingerprint, value); |
| } |
| } |
| } |
| if ((features & EXPAND_DIRECTORIES) != 0) { |
| fingerprint.addUUID(EXPAND_DIRECTORIES_UUID); |
| } |
| if ((features & UNIQUIFY) != 0) { |
| fingerprint.addUUID(UNIQUIFY_UUID); |
| } |
| if ((features & OMIT_IF_EMPTY) != 0) { |
| fingerprint.addUUID(OMIT_IF_EMPTY_UUID); |
| } |
| if ((features & HAS_ARG_NAME) != 0) { |
| String argName = (String) arguments.get(argi++); |
| fingerprint.addUUID(ARG_NAME_UUID); |
| fingerprint.addString(argName); |
| } |
| if ((features & HAS_FORMAT_EACH) != 0) { |
| String formatStr = (String) arguments.get(argi++); |
| fingerprint.addUUID(FORMAT_EACH_UUID); |
| fingerprint.addString(formatStr); |
| } |
| if ((features & HAS_BEFORE_EACH) != 0) { |
| String beforeEach = (String) arguments.get(argi++); |
| fingerprint.addUUID(BEFORE_EACH_UUID); |
| fingerprint.addString(beforeEach); |
| } else if ((features & HAS_JOIN_WITH) != 0) { |
| String joinWith = (String) arguments.get(argi++); |
| fingerprint.addUUID(JOIN_WITH_UUID); |
| fingerprint.addString(joinWith); |
| if ((features & HAS_FORMAT_JOINED) != 0) { |
| String formatJoined = (String) arguments.get(argi++); |
| fingerprint.addUUID(FORMAT_JOINED_UUID); |
| fingerprint.addString(formatJoined); |
| } |
| } |
| if ((features & HAS_TERMINATE_WITH) != 0) { |
| String terminateWith = (String) arguments.get(argi++); |
| fingerprint.addUUID(TERMINATE_WITH_UUID); |
| fingerprint.addString(terminateWith); |
| } |
| return argi; |
| } |
| |
| static final class Builder { |
| @Nullable private final Sequence<?> list; |
| @Nullable private final NestedSet<?> nestedSet; |
| private final StringificationType nestedSetStringificationType; |
| private Location location; |
| private String argName; |
| private boolean expandDirectories; |
| private StarlarkCallable mapEach; |
| private String formatEach; |
| private String beforeEach; |
| private String joinWith; |
| private String formatJoined; |
| private boolean omitIfEmpty; |
| private boolean uniquify; |
| private String terminateWith; |
| |
| Builder(Sequence<?> list) { |
| this.list = list; |
| this.nestedSet = null; |
| this.nestedSetStringificationType = StringificationType.DEFAULT; |
| } |
| |
| Builder(NestedSet<?> nestedSet, Class<?> nestedSetElementType) { |
| this.list = null; |
| this.nestedSet = nestedSet; |
| if (nestedSetElementType == FileApi.class) { |
| this.nestedSetStringificationType = StringificationType.FILE; |
| } else if (nestedSetElementType == Label.class) { |
| this.nestedSetStringificationType = StringificationType.LABEL; |
| } else { |
| this.nestedSetStringificationType = StringificationType.DEFAULT; |
| } |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setLocation(Location location) { |
| this.location = location; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setArgName(String argName) { |
| this.argName = argName; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setExpandDirectories(boolean expandDirectories) { |
| this.expandDirectories = expandDirectories; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setMapEach(StarlarkCallable mapEach) { |
| this.mapEach = mapEach; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setFormatEach(String format) { |
| this.formatEach = format; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setBeforeEach(String beforeEach) { |
| this.beforeEach = beforeEach; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setJoinWith(String joinWith) { |
| this.joinWith = joinWith; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setFormatJoined(String formatJoined) { |
| this.formatJoined = formatJoined; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder omitIfEmpty(boolean omitIfEmpty) { |
| this.omitIfEmpty = omitIfEmpty; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder uniquify(boolean uniquify) { |
| this.uniquify = uniquify; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder setTerminateWith(String terminateWith) { |
| this.terminateWith = terminateWith; |
| return this; |
| } |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| VectorArg vectorArg = (VectorArg) o; |
| return features == vectorArg.features |
| && stringificationType.equals(vectorArg.stringificationType); |
| } |
| |
| @Override |
| public int hashCode() { |
| return 31 * Integer.hashCode(features) + stringificationType.hashCode(); |
| } |
| } |
| |
| /** Representation of a single formatted argument originating from {@code Args.add} */ |
| private static final class SingleFormattedArg { |
| |
| /** Denotes that the following two elements are an object and format string. */ |
| private static final Object MARKER = |
| new Object() { |
| @Override |
| public String toString() { |
| return "SINGLE_FORMATTED_ARG_MARKER"; |
| } |
| }; |
| |
| private static final UUID SINGLE_FORMATTED_ARG_UUID = |
| UUID.fromString("8cb96642-a235-4fe0-b3ed-ebfdae8a0bd9"); |
| |
| static void push(List<Object> arguments, Object object, String format) { |
| arguments.add(MARKER); |
| arguments.add(object); |
| arguments.add(format); |
| } |
| |
| /** |
| * Adds a {@link SingleFormattedArg} to the given {@link PreprocessedCommandLine.Builder}. |
| * |
| * @param arguments result of {@link #rawArgsAsList} |
| * @param argi index in {@code arguments} at which the {@link SingleFormattedArg} begins; should |
| * be directly preceded by {@link #MARKER} |
| * @param builder the {@link PreprocessedCommandLine.Builder} in which to add a preprocessed |
| * representation of this arg |
| * @param pathMapper mapper for exec paths |
| * @param mainRepoMapping the repository mapping to use for formatting labels if needed |
| * @return index in {@code arguments} where the next arg begins, or {@code arguments.size()} if |
| * there are no more arguments |
| */ |
| static int preprocess( |
| List<Object> arguments, |
| int argi, |
| PreprocessedCommandLine.Builder builder, |
| PathMapper pathMapper, |
| @Nullable RepositoryMapping mainRepoMapping) { |
| Object object = arguments.get(argi++); |
| String formatStr = (String) arguments.get(argi++); |
| String stringValue = |
| StarlarkCustomCommandLine.expandToCommandLine(object, pathMapper, mainRepoMapping); |
| builder.addPreprocessedArg(new PreprocessedSingleFormattedArg(formatStr, stringValue)); |
| return argi; |
| } |
| |
| static int addToFingerprint(List<Object> arguments, int argi, Fingerprint fingerprint) { |
| Object object = arguments.get(argi++); |
| addSingleObjectToFingerprint(fingerprint, object); |
| String formatStr = (String) arguments.get(argi++); |
| fingerprint.addString(formatStr); |
| fingerprint.addUUID(SINGLE_FORMATTED_ARG_UUID); |
| return argi; |
| } |
| } |
| |
| static final class Builder { |
| private final StarlarkSemantics starlarkSemantics; |
| private final List<Object> arguments = new ArrayList<>(); |
| // Indexes in arguments list where individual args begin |
| private final ImmutableList.Builder<Integer> argStartIndexes = ImmutableList.builder(); |
| |
| public Builder(StarlarkSemantics starlarkSemantics) { |
| this.starlarkSemantics = checkNotNull(starlarkSemantics); |
| } |
| |
| @CanIgnoreReturnValue |
| Builder recordArgStart() { |
| if (!arguments.isEmpty()) { |
| argStartIndexes.add(arguments.size()); |
| } |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder add(Object object) { |
| arguments.add(object); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder add(VectorArg.Builder vectorArg) { |
| VectorArg.push(arguments, vectorArg, starlarkSemantics); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| Builder addFormatted(Object object, String format) { |
| checkNotNull(object); |
| checkNotNull(format); |
| SingleFormattedArg.push(arguments, object, format); |
| return this; |
| } |
| |
| CommandLine build(boolean flagPerLine, @Nullable RepositoryMapping mainRepoMapping) { |
| if (arguments.isEmpty()) { |
| return CommandLine.empty(); |
| } |
| Object[] args; |
| if (mainRepoMapping != null) { |
| args = arguments.toArray(new Object[arguments.size() + 1]); |
| args[arguments.size()] = mainRepoMapping; |
| } else { |
| args = arguments.toArray(); |
| } |
| return flagPerLine |
| ? new StarlarkCustomCommandLineWithIndexes(args, argStartIndexes.build()) |
| : new StarlarkCustomCommandLine(args); |
| } |
| } |
| |
| /** |
| * Stored as an {@code Object[]} instead of an {@link ImmutableList} to save memory, but is never |
| * modified. Access via {@link #rawArgsAsList} for an unmodifiable {@link List} view. |
| */ |
| private final Object[] arguments; |
| |
| private StarlarkCustomCommandLine(Object[] arguments) { |
| this.arguments = arguments; |
| } |
| |
| /** Wraps {@link #arguments} in an unmodifiable {@link List} view. */ |
| private List<Object> rawArgsAsList() { |
| return Collections.unmodifiableList(Arrays.asList(arguments)); |
| } |
| |
| @Override |
| public final ArgChunk expand() throws CommandLineExpansionException, InterruptedException { |
| return expand(null, PathMapper.NOOP); |
| } |
| |
| @Override |
| public ArgChunk expand(@Nullable ArtifactExpander artifactExpander, PathMapper pathMapper) |
| throws CommandLineExpansionException, InterruptedException { |
| PreprocessedCommandLine.Builder builder = new PreprocessedCommandLine.Builder(); |
| List<Object> arguments = rawArgsAsList(); |
| |
| RepositoryMapping mainRepoMapping; |
| int size; |
| // Added in #build() if any labels in the command line require this to be formatted with an |
| // apparent repository name. |
| if (arguments.getLast() instanceof RepositoryMapping) { |
| mainRepoMapping = (RepositoryMapping) arguments.getLast(); |
| size = arguments.size() - 1; |
| } else { |
| mainRepoMapping = null; |
| size = arguments.size(); |
| } |
| |
| for (int argi = 0; argi < size; ) { |
| Object arg = arguments.get(argi++); |
| if (arg instanceof VectorArg) { |
| argi = |
| ((VectorArg) arg) |
| .preprocess( |
| arguments, argi, builder, artifactExpander, pathMapper, mainRepoMapping); |
| } else if (arg == SingleFormattedArg.MARKER) { |
| argi = SingleFormattedArg.preprocess(arguments, argi, builder, pathMapper, mainRepoMapping); |
| } else { |
| builder.addString(expandToCommandLine(arg, pathMapper, mainRepoMapping)); |
| } |
| } |
| return pathMapper.mapCustomStarlarkArgs(builder.build()); |
| } |
| |
| @Override |
| public final Iterable<String> arguments() |
| throws CommandLineExpansionException, InterruptedException { |
| return expand().arguments(); |
| } |
| |
| @Override |
| public final Iterable<String> arguments(ArtifactExpander artifactExpander, PathMapper pathMapper) |
| throws CommandLineExpansionException, InterruptedException { |
| return expand(artifactExpander, pathMapper).arguments(); |
| } |
| |
| private static String expandToCommandLine( |
| Object object, PathMapper pathMapper, @Nullable RepositoryMapping mainRepoMapping) { |
| if (mainRepoMapping != null && object instanceof Label label) { |
| return label.getDisplayForm(mainRepoMapping); |
| } |
| |
| // It'd be nice to build this into DerivedArtifact's CommandLine interface so we don't have |
| // to explicitly check if an object is a DerivedArtifact. Unfortunately that would require |
| // a lot more dependencies on the Java library DerivedArtifact is built into. |
| return object instanceof DerivedArtifact |
| ? pathMapper.map(((DerivedArtifact) object).getExecPath()).getPathString() |
| : CommandLineItem.expandToCommandLine(object); |
| } |
| |
| private static void addSingleObjectToFingerprint(Fingerprint fingerprint, Object object) { |
| StringificationType stringificationType = |
| switch (object) { |
| case FileApi ignored -> StringificationType.FILE; |
| case Label ignored -> StringificationType.LABEL; |
| default -> StringificationType.DEFAULT; |
| }; |
| fingerprint.addInt(stringificationType.ordinal()); |
| fingerprint.addString(CommandLineItem.expandToCommandLine(object)); |
| } |
| |
| private static class StarlarkCustomCommandLineWithIndexes extends StarlarkCustomCommandLine { |
| /** |
| * An extra level of grouping on top of the 'arguments' list. Each element is the start of a |
| * group of args, with index 0 omitted. For example, if this contains 3, then arguments 0, 1 and |
| * 2 constitute the first group, and arguments 3 to the end constitute the next. The expanded |
| * version of these arguments will be concatenated together to support {@code flag_per_line} |
| * format. |
| */ |
| private final ImmutableList<Integer> argStartIndexes; |
| |
| StarlarkCustomCommandLineWithIndexes( |
| Object[] arguments, ImmutableList<Integer> argStartIndexes) { |
| super(arguments); |
| this.argStartIndexes = argStartIndexes; |
| } |
| |
| @Override |
| public ArgChunk expand(@Nullable ArtifactExpander artifactExpander, PathMapper pathMapper) |
| throws CommandLineExpansionException, InterruptedException { |
| PreprocessedCommandLine.Builder builder = new PreprocessedCommandLine.Builder(); |
| List<Object> arguments = ((StarlarkCustomCommandLine) this).rawArgsAsList(); |
| Iterator<Integer> startIndexIterator = argStartIndexes.iterator(); |
| |
| RepositoryMapping mainRepoMapping; |
| int size; |
| if (arguments.getLast() instanceof RepositoryMapping) { |
| mainRepoMapping = (RepositoryMapping) arguments.getLast(); |
| size = arguments.size() - 1; |
| } else { |
| mainRepoMapping = null; |
| size = arguments.size(); |
| } |
| |
| for (int argi = 0; argi < size; ) { |
| int nextStartIndex = startIndexIterator.hasNext() ? startIndexIterator.next() : size; |
| PreprocessedCommandLine.Builder line = new PreprocessedCommandLine.Builder(); |
| |
| while (argi < nextStartIndex) { |
| Object arg = arguments.get(argi++); |
| if (arg instanceof VectorArg) { |
| argi = |
| ((VectorArg) arg) |
| .preprocess( |
| arguments, argi, line, artifactExpander, pathMapper, mainRepoMapping); |
| } else if (arg == SingleFormattedArg.MARKER) { |
| argi = |
| SingleFormattedArg.preprocess(arguments, argi, line, pathMapper, mainRepoMapping); |
| } else { |
| line.addString(expandToCommandLine(arg, pathMapper, mainRepoMapping)); |
| } |
| } |
| |
| builder.addLineForFlagPerLine(line); |
| } |
| |
| return pathMapper.mapCustomStarlarkArgs(builder.build()); |
| } |
| } |
| |
| @Override |
| public void addToFingerprint( |
| ActionKeyContext actionKeyContext, |
| @Nullable ArtifactExpander artifactExpander, |
| Fingerprint fingerprint) |
| throws CommandLineExpansionException, InterruptedException { |
| List<Object> arguments = rawArgsAsList(); |
| int size; |
| if (arguments.getLast() instanceof RepositoryMapping mainRepoMapping) { |
| fingerprint.addStringMap( |
| Maps.transformValues(mainRepoMapping.entries(), RepositoryName::getName)); |
| size = arguments.size() - 1; |
| } else { |
| size = arguments.size(); |
| } |
| for (int argi = 0; argi < size; ) { |
| Object arg = arguments.get(argi++); |
| if (arg instanceof VectorArg) { |
| argi = |
| ((VectorArg) arg) |
| .addToFingerprint(arguments, argi, actionKeyContext, fingerprint, artifactExpander); |
| } else if (arg == SingleFormattedArg.MARKER) { |
| argi = SingleFormattedArg.addToFingerprint(arguments, argi, fingerprint); |
| } else { |
| addSingleObjectToFingerprint(fingerprint, arg); |
| } |
| } |
| } |
| |
| /** Used during action key evaluation when we don't have an artifact expander. */ |
| private static class NoopExpander implements DirectoryExpander { |
| @Override |
| public ImmutableList<FileApi> list(FileApi file) { |
| return ImmutableList.of(file); |
| } |
| |
| static final DirectoryExpander INSTANCE = new NoopExpander(); |
| } |
| |
| private static final class FullExpander implements DirectoryExpander { |
| private final ArtifactExpander expander; |
| |
| FullExpander(ArtifactExpander expander) { |
| this.expander = expander; |
| } |
| |
| @Override |
| public ImmutableList<FileApi> list(FileApi file) { |
| Artifact artifact = (Artifact) file; |
| if (artifact.isTreeArtifact()) { |
| return ImmutableList.copyOf(expander.expandTreeArtifact(artifact)); |
| } else { |
| return ImmutableList.of(file); |
| } |
| } |
| } |
| |
| private static void applyMapEach( |
| StarlarkCallable mapFn, |
| List<Object> originalValues, |
| Consumer<String> consumer, |
| Location loc, |
| @Nullable ArtifactExpander artifactExpander, |
| StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException, InterruptedException { |
| try (Mutability mu = Mutability.create("map_each")) { |
| // This computation produces only a String list, which doesn't require reference semantics, |
| // so createTransient() is safe. |
| StarlarkThread thread = StarlarkThread.createTransient(mu, starlarkSemantics); |
| // TODO(b/77140311): Error if we issue print statements. |
| thread.setPrintHandler((th, msg) -> {}); |
| int count = originalValues.size(); |
| // map_each can accept either each object, or each object + a directory expander. |
| boolean wantsDirectoryExpander = |
| (mapFn instanceof StarlarkFunction) |
| && ((StarlarkFunction) mapFn).getParameterNames().size() >= 2; |
| // We create a list that we reuse for the args to map_each |
| List<Object> args = new ArrayList<>(2); |
| args.add(null); // This will be overwritten each iteration. |
| if (wantsDirectoryExpander) { |
| final DirectoryExpander expander; |
| if (artifactExpander != null) { |
| expander = new FullExpander(artifactExpander); |
| } else { |
| expander = NoopExpander.INSTANCE; |
| } |
| args.add(expander); // This will remain constant each iteration |
| } |
| for (int i = 0; i < count; ++i) { |
| args.set(0, originalValues.get(i)); |
| Object ret = Starlark.call(thread, mapFn, args, /*kwargs=*/ ImmutableMap.of()); |
| if (ret instanceof String) { |
| consumer.accept((String) ret); |
| } else if (ret instanceof Sequence) { |
| for (Object val : ((Sequence<?>) ret)) { |
| if (!(val instanceof String)) { |
| throw new CommandLineExpansionException( |
| "Expected map_each to return string, None, or list of strings, " |
| + "found list containing " |
| + Starlark.type(val)); |
| } |
| consumer.accept((String) val); |
| } |
| } else if (ret != Starlark.NONE) { |
| throw new CommandLineExpansionException( |
| "Expected map_each to return string, None, or list of strings, found " |
| + Starlark.type(ret)); |
| } |
| } |
| } catch (EvalException e) { |
| // TODO(adonovan): consider calling a wrapper function to interpose a fake stack |
| // frame that establishes the args.add_all call at loc. Or manipulating the stack |
| // before printing it. |
| throw new CommandLineExpansionException( |
| errorMessage(e.getMessageWithStack(), loc, e.getCause())); |
| } |
| } |
| |
| private static class CommandLineItemMapEachAdaptor |
| extends CommandLineItem.ParametrizedMapFn<Object> { |
| private final StarlarkCallable mapFn; |
| private final Location location; |
| private final StarlarkSemantics starlarkSemantics; |
| /** |
| * Indicates whether artifactExpander was provided on construction. This is used to distinguish |
| * the case where it's not provided from the case where it was provided but subsequently |
| * cleared. |
| */ |
| private final boolean hasArtifactExpander; |
| |
| @Nullable private ArtifactExpander artifactExpander; |
| |
| CommandLineItemMapEachAdaptor( |
| StarlarkCallable mapFn, |
| Location location, |
| StarlarkSemantics starlarkSemantics, |
| @Nullable ArtifactExpander artifactExpander) { |
| this.mapFn = mapFn; |
| this.location = location; |
| this.starlarkSemantics = starlarkSemantics; |
| this.hasArtifactExpander = artifactExpander != null; |
| this.artifactExpander = artifactExpander; |
| } |
| |
| @Override |
| public void expandToCommandLine(Object object, Consumer<String> args) |
| throws CommandLineExpansionException, InterruptedException { |
| checkState(artifactExpander != null || !hasArtifactExpander); |
| applyMapEach( |
| mapFn, maybeExpandDirectory(object), args, location, artifactExpander, starlarkSemantics); |
| } |
| |
| private List<Object> maybeExpandDirectory(Object object) throws CommandLineExpansionException { |
| if (artifactExpander == null || !VectorArg.isDirectory(object)) { |
| return ImmutableList.of(object); |
| } |
| |
| return VectorArg.expandDirectories( |
| artifactExpander, ImmutableList.of(object), PathMapper.NOOP); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof CommandLineItemMapEachAdaptor other)) { |
| return false; |
| } |
| // Instance compare intentional |
| // The normal implementation uses location + name of function, |
| // which can conceivably conflict in tests |
| // We only compare presence of artifactExpander vs absence of it since the nestedset |
| // fingerprint cache is emptied after every build, therefore if the artifact expander is |
| // provided, it will be the same. |
| return mapFn == other.mapFn && hasArtifactExpander == other.hasArtifactExpander; |
| } |
| |
| @Override |
| public int hashCode() { |
| // Force use of identityHashCode, in case the callable uses a custom hash function. (As of |
| // this writing, only providers seem to have a custom hashCode, and those shouldn't be used |
| // as map_each functions, but doesn't hurt to be safe...). |
| return 31 * System.identityHashCode(mapFn) + Boolean.hashCode(hasArtifactExpander); |
| } |
| |
| @Override |
| public int maxInstancesAllowed() { |
| // No limit to these, as this is just a wrapper for Starlark functions, which are |
| // always static |
| return Integer.MAX_VALUE; |
| } |
| |
| /** |
| * Clears the artifact expander in order not to prolong the lifetime of it unnecessarily. |
| * |
| * <p>Although this operation technically changes this object, it can be called after we add the |
| * object to a {@link HashSet}. Clearing the artifactExpander does not affect the result of |
| * {@link #equals} or {@link #hashCode}. Please note that once we call this function, we can no |
| * longer call {@link #expandToCommandLine}. |
| */ |
| void clearArtifactExpander() { |
| artifactExpander = null; |
| } |
| } |
| |
| private static String errorMessage( |
| String message, @Nullable Location location, @Nullable Throwable cause) { |
| return LINE_JOINER.join( |
| "\n", FIELD_JOINER.join(location, message), getCauseMessage(cause, message)); |
| } |
| |
| @Nullable |
| private static String getCauseMessage(@Nullable Throwable cause, String message) { |
| if (cause == null) { |
| return null; |
| } |
| String causeMessage = cause.getMessage(); |
| if (causeMessage == null) { |
| return null; |
| } |
| if (message == null) { |
| return causeMessage; |
| } |
| // Skip the cause if it is redundant with the message so far. |
| if (message.contains(causeMessage)) { |
| return null; |
| } |
| return causeMessage; |
| } |
| |
| /** |
| * When we expand filesets the user might still expect a File object (since the results may be fed |
| * into map_each. Therefore we synthesize a File object from the fileset symlink. |
| */ |
| static class FilesetSymlinkFile implements FileApi, CommandLineItem { |
| private final Artifact fileset; |
| private final PathFragment execPath; |
| |
| FilesetSymlinkFile(Artifact fileset, PathFragment execPath) { |
| this.fileset = fileset; |
| this.execPath = execPath; |
| } |
| |
| @Override |
| public String getDirname() { |
| PathFragment parent = execPath.getParentDirectory(); |
| return (parent == null) ? "/" : parent.getSafePathString(); |
| } |
| |
| @Override |
| public String getFilename() { |
| return execPath.getBaseName(); |
| } |
| |
| @Override |
| public String getExtension() { |
| return execPath.getFileExtension(); |
| } |
| |
| @Override |
| public Label getOwnerLabel() { |
| return fileset.getOwnerLabel(); |
| } |
| |
| @Override |
| public FileRootApi getRoot() { |
| return fileset.getRoot(); |
| } |
| |
| @Override |
| public boolean isSourceArtifact() { |
| // This information is lost to us. |
| // Since the symlinks are always in the output tree, settle for saying "no" |
| return false; |
| } |
| |
| @Override |
| public boolean isDirectory() { |
| return false; |
| } |
| |
| @Override |
| public String getRunfilesPathString() { |
| PathFragment relativePath = execPath.relativeTo(fileset.getExecPath()); |
| return fileset.getRunfilesPath().getRelative(relativePath).getPathString(); |
| } |
| |
| @Override |
| public String getExecPathString() { |
| return execPath.getPathString(); |
| } |
| |
| @Override |
| public String getTreeRelativePathString() throws EvalException { |
| throw Starlark.errorf( |
| "tree_relative_path not allowed for files that are not tree artifact files."); |
| } |
| |
| @Override |
| public String expandToCommandLine() { |
| return getExecPathString(); |
| } |
| |
| @Override |
| public void repr(Printer printer) { |
| if (isSourceArtifact()) { |
| printer.append("<source file " + getRunfilesPathString() + ">"); |
| } else { |
| printer.append("<generated file " + getRunfilesPathString() + ">"); |
| } |
| } |
| } |
| |
| /** An element in a {@link PreprocessedCommandLine}. */ |
| private interface PreprocessedArg extends Iterable<String> { |
| int numArgs(); |
| |
| int totalArgLength(); |
| } |
| |
| /** |
| * Intermediate command line representation with directory expansion and {@code map_each} already |
| * applied, but with string formatting not yet applied. See {@link StarlarkCustomCommandLine} |
| * class-level documentation for details. |
| * |
| * <p>Implements {@link #totalArgLength} without applying string formatting so that the total |
| * command line length can be efficiently tested against {@link CommandLineLimits} and param file |
| * thresholds. |
| */ |
| private static final class PreprocessedCommandLine implements ArgChunk { |
| private final ImmutableList<PreprocessedArg> preprocessedArgs; |
| |
| PreprocessedCommandLine(ImmutableList<PreprocessedArg> preprocessedArgs) { |
| this.preprocessedArgs = preprocessedArgs; |
| } |
| |
| @Override |
| public Iterable<String> arguments() { |
| return Iterables.concat(preprocessedArgs); |
| } |
| |
| @Override |
| public int totalArgLength() { |
| int total = 0; |
| for (PreprocessedArg arg : preprocessedArgs) { |
| total += arg.totalArgLength(); |
| } |
| return total; |
| } |
| |
| static final class Builder { |
| private final ImmutableList.Builder<PreprocessedArg> preprocessedArgs = |
| ImmutableList.builder(); |
| private int numArgs = 0; |
| |
| void addPreprocessedArg(PreprocessedArg arg) { |
| preprocessedArgs.add(arg); |
| numArgs += arg.numArgs(); |
| } |
| |
| void addString(String arg) { |
| addPreprocessedArg(new PreprocessedStringArg(arg)); |
| } |
| |
| void addLineForFlagPerLine(PreprocessedCommandLine.Builder line) { |
| ImmutableList<PreprocessedArg> group = line.preprocessedArgs.build(); |
| if (line.numArgs < 2) { |
| for (PreprocessedArg arg : group) { |
| addPreprocessedArg(arg); |
| } |
| } else { |
| addPreprocessedArg(new GroupedPreprocessedArgs(group)); |
| } |
| } |
| |
| PreprocessedCommandLine build() { |
| return new PreprocessedCommandLine(preprocessedArgs.build()); |
| } |
| } |
| } |
| |
| /** Preprocessed version a single string argument. */ |
| private static final class PreprocessedStringArg implements PreprocessedArg { |
| private final String arg; |
| |
| PreprocessedStringArg(String arg) { |
| this.arg = arg; |
| } |
| |
| @Override |
| public Iterator<String> iterator() { |
| return Iterators.singletonIterator(arg); |
| } |
| |
| @Override |
| public int numArgs() { |
| return 1; |
| } |
| |
| @Override |
| public int totalArgLength() { |
| return arg.length() + 1; |
| } |
| } |
| |
| /** Preprocessed version of a {@link SingleFormattedArg}. */ |
| private static final class PreprocessedSingleFormattedArg implements PreprocessedArg { |
| private final String format; |
| private final String stringValue; |
| |
| PreprocessedSingleFormattedArg(String format, String stringValue) { |
| this.format = format; |
| this.stringValue = stringValue; |
| } |
| |
| @Override |
| public Iterator<String> iterator() { |
| return Iterators.singletonIterator(SingleStringArgFormatter.format(format, stringValue)); |
| } |
| |
| @Override |
| public int numArgs() { |
| return 1; |
| } |
| |
| @Override |
| public int totalArgLength() { |
| return SingleStringArgFormatter.formattedLength(format) + stringValue.length() + 1; |
| } |
| } |
| |
| /** Preprocessed version of a {@link VectorArg} originating from {@code Args.add_all}. */ |
| private static final class UnjoinedPreprocessedVectorArg implements PreprocessedArg { |
| private final List<String> stringValues; |
| @Nullable private final String formatEach; |
| @Nullable private final String beforeEach; |
| |
| UnjoinedPreprocessedVectorArg( |
| List<String> stringValues, @Nullable String formatEach, @Nullable String beforeEach) { |
| this.stringValues = stringValues; |
| this.formatEach = formatEach; |
| this.beforeEach = beforeEach; |
| } |
| |
| @Override |
| public Iterator<String> iterator() { |
| Iterator<String> it = stringValues.iterator(); |
| if (formatEach != null) { |
| it = Iterators.transform(it, s -> SingleStringArgFormatter.format(formatEach, s)); |
| } |
| if (beforeEach != null) { |
| it = new BeforeEachIterator(it, beforeEach); |
| } |
| return it; |
| } |
| |
| @Override |
| public int numArgs() { |
| return (beforeEach != null ? 2 : 1) * stringValues.size(); |
| } |
| |
| @Override |
| public int totalArgLength() { |
| int total = 0; |
| for (String arg : stringValues) { |
| total += arg.length(); |
| } |
| if (formatEach != null) { |
| total += SingleStringArgFormatter.formattedLength(formatEach) * stringValues.size(); |
| } |
| if (beforeEach != null) { |
| total += beforeEach.length() * stringValues.size(); |
| } |
| return total + numArgs(); |
| } |
| } |
| |
| /** Preprocessed version of a {@link VectorArg} originating from {@code Args.add_joined}. */ |
| private static final class JoinedPreprocessedVectorArg implements PreprocessedArg { |
| private final List<String> stringValues; |
| @Nullable private final String formatEach; |
| private final String joinWith; |
| @Nullable private final String formatJoined; |
| |
| JoinedPreprocessedVectorArg( |
| List<String> stringValues, |
| @Nullable String formatEach, |
| String joinWith, |
| @Nullable String formatJoined) { |
| this.stringValues = stringValues; |
| this.formatEach = formatEach; |
| this.joinWith = joinWith; |
| this.formatJoined = formatJoined; |
| } |
| |
| @Override |
| public Iterator<String> iterator() { |
| Iterator<String> it = stringValues.iterator(); |
| if (formatEach != null) { |
| it = Iterators.transform(it, s -> SingleStringArgFormatter.format(formatEach, s)); |
| } |
| String result = Joiner.on(joinWith).join(it); |
| if (formatJoined != null) { |
| result = SingleStringArgFormatter.format(formatJoined, result); |
| } |
| return Iterators.singletonIterator(result); |
| } |
| |
| @Override |
| public int numArgs() { |
| return 1; |
| } |
| |
| @Override |
| public int totalArgLength() { |
| int total = 0; |
| for (String arg : stringValues) { |
| total += arg.length(); |
| } |
| if (formatEach != null) { |
| total += SingleStringArgFormatter.formattedLength(formatEach) * stringValues.size(); |
| } |
| if (stringValues.size() > 1) { |
| total += joinWith.length() * (stringValues.size() - 1); |
| } |
| if (formatJoined != null) { |
| total += SingleStringArgFormatter.formattedLength(formatJoined); |
| } |
| return total + 1; |
| } |
| } |
| |
| /** Preprocessed representation of a single line in {@code flag_per_line} format. */ |
| private static final class GroupedPreprocessedArgs implements PreprocessedArg { |
| private static final Joiner SPACE_JOINER = Joiner.on(' '); |
| |
| private final ImmutableList<PreprocessedArg> args; |
| |
| GroupedPreprocessedArgs(ImmutableList<PreprocessedArg> args) { |
| this.args = args; |
| } |
| |
| @Override |
| public Iterator<String> iterator() { |
| Iterator<String> it = Iterables.concat(args).iterator(); |
| String first = it.next(); |
| String rest = SPACE_JOINER.join(it); |
| String line = first.isEmpty() ? rest : first + '=' + rest; |
| return Iterators.singletonIterator(line); |
| } |
| |
| @Override |
| public int numArgs() { |
| return 1; |
| } |
| |
| @Override |
| public int totalArgLength() { |
| int total = 0; |
| for (PreprocessedArg arg : args) { |
| total += arg.totalArgLength(); |
| } |
| String first = Iterables.concat(args).iterator().next(); |
| if (first.isEmpty()) { |
| total--; |
| } |
| return total; |
| } |
| } |
| |
| /** Implements the {@code before_each} behavior of {@code Args.add_all}. */ |
| private static final class BeforeEachIterator extends UnmodifiableIterator<String> { |
| private final Iterator<String> strings; |
| private final String beforeEach; |
| private boolean before = true; |
| |
| BeforeEachIterator(Iterator<String> strings, String beforeEach) { |
| this.strings = strings; |
| this.beforeEach = beforeEach; |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return strings.hasNext(); |
| } |
| |
| @Override |
| public String next() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| String next = before ? beforeEach : strings.next(); |
| before = !before; |
| return next; |
| } |
| } |
| } |