blob: ebba4d14575112662c6f4327400e6d75ffeef454 [file] [log] [blame]
Colin Cross31a67452023-11-02 16:57:08 -07001// 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"
Colin Cross31a67452023-11-02 16:57:08 -070021 "io"
22 "io/fs"
23 "os"
24 "path/filepath"
25 "strings"
26 "testing"
27
Jihoon Kangd56da322025-01-08 01:57:24 +000028 "github.com/google/blueprint"
Yu Liu2d6bed52025-03-20 22:21:05 +000029 "github.com/google/blueprint/syncmap"
Jihoon Kangd56da322025-01-08 01:57:24 +000030
Colin Cross31a67452023-11-02 16:57:08 -070031 "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.
Jihoon Kangd56da322025-01-08 01:57:24 +000037func WriteFileRule(ctx BuilderContext, outputFile WritablePath, content string, validations ...Path) {
38 writeFileRule(ctx, outputFile, content, true, false, validations)
Colin Cross31a67452023-11-02 16:57:08 -070039}
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.
Jihoon Kangd56da322025-01-08 01:57:24 +000043func WriteFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string, validations ...Path) {
44 writeFileRule(ctx, outputFile, content, false, false, validations)
Colin Cross31a67452023-11-02 16:57:08 -070045}
46
47// WriteExecutableFileRuleVerbatim is the same as WriteFileRuleVerbatim, but runs chmod +x on the result
Jihoon Kangd56da322025-01-08 01:57:24 +000048func WriteExecutableFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string, validations ...Path) {
49 writeFileRule(ctx, outputFile, content, false, true, validations)
Colin Cross31a67452023-11-02 16:57:08 -070050}
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
Jihoon Kangd56da322025-01-08 01:57:24 +0000129func writeFileRule(ctx BuilderContext, outputFile WritablePath, content string, newline bool, executable bool, validations Paths) {
Colin Cross31a67452023-11-02 16:57:08 -0700130 // 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(),
Jihoon Kangd56da322025-01-08 01:57:24 +0000191 Validations: validations,
Colin Cross31a67452023-11-02 16:57:08 -0700192 })
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
Yu Liu2d6bed52025-03-20 22:21:05 +0000217func getRawFileSet(config Config) *syncmap.SyncMap[string, rawFileInfo] {
Colin Cross31a67452023-11-02 16:57:08 -0700218 return config.Once(rawFileSetKey, func() any {
Yu Liu2d6bed52025-03-20 22:21:05 +0000219 return &syncmap.SyncMap[string, rawFileInfo]{}
220 }).(*syncmap.SyncMap[string, rawFileInfo])
Colin Cross31a67452023-11-02 16:57:08 -0700221}
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}