Write raw files to disk instead of the ninja file

Writing raw files as rules in the ninja file unnecessarily bloats
the ninja file.  Write files immediately to disk instead to files
based on the hash of the contents, and then emit ninja rules to
copy the files into place during the build.  Delete obsolete files
in a singleton at the end of analysis.

Bug: 306029038
Test: Run: m libc_musl_version.h
           touch build/soong/Android.bp
           m libc_musl_version.h
      libc_musl_version.h/genrule.sbox.textproto is not recopied.
Test: Run: lunch aosp_cf_x86_64_phone-userdebug
           m libc_musl_version.h
	   lunch aosp_x86_64-userdebug
	   m libc_musl_version.h
	   lunch aosp_cf_x86_64_phone-userdebug
	   m libc_musl_version.h
      libc_musl_version.h/genrule.sbox.textproto is recopied but restat prevents rerunning the genrule.
Test: Run: touch out/soong/raw-aosp_cf_x86_64_phone/00/foo
           touch build/soong/Android.bp
	   m nothing
      out/soong/raw-aosp_cf_x86_64_phone/00/foo is removed.
Change-Id: I172869c4d49565504794c051e2e8c1f7cf46486e
This commit is contained in:
Colin Cross 2023-11-02 16:57:08 -07:00
parent 51428c451a
commit 31a674571e
12 changed files with 340 additions and 144 deletions

View file

