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