aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java736
1 files changed, 736 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
new file mode 100644
index 0000000000..3452c3781f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkActionFactory.java
@@ -0,0 +1,736 @@
+// 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.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.actions.RunfilesSupplier;
+import com.google.devtools.build.lib.actions.extra.SpawnInfo;
+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.actions.FileWriteAction;
+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.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.skylarkinterface.Param;
+import com.google.devtools.build.lib.skylarkinterface.ParamType;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
+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.skylarkinterface.SkylarkValue;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.Runtime;
+import com.google.devtools.build.lib.syntax.SkylarkDict;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Provides a Skylark interface for all action creation needs.
+ */
+@SkylarkModule(
+ name = "actions",
+ category = SkylarkModuleCategory.BUILTIN,
+ doc = "Module providing functions to create actions."
+)
+
+public class SkylarkActionFactory implements SkylarkValue {
+ private final SkylarkRuleContext context;
+ private RuleContext ruleContext;
+
+
+ public SkylarkActionFactory(SkylarkRuleContext context, RuleContext ruleContext) {
+ this.context = context;
+ this.ruleContext = ruleContext;
+ }
+
+ Root newFileRoot() throws EvalException {
+ return context.isForAspect()
+ ? ruleContext.getConfiguration().getBinDirectory(ruleContext.getRule().getRepository())
+ : ruleContext.getBinOrGenfilesDirectory();
+ }
+
+ @SkylarkCallable(
+ name = "declare_file",
+ doc =
+ "Declares that rule or aspect creates a file with the given filename. "
+ + "If <code>sibling</code> is not specified, file name is relative to "
+ + "package directory, otherwise the file is in the same directory as "
+ + "<code>sibling</code>. "
+ + "You must create an action that generates the file. <br>"
+ + "Files that are specified in rule's outputs do not need to be declared and are "
+ + "available through <a href=\"ctx.html#outputs\">ctx.outputs</a>.",
+ parameters = {
+ @Param(
+ name = "filename",
+ type = String.class,
+ doc =
+ "If no 'sibling' provided, path of the new file, relative "
+ + "to the current package. Otherwise a base name for a file "
+ + "('sibling' determines a directory)."
+ ),
+ @Param(
+ name = "sibling",
+ doc = "A file that lives in the same directory as the newly created file.",
+ type = Artifact.class,
+ noneable = true,
+ positional = false,
+ named = true,
+ defaultValue = "None"
+ )
+ }
+ )
+ 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());
+ }
+ }
+
+ @SkylarkCallable(
+ name = "declare_directory",
+ doc =
+ "Declares that rule or aspect create a directory with the given name, in the "
+ + "current package. You must create an action that generates the directory.",
+ parameters = {
+ @Param(
+ name = "filename",
+ type = String.class,
+ doc =
+ "If no 'sibling' provided, path of the new directory, relative "
+ + "to the current package. Otherwise a base name for a file "
+ + "('sibling' defines a directory)."
+ ),
+ @Param(
+ name = "sibling",
+ doc = "A file that lives in the same directory as the newly declared directory.",
+ type = Artifact.class,
+ noneable = true,
+ positional = false,
+ named = true,
+ defaultValue = "None"
+ )
+ }
+ )
+ 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());
+ }
+ }
+
+
+ @SkylarkCallable(
+ name = "do_nothing",
+ doc =
+ "Creates an empty action that neither executes a command nor produces any "
+ + "output, but that is useful for inserting 'extra actions'.",
+ parameters = {
+ @Param(
+ name = "mnemonic",
+ type = String.class,
+ named = true,
+ positional = false,
+ doc = "a one-word description of the action, e.g. CppCompile or GoLink."
+ ),
+ @Param(
+ name = "inputs",
+ allowedTypes = {
+ @ParamType(type = SkylarkList.class),
+ @ParamType(type = SkylarkNestedSet.class),
+ },
+ generic1 = Artifact.class,
+ named = true,
+ positional = false,
+ defaultValue = "[]",
+ doc = "list of the input files of the action."
+ ),
+ }
+ )
+ 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);
+ }
+
+ @SkylarkCallable(
+ name = "write",
+ doc =
+ "Creates a file write action. When the action is executed, it will write the given content "
+ + "to a file. This is used to generate files using information available in the "
+ + "analysis phase. If the file is large and with a lot of static content, consider "
+ + "using <a href=\"#expand_template\">expand_template</a>. "
+ + "<a href=\"https://github.com/laurentlb/examples/blob/master/rules/executable/executable.bzl\">"
+ + "See example of use</a>",
+ parameters = {
+ @Param(name = "output", type = Artifact.class, doc = "the output file.", named = true),
+ @Param(
+ name = "content",
+ type = String.class,
+ doc = "the contents of the file.",
+ named = true
+ ),
+ @Param(
+ name = "is_executable",
+ type = Boolean.class,
+ defaultValue = "False",
+ doc = "whether the output file should be executable.",
+ named = true
+ )
+ }
+ )
+ public void write(Artifact output, String content, Boolean isExecutable) throws EvalException {
+ context.checkMutable("actions.write");
+ FileWriteAction action =
+ FileWriteAction.create(ruleContext, output, content, isExecutable);
+ ruleContext.registerAction(action);
+ }
+
+ @SkylarkCallable(
+ name = "run",
+ doc =
+ "Creates an action that runs an executable.",
+ parameters = {
+ @Param(
+ name = "outputs",
+ type = SkylarkList.class,
+ generic1 = Artifact.class,
+ named = true,
+ positional = false,
+ doc = "list of the output files of the action."
+ ),
+ @Param(
+ name = "inputs",
+ allowedTypes = {
+ @ParamType(type = SkylarkList.class),
+ @ParamType(type = SkylarkNestedSet.class),
+ },
+ generic1 = Artifact.class,
+ defaultValue = "[]",
+ named = true,
+ positional = false,
+ doc = "list of the input files of the action."
+ ),
+ @Param(
+ name = "executable",
+ type = Object.class,
+ allowedTypes = {
+ @ParamType(type = Artifact.class),
+ @ParamType(type = String.class),
+ },
+ named = true,
+ positional = false,
+ doc = "the executable file to be called by the action."
+ ),
+ @Param(
+ name = "arguments",
+ type = SkylarkList.class,
+ generic1 = String.class,
+ defaultValue = "[]",
+ named = true,
+ positional = false,
+ doc = "command line arguments of the action."
+ ),
+ @Param(
+ name = "mnemonic",
+ type = String.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc = "a one-word description of the action, e.g. CppCompile or GoLink."
+ ),
+ @Param(
+ name = "progress_message",
+ type = String.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "progress message to show to the user during the build, "
+ + "e.g. \"Compiling foo.cc to create foo.o\"."
+ ),
+ @Param(
+ name = "use_default_shell_env",
+ type = Boolean.class,
+ defaultValue = "False",
+ named = true,
+ positional = false,
+ doc = "whether the action should use the built in shell environment or not."
+ ),
+ @Param(
+ name = "env",
+ type = SkylarkDict.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc = "sets the dictionary of environment variables."
+ ),
+ @Param(
+ name = "execution_requirements",
+ type = SkylarkDict.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "information for scheduling the action. See "
+ + "<a href=\"/docs/be/common-definitions.html#common.tags\">tags</a> "
+ + "for useful keys."
+ ),
+ @Param(
+ // TODO(bazel-team): The name here isn't accurate anymore.
+ // This is technically experimental, so folks shouldn't be too attached,
+ // but consider renaming to be more accurate/opaque.
+ name = "input_manifests",
+ type = SkylarkList.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "(Experimental) sets the input runfiles metadata; "
+ + "they are typically generated by resolve_command."
+ )
+ }
+ )
+ public void run(
+ SkylarkList outputs,
+ Object inputs,
+ Object executableUnchecked,
+ SkylarkList arguments,
+ Object mnemonicUnchecked,
+ Object progressMessage,
+ Boolean useDefaultShellEnv,
+ Object envUnchecked,
+ Object executionRequirementsUnchecked,
+ Object inputManifestsUnchecked)
+ throws EvalException {
+ context.checkMutable("actions.run");
+ SpawnAction.Builder builder = new SpawnAction.Builder();
+
+ @SuppressWarnings("unchecked")
+ List<String> argumentsContents = arguments.getContents(String.class, "arguments");
+
+ builder.addArguments(argumentsContents);
+ 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, mnemonicUnchecked, progressMessage, useDefaultShellEnv,
+ envUnchecked, executionRequirementsUnchecked, inputManifestsUnchecked, builder);
+ }
+
+
+ @SkylarkCallable(
+ name = "run_shell",
+ doc = "Creates an action that runs a shell command.",
+ parameters = {
+ @Param(
+ name = "outputs",
+ type = SkylarkList.class,
+ generic1 = Artifact.class,
+ named = true,
+ positional = false,
+ doc = "list of the output files of the action."
+ ),
+ @Param(
+ name = "inputs",
+ allowedTypes = {
+ @ParamType(type = SkylarkList.class),
+ @ParamType(type = SkylarkNestedSet.class),
+ },
+ generic1 = Artifact.class,
+ defaultValue = "[]",
+ named = true,
+ positional = false,
+ doc = "list of the input files of the action."
+ ),
+ @Param(
+ name = "arguments",
+ type = SkylarkList.class,
+ generic1 = String.class,
+ defaultValue = "[]",
+ named = true,
+ positional = false,
+ doc = "command line arguments of the action."
+ ),
+ @Param(
+ name = "mnemonic",
+ type = String.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc = "a one-word description of the action, e.g. CppCompile or GoLink."
+ ),
+ @Param(
+ name = "command",
+ type = Object.class,
+ allowedTypes = {
+ @ParamType(type = String.class),
+ @ParamType(type = SkylarkList.class, generic1 = String.class),
+ @ParamType(type = Runtime.NoneType.class),
+ },
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "shell command to execute. "
+ + "Arguments are available with <code>$1</code>, <code>$2</code>, etc."
+ ),
+ @Param(
+ name = "progress_message",
+ type = String.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "progress message to show to the user during the build, "
+ + "e.g. \"Compiling foo.cc to create foo.o\"."
+ ),
+ @Param(
+ name = "use_default_shell_env",
+ type = Boolean.class,
+ defaultValue = "False",
+ named = true,
+ positional = false,
+ doc = "whether the action should use the built in shell environment or not."
+ ),
+ @Param(
+ name = "env",
+ type = SkylarkDict.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc = "sets the dictionary of environment variables."
+ ),
+ @Param(
+ name = "execution_requirements",
+ type = SkylarkDict.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "information for scheduling the action. See "
+ + "<a href=\"/docs/be/common-definitions.html#common.tags\">tags</a> "
+ + "for useful keys."
+ ),
+ @Param(
+ // TODO(bazel-team): The name here isn't accurate anymore.
+ // This is technically experimental, so folks shouldn't be too attached,
+ // but consider renaming to be more accurate/opaque.
+ name = "input_manifests",
+ type = SkylarkList.class,
+ noneable = true,
+ defaultValue = "None",
+ named = true,
+ positional = false,
+ doc =
+ "(Experimental) sets the input runfiles metadata; "
+ + "they are typically generated by resolve_command."
+ )
+ }
+ )
+ public void runShell(
+ SkylarkList outputs,
+ Object inputs,
+ SkylarkList arguments,
+ Object mnemonicUnchecked,
+ Object commandUnchecked,
+ Object progressMessage,
+ Boolean useDefaultShellEnv,
+ Object envUnchecked,
+ Object executionRequirementsUnchecked,
+ Object inputManifestsUnchecked)
+ throws EvalException {
+ context.checkMutable("actions.run_shell");
+
+ // TODO(bazel-team): builder still makes unnecessary copies of inputs, outputs and args.
+ SpawnAction.Builder builder = new SpawnAction.Builder();
+ if (arguments.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.addArgument("");
+ }
+
+ @SuppressWarnings("unchecked")
+ List<String> argumentsContents = arguments.getContents(String.class, "arguments");
+ builder.addArguments(argumentsContents);
+
+ if (commandUnchecked instanceof String) {
+ builder.setShellCommand((String) commandUnchecked);
+ } 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));
+ }
+ registerSpawnAction(
+ outputs,
+ inputs,
+ mnemonicUnchecked,
+ progressMessage,
+ useDefaultShellEnv,
+ envUnchecked,
+ executionRequirementsUnchecked,
+ inputManifestsUnchecked,
+ builder);
+ }
+
+ /**
+ * Stup 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 mnemonicUnchecked,
+ Object progressMessage,
+ Boolean useDefaultShellEnv, Object envUnchecked,
+ Object executionRequirementsUnchecked,
+ Object inputManifestsUnchecked, SpawnAction.Builder builder)
+ throws EvalException {
+ // TODO(bazel-team): builder still makes unnecessary copies of inputs, outputs and args.
+ Iterable<Artifact> inputArtifacts;
+ if (inputs instanceof SkylarkList) {
+ inputArtifacts = ((SkylarkList) inputs).getContents(Artifact.class, "inputs");
+ builder.addInputs(inputArtifacts);
+ } else {
+ inputArtifacts = ((SkylarkNestedSet) inputs).toCollection(Artifact.class);
+ builder.addInputs(((SkylarkNestedSet) inputs).getSet(Artifact.class));
+ }
+ builder.addOutputs(outputs.getContents(Artifact.class, "outputs"));
+
+ // The actual command can refer to an executable from the inputs, which could require
+ // some runfiles. Consequently, we add the runfiles of all inputs of this action manually.
+ for (Artifact current : inputArtifacts) {
+ FilesToRunProvider provider = context.getExecutableRunfiles(current);
+ 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(
+ ImmutableMap.copyOf(
+ 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";
+ }
+
+ @SkylarkCallable(
+ name = "expand_template",
+ doc =
+ "Creates a template expansion action. When the action is executed, it will "
+ + "generate a file based on a template. Parts of the template will be replaced "
+ + "using the <code>substitutions</code> dictionary. Whenever a key of the "
+ + "dictionary appears in the template, it is replaced with the associated value. "
+ + "There is no special syntax for the keys. You may for example use curly braces "
+ + "to avoid conflicts (e.g. <code>{KEY}</code>). "
+ + "<a href=\"https://github.com/laurentlb/examples/blob/master/rules/expand_template/hello.bzl\">"
+ + "See example of use</a>",
+ parameters = {
+ @Param(
+ name = "template",
+ type = Artifact.class,
+ named = true,
+ positional = false,
+ doc = "the template file, which is a UTF-8 encoded text file."
+ ),
+ @Param(
+ name = "output",
+ type = Artifact.class,
+ named = true,
+ positional = false,
+ doc = "the output file, which is a UTF-8 encoded text file."
+ ),
+ @Param(
+ name = "substitutions",
+ type = SkylarkDict.class,
+ named = true,
+ positional = false,
+ doc = "substitutions to make when expanding the template."
+ ),
+ @Param(
+ name = "is_executable",
+ type = Boolean.class,
+ defaultValue = "False",
+ named = true,
+ positional = false,
+ doc = "whether the output file should be executable."
+ )
+ }
+ )
+ public void expandTemplate(
+ Artifact template,
+ Artifact 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(),
+ template,
+ 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);
+ }
+
+
+
+ @Override
+ public boolean isImmutable() {
+ return context.isImmutable();
+ }
+
+ @Override
+ public void repr(SkylarkPrinter printer) {
+ printer.append("actions for");
+ context.repr(printer);
+ }
+
+ void nullify() {
+ ruleContext = null;
+ }
+}