xref: /aosp_15_r20/build/soong/ui/build/staging_snapshot.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
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 build
16
17import (
18	"crypto/sha1"
19	"encoding/hex"
20	"encoding/json"
21	"io"
22	"io/fs"
23	"os"
24	"path/filepath"
25	"sort"
26	"strings"
27
28	"android/soong/shared"
29	"android/soong/ui/metrics"
30)
31
32// Metadata about a staged file
33type fileEntry struct {
34	Name string      `json:"name"`
35	Mode fs.FileMode `json:"mode"`
36	Size int64       `json:"size"`
37	Sha1 string      `json:"sha1"`
38}
39
40func fileEntryEqual(a fileEntry, b fileEntry) bool {
41	return a.Name == b.Name && a.Mode == b.Mode && a.Size == b.Size && a.Sha1 == b.Sha1
42}
43
44func sha1_hash(filename string) (string, error) {
45	f, err := os.Open(filename)
46	if err != nil {
47		return "", err
48	}
49	defer f.Close()
50
51	h := sha1.New()
52	if _, err := io.Copy(h, f); err != nil {
53		return "", err
54	}
55
56	return hex.EncodeToString(h.Sum(nil)), nil
57}
58
59// Subdirs of PRODUCT_OUT to scan
60var stagingSubdirs = []string{
61	"apex",
62	"cache",
63	"coverage",
64	"data",
65	"debug_ramdisk",
66	"fake_packages",
67	"installer",
68	"oem",
69	"product",
70	"ramdisk",
71	"recovery",
72	"root",
73	"sysloader",
74	"system",
75	"system_dlkm",
76	"system_ext",
77	"system_other",
78	"testcases",
79	"test_harness_ramdisk",
80	"vendor",
81	"vendor_debug_ramdisk",
82	"vendor_kernel_ramdisk",
83	"vendor_ramdisk",
84}
85
86// Return an array of stagedFileEntrys, one for each file in the staging directories inside
87// productOut
88func takeStagingSnapshot(ctx Context, productOut string, subdirs []string) ([]fileEntry, error) {
89	var outer_err error
90	if !strings.HasSuffix(productOut, "/") {
91		productOut += "/"
92	}
93	result := []fileEntry{}
94	for _, subdir := range subdirs {
95		filepath.WalkDir(productOut+subdir,
96			func(filename string, dirent fs.DirEntry, err error) error {
97				// Ignore errors. The most common one is that one of the subdirectories
98				// hasn't been built, in which case we just report it as empty.
99				if err != nil {
100					ctx.Verbosef("scanModifiedStagingOutputs error: %s", err)
101					return nil
102				}
103				if dirent.Type().IsRegular() {
104					fileInfo, _ := dirent.Info()
105					relative := strings.TrimPrefix(filename, productOut)
106					sha, err := sha1_hash(filename)
107					if err != nil {
108						outer_err = err
109					}
110					result = append(result, fileEntry{
111						Name: relative,
112						Mode: fileInfo.Mode(),
113						Size: fileInfo.Size(),
114						Sha1: sha,
115					})
116				}
117				return nil
118			})
119	}
120
121	sort.Slice(result, func(l, r int) bool { return result[l].Name < result[r].Name })
122
123	return result, outer_err
124}
125
126// Read json into an array of fileEntry. On error return empty array.
127func readJson(filename string) ([]fileEntry, error) {
128	buf, err := os.ReadFile(filename)
129	if err != nil {
130		// Not an error, just missing, which is empty.
131		return []fileEntry{}, nil
132	}
133
134	var result []fileEntry
135	err = json.Unmarshal(buf, &result)
136	if err != nil {
137		// Bad formatting. This is an error
138		return []fileEntry{}, err
139	}
140
141	return result, nil
142}
143
144// Write obj to filename.
145func writeJson(filename string, obj interface{}) error {
146	buf, err := json.MarshalIndent(obj, "", "  ")
147	if err != nil {
148		return err
149	}
150
151	return os.WriteFile(filename, buf, 0660)
152}
153
154type snapshotDiff struct {
155	Added   []string `json:"added"`
156	Changed []string `json:"changed"`
157	Removed []string `json:"removed"`
158}
159
160// Diff the two snapshots, returning a snapshotDiff.
161func diffSnapshots(previous []fileEntry, current []fileEntry) snapshotDiff {
162	result := snapshotDiff{
163		Added:   []string{},
164		Changed: []string{},
165		Removed: []string{},
166	}
167
168	found := make(map[string]bool)
169
170	prev := make(map[string]fileEntry)
171	for _, pre := range previous {
172		prev[pre.Name] = pre
173	}
174
175	for _, cur := range current {
176		pre, ok := prev[cur.Name]
177		found[cur.Name] = true
178		// Added
179		if !ok {
180			result.Added = append(result.Added, cur.Name)
181			continue
182		}
183		// Changed
184		if !fileEntryEqual(pre, cur) {
185			result.Changed = append(result.Changed, cur.Name)
186		}
187	}
188
189	// Removed
190	for _, pre := range previous {
191		if !found[pre.Name] {
192			result.Removed = append(result.Removed, pre.Name)
193		}
194	}
195
196	// Sort the results
197	sort.Strings(result.Added)
198	sort.Strings(result.Changed)
199	sort.Strings(result.Removed)
200
201	return result
202}
203
204// Write a json files to dist:
205//   - A list of which files have changed in this build.
206//
207// And record in out/soong:
208//   - A list of all files in the staging directories, including their hashes.
209func runStagingSnapshot(ctx Context, config Config) {
210	ctx.BeginTrace(metrics.RunSoong, "runStagingSnapshot")
211	defer ctx.EndTrace()
212
213	snapshotFilename := shared.JoinPath(config.SoongOutDir(), "staged_files.json")
214
215	// Read the existing snapshot file. If it doesn't exist, this is a full
216	// build, so all files will be treated as new.
217	previous, err := readJson(snapshotFilename)
218	if err != nil {
219		ctx.Fatal(err)
220		return
221	}
222
223	// Take a snapshot of the current out directory
224	current, err := takeStagingSnapshot(ctx, config.ProductOut(), stagingSubdirs)
225	if err != nil {
226		ctx.Fatal(err)
227		return
228	}
229
230	// Diff the snapshots
231	diff := diffSnapshots(previous, current)
232
233	// Write the diff (use RealDistDir, not one that might have been faked for bazel)
234	err = writeJson(shared.JoinPath(config.RealDistDir(), "modified_files.json"), diff)
235	if err != nil {
236		ctx.Fatal(err)
237		return
238	}
239
240	// Update the snapshot
241	err = writeJson(snapshotFilename, current)
242	if err != nil {
243		ctx.Fatal(err)
244		return
245	}
246}
247