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