// 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 static java.util.stream.Collectors.toList; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; 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.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.SingleStringArgFormatter; import com.google.devtools.build.lib.actions.extra.ExtraActionInfo; 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.Substitution; import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction; 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.skyframe.serialization.autocodec.AutoCodec; 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.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 com.google.protobuf.GeneratedMessage; 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"); Artifact result; if (Runtime.NONE.equals(sibling)) { result = ruleContext.getPackageRelativeTreeArtifact(PathFragment.create(filename), newFileRoot()); } else { PathFragment original = ((Artifact) sibling).getRootRelativePath(); PathFragment fragment = original.replaceName(filename); result = ruleContext.getTreeArtifact(fragment, newFileRoot()); } if (!result.isTreeArtifact()) { throw new EvalException( null, String.format( "'%s' has already been declared as a regular file, not directory.", filename)); } return result; } @Override public void doNothing(String mnemonic, Object inputs) throws EvalException { context.checkMutable("actions.do_nothing"); NestedSet inputSet = inputs instanceof SkylarkNestedSet ? ((SkylarkNestedSet) inputs).getSet(Artifact.class) : NestedSetBuilder.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, SPAWN_INFO, SpawnInfo.newBuilder().build()); ruleContext.registerAction(action); } @AutoCodec @AutoCodec.VisibleForSerialization static final GeneratedMessage.GeneratedExtension SPAWN_INFO = SpawnInfo.spawnInfo; @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 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 (argumentList.size() > 0) { throw new EvalException(location, "'arguments' must be empty if 'command' is a sequence of strings"); } @SuppressWarnings("unchecked") List 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 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}. * *

{@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 inputArtifacts; if (inputs instanceof SkylarkList) { inputArtifacts = ((SkylarkList) inputs).getContents(Artifact.class, "inputs"); builder.addInputs(inputArtifacts); } else { NestedSet inputSet = ((SkylarkNestedSet) inputs).getSet(Artifact.class); builder.addTransitiveInputs(inputSet); inputArtifacts = inputSet; } builder.addOutputs(outputs.getContents(Artifact.class, "outputs")); if (toolsUnchecked != Runtime.UNBOUND) { final Iterable 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 if (skylarkSemantics.incompatibleNoSupportToolsInActionInputs()) { // In this mode we error out if we find any tools among the inputs List tools = null; for (Artifact artifact : inputArtifacts) { FilesToRunProvider provider = context.getExecutableRunfiles(artifact); if (provider != null) { tools = tools != null ? tools : new ArrayList<>(1); tools.add(artifact); } } if (tools != null) { String toolsAsString = Joiner.on(", ") .join( tools .stream() .map(Artifact::getExecPathString) .map(s -> "'" + s + "'") .collect(toList())); throw new EvalException( location, String.format( "Found tool(s) %s in inputs. " + "A tool is an input with executable=True set. " + "All tools should be passed using the 'tools' " + "argument instead of 'inputs' in order to make their runfiles available " + "to the action. This safety check will not be performed once the action " + "is modified to take a 'tools' argument. " + "To temporarily disable this check, " + "set --incompatible_no_support_tools_in_action_inputs=false.", toolsAsString)); } } else { // 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 substitutionsBuilder = ImmutableList.builder(); for (Map.Entry 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. */ @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() && !SingleStringArgFormatter.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 (!SingleStringArgFormatter.isValid(paramFileArg)) { 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; } }