@ -80,6 +80,7 @@ bootstrap_go_package {
"prebuilt_build_tool.go",
"proto.go",
"provider.go",
"raw_files.go",
"register.go",
"rule_builder.go",
"sandbox.go",

View file

@ -18,6 +18,7 @@ package android
// product variables necessary for soong_build's operation.
import (
"android/soong/shared"
"encoding/json"
"fmt"
"os"
@ -118,6 +119,11 @@ func (c Config) SoongOutDir() string {
return c.soongOutDir
}
// tempDir returns the path to out/soong/.temp, which is cleared at the beginning of every build.
func (c Config) tempDir() string {
return shared.TempDirForOutDir(c.soongOutDir)
}
func (c Config) OutDir() string {
return c.outDir
}

View file

@ -15,13 +15,8 @@
package android
import (
"fmt"
"strings"
"testing"
"github.com/google/blueprint"
"github.com/google/blueprint/bootstrap"
"github.com/google/blueprint/proptools"
)
var (
@ -72,8 +67,7 @@ var (
Command: "if ! cmp -s $in $out; then cp $in $out; fi",
Description: "cp if changed $out",
Restat: true,
},
"cpFlags")
})
CpExecutable = pctx.AndroidStaticRule("CpExecutable",
blueprint.RuleParams{
@ -146,106 +140,6 @@ func BazelCcToolchainVars(config Config) string {
return BazelToolchainVars(config, exportedVars)
}
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(`'\''`, `'`)
)
func buildWriteFileRule(ctx BuilderContext, outputFile WritablePath, content string) {
content = echoEscaper.Replace(content)
content = proptools.NinjaEscape(proptools.ShellEscapeIncludingSpaces(content))
if content == "" {
content = "''"
}
ctx.Build(pctx, BuildParams{
Rule: writeFile,
Output: outputFile,
Description: "write " + outputFile.Base(),
Args: map[string]string{
"content": content,
},
})
}
// 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) {
WriteFileRuleVerbatim(ctx, outputFile, content+"\n")
}
// WriteFileRuleVerbatim 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.
func WriteFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string) {
// This is MAX_ARG_STRLEN subtracted with some safety to account for shell escapes
const SHARD_SIZE = 131072 - 10000
if len(content) > SHARD_SIZE {
var chunks WritablePaths
for i, c := range ShardString(content, SHARD_SIZE) {
tempPath := outputFile.ReplaceExtension(ctx, fmt.Sprintf("%s.%d", outputFile.Ext(), i))
buildWriteFileRule(ctx, tempPath, c)
chunks = append(chunks, tempPath)
}
ctx.Build(pctx, BuildParams{
Rule: Cat,
Inputs: chunks.Paths(),
Output: outputFile,
Description: "Merging to " + outputFile.Base(),
})
return
}
buildWriteFileRule(ctx, outputFile, content)
}
// WriteExecutableFileRuleVerbatim is the same as WriteFileRuleVerbatim, but runs chmod +x on the result
func WriteExecutableFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string) {
intermediate := PathForIntermediates(ctx, "write_executable_file_intermediates").Join(ctx, outputFile.String())
WriteFileRuleVerbatim(ctx, intermediate, content)
ctx.Build(pctx, BuildParams{
Rule: CpExecutable,
Output: outputFile,
Input: intermediate,
})
}
// 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, ctx *TestContext, 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
}
// GlobToListFileRule creates a rule that writes a list of files matching a pattern to a file.
func GlobToListFileRule(ctx ModuleContext, pattern string, excludes []string, file WritablePath) {
bootstrap.GlobFile(ctx.blueprintModuleContext(), pattern, excludes, file.String())

279
android/raw_files.go Normal file
View file

@ -0,0 +1,279 @@
// Copyright 2023 Google Inc. 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 android
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"github.com/google/blueprint"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/blueprint/proptools"
)
// WriteFileRule creates a ninja rule to write contents to a file by immediately writing the
// contents, plus a trailing newline, to a file in out/soong/raw-${TARGET_PRODUCT}, and then creating
// a ninja rule to copy the file into place.
func WriteFileRule(ctx BuilderContext, outputFile WritablePath, content string) {
writeFileRule(ctx, outputFile, content, true, false)
}
// WriteFileRuleVerbatim creates a ninja rule to write contents to a file by immediately writing the
// contents to a file in out/soong/raw-${TARGET_PRODUCT}, and then creating a ninja rule to copy the file into place.
func WriteFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string) {
writeFileRule(ctx, outputFile, content, false, false)
}
// WriteExecutableFileRuleVerbatim is the same as WriteFileRuleVerbatim, but runs chmod +x on the result
func WriteExecutableFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string) {
writeFileRule(ctx, outputFile, content, false, true)
}
// tempFile provides a testable wrapper around a file in out/soong/.temp. It writes to a temporary file when
// not in tests, but writes to a buffer in memory when used in tests.
type tempFile struct {
// tempFile contains wraps an io.Writer, which will be file if testMode is false, or testBuf if it is true.
io.Writer
file *os.File
testBuf *strings.Builder
}
func newTempFile(ctx BuilderContext, pattern string, testMode bool) *tempFile {
if testMode {
testBuf := &strings.Builder{}
return &tempFile{
Writer: testBuf,
testBuf: testBuf,
}
} else {
f, err := os.CreateTemp(absolutePath(ctx.Config().tempDir()), pattern)
if err != nil {
panic(fmt.Errorf("failed to open temporary raw file: %w", err))
}
return &tempFile{
Writer: f,
file: f,
}
}
}
func (t *tempFile) close() error {
if t.file != nil {
return t.file.Close()
}
return nil
}
func (t *tempFile) name() string {
if t.file != nil {
return t.file.Name()
}
return "temp_file_in_test"
}
func (t *tempFile) rename(to string) {
if t.file != nil {
os.MkdirAll(filepath.Dir(to), 0777)
err := os.Rename(t.file.Name(), to)
if err != nil {
panic(fmt.Errorf("failed to rename %s to %s: %w", t.file.Name(), to, err))
}
}
}
func (t *tempFile) remove() error {
if t.file != nil {
return os.Remove(t.file.Name())
}
return nil
}
func writeContentToTempFileAndHash(ctx BuilderContext, content string, newline bool) (*tempFile, string) {
tempFile := newTempFile(ctx, "raw", ctx.Config().captureBuild)
defer tempFile.close()
hash := sha1.New()
w := io.MultiWriter(tempFile, hash)
_, err := io.WriteString(w, content)
if err == nil && newline {
_, err = io.WriteString(w, "\n")
}
if err != nil {
panic(fmt.Errorf("failed to write to temporary raw file %s: %w", tempFile.name(), err))
}
return tempFile, hex.EncodeToString(hash.Sum(nil))
}
func writeFileRule(ctx BuilderContext, outputFile WritablePath, content string, newline bool, executable bool) {
// Write the contents to a temporary file while computing its hash.
tempFile, hash := writeContentToTempFileAndHash(ctx, content, newline)
// Shard the final location of the raw file into a subdirectory based on the first two characters of the
// hash to avoid making the raw directory too large and slowing down accesses.
relPath := filepath.Join(hash[0:2], hash)
// These files are written during soong_build. If something outside the build deleted them there would be no
// trigger to rerun soong_build, and the build would break with dependencies on missing files. Writing them
// to their final locations would risk having them deleted when cleaning a module, and would also pollute the
// output directory with files for modules that have never been built.
// Instead, the files are written to a separate "raw" directory next to the build.ninja file, and a ninja
// rule is created to copy the files into their final location as needed.
// Obsolete files written by previous runs of soong_build must be cleaned up to avoid continually growing
// disk usage as the hashes of the files change over time. The cleanup must not remove files that were
// created by previous runs of soong_build for other products, as the build.ninja files for those products
// may still exist and still reference those files. The raw files from different products are kept
// separate by appending the Make_suffix to the directory name.
rawPath := PathForOutput(ctx, "raw"+proptools.String(ctx.Config().productVariables.Make_suffix), relPath)
rawFileInfo := rawFileInfo{
relPath: relPath,
}
if ctx.Config().captureBuild {
// When running tests tempFile won't write to disk, instead store the contents for later retrieval by
// ContentFromFileRuleForTests.
rawFileInfo.contentForTests = tempFile.testBuf.String()
}
rawFileSet := getRawFileSet(ctx.Config())
if _, exists := rawFileSet.LoadOrStore(hash, rawFileInfo); exists {
// If a raw file with this hash has already been created delete the temporary file.
tempFile.remove()
} else {
// If this is the first time this hash has been seen then move it from the temporary directory
// to the raw directory. If the file already exists in the raw directory assume it has the correct
// contents.
absRawPath := absolutePath(rawPath.String())
_, err := os.Stat(absRawPath)
if os.IsNotExist(err) {
tempFile.rename(absRawPath)
} else if err != nil {
panic(fmt.Errorf("failed to stat %q: %w", absRawPath, err))
} else {
tempFile.remove()
}
}
// Emit a rule to copy the file from raw directory to the final requested location in the output tree.
// Restat is used to ensure that two different products that produce identical files copied from their
// own raw directories they don't cause everything downstream to rebuild.
rule := rawFileCopy
if executable {
rule = rawFileCopyExecutable
}
ctx.Build(pctx, BuildParams{
Rule: rule,
Input: rawPath,
Output: outputFile,
Description: "raw " + outputFile.Base(),
})
}
var (
rawFileCopy = pctx.AndroidStaticRule("rawFileCopy",
blueprint.RuleParams{
Command: "if ! cmp -s $in $out; then cp $in $out; fi",
Description: "copy raw file $out",
Restat: true,
})
rawFileCopyExecutable = pctx.AndroidStaticRule("rawFileCopyExecutable",
blueprint.RuleParams{
Command: "if ! cmp -s $in $out; then cp $in $out; fi && chmod +x $out",
Description: "copy raw exectuable file $out",
Restat: true,
})
)
type rawFileInfo struct {
relPath string
contentForTests string
}
var rawFileSetKey OnceKey = NewOnceKey("raw file set")
func getRawFileSet(config Config) *SyncMap[string, rawFileInfo] {
return config.Once(rawFileSetKey, func() any {
return &SyncMap[string, rawFileInfo]{}
}).(*SyncMap[string, rawFileInfo])
}
// ContentFromFileRuleForTests returns the content that was passed to a WriteFileRule for use
// in tests.
func ContentFromFileRuleForTests(t *testing.T, ctx *TestContext, params TestingBuildParams) string {
t.Helper()
if params.Rule != rawFileCopy && params.Rule != rawFileCopyExecutable {
t.Errorf("expected params.Rule to be rawFileCopy or rawFileCopyExecutable, was %q", params.Rule)
return ""
}
key := filepath.Base(params.Input.String())
rawFileSet := getRawFileSet(ctx.Config())
rawFileInfo, _ := rawFileSet.Load(key)
return rawFileInfo.contentForTests
}
func rawFilesSingletonFactory() Singleton {
return &rawFilesSingleton{}
}
type rawFilesSingleton struct{}
func (rawFilesSingleton) GenerateBuildActions(ctx SingletonContext) {
if ctx.Config().captureBuild {
// Nothing to do when running in tests, no temporary files were created.
return
}
rawFileSet := getRawFileSet(ctx.Config())
rawFilesDir := PathForOutput(ctx, "raw"+proptools.String(ctx.Config().productVariables.Make_suffix)).String()
absRawFilesDir := absolutePath(rawFilesDir)
err := filepath.WalkDir(absRawFilesDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
// Ignore obsolete directories for now.
return nil
}
// Assume the basename of the file is a hash
key := filepath.Base(path)
relPath, err := filepath.Rel(absRawFilesDir, path)
if err != nil {
return err
}
// Check if a file with the same hash was written by this run of soong_build. If the file was not written,
// or if a file with the same hash was written but to a different path in the raw directory, then delete it.
// Checking that the path matches allows changing the structure of the raw directory, for example to increase
// the sharding.
rawFileInfo, written := rawFileSet.Load(key)
if !written || rawFileInfo.relPath != relPath {
os.Remove(path)
}
return nil
})
if err != nil {
panic(fmt.Errorf("failed to clean %q: %w", rawFilesDir, err))
}
}

