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