| // 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.skylark; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Objects; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Interner; |
| 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.CommandLine; |
| import com.google.devtools.build.lib.actions.CommandLineExpansionException; |
| import com.google.devtools.build.lib.actions.CommandLineItem; |
| import com.google.devtools.build.lib.actions.SingleStringArgFormatter; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| import com.google.devtools.build.lib.concurrent.BlazeInterners; |
| import com.google.devtools.build.lib.events.Location; |
| import com.google.devtools.build.lib.events.NullEventHandler; |
| import com.google.devtools.build.lib.exec.FilesetManifest; |
| import com.google.devtools.build.lib.exec.FilesetManifest.RelativeSymlinkBehavior; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; |
| import com.google.devtools.build.lib.skylarkbuildapi.FileApi; |
| import com.google.devtools.build.lib.skylarkbuildapi.FileRootApi; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; |
| import com.google.devtools.build.lib.syntax.BaseFunction; |
| import com.google.devtools.build.lib.syntax.Environment; |
| import com.google.devtools.build.lib.syntax.EvalException; |
| import com.google.devtools.build.lib.syntax.Mutability; |
| import com.google.devtools.build.lib.syntax.Printer; |
| import com.google.devtools.build.lib.syntax.Runtime; |
| import com.google.devtools.build.lib.syntax.SkylarkList; |
| import com.google.devtools.build.lib.syntax.StarlarkSemantics; |
| import com.google.devtools.build.lib.util.Fingerprint; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.IllegalFormatException; |
| import java.util.List; |
| import java.util.UUID; |
| import java.util.function.Consumer; |
| import javax.annotation.Nullable; |
| |
| /** Supports ctx.actions.args() from Skylark. */ |
| @AutoCodec |
| public class SkylarkCustomCommandLine extends CommandLine { |
| private final StarlarkSemantics starlarkSemantics; |
| private final ImmutableList<Object> arguments; |
| |
| private static final Joiner LINE_JOINER = Joiner.on("\n").skipNulls(); |
| private static final Joiner FIELD_JOINER = Joiner.on(": ").skipNulls(); |
| |
| @AutoCodec |
| static final class VectorArg { |
| private static final Interner<VectorArg> interner = BlazeInterners.newStrongInterner(); |
| |
| private static final int HAS_LOCATION = 1; |
| private static final int HAS_MAP_ALL = 1 << 1; |
| private static final int HAS_MAP_EACH = 1 << 2; |
| private static final int IS_NESTED_SET = 1 << 3; |
| private static final int EXPAND_DIRECTORIES = 1 << 4; |
| private static final int UNIQUIFY = 1 << 5; |
| private static final int OMIT_IF_EMPTY = 1 << 6; |
| private static final int HAS_ARG_NAME = 1 << 7; |
| private static final int HAS_FORMAT_EACH = 1 << 8; |
| private static final int HAS_BEFORE_EACH = 1 << 9; |
| private static final int HAS_JOIN_WITH = 1 << 10; |
| private static final int HAS_FORMAT_JOINED = 1 << 11; |
| private static final int HAS_TERMINATE_WITH = 1 << 12; |
| |
| 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 VectorArg(int features) { |
| this.features = features; |
| } |
| |
| @AutoCodec.VisibleForSerialization |
| @AutoCodec.Instantiator |
| static VectorArg create(int features) { |
| return interner.intern(new VectorArg(features)); |
| } |
| |
| private static void push(ImmutableList.Builder<Object> arguments, Builder arg) { |
| int features = 0; |
| features |= arg.mapAll != null ? HAS_MAP_ALL : 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; |
| boolean hasLocation = |
| arg.location != null |
| && (features & (HAS_FORMAT_EACH | HAS_FORMAT_JOINED | HAS_MAP_ALL | HAS_MAP_EACH)) |
| != 0; |
| features |= hasLocation ? HAS_LOCATION : 0; |
| Preconditions.checkState( |
| (features & (HAS_MAP_ALL | HAS_MAP_EACH)) != (HAS_MAP_ALL | HAS_MAP_EACH), |
| "Cannot use both map_all and map_each"); |
| VectorArg vectorArg = VectorArg.create(features); |
| arguments.add(vectorArg); |
| if (hasLocation) { |
| arguments.add(arg.location); |
| } |
| if (arg.mapAll != null) { |
| arguments.add(arg.mapAll); |
| } |
| if (arg.mapEach != null) { |
| arguments.add(arg.mapEach); |
| } |
| 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) { |
| arguments.add(arg.beforeEach); |
| } |
| if (arg.joinWith != null) { |
| arguments.add(arg.joinWith); |
| } |
| if (arg.formatJoined != null) { |
| arguments.add(arg.formatJoined); |
| } |
| if (arg.terminateWith != null) { |
| arguments.add(arg.terminateWith); |
| } |
| } |
| |
| private int eval( |
| List<Object> arguments, |
| int argi, |
| ImmutableList.Builder<String> builder, |
| @Nullable ArtifactExpander artifactExpander, |
| StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException { |
| final Location location = |
| ((features & HAS_LOCATION) != 0) ? (Location) arguments.get(argi++) : null; |
| final List<Object> originalValues; |
| BaseFunction mapAll = |
| ((features & HAS_MAP_ALL) != 0) ? (BaseFunction) arguments.get(argi++) : null; |
| BaseFunction mapEach = |
| ((features & HAS_MAP_EACH) != 0) ? (BaseFunction) arguments.get(argi++) : null; |
| if ((features & IS_NESTED_SET) != 0) { |
| NestedSet<Object> nestedSet = (NestedSet<Object>) arguments.get(argi++); |
| originalValues = nestedSet.toList(); |
| } else { |
| int count = (Integer) arguments.get(argi++); |
| originalValues = arguments.subList(argi, argi + count); |
| argi += count; |
| } |
| List<Object> expandedValues = originalValues; |
| if (artifactExpander != null && (features & EXPAND_DIRECTORIES) != 0) { |
| if (hasDirectory(originalValues)) { |
| expandedValues = expandDirectories(artifactExpander, originalValues); |
| } |
| } |
| List<String> stringValues; |
| if (mapEach != null) { |
| stringValues = new ArrayList<>(expandedValues.size()); |
| applyMapEach(mapEach, expandedValues, stringValues::add, location, starlarkSemantics); |
| } else if (mapAll != null) { |
| Object result = applyMapFn(mapAll, expandedValues, location, starlarkSemantics); |
| if (!(result instanceof List)) { |
| throw new CommandLineExpansionException( |
| errorMessage( |
| "map_fn must return a list, got " + result.getClass().getSimpleName(), |
| location, |
| null)); |
| } |
| List resultAsList = (List) result; |
| if (resultAsList.size() != expandedValues.size()) { |
| throw new CommandLineExpansionException( |
| errorMessage( |
| String.format( |
| "map_fn must return a list of the same length as the input. " |
| + "Found list of length %d, expected %d.", |
| resultAsList.size(), expandedValues.size()), |
| location, |
| null)); |
| } |
| int count = resultAsList.size(); |
| stringValues = new ArrayList<>(count); |
| // map_fn contract doesn't guarantee that the values returned are strings, |
| // so convert here |
| for (int i = 0; i < count; ++i) { |
| stringValues.add(CommandLineItem.expandToCommandLine(resultAsList.get(i))); |
| } |
| } else { |
| int count = expandedValues.size(); |
| stringValues = new ArrayList<>(expandedValues.size()); |
| for (int i = 0; i < count; ++i) { |
| stringValues.add(CommandLineItem.expandToCommandLine(expandedValues.get(i))); |
| } |
| } |
| // It's safe to uniquify at this stage, any transformations after this |
| // will ensure continued uniqueness of the values |
| if ((features & UNIQUIFY) != 0) { |
| HashSet<String> seen = new HashSet<>(stringValues.size()); |
| int count = stringValues.size(); |
| 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 isEmptyAndShouldOmit = stringValues.isEmpty() && (features & OMIT_IF_EMPTY) != 0; |
| if ((features & HAS_ARG_NAME) != 0) { |
| String argName = (String) arguments.get(argi++); |
| if (!isEmptyAndShouldOmit) { |
| builder.add(argName); |
| } |
| } |
| if ((features & HAS_FORMAT_EACH) != 0) { |
| String formatStr = (String) arguments.get(argi++); |
| Formatter formatter = Formatter.get(location, starlarkSemantics); |
| try { |
| int count = stringValues.size(); |
| for (int i = 0; i < count; ++i) { |
| stringValues.set(i, formatter.format(formatStr, stringValues.get(i))); |
| } |
| } catch (IllegalFormatException e) { |
| throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, null)); |
| } |
| } |
| if ((features & HAS_BEFORE_EACH) != 0) { |
| String beforeEach = (String) arguments.get(argi++); |
| int count = stringValues.size(); |
| for (int i = 0; i < count; ++i) { |
| builder.add(beforeEach); |
| builder.add(stringValues.get(i)); |
| } |
| } else if ((features & HAS_JOIN_WITH) != 0) { |
| String joinWith = (String) arguments.get(argi++); |
| String formatJoined = |
| ((features & HAS_FORMAT_JOINED) != 0) ? (String) arguments.get(argi++) : null; |
| if (!isEmptyAndShouldOmit) { |
| String result = Joiner.on(joinWith).join(stringValues); |
| if (formatJoined != null) { |
| Formatter formatter = Formatter.get(location, starlarkSemantics); |
| try { |
| result = formatter.format(formatJoined, result); |
| } catch (IllegalFormatException e) { |
| throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, null)); |
| } |
| } |
| builder.add(result); |
| } |
| } else { |
| builder.addAll(stringValues); |
| } |
| if ((features & HAS_TERMINATE_WITH) != 0) { |
| String terminateWith = (String) arguments.get(argi++); |
| if (!isEmptyAndShouldOmit) { |
| builder.add(terminateWith); |
| } |
| } |
| return argi; |
| } |
| |
| 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) |
| throws CommandLineExpansionException { |
| List<Object> expandedValues; |
| int n = originalValues.size(); |
| expandedValues = new ArrayList<>(n); |
| for (int i = 0; i < n; ++i) { |
| Object object = originalValues.get(i); |
| if (isDirectory(object)) { |
| Artifact artifact = (Artifact) object; |
| if (artifact.isTreeArtifact()) { |
| artifactExpander.expand((Artifact) object, expandedValues); |
| } else if (artifact.isFileset()) { |
| expandFileset(artifactExpander, artifact, expandedValues); |
| } 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) |
| throws CommandLineExpansionException { |
| try { |
| FilesetManifest filesetManifest = |
| FilesetManifest.constructFilesetManifest( |
| artifactExpander.getFileset(fileset), |
| fileset.getExecPath(), |
| RelativeSymlinkBehavior.IGNORE); |
| for (PathFragment relativePath : filesetManifest.getEntries().keySet()) { |
| expandedValues.add(new FilesetSymlinkFile(fileset, relativePath)); |
| } |
| } catch (IOException e) { |
| throw new CommandLineExpansionException("Could not expand fileset: " + e.getMessage()); |
| } |
| } |
| |
| private int addToFingerprint( |
| List<Object> arguments, |
| int argi, |
| ActionKeyContext actionKeyContext, |
| Fingerprint fingerprint, |
| StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException { |
| if ((features & HAS_MAP_ALL) != 0) { |
| return addToFingerprintLegacy(arguments, argi, fingerprint, starlarkSemantics); |
| } |
| final Location location = |
| ((features & HAS_LOCATION) != 0) ? (Location) arguments.get(argi++) : null; |
| BaseFunction mapEach = |
| ((features & HAS_MAP_EACH) != 0) ? (BaseFunction) arguments.get(argi++) : null; |
| if ((features & IS_NESTED_SET) != 0) { |
| NestedSet<Object> values = (NestedSet<Object>) arguments.get(argi++); |
| if (mapEach != null) { |
| CommandLineItem.MapFn<Object> commandLineItemMapFn = |
| new CommandLineItemMapEachAdaptor(mapEach, location, starlarkSemantics); |
| try { |
| actionKeyContext.addNestedSetToFingerprint(commandLineItemMapFn, fingerprint, values); |
| } catch (UncheckedCommandLineExpansionException e) { |
| // We wrap the CommandLineExpansionException below, unwrap here |
| throw e.cause; |
| } |
| } else { |
| actionKeyContext.addNestedSetToFingerprint(fingerprint, values); |
| } |
| } else { |
| int count = (Integer) arguments.get(argi++); |
| final List<Object> originalValues = arguments.subList(argi, argi + count); |
| argi += count; |
| if (mapEach != null) { |
| List<String> stringValues = new ArrayList<>(count); |
| applyMapEach(mapEach, originalValues, stringValues::add, location, starlarkSemantics); |
| for (String s : stringValues) { |
| fingerprint.addString(s); |
| } |
| } else { |
| for (int i = 0; i < count; ++i) { |
| fingerprint.addString(CommandLineItem.expandToCommandLine(originalValues.get(i))); |
| } |
| } |
| } |
| 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; |
| } |
| |
| private int addToFingerprintLegacy( |
| List<Object> arguments, |
| int argi, |
| Fingerprint fingerprint, |
| StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException { |
| ImmutableList.Builder<String> builder = ImmutableList.builder(); |
| argi = eval(arguments, argi, builder, null, starlarkSemantics); |
| for (String s : builder.build()) { |
| fingerprint.addString(s); |
| } |
| return argi; |
| } |
| |
| static class Builder { |
| @Nullable private final SkylarkList<?> list; |
| @Nullable private final NestedSet<?> nestedSet; |
| private Location location; |
| public String argName; |
| private boolean expandDirectories; |
| private BaseFunction mapAll; |
| private BaseFunction mapEach; |
| private String formatEach; |
| private String beforeEach; |
| private String joinWith; |
| private String formatJoined; |
| private boolean omitIfEmpty; |
| private boolean uniquify; |
| private String terminateWith; |
| |
| Builder(SkylarkList<?> list) { |
| this.list = list; |
| this.nestedSet = null; |
| } |
| |
| Builder(NestedSet<?> nestedSet) { |
| this.list = null; |
| this.nestedSet = nestedSet; |
| } |
| |
| Builder setLocation(Location location) { |
| this.location = location; |
| return this; |
| } |
| |
| Builder setArgName(String argName) { |
| this.argName = argName; |
| return this; |
| } |
| |
| Builder setExpandDirectories(boolean expandDirectories) { |
| this.expandDirectories = expandDirectories; |
| return this; |
| } |
| |
| Builder setMapAll(BaseFunction mapAll) { |
| this.mapAll = mapAll; |
| return this; |
| } |
| |
| Builder setMapEach(BaseFunction mapEach) { |
| this.mapEach = mapEach; |
| return this; |
| } |
| |
| Builder setFormatEach(String format) { |
| this.formatEach = format; |
| return this; |
| } |
| |
| Builder setBeforeEach(String beforeEach) { |
| this.beforeEach = beforeEach; |
| return this; |
| } |
| |
| Builder setJoinWith(String joinWith) { |
| this.joinWith = joinWith; |
| return this; |
| } |
| |
| Builder setFormatJoined(String formatJoined) { |
| this.formatJoined = formatJoined; |
| return this; |
| } |
| |
| Builder omitIfEmpty(boolean omitIfEmpty) { |
| this.omitIfEmpty = omitIfEmpty; |
| return this; |
| } |
| |
| Builder uniquify(boolean uniquify) { |
| this.uniquify = uniquify; |
| return this; |
| } |
| |
| 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; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(features); |
| } |
| } |
| |
| @AutoCodec |
| static final class ScalarArg { |
| private static final Interner<ScalarArg> interner = BlazeInterners.newStrongInterner(); |
| private static final UUID FORMAT_UUID = UUID.fromString("8cb96642-a235-4fe0-b3ed-ebfdae8a0bd9"); |
| |
| private final boolean hasFormat; |
| private final boolean hasMapFn; |
| private final boolean hasLocation; |
| |
| private ScalarArg(boolean hasFormat, boolean hasMapFn, boolean hasLocation) { |
| this.hasFormat = hasFormat; |
| this.hasMapFn = hasMapFn; |
| this.hasLocation = hasLocation; |
| } |
| |
| @AutoCodec.VisibleForSerialization |
| @AutoCodec.Instantiator |
| static ScalarArg create(boolean hasFormat, boolean hasMapFn, boolean hasLocation) { |
| return interner.intern(new ScalarArg(hasFormat, hasMapFn, hasLocation)); |
| } |
| |
| private static void push(ImmutableList.Builder<Object> arguments, Builder arg) { |
| boolean wantsLocation = arg.format != null || arg.mapFn != null; |
| boolean hasLocation = arg.location != null && wantsLocation; |
| ScalarArg scalarArg = ScalarArg.create(arg.format != null, arg.mapFn != null, hasLocation); |
| arguments.add(scalarArg); |
| arguments.add(arg.object); |
| if (hasLocation) { |
| arguments.add(arg.location); |
| } |
| if (scalarArg.hasMapFn) { |
| arguments.add(arg.mapFn); |
| } |
| if (scalarArg.hasFormat) { |
| arguments.add(arg.format); |
| } |
| } |
| |
| private int eval( |
| List<Object> arguments, |
| int argi, |
| ImmutableList.Builder<String> builder, |
| StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException { |
| Object object = arguments.get(argi++); |
| final Location location = hasLocation ? (Location) arguments.get(argi++) : null; |
| if (hasMapFn) { |
| BaseFunction mapFn = (BaseFunction) arguments.get(argi++); |
| object = applyMapFn(mapFn, object, location, starlarkSemantics); |
| } |
| String stringValue = CommandLineItem.expandToCommandLine(object); |
| if (hasFormat) { |
| String formatStr = (String) arguments.get(argi++); |
| Formatter formatter = Formatter.get(location, starlarkSemantics); |
| stringValue = formatter.format(formatStr, stringValue); |
| } |
| builder.add(stringValue); |
| return argi; |
| } |
| |
| private int addToFingerprint( |
| List<Object> arguments, |
| int argi, |
| Fingerprint fingerprint, |
| StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException { |
| if (hasMapFn) { |
| return addToFingerprintLegacy(arguments, argi, fingerprint, starlarkSemantics); |
| } |
| Object object = arguments.get(argi++); |
| String stringValue = CommandLineItem.expandToCommandLine(object); |
| fingerprint.addString(stringValue); |
| if (hasLocation) { |
| argi++; // Skip past location slot |
| } |
| if (hasFormat) { |
| String formatStr = (String) arguments.get(argi++); |
| fingerprint.addUUID(FORMAT_UUID); |
| fingerprint.addString(formatStr); |
| } |
| return argi; |
| } |
| |
| private int addToFingerprintLegacy( |
| List<Object> arguments, |
| int argi, |
| Fingerprint fingerprint, |
| StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException { |
| ImmutableList.Builder<String> builder = ImmutableList.builderWithExpectedSize(1); |
| argi = eval(arguments, argi, builder, starlarkSemantics); |
| for (String s : builder.build()) { |
| fingerprint.addString(s); |
| } |
| return argi; |
| } |
| |
| static class Builder { |
| private Object object; |
| private String format; |
| private BaseFunction mapFn; |
| private Location location; |
| |
| Builder(Object object) { |
| this.object = object; |
| } |
| |
| Builder setLocation(Location location) { |
| this.location = location; |
| return this; |
| } |
| |
| Builder setFormat(String format) { |
| this.format = format; |
| return this; |
| } |
| |
| Builder setMapFn(BaseFunction mapFn) { |
| this.mapFn = mapFn; |
| return this; |
| } |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| ScalarArg scalarArg = (ScalarArg) o; |
| return hasFormat == scalarArg.hasFormat |
| && hasMapFn == scalarArg.hasMapFn |
| && hasLocation == scalarArg.hasLocation; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(hasFormat, hasMapFn, hasLocation); |
| } |
| } |
| |
| static class Builder { |
| private final StarlarkSemantics starlarkSemantics; |
| private final ImmutableList.Builder<Object> arguments = ImmutableList.builder(); |
| |
| public Builder(StarlarkSemantics starlarkSemantics) { |
| this.starlarkSemantics = starlarkSemantics; |
| } |
| |
| Builder add(Object object) { |
| arguments.add(object); |
| return this; |
| } |
| |
| Builder add(VectorArg.Builder vectorArg) { |
| VectorArg.push(arguments, vectorArg); |
| return this; |
| } |
| |
| Builder add(ScalarArg.Builder scalarArg) { |
| ScalarArg.push(arguments, scalarArg); |
| return this; |
| } |
| |
| SkylarkCustomCommandLine build() { |
| return new SkylarkCustomCommandLine(starlarkSemantics, arguments.build()); |
| } |
| } |
| |
| @AutoCodec.VisibleForSerialization |
| @AutoCodec.Instantiator |
| SkylarkCustomCommandLine(StarlarkSemantics starlarkSemantics, ImmutableList<Object> arguments) { |
| this.arguments = arguments; |
| this.starlarkSemantics = starlarkSemantics; |
| } |
| |
| @Override |
| public Iterable<String> arguments() throws CommandLineExpansionException { |
| return arguments(null); |
| } |
| |
| @Override |
| public Iterable<String> arguments(@Nullable ArtifactExpander artifactExpander) |
| throws CommandLineExpansionException { |
| ImmutableList.Builder<String> result = ImmutableList.builder(); |
| for (int argi = 0; argi < arguments.size(); ) { |
| Object arg = arguments.get(argi++); |
| if (arg instanceof VectorArg) { |
| argi = ((VectorArg) arg).eval(arguments, argi, result, artifactExpander, starlarkSemantics); |
| } else if (arg instanceof ScalarArg) { |
| argi = ((ScalarArg) arg).eval(arguments, argi, result, starlarkSemantics); |
| } else { |
| result.add(CommandLineItem.expandToCommandLine(arg)); |
| } |
| } |
| return result.build(); |
| } |
| |
| @Override |
| public void addToFingerprint(ActionKeyContext actionKeyContext, Fingerprint fingerprint) |
| throws CommandLineExpansionException { |
| for (int argi = 0; argi < arguments.size(); ) { |
| Object arg = arguments.get(argi++); |
| if (arg instanceof VectorArg) { |
| argi = |
| ((VectorArg) arg) |
| .addToFingerprint( |
| arguments, argi, actionKeyContext, fingerprint, starlarkSemantics); |
| } else if (arg instanceof ScalarArg) { |
| argi = ((ScalarArg) arg).addToFingerprint(arguments, argi, fingerprint, starlarkSemantics); |
| } else { |
| fingerprint.addString(CommandLineItem.expandToCommandLine(arg)); |
| } |
| } |
| } |
| |
| private interface Formatter { |
| String format(String formatStr, String subject) throws CommandLineExpansionException; |
| |
| static Formatter get(Location location, StarlarkSemantics starlarkSemantics) { |
| return starlarkSemantics.incompatibleDisallowOldStyleArgsAdd() |
| ? SingleStringArgFormatter::format |
| : new LegacyFormatter(location); |
| } |
| } |
| |
| private static class LegacyFormatter implements Formatter { |
| @Nullable private final Location location; |
| private final ArrayList<Object> args; |
| |
| public LegacyFormatter(Location location) { |
| this.location = location; |
| this.args = new ArrayList<>(1); // Reused arg list to reduce GC |
| this.args.add(null); |
| } |
| |
| @Override |
| public String format(String formatStr, String subject) throws CommandLineExpansionException { |
| try { |
| args.set(0, subject); |
| SkylarkPrinter printer = Printer.getPrinter(); |
| return printer.formatWithList(formatStr, args).toString(); |
| } catch (IllegalFormatException e) { |
| throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, null)); |
| } |
| } |
| } |
| |
| private static Object applyMapFn( |
| BaseFunction mapFn, Object arg, Location location, StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException { |
| ImmutableList<Object> args = ImmutableList.of(arg); |
| try (Mutability mutability = Mutability.create("map_fn")) { |
| Environment env = |
| Environment.builder(mutability) |
| .setSemantics(starlarkSemantics) |
| .setEventHandler(NullEventHandler.INSTANCE) |
| .build(); |
| return mapFn.call(args, ImmutableMap.of(), null, env); |
| } catch (EvalException e) { |
| throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, e.getCause())); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| throw new CommandLineExpansionException( |
| errorMessage("Thread was interrupted", location, null)); |
| } |
| } |
| |
| private static void applyMapEach( |
| BaseFunction mapFn, |
| List<Object> originalValues, |
| Consumer<String> consumer, |
| Location location, |
| StarlarkSemantics starlarkSemantics) |
| throws CommandLineExpansionException { |
| try (Mutability mutability = Mutability.create("map_each")) { |
| Environment env = |
| Environment.builder(mutability) |
| .setSemantics(starlarkSemantics) |
| // TODO(b/77140311): Error if we issue print statements |
| .setEventHandler(NullEventHandler.INSTANCE) |
| .build(); |
| Object[] args = new Object[1]; |
| int count = originalValues.size(); |
| for (int i = 0; i < count; ++i) { |
| args[0] = originalValues.get(i); |
| Object ret = mapFn.callWithArgArray(args, null, env, location); |
| if (ret instanceof String) { |
| consumer.accept((String) ret); |
| } else if (ret instanceof SkylarkList) { |
| for (Object val : ((SkylarkList) ret)) { |
| if (!(val instanceof String)) { |
| throw new CommandLineExpansionException( |
| "Expected map_each to return string, None, or list of strings, " |
| + "found list containing " |
| + val.getClass().getSimpleName()); |
| } |
| consumer.accept((String) val); |
| } |
| } else if (ret != Runtime.NONE) { |
| throw new CommandLineExpansionException( |
| "Expected map_each to return string, None, or list of strings, found " |
| + ret.getClass().getSimpleName()); |
| } |
| } |
| } catch (EvalException e) { |
| throw new CommandLineExpansionException(errorMessage(e.getMessage(), location, e.getCause())); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| throw new CommandLineExpansionException( |
| errorMessage("Thread was interrupted", location, null)); |
| } |
| } |
| |
| private static class CommandLineItemMapEachAdaptor |
| extends CommandLineItem.ParametrizedMapFn<Object> { |
| private final BaseFunction mapFn; |
| private final Location location; |
| private final StarlarkSemantics starlarkSemantics; |
| |
| CommandLineItemMapEachAdaptor( |
| BaseFunction mapFn, Location location, StarlarkSemantics starlarkSemantics) { |
| this.mapFn = mapFn; |
| this.location = location; |
| this.starlarkSemantics = starlarkSemantics; |
| } |
| |
| @Override |
| public void expandToCommandLine(Object object, Consumer<String> args) { |
| try { |
| applyMapEach(mapFn, ImmutableList.of(object), args, location, starlarkSemantics); |
| } catch (CommandLineExpansionException e) { |
| // Rather than update CommandLineItem#expandToCommandLine and the numerous callers, |
| // we wrap this in a runtime exception and handle it above |
| throw new UncheckedCommandLineExpansionException(e); |
| } |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof CommandLineItemMapEachAdaptor)) { |
| return false; |
| } |
| CommandLineItemMapEachAdaptor other = (CommandLineItemMapEachAdaptor) obj; |
| // Instance compare intentional |
| // The normal implementation uses location + name of function, |
| // which can conceivably conflict in tests |
| return mapFn == other.mapFn; |
| } |
| |
| @Override |
| public int hashCode() { |
| // identity hashcode intentional |
| return System.identityHashCode(mapFn); |
| } |
| |
| @Override |
| public int maxInstancesAllowed() { |
| // No limit to these, as this is just a wrapper for Skylark functions, which are |
| // always static |
| return Integer.MAX_VALUE; |
| } |
| } |
| |
| 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)); |
| } |
| |
| 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; |
| } |
| |
| private static class UncheckedCommandLineExpansionException extends RuntimeException { |
| final CommandLineExpansionException cause; |
| |
| UncheckedCommandLineExpansionException(CommandLineExpansionException cause) { |
| this.cause = cause; |
| } |
| } |
| |
| /** |
| * 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; |
| |
| public FilesetSymlinkFile(Artifact fileset, PathFragment execPath) { |
| this.fileset = fileset; |
| this.execPath = execPath; |
| } |
| |
| private PathFragment getExecPath() { |
| return execPath; |
| } |
| |
| @Override |
| public String getDirname() { |
| PathFragment parent = getExecPath().getParentDirectory(); |
| return (parent == null) ? "/" : parent.getSafePathString(); |
| } |
| |
| @Override |
| public String getFilename() { |
| return getExecPath().getBaseName(); |
| } |
| |
| @Override |
| public String getExtension() { |
| return getExecPath().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 getExecPath().getPathString(); |
| } |
| |
| @Override |
| public String expandToCommandLine() { |
| return getExecPathString(); |
| } |
| |
| @Override |
| public void repr(SkylarkPrinter printer) { |
| if (isSourceArtifact()) { |
| printer.append("<source file " + getRunfilesPathString() + ">"); |
| } else { |
| printer.append("<generated file " + getRunfilesPathString() + ">"); |
| } |
| } |
| } |
| } |