xref: /aosp_15_r20/build/soong/cmd/find_input_delta/find_input_delta_lib/file_list.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	"fmt"
19	"io"
20	"os"
21	"path/filepath"
22	"slices"
23	"text/template"
24
25	fid_exp "android/soong/cmd/find_input_delta/find_input_delta_proto"
26	"google.golang.org/protobuf/encoding/protowire"
27	"google.golang.org/protobuf/proto"
28)
29
30var DefaultTemplate = `
31	{{- define "contents"}}
32		{{- range .Deletions}}-{{.}} {{end}}
33		{{- range .Additions}}+{{.}} {{end}}
34		{{- range .Changes}}+{{- .Name}} {{end}}
35		{{- range .Changes}}
36		  {{- if or .Additions .Deletions .Changes}}--file {{.Name}} {{template "contents" .}}--endfile {{end}}
37		{{- end}}
38	{{- end}}
39	{{- template "contents" .}}`
40
41type FileList struct {
42	// The name of the parent for the list of file differences.
43	// For the outermost FileList, this is the name of the ninja target.
44	// Under `Changes`, it is the name of the changed file.
45	Name string
46
47	// The added files
48	Additions []string
49
50	// The deleted files
51	Deletions []string
52
53	// The modified files
54	Changes []FileList
55
56	// Map of file_extension:counts
57	ExtCountMap map[string]*FileCounts
58
59	// Total number of added/changed/deleted files.
60	TotalDelta uint32
61}
62
63// The maximum number of files that will be recorded by name.
64var MaxFilesRecorded uint32 = 50
65
66type FileCounts struct {
67	Additions uint32
68	Deletions uint32
69	Changes   uint32
70}
71
72func FileListFactory(name string) *FileList {
73	return &FileList{
74		Name:        name,
75		ExtCountMap: make(map[string]*FileCounts),
76	}
77}
78
79func (fl *FileList) addFile(name string) {
80	fl.Additions = append(fl.Additions, name)
81	fl.TotalDelta += 1
82	ext := filepath.Ext(name)
83	if _, ok := fl.ExtCountMap[ext]; !ok {
84		fl.ExtCountMap[ext] = &FileCounts{}
85	}
86	fl.ExtCountMap[ext].Additions += 1
87}
88
89func (fl *FileList) deleteFile(name string) {
90	fl.Deletions = append(fl.Deletions, name)
91	fl.TotalDelta += 1
92	ext := filepath.Ext(name)
93	if _, ok := fl.ExtCountMap[ext]; !ok {
94		fl.ExtCountMap[ext] = &FileCounts{}
95	}
96	fl.ExtCountMap[ext].Deletions += 1
97}
98
99func (fl *FileList) changeFile(name string, ch *FileList) {
100	fl.Changes = append(fl.Changes, *ch)
101	fl.TotalDelta += 1
102	ext := filepath.Ext(name)
103	if _, ok := fl.ExtCountMap[ext]; !ok {
104		fl.ExtCountMap[ext] = &FileCounts{}
105	}
106	fl.ExtCountMap[ext].Changes += 1
107}
108
109func (fl FileList) ToProto() (*fid_exp.FileList, error) {
110	var count uint32
111	return fl.toProto(&count)
112}
113
114func (fl FileList) toProto(count *uint32) (*fid_exp.FileList, error) {
115	ret := &fid_exp.FileList{
116		Name: proto.String(fl.Name),
117	}
118	for _, a := range fl.Additions {
119		if *count >= MaxFilesRecorded {
120			break
121		}
122		ret.Additions = append(ret.Additions, a)
123		*count += 1
124	}
125	for _, ch := range fl.Changes {
126		if *count >= MaxFilesRecorded {
127			break
128		} else {
129			// Pre-increment to limit what the call adds.
130			*count += 1
131			change, err := ch.toProto(count)
132			if err != nil {
133				return nil, err
134			}
135			ret.Changes = append(ret.Changes, change)
136		}
137	}
138	for _, d := range fl.Deletions {
139		if *count >= MaxFilesRecorded {
140			break
141		}
142		ret.Deletions = append(ret.Deletions, d)
143	}
144	ret.TotalDelta = proto.Uint32(*count)
145	exts := []string{}
146	for k := range fl.ExtCountMap {
147		exts = append(exts, k)
148	}
149	slices.Sort(exts)
150	for _, k := range exts {
151		v := fl.ExtCountMap[k]
152		ret.Counts = append(ret.Counts, &fid_exp.FileCount{
153			Extension:     proto.String(k),
154			Additions:     proto.Uint32(v.Additions),
155			Deletions:     proto.Uint32(v.Deletions),
156			Modifications: proto.Uint32(v.Changes),
157		})
158	}
159	return ret, nil
160}
161
162func (fl FileList) SendMetrics(path string) error {
163	if path == "" {
164		return fmt.Errorf("No path given")
165	}
166	message, err := fl.ToProto()
167	if err != nil {
168		return err
169	}
170
171	// Marshal the message wrapped in SoongCombinedMetrics.
172	data := protowire.AppendVarint(
173		[]byte{},
174		protowire.EncodeTag(
175			protowire.Number(fid_exp.FieldNumbers_FIELD_NUMBERS_FILE_LIST),
176			protowire.BytesType))
177	size := uint64(proto.Size(message))
178	data = protowire.AppendVarint(data, size)
179	data, err = proto.MarshalOptions{UseCachedSize: true}.MarshalAppend(data, message)
180	if err != nil {
181		return err
182	}
183
184	out, err := os.Create(path)
185	if err != nil {
186		return err
187	}
188	defer func() {
189		if err := out.Close(); err != nil {
190			fmt.Fprintf(os.Stderr, "Failed to close %s: %v\n", path, err)
191		}
192	}()
193	_, err = out.Write(data)
194	return err
195}
196
197func (fl FileList) Format(wr io.Writer, format string) error {
198	tmpl, err := template.New("filelist").Parse(format)
199	if err != nil {
200		return err
201	}
202	return tmpl.Execute(wr, fl)
203}
204