diff --git a/android/apex.go b/android/apex.go index e70ec4f3c..276f7a4bd 100644 --- a/android/apex.go +++ b/android/apex.go @@ -598,36 +598,22 @@ func (d *ApexBundleDepsInfo) BuildDepsInfoLists(ctx ModuleContext, minSdkVersion var fullContent strings.Builder var flatContent strings.Builder - fmt.Fprintf(&fullContent, "%s(minSdkVersion:%s):\\n", ctx.ModuleName(), minSdkVersion) + fmt.Fprintf(&fullContent, "%s(minSdkVersion:%s):\n", ctx.ModuleName(), minSdkVersion) for _, key := range FirstUniqueStrings(SortedStringKeys(depInfos)) { info := depInfos[key] toName := fmt.Sprintf("%s(minSdkVersion:%s)", info.To, info.MinSdkVersion) if info.IsExternal { toName = toName + " (external)" } - fmt.Fprintf(&fullContent, " %s <- %s\\n", toName, strings.Join(SortedUniqueStrings(info.From), ", ")) - fmt.Fprintf(&flatContent, "%s\\n", toName) + fmt.Fprintf(&fullContent, " %s <- %s\n", toName, strings.Join(SortedUniqueStrings(info.From), ", ")) + fmt.Fprintf(&flatContent, "%s\n", toName) } d.fullListPath = PathForModuleOut(ctx, "depsinfo", "fulllist.txt").OutputPath - ctx.Build(pctx, BuildParams{ - Rule: WriteFile, - Description: "Full Dependency Info", - Output: d.fullListPath, - Args: map[string]string{ - "content": fullContent.String(), - }, - }) + WriteFileRule(ctx, d.fullListPath, fullContent.String()) d.flatListPath = PathForModuleOut(ctx, "depsinfo", "flatlist.txt").OutputPath - ctx.Build(pctx, BuildParams{ - Rule: WriteFile, - Description: "Flat Dependency Info", - Output: d.flatListPath, - Args: map[string]string{ - "content": flatContent.String(), - }, - }) + WriteFileRule(ctx, d.flatListPath, flatContent.String()) } // TODO(b/158059172): remove minSdkVersion allowlist diff --git a/android/api_levels.go b/android/api_levels.go index bace3d455..08328e16d 100644 --- a/android/api_levels.go +++ b/android/api_levels.go @@ -225,14 +225,7 @@ func createApiLevelsJson(ctx SingletonContext, file WritablePath, ctx.Errorf(err.Error()) } - ctx.Build(pctx, BuildParams{ - Rule: WriteFile, - Description: "generate " + file.Base(), - Output: file, - Args: map[string]string{ - "content": string(jsonStr[:]), - }, - }) + WriteFileRule(ctx, file, string(jsonStr)) } func GetApiLevelsJson(ctx PathContext) WritablePath { diff --git a/android/defs.go b/android/defs.go index 2b1bd85c9..631dfe8f8 100644 --- a/android/defs.go +++ b/android/defs.go @@ -15,8 +15,12 @@ package android import ( + "strings" + "testing" + "github.com/google/blueprint" _ "github.com/google/blueprint/bootstrap" + "github.com/google/blueprint/proptools" ) var ( @@ -91,9 +95,9 @@ var ( // ubuntu 14.04 offcially use dash for /bin/sh, and its builtin echo command // doesn't support -e option. Therefore we force to use /bin/bash when writing out // content to file. - WriteFile = pctx.AndroidStaticRule("WriteFile", + writeFile = pctx.AndroidStaticRule("writeFile", blueprint.RuleParams{ - Command: "/bin/bash -c 'echo -e $$0 > $out' '$content'", + Command: `/bin/bash -c 'echo -e "$$0" > $out' $content`, Description: "writing file $out", }, "content") @@ -111,3 +115,64 @@ var ( func init() { pctx.Import("github.com/google/blueprint/bootstrap") } + +var ( + // echoEscaper escapes a string such that passing it to "echo -e" will produce the input value. + echoEscaper = strings.NewReplacer( + `\`, `\\`, // First escape existing backslashes so they aren't interpreted by `echo -e`. + "\n", `\n`, // Then replace newlines with \n + ) + + // echoEscaper reverses echoEscaper. + echoUnescaper = strings.NewReplacer( + `\n`, "\n", + `\\`, `\`, + ) + + // shellUnescaper reverses the replacer in proptools.ShellEscape + shellUnescaper = strings.NewReplacer(`'\''`, `'`) +) + +// WriteFileRule creates a ninja rule to write contents to a file. The contents will be escaped +// so that the file contains exactly the contents passed to the function, plus a trailing newline. +func WriteFileRule(ctx BuilderContext, outputFile WritablePath, content string) { + content = echoEscaper.Replace(content) + content = proptools.ShellEscape(content) + if content == "" { + content = "''" + } + ctx.Build(pctx, BuildParams{ + Rule: writeFile, + Output: outputFile, + Description: "write " + outputFile.Base(), + Args: map[string]string{ + "content": content, + }, + }) +} + +// shellUnescape reverses proptools.ShellEscape +func shellUnescape(s string) string { + // Remove leading and trailing quotes if present + if len(s) >= 2 && s[0] == '\'' { + s = s[1 : len(s)-1] + } + s = shellUnescaper.Replace(s) + return s +} + +// ContentFromFileRuleForTests returns the content that was passed to a WriteFileRule for use +// in tests. +func ContentFromFileRuleForTests(t *testing.T, params TestingBuildParams) string { + t.Helper() + if g, w := params.Rule, writeFile; g != w { + t.Errorf("expected params.Rule to be %q, was %q", w, g) + return "" + } + + content := params.Args["content"] + content = shellUnescape(content) + content = echoUnescaper.Replace(content) + + return content +} diff --git a/apex/apex_test.go b/apex/apex_test.go index 532a7aaaf..321883f2c 100644 --- a/apex/apex_test.go +++ b/apex/apex_test.go @@ -5574,7 +5574,7 @@ func TestAppBundle(t *testing.T) { } `, withManifestPackageNameOverrides([]string{"AppFoo:com.android.foo"})) - bundleConfigRule := ctx.ModuleForTests("myapex", "android_common_myapex_image").Description("Bundle Config") + bundleConfigRule := ctx.ModuleForTests("myapex", "android_common_myapex_image").Output("bundle_config.json") content := bundleConfigRule.Args["content"] ensureContains(t, content, `"compression":{"uncompressed_glob":["apex_payload.img","apex_manifest.*"]}`) @@ -5600,7 +5600,7 @@ func TestAppSetBundle(t *testing.T) { set: "AppSet.apks", }`) mod := ctx.ModuleForTests("myapex", "android_common_myapex_image") - bundleConfigRule := mod.Description("Bundle Config") + bundleConfigRule := mod.Output("bundle_config.json") content := bundleConfigRule.Args["content"] ensureContains(t, content, `"compression":{"uncompressed_glob":["apex_payload.img","apex_manifest.*"]}`) s := mod.Rule("apexRule").Args["copy_commands"] diff --git a/apex/builder.go b/apex/builder.go index ad673d656..acfb8c577 100644 --- a/apex/builder.go +++ b/apex/builder.go @@ -373,14 +373,7 @@ func (a *apexBundle) buildBundleConfig(ctx android.ModuleContext) android.Output panic(fmt.Errorf("error while marshalling to %q: %#v", output, err)) } - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Output: output, - Description: "Bundle Config " + output.String(), - Args: map[string]string{ - "content": string(j), - }, - }) + android.WriteFileRule(ctx, output, string(j)) return output.OutputPath } diff --git a/apex/key.go b/apex/key.go index d2d5786b3..43764da59 100644 --- a/apex/key.go +++ b/apex/key.go @@ -116,7 +116,7 @@ func (s *apexKeysText) GenerateBuildActions(ctx android.SingletonContext) { partition string } toString := func(e apexKeyEntry) string { - format := "name=%q public_key=%q private_key=%q container_certificate=%q container_private_key=%q partition=%q\\n" + format := "name=%q public_key=%q private_key=%q container_certificate=%q container_private_key=%q partition=%q\n" if e.presigned { return fmt.Sprintf(format, e.name, "PRESIGNED", "PRESIGNED", "PRESIGNED", "PRESIGNED", e.partition) } else { @@ -173,17 +173,9 @@ func (s *apexKeysText) GenerateBuildActions(ctx android.SingletonContext) { var filecontent strings.Builder for _, name := range moduleNames { - fmt.Fprintf(&filecontent, "%s", toString(apexKeyMap[name])) + filecontent.WriteString(toString(apexKeyMap[name])) } - - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Description: "apexkeys.txt", - Output: s.output, - Args: map[string]string{ - "content": filecontent.String(), - }, - }) + android.WriteFileRule(ctx, s.output, filecontent.String()) } func apexKeysTextFactory() android.Singleton { diff --git a/cc/cc_test.go b/cc/cc_test.go index b803cbaa0..f616cf3a7 100644 --- a/cc/cc_test.go +++ b/cc/cc_test.go @@ -296,8 +296,8 @@ func checkSnapshotExclude(t *testing.T, ctx *android.TestContext, singleton andr func checkWriteFileOutput(t *testing.T, params android.TestingBuildParams, expected []string) { t.Helper() - assertString(t, params.Rule.String(), android.WriteFile.String()) - actual := strings.FieldsFunc(strings.ReplaceAll(params.Args["content"], "\\n", "\n"), func(r rune) bool { return r == '\n' }) + content := android.ContentFromFileRuleForTests(t, params) + actual := strings.FieldsFunc(content, func(r rune) bool { return r == '\n' }) assertArrayString(t, actual, expected) } diff --git a/cc/fuzz.go b/cc/fuzz.go index fe3c12bf5..dddfe9400 100644 --- a/cc/fuzz.go +++ b/cc/fuzz.go @@ -278,14 +278,7 @@ func (fuzz *fuzzBinary) install(ctx ModuleContext, file android.Path) { if fuzz.Properties.Fuzz_config != nil { configPath := android.PathForModuleOut(ctx, "config").Join(ctx, "config.json") - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Description: "fuzzer infrastructure configuration", - Output: configPath, - Args: map[string]string{ - "content": fuzz.Properties.Fuzz_config.String(), - }, - }) + android.WriteFileRule(ctx, configPath, fuzz.Properties.Fuzz_config.String()) fuzz.config = configPath } diff --git a/cc/snapshot_utils.go b/cc/snapshot_utils.go index 238508dc9..05c06ac25 100644 --- a/cc/snapshot_utils.go +++ b/cc/snapshot_utils.go @@ -93,13 +93,6 @@ func combineNotices(ctx android.SingletonContext, paths android.Paths, out strin func writeStringToFile(ctx android.SingletonContext, content, out string) android.OutputPath { outPath := android.PathForOutput(ctx, out) - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Output: outPath, - Description: "WriteFile " + out, - Args: map[string]string{ - "content": content, - }, - }) + android.WriteFileRule(ctx, outPath, content) return outPath } diff --git a/cc/vndk.go b/cc/vndk.go index 2cac03c82..4a005f31f 100644 --- a/cc/vndk.go +++ b/cc/vndk.go @@ -487,14 +487,7 @@ func (txt *vndkLibrariesTxt) GenerateAndroidBuildActions(ctx android.ModuleConte } txt.outputFile = android.PathForModuleOut(ctx, filename).OutputPath - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Output: txt.outputFile, - Description: "Writing " + txt.outputFile.String(), - Args: map[string]string{ - "content": strings.Join(list, "\\n"), - }, - }) + android.WriteFileRule(ctx, txt.outputFile, strings.Join(list, "\n")) installPath := android.PathForModuleInstall(ctx, "etc") ctx.InstallFile(installPath, filename, txt.outputFile) @@ -825,14 +818,7 @@ func (c *vndkSnapshotSingleton) buildVndkLibrariesTxtFiles(ctx android.Singleton merged = append(merged, addPrefix(filterOutLibClangRt(vndkcore), "VNDK-core: ")...) merged = append(merged, addPrefix(vndkprivate, "VNDK-private: ")...) c.vndkLibrariesFile = android.PathForOutput(ctx, "vndk", "vndk.libraries.txt") - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Output: c.vndkLibrariesFile, - Description: "Writing " + c.vndkLibrariesFile.String(), - Args: map[string]string{ - "content": strings.Join(merged, "\\n"), - }, - }) + android.WriteFileRule(ctx, c.vndkLibrariesFile, strings.Join(merged, "\n")) } func (c *vndkSnapshotSingleton) MakeVars(ctx android.MakeVarsContext) { diff --git a/dexpreopt/config.go b/dexpreopt/config.go index 03accc8e1..f77246e1b 100644 --- a/dexpreopt/config.go +++ b/dexpreopt/config.go @@ -488,13 +488,7 @@ func (s *globalSoongConfigSingleton) GenerateBuildActions(ctx android.SingletonC return } - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Output: android.PathForOutput(ctx, "dexpreopt_soong.config"), - Args: map[string]string{ - "content": string(data), - }, - }) + android.WriteFileRule(ctx, android.PathForOutput(ctx, "dexpreopt_soong.config"), string(data)) } func (s *globalSoongConfigSingleton) MakeVars(ctx android.MakeVarsContext) { diff --git a/java/builder.go b/java/builder.go index 3043e46db..cd3524542 100644 --- a/java/builder.go +++ b/java/builder.go @@ -572,14 +572,7 @@ func TransformJetifier(ctx android.ModuleContext, outputFile android.WritablePat } func GenerateMainClassManifest(ctx android.ModuleContext, outputFile android.WritablePath, mainClass string) { - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Description: "manifest", - Output: outputFile, - Args: map[string]string{ - "content": "Main-Class: " + mainClass + "\n", - }, - }) + android.WriteFileRule(ctx, outputFile, "Main-Class: "+mainClass+"\n") } func TransformZipAlign(ctx android.ModuleContext, outputFile android.WritablePath, inputFile android.Path) { diff --git a/java/dexpreopt_bootjars.go b/java/dexpreopt_bootjars.go index 629d34f25..f9975ba5c 100644 --- a/java/dexpreopt_bootjars.go +++ b/java/dexpreopt_bootjars.go @@ -651,14 +651,8 @@ func updatableBcpPackagesRule(ctx android.SingletonContext, image *bootImageConf updatableBcpPackagesName := "updatable-bcp-packages.txt" updatableBcpPackages := image.dir.Join(ctx, updatableBcpPackagesName) - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Output: updatableBcpPackages, - Args: map[string]string{ - // WriteFile automatically adds the last end-of-line. - "content": strings.Join(updatablePackages, "\\n"), - }, - }) + // WriteFileRule automatically adds the last end-of-line. + android.WriteFileRule(ctx, updatableBcpPackages, strings.Join(updatablePackages, "\n")) rule := android.NewRuleBuilder() rule.MissingDeps(missingDeps) @@ -720,13 +714,7 @@ func dumpOatRules(ctx android.SingletonContext, image *bootImageConfig) { func writeGlobalConfigForMake(ctx android.SingletonContext, path android.WritablePath) { data := dexpreopt.GetGlobalConfigRawData(ctx) - ctx.Build(pctx, android.BuildParams{ - Rule: android.WriteFile, - Output: path, - Args: map[string]string{ - "content": string(data), - }, - }) + android.WriteFileRule(ctx, path, string(data)) } // Export paths for default boot image to Make