Add RuleBuilder.Installs().String()

Add a RuleBuilderInstalls type for a slice of RuleBuilderInstalls,
and give it a String() method that returns the list of installs
in the format that is convenient for passing to Make.

Test: rule_builder_test.go
Change-Id: I2e9cd9abf4dfb0ad312d0a6662f1567baf9cd222
diff --git a/android/rule_builder.go b/android/rule_builder.go
index 468b617..3b86947 100644
--- a/android/rule_builder.go
+++ b/android/rule_builder.go
@@ -28,7 +28,7 @@
 // graph.
 type RuleBuilder struct {
 	commands       []*RuleBuilderCommand
-	installs       []RuleBuilderInstall
+	installs       RuleBuilderInstalls
 	temporariesSet map[string]bool
 	restat         bool
 	missingDeps    []string
@@ -46,6 +46,23 @@
 	From, To string
 }
 
+type RuleBuilderInstalls []RuleBuilderInstall
+
+// String returns the RuleBuilderInstalls in the form used by $(call copy-many-files) in Make, a space separated
+// list of from:to tuples.
+func (installs RuleBuilderInstalls) String() string {
+	sb := strings.Builder{}
+	for i, install := range installs {
+		if i != 0 {
+			sb.WriteRune(' ')
+		}
+		sb.WriteString(install.From)
+		sb.WriteRune(':')
+		sb.WriteString(install.To)
+	}
+	return sb.String()
+}
+
 // MissingDeps adds modules to the list of missing dependencies.  If MissingDeps
 // is called with a non-empty input, any call to Build will result in a rule
 // that will print an error listing the missing dependencies and fail.
@@ -145,8 +162,8 @@
 }
 
 // Installs returns the list of tuples passed to Install.
-func (r *RuleBuilder) Installs() []RuleBuilderInstall {
-	return append([]RuleBuilderInstall(nil), r.installs...)
+func (r *RuleBuilder) Installs() RuleBuilderInstalls {
+	return append(RuleBuilderInstalls(nil), r.installs...)
 }
 
 func (r *RuleBuilder) toolsSet() map[string]bool {
diff --git a/android/rule_builder_test.go b/android/rule_builder_test.go
index 53a5b48..f947348 100644
--- a/android/rule_builder_test.go
+++ b/android/rule_builder_test.go
@@ -84,6 +84,19 @@
 	// outputs: ["c"]
 }
 
+func ExampleRuleBuilder_Installs() {
+	rule := NewRuleBuilder()
+
+	rule.Command().Tool("ld").Inputs([]string{"a.o", "b.o"}).FlagWithOutput("-o ", "linked")
+	rule.Install("linked", "/bin/linked")
+	rule.Install("linked", "/sbin/linked")
+
+	fmt.Printf("rule.Installs().String() = %q\n", rule.Installs().String())
+
+	// Output:
+	// rule.Installs().String() = "linked:/bin/linked linked:/sbin/linked"
+}
+
 func ExampleRuleBuilderCommand() {
 	rule := NewRuleBuilder()
 
diff --git a/dexpreopt/dexpreopt_test.go b/dexpreopt/dexpreopt_test.go
index ecaf876..40c694f 100644
--- a/dexpreopt/dexpreopt_test.go
+++ b/dexpreopt/dexpreopt_test.go
@@ -100,7 +100,7 @@
 		t.Error(err)
 	}
 
-	wantInstalls := []android.RuleBuilderInstall{
+	wantInstalls := android.RuleBuilderInstalls{
 		{"out/test/oat/arm/package.odex", "/system/app/test/oat/arm/test.odex"},
 		{"out/test/oat/arm/package.vdex", "/system/app/test/oat/arm/test.vdex"},
 	}
@@ -141,7 +141,7 @@
 		t.Error(err)
 	}
 
-	wantInstalls := []android.RuleBuilderInstall{
+	wantInstalls := android.RuleBuilderInstalls{
 		{"out/test/oat/arm/package.odex", "/system_other/app/test/oat/arm/test.odex"},
 		{"out/test/oat/arm/package.vdex", "/system_other/app/test/oat/arm/test.vdex"},
 	}
@@ -164,7 +164,7 @@
 		t.Error(err)
 	}
 
-	wantInstalls := []android.RuleBuilderInstall{
+	wantInstalls := android.RuleBuilderInstalls{
 		{"out/test/profile.prof", "/system/app/test/test.apk.prof"},
 		{"out/test/oat/arm/package.art", "/system/app/test/oat/arm/test.art"},
 		{"out/test/oat/arm/package.odex", "/system/app/test/oat/arm/test.odex"},
diff --git a/java/androidmk.go b/java/androidmk.go
index d86e71f..04b328d 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -65,7 +65,7 @@
 					fmt.Fprintln(w, "LOCAL_SOONG_DEX_JAR :=", library.dexJarFile.String())
 				}
 				if len(library.dexpreopter.builtInstalled) > 0 {
-					fmt.Fprintln(w, "LOCAL_SOONG_BUILT_INSTALLED :=", strings.Join(library.dexpreopter.builtInstalled, " "))
+					fmt.Fprintln(w, "LOCAL_SOONG_BUILT_INSTALLED :=", library.dexpreopter.builtInstalled)
 				}
 				fmt.Fprintln(w, "LOCAL_SDK_VERSION :=", library.sdkVersion())
 				fmt.Fprintln(w, "LOCAL_SOONG_CLASSES_JAR :=", library.implementationAndResourcesJar.String())
@@ -166,7 +166,7 @@
 						fmt.Fprintln(w, "LOCAL_SOONG_DEX_JAR :=", binary.dexJarFile.String())
 					}
 					if len(binary.dexpreopter.builtInstalled) > 0 {
-						fmt.Fprintln(w, "LOCAL_SOONG_BUILT_INSTALLED :=", strings.Join(binary.dexpreopter.builtInstalled, " "))
+						fmt.Fprintln(w, "LOCAL_SOONG_BUILT_INSTALLED :=", binary.dexpreopter.builtInstalled)
 					}
 				},
 			},
@@ -260,7 +260,7 @@
 					fmt.Fprintln(w, "LOCAL_SOONG_JNI_LIBS_"+jniLib.target.Arch.ArchType.String(), "+=", jniLib.name)
 				}
 				if len(app.dexpreopter.builtInstalled) > 0 {
-					fmt.Fprintln(w, "LOCAL_SOONG_BUILT_INSTALLED :=", strings.Join(app.dexpreopter.builtInstalled, " "))
+					fmt.Fprintln(w, "LOCAL_SOONG_BUILT_INSTALLED :=", app.dexpreopter.builtInstalled)
 				}
 			},
 		},
diff --git a/java/dexpreopt.go b/java/dexpreopt.go
index a89731a..127deab 100644
--- a/java/dexpreopt.go
+++ b/java/dexpreopt.go
@@ -28,7 +28,7 @@
 	isTest          bool
 	isInstallable   bool
 
-	builtInstalled []string
+	builtInstalled string
 }
 
 type DexpreoptProperties struct {
@@ -196,9 +196,7 @@
 
 	dexpreoptRule.Build(pctx, ctx, "dexpreopt", "dexpreopt")
 
-	for _, install := range dexpreoptRule.Installs() {
-		d.builtInstalled = append(d.builtInstalled, install.From+":"+install.To)
-	}
+	d.builtInstalled = dexpreoptRule.Installs().String()
 
 	stripRule, err := dexpreopt.GenerateStripRule(globalConfig, dexpreoptConfig)
 	if err != nil {