blob: 4121df20b1114a20ce30644284169e8765b9bcdf [file] [log] [blame]
// 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.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.actions.CommandLine;
import com.google.devtools.build.lib.actions.CommandLineItemSimpleFormatter;
import com.google.devtools.build.lib.actions.ParamFileInfo;
import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
import com.google.devtools.build.lib.actions.RunfilesSupplier;
import com.google.devtools.build.lib.actions.extra.SpawnInfo;
import com.google.devtools.build.lib.analysis.CommandHelper;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.PseudoAction;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.ShToolchain;
import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution;
import com.google.devtools.build.lib.analysis.skylark.SkylarkCustomCommandLine.ScalarArg;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.skylarkbuildapi.CommandLineArgsApi;
import com.google.devtools.build.lib.skylarkbuildapi.FileApi;
import com.google.devtools.build.lib.skylarkbuildapi.SkylarkActionFactoryApi;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
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.EvalUtils;
import com.google.devtools.build.lib.syntax.FunctionSignature.Shape;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.Runtime;
import com.google.devtools.build.lib.syntax.Runtime.NoneType;
import com.google.devtools.build.lib.syntax.SkylarkDict;
import com.google.devtools.build.lib.syntax.SkylarkList;
import com.google.devtools.build.lib.syntax.SkylarkMutable;
import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
import com.google.devtools.build.lib.syntax.SkylarkSemantics;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
/** Provides a Skylark interface for all action creation needs. */
public class SkylarkActionFactory implements SkylarkActionFactoryApi {
private final SkylarkRuleContext context;
private final SkylarkSemantics skylarkSemantics;
private RuleContext ruleContext;
/** Counter for actions.run_shell helper scripts. Every script must have a unique name. */
private int runShellOutputCounter = 0;
public SkylarkActionFactory(
SkylarkRuleContext context,
SkylarkSemantics skylarkSemantics,
RuleContext ruleContext) {
this.context = context;
this.skylarkSemantics = skylarkSemantics;
this.ruleContext = ruleContext;
}
ArtifactRoot newFileRoot() throws EvalException {
return context.isForAspect()
? ruleContext.getConfiguration().getBinDirectory(ruleContext.getRule().getRepository())
: ruleContext.getBinOrGenfilesDirectory();
}
@Override
public Artifact declareFile(String filename, Object sibling) throws EvalException {
context.checkMutable("actions.declare_file");
if (Runtime.NONE.equals(sibling)) {
return ruleContext.getPackageRelativeArtifact(filename, newFileRoot());
} else {
PathFragment original = ((Artifact) sibling).getRootRelativePath();
PathFragment fragment = original.replaceName(filename);
return ruleContext.getDerivedArtifact(fragment, newFileRoot());
}
}
@Override
public Artifact declareDirectory(String filename, Object sibling) throws EvalException {
context.checkMutable("actions.declare_directory");
if (Runtime.NONE.equals(sibling)) {
return ruleContext.getPackageRelativeTreeArtifact(
PathFragment.create(filename), newFileRoot());
} else {
PathFragment original = ((Artifact) sibling).getRootRelativePath();
PathFragment fragment = original.replaceName(filename);
return ruleContext.getTreeArtifact(fragment, newFileRoot());
}
}
@Override
public void doNothing(String mnemonic, Object inputs) throws EvalException {
context.checkMutable("actions.do_nothing");
NestedSet<Artifact> inputSet = inputs instanceof SkylarkNestedSet
? ((SkylarkNestedSet) inputs).getSet(Artifact.class)
: NestedSetBuilder.<Artifact>compileOrder()
.addAll(((SkylarkList) inputs).getContents(Artifact.class, "inputs"))
.build();
Action action =
new PseudoAction<>(
UUID.nameUUIDFromBytes(
String.format("empty action %s", ruleContext.getLabel())
.getBytes(StandardCharsets.UTF_8)),
ruleContext.getActionOwner(),
inputSet,
ImmutableList.of(PseudoAction.getDummyOutput(ruleContext)),
mnemonic,
SpawnInfo.spawnInfo,
SpawnInfo.newBuilder().build());
ruleContext.registerAction(action);
}
@Override
public void write(FileApi output, Object content, Boolean isExecutable) throws EvalException {
context.checkMutable("actions.write");
final Action action;
if (content instanceof String) {
action =
FileWriteAction.create(ruleContext, (Artifact) output, (String) content, isExecutable);
} else if (content instanceof Args) {
Args args = (Args) content;
action =
new ParameterFileWriteAction(
ruleContext.getActionOwner(),
(Artifact) output,
args.build(),
args.parameterFileType,
StandardCharsets.UTF_8);
} else {
throw new AssertionError("Unexpected type: " + content.getClass().getSimpleName());
}
ruleContext.registerAction(action);
}
@Override
public void run(
SkylarkList outputs,
Object inputs,
Object executableUnchecked,
Object toolsUnchecked,
Object arguments,
Object mnemonicUnchecked,
Object progressMessage,
Boolean useDefaultShellEnv,
Object envUnchecked,
Object executionRequirementsUnchecked,
Object inputManifestsUnchecked,
Location location)
throws EvalException {
context.checkMutable("actions.run");
SpawnAction.Builder builder = new SpawnAction.Builder();
SkylarkList argumentsList = ((SkylarkList) arguments);
buildCommandLine(builder, argumentsList);
if (executableUnchecked instanceof Artifact) {
Artifact executable = (Artifact) executableUnchecked;
builder.addInput(executable);
FilesToRunProvider provider = context.getExecutableRunfiles(executable);
if (provider == null) {
builder.setExecutable(executable);
} else {
builder.setExecutable(provider);
}
} else if (executableUnchecked instanceof String) {
builder.setExecutable(PathFragment.create((String) executableUnchecked));
} else {
throw new EvalException(
null,
"expected file or string for "
+ "executable but got "
+ EvalUtils.getDataTypeName(executableUnchecked)
+ " instead");
}
registerSpawnAction(
outputs,
inputs,
toolsUnchecked,
mnemonicUnchecked,
progressMessage,
useDefaultShellEnv,
envUnchecked,
executionRequirementsUnchecked,
inputManifestsUnchecked,
location,
builder);
}
/**
* Registers actions in the context of this {@link SkylarkActionFactory}.
*
* Use {@link #getActionConstructionContext()} to obtain the context required to
* create those actions.
*/
public void registerAction(ActionAnalysisMetadata... actions) {
ruleContext.registerAction(actions);
}
/**
* Returns information needed to construct actions that can be
* registered with {@link #registerAction(ActionAnalysisMetadata...)}.
*/
public ActionConstructionContext getActionConstructionContext() {
return ruleContext;
}
@Override
public void runShell(
SkylarkList outputs,
Object inputs,
Object toolsUnchecked,
Object arguments,
Object mnemonicUnchecked,
Object commandUnchecked,
Object progressMessage,
Boolean useDefaultShellEnv,
Object envUnchecked,
Object executionRequirementsUnchecked,
Object inputManifestsUnchecked,
Location location)
throws EvalException {
context.checkMutable("actions.run_shell");
SkylarkList argumentList = (SkylarkList) arguments;
SpawnAction.Builder builder = new SpawnAction.Builder();
buildCommandLine(builder, argumentList);
if (commandUnchecked instanceof String) {
Map<String, String> executionInfo =
ImmutableMap.copyOf(TargetUtils.getExecutionInfo(ruleContext.getRule()));
String helperScriptSuffix = String.format(".run_shell_%d.sh", runShellOutputCounter++);
String command = (String) commandUnchecked;
Artifact helperScript =
CommandHelper.shellCommandHelperScriptMaybe(
ruleContext, command, helperScriptSuffix, executionInfo);
PathFragment shExecutable = ShToolchain.getPathOrError(ruleContext);
if (helperScript == null) {
builder.setShellCommand(shExecutable, command);
} else {
builder.setShellCommand(shExecutable, helperScript.getExecPathString());
builder.addInput(helperScript);
FilesToRunProvider provider = context.getExecutableRunfiles(helperScript);
if (provider != null) {
builder.addTool(provider);
}
}
} else if (commandUnchecked instanceof SkylarkList) {
SkylarkList commandList = (SkylarkList) commandUnchecked;
if (commandList.size() < 3) {
throw new EvalException(null, "'command' list has to be of size at least 3");
}
@SuppressWarnings("unchecked")
List<String> command = commandList.getContents(String.class, "command");
builder.setShellCommand(command);
} else {
throw new EvalException(
null,
"expected string or list of strings for command instead of "
+ EvalUtils.getDataTypeName(commandUnchecked));
}
if (argumentList.size() > 0) {
// When we use a shell command, add an empty argument before other arguments.
// e.g. bash -c "cmd" '' 'arg1' 'arg2'
// bash will use the empty argument as the value of $0 (which we don't care about).
// arg1 and arg2 will be $1 and $2, as a user expects.
builder.addExecutableArguments("");
}
registerSpawnAction(
outputs,
inputs,
toolsUnchecked,
mnemonicUnchecked,
progressMessage,
useDefaultShellEnv,
envUnchecked,
executionRequirementsUnchecked,
inputManifestsUnchecked,
location,
builder);
}
private void buildCommandLine(SpawnAction.Builder builder, SkylarkList argumentsList)
throws EvalException {
List<String> stringArgs = new ArrayList<>();
for (Object value : argumentsList) {
if (value instanceof String) {
stringArgs.add((String) value);
} else if (value instanceof Args) {
if (!stringArgs.isEmpty()) {
builder.addCommandLine(CommandLine.of(stringArgs));
stringArgs = new ArrayList<>();
}
Args args = (Args) value;
ParamFileInfo paramFileInfo = null;
if (args.flagFormatString != null) {
paramFileInfo =
ParamFileInfo.builder(args.parameterFileType)
.setFlagFormatString(args.flagFormatString)
.setUseAlways(args.useAlways)
.setCharset(StandardCharsets.UTF_8)
.build();
}
builder.addCommandLine(args.commandLine.build(), paramFileInfo);
} else {
throw new EvalException(
null,
"expected list of strings or ctx.actions.args() for arguments instead of "
+ EvalUtils.getDataTypeName(value));
}
}
if (!stringArgs.isEmpty()) {
builder.addCommandLine(CommandLine.of(stringArgs));
}
}
/**
* Setup for spawn actions common between {@link #run} and {@link #runShell}.
*
* <p>{@code builder} should have either executable or a command set.
*/
private void registerSpawnAction(
SkylarkList outputs,
Object inputs,
Object toolsUnchecked,
Object mnemonicUnchecked,
Object progressMessage,
Boolean useDefaultShellEnv,
Object envUnchecked,
Object executionRequirementsUnchecked,
Object inputManifestsUnchecked,
Location location,
SpawnAction.Builder builder)
throws EvalException {
Iterable<Artifact> inputArtifacts;
if (inputs instanceof SkylarkList) {
inputArtifacts = ((SkylarkList) inputs).getContents(Artifact.class, "inputs");
builder.addInputs(inputArtifacts);
} else {
NestedSet<Artifact> inputSet = ((SkylarkNestedSet) inputs).getSet(Artifact.class);
builder.addTransitiveInputs(inputSet);
inputArtifacts = inputSet;
}
builder.addOutputs(outputs.getContents(Artifact.class, "outputs"));
if (toolsUnchecked != Runtime.UNBOUND) {
final Iterable<Artifact> toolsIterable;
if (toolsUnchecked instanceof SkylarkList) {
toolsIterable = ((SkylarkList) toolsUnchecked).getContents(Artifact.class, "tools");
} else {
toolsIterable = ((SkylarkNestedSet) toolsUnchecked).getSet(Artifact.class);
}
for (Artifact artifact : toolsIterable) {
builder.addInput(artifact);
FilesToRunProvider provider = context.getExecutableRunfiles(artifact);
if (provider != null) {
builder.addTool(provider);
}
}
} else {
// Users didn't pass 'tools', kick in compatibility modes
// Full legacy support -- add tools from inputs
for (Artifact artifact : inputArtifacts) {
FilesToRunProvider provider = context.getExecutableRunfiles(artifact);
if (provider != null) {
builder.addTool(provider);
}
}
}
String mnemonic = getMnemonic(mnemonicUnchecked);
builder.setMnemonic(mnemonic);
if (envUnchecked != Runtime.NONE) {
builder.setEnvironment(
ImmutableMap.copyOf(
SkylarkDict.castSkylarkDictOrNoneToDict(
envUnchecked, String.class, String.class, "env")));
}
if (progressMessage != Runtime.NONE) {
builder.setProgressMessageNonLazy((String) progressMessage);
}
if (EvalUtils.toBoolean(useDefaultShellEnv)) {
builder.useDefaultShellEnvironment();
}
if (executionRequirementsUnchecked != Runtime.NONE) {
builder.setExecutionInfo(
TargetUtils.filter(
SkylarkDict.castSkylarkDictOrNoneToDict(
executionRequirementsUnchecked,
String.class,
String.class,
"execution_requirements")));
}
if (inputManifestsUnchecked != Runtime.NONE) {
for (RunfilesSupplier supplier : SkylarkList.castSkylarkListOrNoneToList(
inputManifestsUnchecked, RunfilesSupplier.class, "runfiles suppliers")) {
builder.addRunfilesSupplier(supplier);
}
}
// Always register the action
ruleContext.registerAction(builder.build(ruleContext));
}
private String getMnemonic(Object mnemonicUnchecked) {
String mnemonic =
mnemonicUnchecked == Runtime.NONE ? "SkylarkAction" : (String) mnemonicUnchecked;
if (ruleContext.getConfiguration().getReservedActionMnemonics().contains(mnemonic)) {
mnemonic = mangleMnemonic(mnemonic);
}
return mnemonic;
}
private static String mangleMnemonic(String mnemonic) {
return mnemonic + "FromSkylark";
}
@Override
public void expandTemplate(
FileApi template,
FileApi output,
SkylarkDict<?, ?> substitutionsUnchecked,
Boolean executable)
throws EvalException {
context.checkMutable("actions.expand_template");
ImmutableList.Builder<Substitution> substitutionsBuilder = ImmutableList.builder();
for (Map.Entry<String, String> substitution :
substitutionsUnchecked
.getContents(String.class, String.class, "substitutions")
.entrySet()) {
// ParserInputSource.create(Path) uses Latin1 when reading BUILD files, which might
// contain UTF-8 encoded symbols as part of template substitution.
// As a quick fix, the substitution values are corrected before being passed on.
// In the long term, fixing ParserInputSource.create(Path) would be a better approach.
substitutionsBuilder.add(
Substitution.of(
substitution.getKey(), convertLatin1ToUtf8(substitution.getValue())));
}
TemplateExpansionAction action =
new TemplateExpansionAction(
ruleContext.getActionOwner(),
(Artifact) template,
(Artifact) output,
substitutionsBuilder.build(),
executable);
ruleContext.registerAction(action);
}
/**
* Returns the proper UTF-8 representation of a String that was erroneously read using Latin1.
*
* @param latin1 Input string
* @return The input string, UTF8 encoded
*/
private static String convertLatin1ToUtf8(String latin1) {
return new String(latin1.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}
/** Args module. */
@SkylarkModule(
name = "Args",
category = SkylarkModuleCategory.BUILTIN,
doc =
"An object that encapsulates, in a memory-efficient way, the data needed to build part or "
+ "all of a command line."
+ ""
+ "<p>It often happens that an action requires a large command line containing values "
+ "accumulated from transitive dependencies. For example, a linker command line might "
+ "list every object file needed by all of the libraries being linked. It is best "
+ "practice to store such transitive data in <a href='depset.html'><code>depset"
+ "</code></a>s, so that they can be shared by multiple targets. However, if the rule "
+ "author had to convert these depsets into lists of strings in order to construct an "
+ "action command line, it would defeat this memory-sharing optimization."
+ ""
+ "<p>For this reason, the action-constructing functions accept <code>Args</code> "
+ "objects in addition to strings. Each <code>Args</code> object represents a "
+ "concatenation of strings and depsets, with optional transformations for "
+ "manipulating the data. <code>Args</code> objects do not process the depsets they "
+ "encapsulate until the execution phase, when it comes time to calculate the command "
+ "line. This helps defer any expensive copying until after the analysis phase is "
+ "complete. See the <a href='../performance.$DOC_EXT'>Optimizing Performance</a> page "
+ "for more information."
+ ""
+ "<p><code>Args</code> are constructed by calling <a href='actions.html#args'><code>"
+ "ctx.actions.args()</code></a>. They can be passed as the <code>arguments</code> "
+ "parameter of <a href='actions.html#run'><code>ctx.actions.run()</code></a> or "
+ "<a href='actions.html#run_shell'><code>ctx.actions.run_shell()</code></a>. Each "
+ "mutation of an <code>Args</code> object appends values to the eventual command "
+ "line."
+ ""
+ "<p>The <code>map_each</code> feature allows you to customize how items are "
+ "transformed into strings. If you do not provide a <code>map_each</code> function, "
+ "the standard conversion is as follows: "
+ "<ul>"
+ "<li>Values that are already strings are left as-is."
+ "<li><a href='File.html'><code>File</code></a> objects are turned into their "
+ " <code>File.path</code> values."
+ "<li>All other types are turned into strings in an <i>unspecified</i> manner. For "
+ " this reason, you should avoid passing values that are not of string or "
+ " <code>File</code> type to <code>add()</code>, and if you pass them to "
+ " <code>add_all()</code> or <code>add_joined()</code> then you should provide a "
+ " <code>map_each</code> function."
+ "</ul>"
+ ""
+ "<p>When using string formatting (<code>format</code>, <code>format_each</code>, and "
+ "<code>format_joined</code> params of the <code>add*()</code> methods), the format "
+ "template is interpreted in the same way as <code>%</code>-substitution on strings, "
+ "except that the template must have exactly one substitution placeholder and it must "
+ "be <code>%s</code>. Literal percents may be escaped as <code>%%</code>. Formatting "
+ "is applied after the value is converted to a string as per the above."
+ ""
+ "<p>Each of the <code>add*()</code> methods have an alternate form that accepts an "
+ "extra positional parameter, an \"arg name\" string to insert before the rest of the "
+ "arguments. For <code>add_all</code> and <code>add_joined</code> the extra string "
+ "will not be added if the sequence turns out to be empty. "
+ "For instance, the same usage can add either <code>--foo val1 val2 val3 --bar"
+ "</code> or just <code>--bar</code> to the command line, depending on whether the "
+ "given sequence contains <code>val1..val3</code> or is empty."
+ ""
+ "<p>If the size of the command line can grow longer than the maximum size allowed by "
+ "the system, the arguments can be spilled over into parameter files. See "
+ "<a href='#use_param_file'><code>use_param_file()</code></a> and "
+ "<a href='#set_param_file_format'><code>set_param_file_format()</code></a>."
+ ""
+ "<p>Example: Suppose we wanted to generate the command line: "
+ "<pre>\n"
+ "--foo foo1.txt foo2.txt ... fooN.txt --bar bar1.txt,bar2.txt,...,barM.txt --baz\n"
+ "</pre>"
+ "We could use the following <code>Args</code> object: "
+ "<pre class=language-python>\n"
+ "# foo_deps and bar_deps are depsets containing\n"
+ "# File objects for the foo and bar .txt files.\n"
+ "args = ctx.actions.args()\n"
+ "args.add_all(\"--foo\", foo_deps)\n"
+ "args.add_joined(\"--bar\", bar_deps, join_with=\",\")\n"
+ "args.add(\"--baz\")\n"
+ "ctx.actions.run(\n"
+ " ...\n"
+ " arguments = [args],\n"
+ " ...\n"
+ ")\n"
+ "</pre>"
)
@VisibleForTesting
public static class Args extends SkylarkMutable implements CommandLineArgsApi {
private final Mutability mutability;
private final SkylarkSemantics skylarkSemantics;
private final SkylarkCustomCommandLine.Builder commandLine;
private ParameterFileType parameterFileType = ParameterFileType.SHELL_QUOTED;
private String flagFormatString;
private boolean useAlways;
@Override
public NoneType addArgument(
Object argNameOrValue,
Object value,
Object format,
Object beforeEach,
Object joinWith,
Object mapFn,
Location loc)
throws EvalException {
if (isImmutable()) {
throw new EvalException(null, "cannot modify frozen value");
}
final String argName;
if (value == Runtime.UNBOUND) {
value = argNameOrValue;
argName = null;
} else {
validateArgName(argNameOrValue, loc);
argName = (String) argNameOrValue;
}
if (argName != null) {
commandLine.add(argName);
}
if (value instanceof SkylarkNestedSet || value instanceof SkylarkList) {
if (skylarkSemantics.incompatibleDisallowOldStyleArgsAdd()) {
throw new EvalException(
loc,
"Args#add no longer accepts vectorized arguments when "
+ "--incompatible_disallow_old_style_args_add is set. "
+ "Please use Args#add_all or Args#add_joined.");
}
addVectorArg(
value,
/* argName= */ null,
mapFn != Runtime.NONE ? (BaseFunction) mapFn : null,
/* mapEach= */ null,
format != Runtime.NONE ? (String) format : null,
beforeEach != Runtime.NONE ? (String) beforeEach : null,
joinWith != Runtime.NONE ? (String) joinWith : null,
/* formatJoined= */ null,
/* omitIfEmpty= */ false,
/* uniquify= */ false,
/* terminateWith= */ null,
loc);
} else {
if (mapFn != Runtime.NONE && skylarkSemantics.incompatibleDisallowOldStyleArgsAdd()) {
throw new EvalException(
loc,
"Args#add no longer accepts map_fn when"
+ "--incompatible_disallow_old_style_args_add is set. "
+ "Please eagerly map the value.");
}
if (beforeEach != Runtime.NONE) {
throw new EvalException(null, "'before_each' is not supported for scalar arguments");
}
if (joinWith != Runtime.NONE) {
throw new EvalException(null, "'join_with' is not supported for scalar arguments");
}
addScalarArg(
value,
format != Runtime.NONE ? (String) format : null,
mapFn != Runtime.NONE ? (BaseFunction) mapFn : null,
loc);
}
return Runtime.NONE;
}
@Override
public NoneType addAll(
Object argNameOrValue,
Object values,
Object mapEach,
Object formatEach,
Object beforeEach,
Boolean omitIfEmpty,
Boolean uniquify,
Object terminateWith,
Location loc)
throws EvalException {
if (isImmutable()) {
throw new EvalException(null, "cannot modify frozen value");
}
final String argName;
if (values == Runtime.UNBOUND) {
values = argNameOrValue;
validateValues(values, loc);
argName = null;
} else {
validateArgName(argNameOrValue, loc);
argName = (String) argNameOrValue;
}
addVectorArg(
values,
argName,
/* mapAll= */ null,
mapEach != Runtime.NONE ? (BaseFunction) mapEach : null,
formatEach != Runtime.NONE ? (String) formatEach : null,
beforeEach != Runtime.NONE ? (String) beforeEach : null,
/* joinWith= */ null,
/* formatJoined= */ null,
omitIfEmpty,
uniquify,
terminateWith != Runtime.NONE ? (String) terminateWith : null,
loc);
return Runtime.NONE;
}
@Override
public NoneType addJoined(
Object argNameOrValue,
Object values,
String joinWith,
Object mapEach,
Object formatEach,
Object formatJoined,
Boolean omitIfEmpty,
Boolean uniquify,
Location loc)
throws EvalException {
if (isImmutable()) {
throw new EvalException(null, "cannot modify frozen value");
}
final String argName;
if (values == Runtime.UNBOUND) {
values = argNameOrValue;
validateValues(values, loc);
argName = null;
} else {
validateArgName(argNameOrValue, loc);
argName = (String) argNameOrValue;
}
addVectorArg(
values,
argName,
/* mapAll= */ null,
mapEach != Runtime.NONE ? (BaseFunction) mapEach : null,
formatEach != Runtime.NONE ? (String) formatEach : null,
/* beforeEach= */ null,
joinWith,
formatJoined != Runtime.NONE ? (String) formatJoined : null,
omitIfEmpty,
uniquify,
/* terminateWith= */ null,
loc);
return Runtime.NONE;
}
private void addVectorArg(
Object value,
String argName,
BaseFunction mapAll,
BaseFunction mapEach,
String formatEach,
String beforeEach,
String joinWith,
String formatJoined,
boolean omitIfEmpty,
boolean uniquify,
String terminateWith,
Location loc)
throws EvalException {
SkylarkCustomCommandLine.VectorArg.Builder vectorArg;
if (value instanceof SkylarkNestedSet) {
NestedSet<?> nestedSet = ((SkylarkNestedSet) value).getSet(Object.class);
vectorArg = new SkylarkCustomCommandLine.VectorArg.Builder(nestedSet);
} else {
SkylarkList skylarkList = (SkylarkList) value;
vectorArg = new SkylarkCustomCommandLine.VectorArg.Builder(skylarkList);
}
validateMapEach(mapEach, loc);
validateFormatString("format_each", formatEach);
validateFormatString("format_joined", formatJoined);
vectorArg
.setLocation(loc)
.setArgName(argName)
.setMapAll(mapAll)
.setFormatEach(formatEach)
.setBeforeEach(beforeEach)
.setJoinWith(joinWith)
.setFormatJoined(formatJoined)
.omitIfEmpty(omitIfEmpty)
.uniquify(uniquify)
.setTerminateWith(terminateWith)
.setMapEach(mapEach);
commandLine.add(vectorArg);
}
private void validateArgName(Object argName, Location loc) throws EvalException {
if (!(argName instanceof String)) {
throw new EvalException(
loc,
String.format(
"expected value of type 'string' for arg name, got '%s'",
argName.getClass().getSimpleName()));
}
}
private void validateValues(Object values, Location loc) throws EvalException {
if (!(values instanceof SkylarkList || values instanceof SkylarkNestedSet)) {
throw new EvalException(
loc,
String.format(
"expected value of type 'sequence or depset' for values, got '%s'",
values.getClass().getSimpleName()));
}
}
private void validateMapEach(@Nullable BaseFunction mapEach, Location loc)
throws EvalException {
if (mapEach == null) {
return;
}
Shape shape = mapEach.getSignature().getSignature().getShape();
boolean valid =
shape.getMandatoryPositionals() == 1
&& shape.getOptionalPositionals() == 0
&& shape.getMandatoryNamedOnly() == 0
&& shape.getOptionalPositionals() == 0;
if (!valid) {
throw new EvalException(
loc, "map_each must be a function that accepts a single positional argument");
}
}
private void validateFormatString(String argumentName, @Nullable String formatStr)
throws EvalException {
if (formatStr != null
&& skylarkSemantics.incompatibleDisallowOldStyleArgsAdd()
&& !CommandLineItemSimpleFormatter.isValid(formatStr)) {
throw new EvalException(
null,
String.format(
"Invalid value for parameter \"%s\": Expected string with a single \"%%s\"",
argumentName));
}
}
private void addScalarArg(Object value, String format, BaseFunction mapFn, Location loc)
throws EvalException {
validateFormatString("format", format);
if (format == null && mapFn == null) {
commandLine.add(value);
} else {
ScalarArg.Builder scalarArg =
new ScalarArg.Builder(value).setLocation(loc).setFormat(format).setMapFn(mapFn);
commandLine.add(scalarArg);
}
}
@Override
public void useParamsFile(String paramFileArg, Boolean useAlways) throws EvalException {
if (isImmutable()) {
throw new EvalException(null, "cannot modify frozen value");
}
if (!paramFileArg.contains("%s")) {
throw new EvalException(
null,
"Invalid value for parameter \"param_file_arg\": Expected string with a single \"%s\"");
}
this.flagFormatString = paramFileArg;
this.useAlways = useAlways;
}
@Override
public void setParamFileFormat(String format) throws EvalException {
if (isImmutable()) {
throw new EvalException(null, "cannot modify frozen value");
}
final ParameterFileType parameterFileType;
switch (format) {
case "shell":
parameterFileType = ParameterFileType.SHELL_QUOTED;
break;
case "multiline":
parameterFileType = ParameterFileType.UNQUOTED;
break;
default:
throw new EvalException(
null,
"Invalid value for parameter \"format\": Expected one of \"shell\", \"multiline\"");
}
this.parameterFileType = parameterFileType;
}
private Args(@Nullable Mutability mutability, SkylarkSemantics skylarkSemantics) {
this.mutability = mutability != null ? mutability : Mutability.IMMUTABLE;
this.skylarkSemantics = skylarkSemantics;
this.commandLine = new SkylarkCustomCommandLine.Builder(skylarkSemantics);
}
public SkylarkCustomCommandLine build() {
return commandLine.build();
}
@Override
public Mutability mutability() {
return mutability;
}
@Override
public void repr(SkylarkPrinter printer) {
printer.append("context.args() object");
}
}
@Override
public Args args(Environment env) {
return new Args(env.mutability(), skylarkSemantics);
}
@Override
public boolean isImmutable() {
return context.isImmutable();
}
@Override
public void repr(SkylarkPrinter printer) {
printer.append("actions for");
context.repr(printer);
}
void nullify() {
ruleContext = null;
}
}