1// Copyright 2023 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package android 16 17import ( 18 "crypto/sha1" 19 "encoding/hex" 20 "fmt" 21 "io" 22 "io/fs" 23 "os" 24 "path/filepath" 25 "strings" 26 "testing" 27 28 "github.com/google/blueprint" 29 "github.com/google/blueprint/syncmap" 30 31 "github.com/google/blueprint/proptools" 32) 33 34// WriteFileRule creates a ninja rule to write contents to a file by immediately writing the 35// contents, plus a trailing newline, to a file in out/soong/raw-${TARGET_PRODUCT}, and then creating 36// a ninja rule to copy the file into place. 37func WriteFileRule(ctx BuilderContext, outputFile WritablePath, content string, validations ...Path) { 38 writeFileRule(ctx, outputFile, content, true, false, validations) 39} 40 41// WriteFileRuleVerbatim creates a ninja rule to write contents to a file by immediately writing the 42// contents to a file in out/soong/raw-${TARGET_PRODUCT}, and then creating a ninja rule to copy the file into place. 43func WriteFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string, validations ...Path) { 44 writeFileRule(ctx, outputFile, content, false, false, validations) 45} 46 47// WriteExecutableFileRuleVerbatim is the same as WriteFileRuleVerbatim, but runs chmod +x on the result 48func WriteExecutableFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string, validations ...Path) { 49 writeFileRule(ctx, outputFile, content, false, true, validations) 50} 51 52// tempFile provides a testable wrapper around a file in out/soong/.temp. It writes to a temporary file when 53// not in tests, but writes to a buffer in memory when used in tests. 54type tempFile struct { 55 // tempFile contains wraps an io.Writer, which will be file if testMode is false, or testBuf if it is true. 56 io.Writer 57 58 file *os.File 59 testBuf *strings.Builder 60} 61 62func newTempFile(ctx BuilderContext, pattern string, testMode bool) *tempFile { 63 if testMode { 64 testBuf := &strings.Builder{} 65 return &tempFile{ 66 Writer: testBuf, 67 testBuf: testBuf, 68 } 69 } else { 70 f, err := os.CreateTemp(absolutePath(ctx.Config().tempDir()), pattern) 71 if err != nil { 72 panic(fmt.Errorf("failed to open temporary raw file: %w", err)) 73 } 74 return &tempFile{ 75 Writer: f, 76 file: f, 77 } 78 } 79} 80 81func (t *tempFile) close() error { 82 if t.file != nil { 83 return t.file.Close() 84 } 85 return nil 86} 87 88func (t *tempFile) name() string { 89 if t.file != nil { 90 return t.file.Name() 91 } 92 return "temp_file_in_test" 93} 94 95func (t *tempFile) rename(to string) { 96 if t.file != nil { 97 os.MkdirAll(filepath.Dir(to), 0777) 98 err := os.Rename(t.file.Name(), to) 99 if err != nil { 100 panic(fmt.Errorf("failed to rename %s to %s: %w", t.file.Name(), to, err)) 101 } 102 } 103} 104 105func (t *tempFile) remove() error { 106 if t.file != nil { 107 return os.Remove(t.file.Name()) 108 } 109 return nil 110} 111 112func writeContentToTempFileAndHash(ctx BuilderContext, content string, newline bool) (*tempFile, string) { 113 tempFile := newTempFile(ctx, "raw", ctx.Config().captureBuild) 114 defer tempFile.close() 115 116 hash := sha1.New() 117 w := io.MultiWriter(tempFile, hash) 118 119 _, err := io.WriteString(w, content) 120 if err == nil && newline { 121 _, err = io.WriteString(w, "\n") 122 } 123 if err != nil { 124 panic(fmt.Errorf("failed to write to temporary raw file %s: %w", tempFile.name(), err)) 125 } 126 return tempFile, hex.EncodeToString(hash.Sum(nil)) 127} 128 129func writeFileRule(ctx BuilderContext, outputFile WritablePath, content string, newline bool, executable bool, validations Paths) { 130 // Write the contents to a temporary file while computing its hash. 131 tempFile, hash := writeContentToTempFileAndHash(ctx, content, newline) 132 133 // Shard the final location of the raw file into a subdirectory based on the first two characters of the 134 // hash to avoid making the raw directory too large and slowing down accesses. 135 relPath := filepath.Join(hash[0:2], hash) 136 137 // These files are written during soong_build. If something outside the build deleted them there would be no 138 // trigger to rerun soong_build, and the build would break with dependencies on missing files. Writing them 139 // to their final locations would risk having them deleted when cleaning a module, and would also pollute the 140 // output directory with files for modules that have never been built. 141 // Instead, the files are written to a separate "raw" directory next to the build.ninja file, and a ninja 142 // rule is created to copy the files into their final location as needed. 143 // Obsolete files written by previous runs of soong_build must be cleaned up to avoid continually growing 144 // disk usage as the hashes of the files change over time. The cleanup must not remove files that were 145 // created by previous runs of soong_build for other products, as the build.ninja files for those products 146 // may still exist and still reference those files. The raw files from different products are kept 147 // separate by appending the Make_suffix to the directory name. 148 rawPath := PathForOutput(ctx, "raw"+proptools.String(ctx.Config().productVariables.Make_suffix), relPath) 149 150 rawFileInfo := rawFileInfo{ 151 relPath: relPath, 152 } 153 154 if ctx.Config().captureBuild { 155 // When running tests tempFile won't write to disk, instead store the contents for later retrieval by 156 // ContentFromFileRuleForTests. 157 rawFileInfo.contentForTests = tempFile.testBuf.String() 158 } 159 160 rawFileSet := getRawFileSet(ctx.Config()) 161 if _, exists := rawFileSet.LoadOrStore(hash, rawFileInfo); exists { 162 // If a raw file with this hash has already been created delete the temporary file. 163 tempFile.remove() 164 } else { 165 // If this is the first time this hash has been seen then move it from the temporary directory 166 // to the raw directory. If the file already exists in the raw directory assume it has the correct 167 // contents. 168 absRawPath := absolutePath(rawPath.String()) 169 _, err := os.Stat(absRawPath) 170 if os.IsNotExist(err) { 171 tempFile.rename(absRawPath) 172 } else if err != nil { 173 panic(fmt.Errorf("failed to stat %q: %w", absRawPath, err)) 174 } else { 175 tempFile.remove() 176 } 177 } 178 179 // Emit a rule to copy the file from raw directory to the final requested location in the output tree. 180 // Restat is used to ensure that two different products that produce identical files copied from their 181 // own raw directories they don't cause everything downstream to rebuild. 182 rule := rawFileCopy 183 if executable { 184 rule = rawFileCopyExecutable 185 } 186 ctx.Build(pctx, BuildParams{ 187 Rule: rule, 188 Input: rawPath, 189 Output: outputFile, 190 Description: "raw " + outputFile.Base(), 191 Validations: validations, 192 }) 193} 194 195var ( 196 rawFileCopy = pctx.AndroidStaticRule("rawFileCopy", 197 blueprint.RuleParams{ 198 Command: "if ! cmp -s $in $out; then cp $in $out; fi", 199 Description: "copy raw file $out", 200 Restat: true, 201 }) 202 rawFileCopyExecutable = pctx.AndroidStaticRule("rawFileCopyExecutable", 203 blueprint.RuleParams{ 204 Command: "if ! cmp -s $in $out; then cp $in $out; fi && chmod +x $out", 205 Description: "copy raw exectuable file $out", 206 Restat: true, 207 }) 208) 209 210type rawFileInfo struct { 211 relPath string 212 contentForTests string 213} 214 215var rawFileSetKey OnceKey = NewOnceKey("raw file set") 216 217func getRawFileSet(config Config) *syncmap.SyncMap[string, rawFileInfo] { 218 return config.Once(rawFileSetKey, func() any { 219 return &syncmap.SyncMap[string, rawFileInfo]{} 220 }).(*syncmap.SyncMap[string, rawFileInfo]) 221} 222 223// ContentFromFileRuleForTests returns the content that was passed to a WriteFileRule for use 224// in tests. 225func ContentFromFileRuleForTests(t *testing.T, ctx *TestContext, params TestingBuildParams) string { 226 t.Helper() 227 if params.Rule != rawFileCopy && params.Rule != rawFileCopyExecutable { 228 t.Errorf("expected params.Rule to be rawFileCopy or rawFileCopyExecutable, was %q", params.Rule) 229 return "" 230 } 231 232 key := filepath.Base(params.Input.String()) 233 rawFileSet := getRawFileSet(ctx.Config()) 234 rawFileInfo, _ := rawFileSet.Load(key) 235 236 return rawFileInfo.contentForTests 237} 238 239func rawFilesSingletonFactory() Singleton { 240 return &rawFilesSingleton{} 241} 242 243type rawFilesSingleton struct{} 244 245func (rawFilesSingleton) GenerateBuildActions(ctx SingletonContext) { 246 if ctx.Config().captureBuild { 247 // Nothing to do when running in tests, no temporary files were created. 248 return 249 } 250 rawFileSet := getRawFileSet(ctx.Config()) 251 rawFilesDir := PathForOutput(ctx, "raw"+proptools.String(ctx.Config().productVariables.Make_suffix)).String() 252 absRawFilesDir := absolutePath(rawFilesDir) 253 err := filepath.WalkDir(absRawFilesDir, func(path string, d fs.DirEntry, err error) error { 254 if err != nil { 255 return err 256 } 257 if d.IsDir() { 258 // Ignore obsolete directories for now. 259 return nil 260 } 261 262 // Assume the basename of the file is a hash 263 key := filepath.Base(path) 264 relPath, err := filepath.Rel(absRawFilesDir, path) 265 if err != nil { 266 return err 267 } 268 269 // Check if a file with the same hash was written by this run of soong_build. If the file was not written, 270 // or if a file with the same hash was written but to a different path in the raw directory, then delete it. 271 // Checking that the path matches allows changing the structure of the raw directory, for example to increase 272 // the sharding. 273 rawFileInfo, written := rawFileSet.Load(key) 274 if !written || rawFileInfo.relPath != relPath { 275 os.Remove(path) 276 } 277 return nil 278 }) 279 if err != nil { 280 panic(fmt.Errorf("failed to clean %q: %w", rawFilesDir, err)) 281 } 282} 283