// Copyright 2024 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package find_input_delta_lib import ( "errors" "fmt" "io/fs" "regexp" "slices" fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto_internal" "android/soong/third_party/zip" "github.com/google/blueprint/pathtools" "google.golang.org/protobuf/proto" ) // Load the internal state from a file. // If the file does not exist, an empty state is returned. func LoadState(filename string, fsys fs.ReadFileFS) (*fid_proto.PartialCompileInputs, error) { var message = &fid_proto.PartialCompileInputs{} data, err := fsys.ReadFile(filename) if err != nil && !errors.Is(err, fs.ErrNotExist) { return message, err } proto.Unmarshal(data, message) return message, nil } type StatReadFileFS interface { fs.StatFS fs.ReadFileFS } // Create the internal state by examining the inputs. func CreateState(inputs []string, inspect_contents bool, fsys StatReadFileFS) (*fid_proto.PartialCompileInputs, error) { ret := &fid_proto.PartialCompileInputs{} slices.Sort(inputs) for _, input := range inputs { stat, err := fs.Stat(fsys, input) if err != nil { return ret, err } pci := &fid_proto.PartialCompileInput{ Name: proto.String(input), MtimeNsec: proto.Int64(stat.ModTime().UnixNano()), // If we ever have an easy hash, assign it here. } if inspect_contents { // NOTE: When we find it useful, we can parallelize the file inspection for speed. contents, err := InspectFileContents(input) if err != nil { return ret, err } if contents != nil { pci.Contents = contents } } ret.InputFiles = append(ret.InputFiles, pci) } return ret, nil } // We ignore any suffix digit caused by sharding. var InspectExtsZipRegexp = regexp.MustCompile("\\.(jar|apex|apk)[0-9]*$") // Inspect the file and extract the state of the elements in the archive. // If this is not an archive of some sort, nil is returned. func InspectFileContents(name string) ([]*fid_proto.PartialCompileInput, error) { if InspectExtsZipRegexp.Match([]byte(name)) { return inspectZipFileContents(name) } return nil, nil } func inspectZipFileContents(name string) ([]*fid_proto.PartialCompileInput, error) { rc, err := zip.OpenReader(name) if err != nil { return nil, err } ret := []*fid_proto.PartialCompileInput{} for _, v := range rc.File { pci := &fid_proto.PartialCompileInput{ Name: proto.String(v.Name), MtimeNsec: proto.Int64(v.ModTime().UnixNano()), Hash: proto.String(fmt.Sprintf("%08x", v.CRC32)), } ret = append(ret, pci) // We do not support nested inspection. } return ret, nil } func WriteState(s *fid_proto.PartialCompileInputs, path string) error { data, err := proto.Marshal(s) if err != nil { return err } return pathtools.WriteFileIfChanged(path, data, 0644) } func CompareInternalState(prior, other *fid_proto.PartialCompileInputs, target string) *FileList { return CompareInputFiles(prior.GetInputFiles(), other.GetInputFiles(), target) } func CompareInputFiles(prior, other []*fid_proto.PartialCompileInput, name string) *FileList { fl := FileListFactory(name) PriorMap := make(map[string]*fid_proto.PartialCompileInput, len(prior)) // We know that the lists are properly sorted, so we can simply compare them. for _, v := range prior { PriorMap[v.GetName()] = v } otherMap := make(map[string]*fid_proto.PartialCompileInput, len(other)) for _, v := range other { name = v.GetName() otherMap[name] = v if _, ok := PriorMap[name]; !ok { // Added file fl.addFile(name) } else if !proto.Equal(PriorMap[name], v) { // Changed file fl.changeFile(name, CompareInputFiles(PriorMap[name].GetContents(), v.GetContents(), name)) } } for _, v := range prior { name := v.GetName() if _, ok := otherMap[name]; !ok { // Deleted file fl.deleteFile(name) } } return fl }