1// Copyright 2024 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 find_input_delta_lib 16 17import ( 18 "errors" 19 "fmt" 20 "io/fs" 21 "regexp" 22 "slices" 23 24 fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto_internal" 25 "android/soong/third_party/zip" 26 "github.com/google/blueprint/pathtools" 27 "google.golang.org/protobuf/proto" 28) 29 30// Load the internal state from a file. 31// If the file does not exist, an empty state is returned. 32func LoadState(filename string, fsys fs.ReadFileFS) (*fid_proto.PartialCompileInputs, error) { 33 var message = &fid_proto.PartialCompileInputs{} 34 data, err := fsys.ReadFile(filename) 35 if err != nil && !errors.Is(err, fs.ErrNotExist) { 36 return message, err 37 } 38 proto.Unmarshal(data, message) 39 return message, nil 40} 41 42type StatReadFileFS interface { 43 fs.StatFS 44 fs.ReadFileFS 45} 46 47// Create the internal state by examining the inputs. 48func CreateState(inputs []string, inspect_contents bool, fsys StatReadFileFS) (*fid_proto.PartialCompileInputs, error) { 49 ret := &fid_proto.PartialCompileInputs{} 50 slices.Sort(inputs) 51 for _, input := range inputs { 52 stat, err := fs.Stat(fsys, input) 53 if err != nil { 54 if errors.Is(err, fs.ErrNotExist) { 55 continue 56 } 57 return ret, err 58 } 59 pci := &fid_proto.PartialCompileInput{ 60 Name: proto.String(input), 61 MtimeNsec: proto.Int64(stat.ModTime().UnixNano()), 62 // If we ever have an easy hash, assign it here. 63 } 64 if inspect_contents { 65 // NOTE: When we find it useful, we can parallelize the file inspection for speed. 66 contents, err := InspectFileContents(input) 67 if err != nil { 68 return ret, err 69 } 70 if contents != nil { 71 pci.Contents = contents 72 } 73 } 74 ret.InputFiles = append(ret.InputFiles, pci) 75 } 76 return ret, nil 77} 78 79// We ignore any suffix digit caused by sharding. 80var InspectExtsZipRegexp = regexp.MustCompile("\\.(jar|apex|apk)[0-9]*$") 81 82// Inspect the file and extract the state of the elements in the archive. 83// If this is not an archive of some sort, nil is returned. 84func InspectFileContents(name string) ([]*fid_proto.PartialCompileInput, error) { 85 if InspectExtsZipRegexp.Match([]byte(name)) { 86 return inspectZipFileContents(name) 87 } 88 return nil, nil 89} 90 91func inspectZipFileContents(name string) ([]*fid_proto.PartialCompileInput, error) { 92 rc, err := zip.OpenReader(name) 93 if err != nil { 94 return nil, err 95 } 96 ret := []*fid_proto.PartialCompileInput{} 97 for _, v := range rc.File { 98 // Only include timestamp when there is no CRC. 99 timeNsec := proto.Int64(v.ModTime().UnixNano()) 100 if v.CRC32 != 0 { 101 timeNsec = nil 102 } 103 pci := &fid_proto.PartialCompileInput{ 104 Name: proto.String(v.Name), 105 MtimeNsec: timeNsec, 106 Hash: proto.String(fmt.Sprintf("%08x", v.CRC32)), 107 } 108 ret = append(ret, pci) 109 // We do not support nested inspection. 110 } 111 return ret, nil 112} 113 114func WriteState(s *fid_proto.PartialCompileInputs, path string) error { 115 data, err := proto.Marshal(s) 116 if err != nil { 117 return err 118 } 119 return pathtools.WriteFileIfChanged(path, data, 0644) 120} 121 122func CompareInternalState(prior, other *fid_proto.PartialCompileInputs, target string) *FileList { 123 return CompareInputFiles(prior.GetInputFiles(), other.GetInputFiles(), target) 124} 125 126func CompareInputFiles(prior, other []*fid_proto.PartialCompileInput, name string) *FileList { 127 fl := FileListFactory(name) 128 PriorMap := make(map[string]*fid_proto.PartialCompileInput, len(prior)) 129 // We know that the lists are properly sorted, so we can simply compare them. 130 for _, v := range prior { 131 PriorMap[v.GetName()] = v 132 } 133 otherMap := make(map[string]*fid_proto.PartialCompileInput, len(other)) 134 for _, v := range other { 135 name = v.GetName() 136 otherMap[name] = v 137 if _, ok := PriorMap[name]; !ok { 138 // Added file 139 fl.addFile(name) 140 } else if !proto.Equal(PriorMap[name], v) { 141 // Changed file 142 fl.changeFile(name, CompareInputFiles(PriorMap[name].GetContents(), v.GetContents(), name)) 143 } 144 } 145 for _, v := range prior { 146 name := v.GetName() 147 if _, ok := otherMap[name]; !ok { 148 // Deleted file 149 fl.deleteFile(name) 150 } 151 } 152 return fl 153} 154