xref: /aosp_15_r20/build/soong/cmd/find_input_delta/find_input_delta_lib/internal_state.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
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			return ret, err
55		}
56		pci := &fid_proto.PartialCompileInput{
57			Name:      proto.String(input),
58			MtimeNsec: proto.Int64(stat.ModTime().UnixNano()),
59			// If we ever have an easy hash, assign it here.
60		}
61		if inspect_contents {
62			// NOTE: When we find it useful, we can parallelize the file inspection for speed.
63			contents, err := InspectFileContents(input)
64			if err != nil {
65				return ret, err
66			}
67			if contents != nil {
68				pci.Contents = contents
69			}
70		}
71		ret.InputFiles = append(ret.InputFiles, pci)
72	}
73	return ret, nil
74}
75
76// We ignore any suffix digit caused by sharding.
77var InspectExtsZipRegexp = regexp.MustCompile("\\.(jar|apex|apk)[0-9]*$")
78
79// Inspect the file and extract the state of the elements in the archive.
80// If this is not an archive of some sort, nil is returned.
81func InspectFileContents(name string) ([]*fid_proto.PartialCompileInput, error) {
82	if InspectExtsZipRegexp.Match([]byte(name)) {
83		return inspectZipFileContents(name)
84	}
85	return nil, nil
86}
87
88func inspectZipFileContents(name string) ([]*fid_proto.PartialCompileInput, error) {
89	rc, err := zip.OpenReader(name)
90	if err != nil {
91		return nil, err
92	}
93	ret := []*fid_proto.PartialCompileInput{}
94	for _, v := range rc.File {
95		pci := &fid_proto.PartialCompileInput{
96			Name:      proto.String(v.Name),
97			MtimeNsec: proto.Int64(v.ModTime().UnixNano()),
98			Hash:      proto.String(fmt.Sprintf("%08x", v.CRC32)),
99		}
100		ret = append(ret, pci)
101		// We do not support nested inspection.
102	}
103	return ret, nil
104}
105
106func WriteState(s *fid_proto.PartialCompileInputs, path string) error {
107	data, err := proto.Marshal(s)
108	if err != nil {
109		return err
110	}
111	return pathtools.WriteFileIfChanged(path, data, 0644)
112}
113
114func CompareInternalState(prior, other *fid_proto.PartialCompileInputs, target string) *FileList {
115	return CompareInputFiles(prior.GetInputFiles(), other.GetInputFiles(), target)
116}
117
118func CompareInputFiles(prior, other []*fid_proto.PartialCompileInput, name string) *FileList {
119	fl := FileListFactory(name)
120	PriorMap := make(map[string]*fid_proto.PartialCompileInput, len(prior))
121	// We know that the lists are properly sorted, so we can simply compare them.
122	for _, v := range prior {
123		PriorMap[v.GetName()] = v
124	}
125	otherMap := make(map[string]*fid_proto.PartialCompileInput, len(other))
126	for _, v := range other {
127		name = v.GetName()
128		otherMap[name] = v
129		if _, ok := PriorMap[name]; !ok {
130			// Added file
131			fl.addFile(name)
132		} else if !proto.Equal(PriorMap[name], v) {
133			// Changed file
134			fl.changeFile(name, CompareInputFiles(PriorMap[name].GetContents(), v.GetContents(), name))
135		}
136	}
137	for _, v := range prior {
138		name := v.GetName()
139		if _, ok := otherMap[name]; !ok {
140			// Deleted file
141			fl.deleteFile(name)
142		}
143	}
144	return fl
145}
146