View file

@ -191,8 +191,9 @@ func collateGloballyRegisteredSingletons() sortableComponents {
// Register makevars after other singletons so they can export values through makevars
singleton{parallel: false, name: "makevars", factory: makeVarsSingletonFunc},
// Register env and ninjadeps last so that they can track all used environment variables and
// Register rawfiles and ninjadeps last so that they can track all used environment variables and
// Ninja file dependencies stored in the config.
singleton{parallel: false, name: "rawfiles", factory: rawFilesSingletonFactory},
singleton{parallel: false, name: "ninjadeps", factory: ninjaDepsSingletonFactory},
)

View file

@ -816,13 +816,13 @@ func TestRuleBuilderHashInputs(t *testing.T) {
func TestRuleBuilderWithNinjaVarEscaping(t *testing.T) {
bp := `
rule_builder_test {
name: "foo_sbox_escaped_ninja",
name: "foo_sbox_escaped",
flags: ["${cmdFlags}"],
sbox: true,
sbox_inputs: true,
}
rule_builder_test {
name: "foo_sbox",
name: "foo_sbox_unescaped",
flags: ["${cmdFlags}"],
sbox: true,
sbox_inputs: true,
@ -834,15 +834,16 @@ func TestRuleBuilderWithNinjaVarEscaping(t *testing.T) {
FixtureWithRootAndroidBp(bp),
).RunTest(t)
escapedNinjaMod := result.ModuleForTests("foo_sbox_escaped_ninja", "").Rule("writeFile")
escapedNinjaMod := result.ModuleForTests("foo_sbox_escaped", "").Output("sbox.textproto")
AssertStringEquals(t, "expected rule", "android/soong/android.rawFileCopy", escapedNinjaMod.Rule.String())
AssertStringDoesContain(
t,
"",
escapedNinjaMod.BuildParams.Args["content"],
"$${cmdFlags}",
ContentFromFileRuleForTests(t, result.TestContext, escapedNinjaMod),
"${cmdFlags}",
)
unescapedNinjaMod := result.ModuleForTests("foo_sbox", "").Rule("unescapedWriteFile")
unescapedNinjaMod := result.ModuleForTests("foo_sbox_unescaped", "").Rule("unescapedWriteFile")
AssertStringDoesContain(
t,
"",

View file

@ -22,6 +22,7 @@ import (
"runtime"
"sort"
"strings"
"sync"
)
// CopyOf returns a new slice that has the same contents as s.
@ -597,3 +598,32 @@ func AddToStringSet(set map[string]bool, items []string) {
set[item] = true
}
}
// SyncMap is a wrapper around sync.Map that provides type safety via generics.
type SyncMap[K comparable, V any] struct {
sync.Map
}
// Load returns the value stored in the map for a key, or the zero value if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) {
v, ok := m.Map.Load(key)
if !ok {
return *new(V), false
}
return v.(V), true
}
// Store sets the value for a key.
func (m *SyncMap[K, V]) Store(key K, value V) {
m.Map.Store(key, value)
}
// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
v, loaded := m.Map.LoadOrStore(key, value)
return v.(V), loaded
}

View file

@ -25,12 +25,10 @@ func TestCodeMetadata(t *testing.T) {
}`
result := runCodeMetadataTest(t, android.FixtureExpectsNoErrors, bp)
module := result.ModuleForTests(
"module-name", "",
).Module().(*soongTesting.CodeMetadataModule)
module := result.ModuleForTests("module-name", "")
// Check that the provider has the right contents
data, _ := android.SingletonModuleProvider(result, module, soongTesting.CodeMetadataProviderKey)
data, _ := android.SingletonModuleProvider(result, module.Module(), soongTesting.CodeMetadataProviderKey)
if !strings.HasSuffix(
data.IntermediatePath.String(), "/intermediateCodeMetadata.pb",
) {
@ -40,13 +38,8 @@ func TestCodeMetadata(t *testing.T) {
)
}
buildParamsSlice := module.BuildParamsForTests()
var metadata = ""
for _, params := range buildParamsSlice {
if params.Rule.String() == "android/soong/android.writeFile" {
metadata = params.Args["content"]
}
}
metadata := android.ContentFromFileRuleForTests(t, result.TestContext,
module.Output(data.IntermediatePath.String()))
metadataList := make([]*code_metadata_internal_proto.CodeMetadataInternal_TargetOwnership, 0, 2)
teamId := "12345"
@ -63,9 +56,7 @@ func TestCodeMetadata(t *testing.T) {
CodeMetadataMetadata := code_metadata_internal_proto.CodeMetadataInternal{TargetOwnershipList: metadataList}
protoData, _ := proto.Marshal(&CodeMetadataMetadata)
rawData := string(protoData)
formattedData := strings.ReplaceAll(rawData, "\n", "\\n")
expectedMetadata := "'" + formattedData + "\\n'"
expectedMetadata := string(protoData)
if metadata != expectedMetadata {
t.Errorf(

View file

@ -29,12 +29,10 @@ func TestTestSpec(t *testing.T) {
}`
result := runTestSpecTest(t, android.FixtureExpectsNoErrors, bp)
module := result.ModuleForTests(
"module-name", "",
).Module().(*soongTesting.TestSpecModule)
module := result.ModuleForTests("module-name", "")
// Check that the provider has the right contents
data, _ := android.SingletonModuleProvider(result, module, soongTesting.TestSpecProviderKey)
data, _ := android.SingletonModuleProvider(result, module.Module(), soongTesting.TestSpecProviderKey)
if !strings.HasSuffix(
data.IntermediatePath.String(), "/intermediateTestSpecMetadata.pb",
) {
@ -44,13 +42,8 @@ func TestTestSpec(t *testing.T) {
)
}
buildParamsSlice := module.BuildParamsForTests()
var metadata = ""
for _, params := range buildParamsSlice {
if params.Rule.String() == "android/soong/android.writeFile" {
metadata = params.Args["content"]
}
}
metadata := android.ContentFromFileRuleForTests(t, result.TestContext,
module.Output(data.IntermediatePath.String()))
metadataList := make([]*test_spec_proto.TestSpec_OwnershipMetadata, 0, 2)
teamId := "12345"
@ -70,9 +63,7 @@ func TestTestSpec(t *testing.T) {
}
testSpecMetadata := test_spec_proto.TestSpec{OwnershipMetadataList: metadataList}
protoData, _ := proto.Marshal(&testSpecMetadata)
rawData := string(protoData)
formattedData := strings.ReplaceAll(rawData, "\n", "\\n")
expectedMetadata := "'" + formattedData + "\\n'"
expectedMetadata := string(protoData)
if metadata != expectedMetadata {
t.Errorf(

View file

@ -128,7 +128,7 @@ func (module *CodeMetadataModule) GenerateAndroidBuildActions(ctx android.Module
intermediatePath := android.PathForModuleOut(
ctx, "intermediateCodeMetadata.pb",
)
android.WriteFileRule(ctx, intermediatePath, string(protoData))
android.WriteFileRuleVerbatim(ctx, intermediatePath, string(protoData))
android.SetProvider(ctx,
CodeMetadataProviderKey,

View file

@ -117,7 +117,7 @@ func (module *TestSpecModule) GenerateAndroidBuildActions(ctx android.ModuleCont
if err != nil {
ctx.ModuleErrorf("Error: %s", err.Error())
}
android.WriteFileRule(ctx, intermediatePath, string(protoData))
android.WriteFileRuleVerbatim(ctx, intermediatePath, string(protoData))
android.SetProvider(ctx,
TestSpecProviderKey, TestSpecProviderData{

View file

@ -63,6 +63,7 @@ func testForDanglingRules(ctx Context, config Config) {
outDir := config.OutDir()
modulePathsDir := filepath.Join(outDir, ".module_paths")
rawFilesDir := filepath.Join(outDir, "soong", "raw")
variablesFilePath := filepath.Join(outDir, "soong", "soong.variables")
// dexpreopt.config is an input to the soong_docs action, which runs the
@ -88,6 +89,7 @@ func testForDanglingRules(ctx Context, config Config) {
continue
}
if strings.HasPrefix(line, modulePathsDir) ||
strings.HasPrefix(line, rawFilesDir) ||
line == variablesFilePath ||
line == dexpreoptConfigFilePath ||
line == buildDatetimeFilePath ||