diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/TemplateDict.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/TemplateDict.java
index 153ef175f342c5..6d0001f3d3db95 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/TemplateDict.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/TemplateDict.java
@@ -29,6 +29,7 @@
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Mutability;
+import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkCallable;
import net.starlark.java.eval.StarlarkFunction;
@@ -134,11 +135,22 @@ public String getValue() throws EvalException {
/*kwargs=*/ ImmutableMap.of());
if (ret instanceof String) {
parts.add((String) ret);
+ } else if (ret instanceof Sequence) {
+ for (Object v : ((Sequence) ret)) {
+ if (!(v instanceof String)) {
+ throw Starlark.errorf(
+ "Function provided to map_each must return string, None, or list of strings,"
+ + " but returned list containing element '%s' of type %s for key '%s' and"
+ + " value: %s",
+ v, Starlark.type(v), getKey(), val);
+ }
+ parts.add((String) v);
+ }
} else if (ret != Starlark.NONE) {
throw Starlark.errorf(
- "Function provided to map_each must return a String or None, but returned type "
- + "%s for key '%s' and value: %s",
- Starlark.type(ret), getKey(), Starlark.repr(val));
+ "Function provided to map_each must return string, None, or list of strings, but "
+ + "returned type %s for key '%s' and value: %s",
+ Starlark.type(ret), getKey(), val);
}
} catch (InterruptedException e) {
// Report the error to the user, but the stack trace is not of use to them
diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/TemplateDictApi.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/TemplateDictApi.java
index e129f5e5fb740c..e7315c4016516c 100644
--- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/TemplateDictApi.java
+++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/TemplateDictApi.java
@@ -71,9 +71,9 @@ public interface TemplateDictApi extends StarlarkValue {
named = true,
positional = false,
doc =
- "A Starlark function accepting a single argument and returning either a String or "
- + "None
. This function is applied to each item of the depset "
- + "specified in the values
parameter"),
+ "A Starlark function accepting a single argument and returning either a string, "
+ + "None
, or a list of strings. This function is applied to each "
+ + "item of the depset specified in the values
parameter"),
@Param(
name = "uniquify",
named = true,
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java
index c1b785cc055e12..c451fe1c16cc84 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java
@@ -3741,6 +3741,68 @@ public void testTemplateExpansionComputedSubstitutionWithUniquify() throws Excep
assertThat(substitutionsUnchecked).isEqualTo(ImmutableMap.of("exts", "txt%%log%%exe%%sh"));
}
+ @Test
+ public void testTemplateExpansionComputedSubstitutionWithUniquifyAndListMapEach()
+ throws Exception {
+ setBuildLanguageOptions("--experimental_lazy_template_expansion");
+ scratch.file(
+ "test/rules.bzl",
+ "def _artifact_to_extension(file):",
+ " if file.extension == 'sh':",
+ " return [file.extension]",
+ " return [file.extension, '.' + file.extension]",
+ "",
+ "def _undertest_impl(ctx):",
+ " template_dict = ctx.actions.template_dict()",
+ " template_dict.add_joined('exts', depset(ctx.files.srcs),",
+ " map_each = _artifact_to_extension,",
+ " uniquify = True,",
+ " join_with = '%%',",
+ " )",
+ " ctx.actions.expand_template(output=ctx.outputs.out,",
+ " template=ctx.file.template,",
+ " computed_substitutions=template_dict,",
+ " )",
+ "undertest_rule = rule(",
+ " implementation = _undertest_impl,",
+ " outputs = {'out': '%{name}.txt'},",
+ " attrs = {'template': attr.label(allow_single_file=True),",
+ " 'srcs':attr.label_list(allow_files=True)",
+ " },",
+ " _skylark_testable = True,",
+ ")",
+ testingRuleDefinition);
+ scratch.file("test/template.txt", "exts", "exts");
+ scratch.file(
+ "test/BUILD",
+ "load(':rules.bzl', 'undertest_rule', 'testing_rule')",
+ "undertest_rule(",
+ " name = 'undertest',",
+ " template = ':template.txt',",
+ " srcs = ['foo.txt', 'bar.log', 'baz.txt', 'bak.exe', 'far.sh', 'boo.sh'],",
+ ")",
+ "testing_rule(",
+ " name = 'testing',",
+ " dep = ':undertest',",
+ ")");
+ StarlarkRuleContext ruleContext = createRuleContext("//test:testing");
+ setRuleContext(ruleContext);
+ ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]"));
+ ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]"));
+
+ assertThat(ev.eval("type(action)")).isEqualTo("Action");
+
+ Object contentUnchecked = ev.eval("action.content");
+ assertThat(contentUnchecked).isInstanceOf(String.class);
+ assertThat(contentUnchecked)
+ .isEqualTo("txt%%.txt%%log%%.log%%exe%%.exe%%sh\ntxt%%.txt%%log%%.log%%exe%%.exe%%sh\n");
+
+ Object substitutionsUnchecked = ev.eval("action.substitutions");
+ assertThat(substitutionsUnchecked).isInstanceOf(Dict.class);
+ assertThat(substitutionsUnchecked)
+ .isEqualTo(ImmutableMap.of("exts", "txt%%.txt%%log%%.log%%exe%%.exe%%sh"));
+ }
+
@Test
public void testTemplateExpansionComputedSubstitutionDuplicateKeys() throws Exception {
setBuildLanguageOptions("--experimental_lazy_template_expansion");
@@ -3913,8 +3975,60 @@ public void testTemplateExpansionComputedSubstitutionMapEachBadReturnType() thro
assertThat(evalException)
.hasMessageThat()
.isEqualTo(
- "Function provided to map_each must return a String or None, but returned "
- + "type Label for key '%files%' and value: