genrule: add $(location) for inputs and outputs

There was no way to select a single source file from a genrule that
has multiple source files.  Make Soong's genrule closer to Bazel
by supporting $(location) for input and outputs.  Change the
label used for tools referenced through filegroups to the name
of the module instead of the name of the file, which means it
matches the string used in the tools property.

Also support :module format in the tools property in preparation
for deprecating the tool_files property and putting both files
and modules in the tools property.

Bug: 117354232
Test: genrule_test.go
Change-Id: Id31a4ac4ff7064076a576c1d8ffeb7c19fc6b9a4
Merged-In: Id31a4ac4ff7064076a576c1d8ffeb7c19fc6b9a4
(cherry picked from commit 098d22b5f6d965872b524ce798ce728580ce405c)
diff --git a/genrule/genrule.go b/genrule/genrule.go
index f19e2aa..2824e49 100644
--- a/genrule/genrule.go
+++ b/genrule/genrule.go
@@ -52,10 +52,9 @@
 
 type hostToolDependencyTag struct {
 	blueprint.BaseDependencyTag
+	label string
 }
 
-var hostToolDepTag hostToolDependencyTag
-
 type generatorProperties struct {
 	// The command to run on one or more input files. Cmd supports substitution of a few variables
 	// (the actual substitution is implemented in GenerateAndroidBuildActions below)
@@ -63,7 +62,7 @@
 	// Available variables for substitution:
 	//
 	//  $(location): the path to the first entry in tools or tool_files
-	//  $(location <label>): the path to the tool or tool_file with name <label>
+	//  $(location <label>): the path to the tool, tool_file, input or output with name <label>
 	//  $(in): one or more input files
 	//  $(out): a single output file
 	//  $(depfile): a file to which dependencies will be written, if the depfile property is set to true
@@ -141,10 +140,14 @@
 	android.ExtractSourcesDeps(ctx, g.properties.Srcs)
 	android.ExtractSourcesDeps(ctx, g.properties.Tool_files)
 	if g, ok := ctx.Module().(*Module); ok {
-		if len(g.properties.Tools) > 0 {
+		for _, tool := range g.properties.Tools {
+			tag := hostToolDependencyTag{label: tool}
+			if m := android.SrcIsModule(tool); m != "" {
+				tool = m
+			}
 			ctx.AddFarVariationDependencies([]blueprint.Variation{
 				{Mutator: "arch", Variation: ctx.Config().BuildOsVariant},
-			}, hostToolDepTag, g.properties.Tools...)
+			}, tag, tool)
 		}
 	}
 }
@@ -159,12 +162,25 @@
 		g.exportedIncludeDirs = append(g.exportedIncludeDirs, android.PathForModuleGen(ctx, ""))
 	}
 
-	tools := map[string]android.Path{}
+	locationLabels := map[string][]string{}
+	firstLabel := ""
+
+	addLocationLabel := func(label string, paths []string) {
+		if firstLabel == "" {
+			firstLabel = label
+		}
+		if _, exists := locationLabels[label]; !exists {
+			locationLabels[label] = paths
+		} else {
+			ctx.ModuleErrorf("multiple labels for %q, %q and %q",
+				label, strings.Join(locationLabels[label], " "), strings.Join(paths, " "))
+		}
+	}
 
 	if len(g.properties.Tools) > 0 {
 		ctx.VisitDirectDepsBlueprint(func(module blueprint.Module) {
-			switch ctx.OtherModuleDependencyTag(module) {
-			case hostToolDepTag:
+			switch tag := ctx.OtherModuleDependencyTag(module).(type) {
+			case hostToolDependencyTag:
 				tool := ctx.OtherModuleName(module)
 				var path android.OptionalPath
 
@@ -192,11 +208,7 @@
 
 				if path.Valid() {
 					g.deps = append(g.deps, path.Path())
-					if _, exists := tools[tool]; !exists {
-						tools[tool] = path.Path()
-					} else {
-						ctx.ModuleErrorf("multiple tools for %q, %q and %q", tool, tools[tool], path.Path().String())
-					}
+					addLocationLabel(tag.label, []string{path.Path().String()})
 				} else {
 					ctx.ModuleErrorf("host tool %q missing output file", tool)
 				}
@@ -208,21 +220,27 @@
 		return
 	}
 
-	toolFiles := ctx.ExpandSources(g.properties.Tool_files, nil)
-	for _, tool := range toolFiles {
-		g.deps = append(g.deps, tool)
-		if _, exists := tools[tool.Rel()]; !exists {
-			tools[tool.Rel()] = tool
-		} else {
-			ctx.ModuleErrorf("multiple tools for %q, %q and %q", tool, tools[tool.Rel()], tool.Rel())
-		}
+	for _, toolFile := range g.properties.Tool_files {
+		paths := ctx.ExpandSources([]string{toolFile}, nil)
+		g.deps = append(g.deps, paths...)
+		addLocationLabel(toolFile, paths.Strings())
+	}
+
+	var srcFiles android.Paths
+	for _, in := range g.properties.Srcs {
+		paths := ctx.ExpandSources([]string{in}, nil)
+		srcFiles = append(srcFiles, paths...)
+		addLocationLabel(in, paths.Strings())
+	}
+
+	task := g.taskGenerator(ctx, String(g.properties.Cmd), srcFiles)
+
+	for _, out := range task.out {
+		addLocationLabel(out.Rel(), []string{filepath.Join("__SBOX_OUT_DIR__", out.Rel())})
 	}
 
 	referencedDepfile := false
 
-	srcFiles := ctx.ExpandSources(g.properties.Srcs, nil)
-	task := g.taskGenerator(ctx, String(g.properties.Cmd), srcFiles)
-
 	rawCommand, err := android.Expand(task.cmd, func(name string) (string, error) {
 		// report the error directly without returning an error to android.Expand to catch multiple errors in a
 		// single run
@@ -233,13 +251,17 @@
 
 		switch name {
 		case "location":
-			if len(g.properties.Tools) == 0 && len(toolFiles) == 0 {
+			if len(g.properties.Tools) == 0 && len(g.properties.Tool_files) == 0 {
 				return reportError("at least one `tools` or `tool_files` is required if $(location) is used")
-			} else if len(g.properties.Tools) > 0 {
-				return tools[g.properties.Tools[0]].String(), nil
-			} else {
-				return tools[toolFiles[0].Rel()].String(), nil
 			}
+			paths := locationLabels[firstLabel]
+			if len(paths) == 0 {
+				return reportError("default label %q has no files", firstLabel)
+			} else if len(paths) > 1 {
+				return reportError("default label %q has multiple files, use $(locations %s) to reference it",
+					firstLabel, firstLabel)
+			}
+			return locationLabels[firstLabel][0], nil
 		case "in":
 			return "${in}", nil
 		case "out":
@@ -255,13 +277,30 @@
 		default:
 			if strings.HasPrefix(name, "location ") {
 				label := strings.TrimSpace(strings.TrimPrefix(name, "location "))
-				if tool, ok := tools[label]; ok {
-					return tool.String(), nil
+				if paths, ok := locationLabels[label]; ok {
+					if len(paths) == 0 {
+						return reportError("label %q has no files", label)
+					} else if len(paths) > 1 {
+						return reportError("label %q has multiple files, use $(locations %s) to reference it",
+							label, label)
+					}
+					return paths[0], nil
 				} else {
 					return reportError("unknown location label %q", label)
 				}
+			} else if strings.HasPrefix(name, "locations ") {
+				label := strings.TrimSpace(strings.TrimPrefix(name, "locations "))
+				if paths, ok := locationLabels[label]; ok {
+					if len(paths) == 0 {
+						return reportError("label %q has no files", label)
+					}
+					return strings.Join(paths, " "), nil
+				} else {
+					return reportError("unknown locations label %q", label)
+				}
+			} else {
+				return reportError("unknown variable '$(%s)'", name)
 			}
-			return reportError("unknown variable '$(%s)'", name)
 		}
 	})
 
diff --git a/genrule/genrule_test.go b/genrule/genrule_test.go
index 0d690d4..7e16ce1 100644
--- a/genrule/genrule_test.go
+++ b/genrule/genrule_test.go
@@ -133,6 +133,15 @@
 			expect: "out/tool > __SBOX_OUT_FILES__",
 		},
 		{
+			name: "empty location tool2",
+			prop: `
+				tools: [":tool"],
+				out: ["out"],
+				cmd: "$(location) > $(out)",
+			`,
+			expect: "out/tool > __SBOX_OUT_FILES__",
+		},
+		{
 			name: "empty location tool file",
 			prop: `
 				tool_files: ["tool_file1"],
@@ -170,6 +179,15 @@
 			expect: "out/tool > __SBOX_OUT_FILES__",
 		},
 		{
+			name: "tool2",
+			prop: `
+				tools: [":tool"],
+				out: ["out"],
+				cmd: "$(location :tool) > $(out)",
+			`,
+			expect: "out/tool > __SBOX_OUT_FILES__",
+		},
+		{
 			name: "tool file",
 			prop: `
 				tool_files: ["tool_file1"],
@@ -183,7 +201,7 @@
 			prop: `
 				tool_files: [":1tool_file"],
 				out: ["out"],
-				cmd: "$(location tool_file1) > $(out)",
+				cmd: "$(location :1tool_file) > $(out)",
 			`,
 			expect: "tool_file1 > __SBOX_OUT_FILES__",
 		},
@@ -192,7 +210,7 @@
 			prop: `
 				tool_files: [":tool_files"],
 				out: ["out"],
-				cmd: "$(location tool_file1) $(location tool_file2) > $(out)",
+				cmd: "$(locations :tool_files) > $(out)",
 			`,
 			expect: "tool_file1 tool_file2 > __SBOX_OUT_FILES__",
 		},
@@ -233,6 +251,42 @@
 			expect: "cat ${in} > __SBOX_OUT_FILES__",
 		},
 		{
+			name: "location in1",
+			prop: `
+				srcs: ["in1"],
+				out: ["out"],
+				cmd: "cat $(location in1) > $(out)",
+			`,
+			expect: "cat in1 > __SBOX_OUT_FILES__",
+		},
+		{
+			name: "location in1 fg",
+			prop: `
+				srcs: [":1in"],
+				out: ["out"],
+				cmd: "cat $(location :1in) > $(out)",
+			`,
+			expect: "cat in1 > __SBOX_OUT_FILES__",
+		},
+		{
+			name: "location ins",
+			prop: `
+				srcs: ["in1", "in2"],
+				out: ["out"],
+				cmd: "cat $(location in1) > $(out)",
+			`,
+			expect: "cat in1 > __SBOX_OUT_FILES__",
+		},
+		{
+			name: "location ins fg",
+			prop: `
+				srcs: [":ins"],
+				out: ["out"],
+				cmd: "cat $(locations :ins) > $(out)",
+			`,
+			expect: "cat in1 in2 > __SBOX_OUT_FILES__",
+		},
+		{
 			name: "outs",
 			prop: `
 				out: ["out", "out2"],
@@ -241,6 +295,14 @@
 			expect: "echo foo > __SBOX_OUT_FILES__",
 		},
 		{
+			name: "location out",
+			prop: `
+				out: ["out", "out2"],
+				cmd: "echo foo > $(location out2)",
+			`,
+			expect: "echo foo > __SBOX_OUT_DIR__/out2",
+		},
+		{
 			name: "depfile",
 			prop: `
 				out: ["out"],
@@ -267,6 +329,24 @@
 			err: "at least one `tools` or `tool_files` is required if $(location) is used",
 		},
 		{
+			name: "error empty location no files",
+			prop: `
+				tool_files: [":empty"],
+				out: ["out"],
+				cmd: "$(location) > $(out)",
+			`,
+			err: `default label ":empty" has no files`,
+		},
+		{
+			name: "error empty location multiple files",
+			prop: `
+				tool_files: [":tool_files"],
+				out: ["out"],
+				cmd: "$(location) > $(out)",
+			`,
+			err: `default label ":tool_files" has multiple files`,
+		},
+		{
 			name: "error location",
 			prop: `
 				out: ["out"],
@@ -275,6 +355,41 @@
 			err: `unknown location label "missing"`,
 		},
 		{
+			name: "error locations",
+			prop: `
+					out: ["out"],
+					cmd: "echo foo > $(locations missing)",
+			`,
+			err: `unknown locations label "missing"`,
+		},
+		{
+			name: "error location no files",
+			prop: `
+					out: ["out"],
+					srcs: [":empty"],
+					cmd: "echo $(location :empty) > $(out)",
+			`,
+			err: `label ":empty" has no files`,
+		},
+		{
+			name: "error locations no files",
+			prop: `
+					out: ["out"],
+					srcs: [":empty"],
+					cmd: "echo $(locations :empty) > $(out)",
+			`,
+			err: `label ":empty" has no files`,
+		},
+		{
+			name: "error location multiple files",
+			prop: `
+					out: ["out"],
+					srcs: [":ins"],
+					cmd: "echo $(location :ins) > $(out)",
+			`,
+			err: `label ":ins" has multiple files`,
+		},
+		{
 			name: "error variable",
 			prop: `
 					out: ["out"],