xref: /aosp_15_r20/build/make/tools/compliance/projectmetadata/projectmetadata.go (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
1*9e94795aSAndroid Build Coastguard Worker// Copyright 2022 Google LLC
2*9e94795aSAndroid Build Coastguard Worker//
3*9e94795aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*9e94795aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*9e94795aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*9e94795aSAndroid Build Coastguard Worker//
7*9e94795aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*9e94795aSAndroid Build Coastguard Worker//
9*9e94795aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*9e94795aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*9e94795aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*9e94795aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*9e94795aSAndroid Build Coastguard Worker// limitations under the License.
14*9e94795aSAndroid Build Coastguard Worker
15*9e94795aSAndroid Build Coastguard Workerpackage projectmetadata
16*9e94795aSAndroid Build Coastguard Worker
17*9e94795aSAndroid Build Coastguard Workerimport (
18*9e94795aSAndroid Build Coastguard Worker	"fmt"
19*9e94795aSAndroid Build Coastguard Worker	"io"
20*9e94795aSAndroid Build Coastguard Worker	"io/fs"
21*9e94795aSAndroid Build Coastguard Worker	"path/filepath"
22*9e94795aSAndroid Build Coastguard Worker	"strings"
23*9e94795aSAndroid Build Coastguard Worker	"sync"
24*9e94795aSAndroid Build Coastguard Worker
25*9e94795aSAndroid Build Coastguard Worker	"android/soong/compliance/project_metadata_proto"
26*9e94795aSAndroid Build Coastguard Worker
27*9e94795aSAndroid Build Coastguard Worker	"google.golang.org/protobuf/encoding/prototext"
28*9e94795aSAndroid Build Coastguard Worker)
29*9e94795aSAndroid Build Coastguard Worker
30*9e94795aSAndroid Build Coastguard Workervar (
31*9e94795aSAndroid Build Coastguard Worker	// ConcurrentReaders is the size of the task pool for limiting resource usage e.g. open files.
32*9e94795aSAndroid Build Coastguard Worker	ConcurrentReaders = 5
33*9e94795aSAndroid Build Coastguard Worker)
34*9e94795aSAndroid Build Coastguard Worker
35*9e94795aSAndroid Build Coastguard Worker// ProjectMetadata contains the METADATA for a git project.
36*9e94795aSAndroid Build Coastguard Workertype ProjectMetadata struct {
37*9e94795aSAndroid Build Coastguard Worker	proto project_metadata_proto.Metadata
38*9e94795aSAndroid Build Coastguard Worker
39*9e94795aSAndroid Build Coastguard Worker	// project is the path to the directory containing the METADATA file.
40*9e94795aSAndroid Build Coastguard Worker	project string
41*9e94795aSAndroid Build Coastguard Worker}
42*9e94795aSAndroid Build Coastguard Worker
43*9e94795aSAndroid Build Coastguard Worker// ProjectUrlMap maps url type name to url value
44*9e94795aSAndroid Build Coastguard Workertype ProjectUrlMap map[string]string
45*9e94795aSAndroid Build Coastguard Worker
46*9e94795aSAndroid Build Coastguard Worker// DownloadUrl returns the address of a download location
47*9e94795aSAndroid Build Coastguard Workerfunc (m ProjectUrlMap) DownloadUrl() string {
48*9e94795aSAndroid Build Coastguard Worker	for _, urlType := range []string{"GIT", "SVN", "HG", "DARCS"} {
49*9e94795aSAndroid Build Coastguard Worker		if url, ok := m[urlType]; ok {
50*9e94795aSAndroid Build Coastguard Worker			return url
51*9e94795aSAndroid Build Coastguard Worker		}
52*9e94795aSAndroid Build Coastguard Worker	}
53*9e94795aSAndroid Build Coastguard Worker	return ""
54*9e94795aSAndroid Build Coastguard Worker}
55*9e94795aSAndroid Build Coastguard Worker
56*9e94795aSAndroid Build Coastguard Worker// String returns a string representation of the metadata for error messages.
57*9e94795aSAndroid Build Coastguard Workerfunc (pm *ProjectMetadata) String() string {
58*9e94795aSAndroid Build Coastguard Worker	return fmt.Sprintf("project: %q\n%s", pm.project, pm.proto.String())
59*9e94795aSAndroid Build Coastguard Worker}
60*9e94795aSAndroid Build Coastguard Worker
61*9e94795aSAndroid Build Coastguard Worker// Project returns the path to the directory containing the METADATA file
62*9e94795aSAndroid Build Coastguard Workerfunc (pm *ProjectMetadata) Project() string {
63*9e94795aSAndroid Build Coastguard Worker	return pm.project
64*9e94795aSAndroid Build Coastguard Worker}
65*9e94795aSAndroid Build Coastguard Worker
66*9e94795aSAndroid Build Coastguard Worker// Name returns the name of the project.
67*9e94795aSAndroid Build Coastguard Workerfunc (pm *ProjectMetadata) Name() string {
68*9e94795aSAndroid Build Coastguard Worker	return pm.proto.GetName()
69*9e94795aSAndroid Build Coastguard Worker}
70*9e94795aSAndroid Build Coastguard Worker
71*9e94795aSAndroid Build Coastguard Worker// Version returns the version of the project if available.
72*9e94795aSAndroid Build Coastguard Workerfunc (pm *ProjectMetadata) Version() string {
73*9e94795aSAndroid Build Coastguard Worker	tp := pm.proto.GetThirdParty()
74*9e94795aSAndroid Build Coastguard Worker	if tp != nil {
75*9e94795aSAndroid Build Coastguard Worker		version := tp.GetVersion()
76*9e94795aSAndroid Build Coastguard Worker		return version
77*9e94795aSAndroid Build Coastguard Worker	}
78*9e94795aSAndroid Build Coastguard Worker	return ""
79*9e94795aSAndroid Build Coastguard Worker}
80*9e94795aSAndroid Build Coastguard Worker
81*9e94795aSAndroid Build Coastguard Worker// VersionedName returns the name of the project including the version if any.
82*9e94795aSAndroid Build Coastguard Workerfunc (pm *ProjectMetadata) VersionedName() string {
83*9e94795aSAndroid Build Coastguard Worker	name := pm.proto.GetName()
84*9e94795aSAndroid Build Coastguard Worker	if name != "" {
85*9e94795aSAndroid Build Coastguard Worker		tp := pm.proto.GetThirdParty()
86*9e94795aSAndroid Build Coastguard Worker		if tp != nil {
87*9e94795aSAndroid Build Coastguard Worker			version := tp.GetVersion()
88*9e94795aSAndroid Build Coastguard Worker			if version != "" {
89*9e94795aSAndroid Build Coastguard Worker				if version[0] == 'v' || version[0] == 'V' {
90*9e94795aSAndroid Build Coastguard Worker					return name + "_" + version
91*9e94795aSAndroid Build Coastguard Worker				} else {
92*9e94795aSAndroid Build Coastguard Worker					return name + "_v_" + version
93*9e94795aSAndroid Build Coastguard Worker				}
94*9e94795aSAndroid Build Coastguard Worker			}
95*9e94795aSAndroid Build Coastguard Worker		}
96*9e94795aSAndroid Build Coastguard Worker		return name
97*9e94795aSAndroid Build Coastguard Worker	}
98*9e94795aSAndroid Build Coastguard Worker	return pm.proto.GetDescription()
99*9e94795aSAndroid Build Coastguard Worker}
100*9e94795aSAndroid Build Coastguard Worker
101*9e94795aSAndroid Build Coastguard Worker// UrlsByTypeName returns a map of URLs by Type Name
102*9e94795aSAndroid Build Coastguard Workerfunc (pm *ProjectMetadata) UrlsByTypeName() ProjectUrlMap {
103*9e94795aSAndroid Build Coastguard Worker	tp := pm.proto.GetThirdParty()
104*9e94795aSAndroid Build Coastguard Worker	if tp == nil {
105*9e94795aSAndroid Build Coastguard Worker		return nil
106*9e94795aSAndroid Build Coastguard Worker	}
107*9e94795aSAndroid Build Coastguard Worker	if len(tp.Url) == 0 {
108*9e94795aSAndroid Build Coastguard Worker		return nil
109*9e94795aSAndroid Build Coastguard Worker	}
110*9e94795aSAndroid Build Coastguard Worker	urls := make(ProjectUrlMap)
111*9e94795aSAndroid Build Coastguard Worker
112*9e94795aSAndroid Build Coastguard Worker	for _, url := range tp.Url {
113*9e94795aSAndroid Build Coastguard Worker		uri := url.GetValue()
114*9e94795aSAndroid Build Coastguard Worker		if uri == "" {
115*9e94795aSAndroid Build Coastguard Worker			continue
116*9e94795aSAndroid Build Coastguard Worker		}
117*9e94795aSAndroid Build Coastguard Worker		urls[project_metadata_proto.URL_Type_name[int32(url.GetType())]] = uri
118*9e94795aSAndroid Build Coastguard Worker	}
119*9e94795aSAndroid Build Coastguard Worker	return urls
120*9e94795aSAndroid Build Coastguard Worker}
121*9e94795aSAndroid Build Coastguard Worker
122*9e94795aSAndroid Build Coastguard Worker// projectIndex describes a project to be read; after `wg.Wait()`, will contain either
123*9e94795aSAndroid Build Coastguard Worker// a `ProjectMetadata`, pm (can be nil even without error), or a non-nil `err`.
124*9e94795aSAndroid Build Coastguard Workertype projectIndex struct {
125*9e94795aSAndroid Build Coastguard Worker	project string
126*9e94795aSAndroid Build Coastguard Worker	path    string
127*9e94795aSAndroid Build Coastguard Worker	pm      *ProjectMetadata
128*9e94795aSAndroid Build Coastguard Worker	err     error
129*9e94795aSAndroid Build Coastguard Worker	done    chan struct{}
130*9e94795aSAndroid Build Coastguard Worker}
131*9e94795aSAndroid Build Coastguard Worker
132*9e94795aSAndroid Build Coastguard Worker// finish marks the task to read the `projectIndex` completed.
133*9e94795aSAndroid Build Coastguard Workerfunc (pi *projectIndex) finish() {
134*9e94795aSAndroid Build Coastguard Worker	close(pi.done)
135*9e94795aSAndroid Build Coastguard Worker}
136*9e94795aSAndroid Build Coastguard Worker
137*9e94795aSAndroid Build Coastguard Worker// wait suspends execution until the `projectIndex` task completes.
138*9e94795aSAndroid Build Coastguard Workerfunc (pi *projectIndex) wait() {
139*9e94795aSAndroid Build Coastguard Worker	<-pi.done
140*9e94795aSAndroid Build Coastguard Worker}
141*9e94795aSAndroid Build Coastguard Worker
142*9e94795aSAndroid Build Coastguard Worker// Index reads and caches ProjectMetadata (thread safe)
143*9e94795aSAndroid Build Coastguard Workertype Index struct {
144*9e94795aSAndroid Build Coastguard Worker	// projecs maps project name to a wait group if read has already started, and
145*9e94795aSAndroid Build Coastguard Worker	// to a `ProjectMetadata` or to an `error` after the read completes.
146*9e94795aSAndroid Build Coastguard Worker	projects sync.Map
147*9e94795aSAndroid Build Coastguard Worker
148*9e94795aSAndroid Build Coastguard Worker	// task provides a fixed-size task pool to limit concurrent open files etc.
149*9e94795aSAndroid Build Coastguard Worker	task chan bool
150*9e94795aSAndroid Build Coastguard Worker
151*9e94795aSAndroid Build Coastguard Worker	// rootFS locates the root of the file system from which to read the files.
152*9e94795aSAndroid Build Coastguard Worker	rootFS fs.FS
153*9e94795aSAndroid Build Coastguard Worker}
154*9e94795aSAndroid Build Coastguard Worker
155*9e94795aSAndroid Build Coastguard Worker// NewIndex constructs a project metadata `Index` for the given file system.
156*9e94795aSAndroid Build Coastguard Workerfunc NewIndex(rootFS fs.FS) *Index {
157*9e94795aSAndroid Build Coastguard Worker	ix := &Index{task: make(chan bool, ConcurrentReaders), rootFS: rootFS}
158*9e94795aSAndroid Build Coastguard Worker	for i := 0; i < ConcurrentReaders; i++ {
159*9e94795aSAndroid Build Coastguard Worker		ix.task <- true
160*9e94795aSAndroid Build Coastguard Worker	}
161*9e94795aSAndroid Build Coastguard Worker	return ix
162*9e94795aSAndroid Build Coastguard Worker}
163*9e94795aSAndroid Build Coastguard Worker
164*9e94795aSAndroid Build Coastguard Worker// MetadataForProjects returns 0..n ProjectMetadata for n `projects`, or an error.
165*9e94795aSAndroid Build Coastguard Worker// Each project that has a METADATA.android or a METADATA file in the root of the project will have
166*9e94795aSAndroid Build Coastguard Worker// a corresponding ProjectMetadata in the result. Projects with neither file get skipped. A nil
167*9e94795aSAndroid Build Coastguard Worker// result with no error indicates none of the given `projects` has a METADATA file.
168*9e94795aSAndroid Build Coastguard Worker// (thread safe -- can be called concurrently from multiple goroutines)
169*9e94795aSAndroid Build Coastguard Workerfunc (ix *Index) MetadataForProjects(projects ...string) ([]*ProjectMetadata, error) {
170*9e94795aSAndroid Build Coastguard Worker	if ConcurrentReaders < 1 {
171*9e94795aSAndroid Build Coastguard Worker		return nil, fmt.Errorf("need at least one task in project metadata pool")
172*9e94795aSAndroid Build Coastguard Worker	}
173*9e94795aSAndroid Build Coastguard Worker	if len(projects) == 0 {
174*9e94795aSAndroid Build Coastguard Worker		return nil, nil
175*9e94795aSAndroid Build Coastguard Worker	}
176*9e94795aSAndroid Build Coastguard Worker	// Identify the projects that have never been read
177*9e94795aSAndroid Build Coastguard Worker	projectsToRead := make([]*projectIndex, 0, len(projects))
178*9e94795aSAndroid Build Coastguard Worker	projectIndexes := make([]*projectIndex, 0, len(projects))
179*9e94795aSAndroid Build Coastguard Worker	for _, p := range projects {
180*9e94795aSAndroid Build Coastguard Worker		pi, loaded := ix.projects.LoadOrStore(p, &projectIndex{project: p, done: make(chan struct{})})
181*9e94795aSAndroid Build Coastguard Worker		if !loaded {
182*9e94795aSAndroid Build Coastguard Worker			projectsToRead = append(projectsToRead, pi.(*projectIndex))
183*9e94795aSAndroid Build Coastguard Worker		}
184*9e94795aSAndroid Build Coastguard Worker		projectIndexes = append(projectIndexes, pi.(*projectIndex))
185*9e94795aSAndroid Build Coastguard Worker	}
186*9e94795aSAndroid Build Coastguard Worker	// findMeta locates and reads the appropriate METADATA file, if any.
187*9e94795aSAndroid Build Coastguard Worker	findMeta := func(pi *projectIndex) {
188*9e94795aSAndroid Build Coastguard Worker		<-ix.task
189*9e94795aSAndroid Build Coastguard Worker		defer func() {
190*9e94795aSAndroid Build Coastguard Worker			ix.task <- true
191*9e94795aSAndroid Build Coastguard Worker			pi.finish()
192*9e94795aSAndroid Build Coastguard Worker		}()
193*9e94795aSAndroid Build Coastguard Worker
194*9e94795aSAndroid Build Coastguard Worker		// Support METADATA.android for projects that already have a different sort of METADATA file.
195*9e94795aSAndroid Build Coastguard Worker		path := filepath.Join(pi.project, "METADATA.android")
196*9e94795aSAndroid Build Coastguard Worker		fi, err := fs.Stat(ix.rootFS, path)
197*9e94795aSAndroid Build Coastguard Worker		if err == nil {
198*9e94795aSAndroid Build Coastguard Worker			if fi.Mode().IsRegular() {
199*9e94795aSAndroid Build Coastguard Worker				ix.readMetadataFile(pi, path)
200*9e94795aSAndroid Build Coastguard Worker				return
201*9e94795aSAndroid Build Coastguard Worker			}
202*9e94795aSAndroid Build Coastguard Worker		}
203*9e94795aSAndroid Build Coastguard Worker		// No METADATA.android try METADATA file.
204*9e94795aSAndroid Build Coastguard Worker		path = filepath.Join(pi.project, "METADATA")
205*9e94795aSAndroid Build Coastguard Worker		fi, err = fs.Stat(ix.rootFS, path)
206*9e94795aSAndroid Build Coastguard Worker		if err == nil {
207*9e94795aSAndroid Build Coastguard Worker			if fi.Mode().IsRegular() {
208*9e94795aSAndroid Build Coastguard Worker				ix.readMetadataFile(pi, path)
209*9e94795aSAndroid Build Coastguard Worker				return
210*9e94795aSAndroid Build Coastguard Worker			}
211*9e94795aSAndroid Build Coastguard Worker		}
212*9e94795aSAndroid Build Coastguard Worker		// no METADATA file exists -- leave nil and finish
213*9e94795aSAndroid Build Coastguard Worker	}
214*9e94795aSAndroid Build Coastguard Worker	// Look for the METADATA files to read, and record any missing.
215*9e94795aSAndroid Build Coastguard Worker	for _, p := range projectsToRead {
216*9e94795aSAndroid Build Coastguard Worker		go findMeta(p)
217*9e94795aSAndroid Build Coastguard Worker	}
218*9e94795aSAndroid Build Coastguard Worker	// Wait until all of the projects have been read.
219*9e94795aSAndroid Build Coastguard Worker	var msg strings.Builder
220*9e94795aSAndroid Build Coastguard Worker	result := make([]*ProjectMetadata, 0, len(projects))
221*9e94795aSAndroid Build Coastguard Worker	for _, pi := range projectIndexes {
222*9e94795aSAndroid Build Coastguard Worker		pi.wait()
223*9e94795aSAndroid Build Coastguard Worker		// Combine any errors into a single error.
224*9e94795aSAndroid Build Coastguard Worker		if pi.err != nil {
225*9e94795aSAndroid Build Coastguard Worker			fmt.Fprintf(&msg, "  %v\n", pi.err)
226*9e94795aSAndroid Build Coastguard Worker		} else if pi.pm != nil {
227*9e94795aSAndroid Build Coastguard Worker			result = append(result, pi.pm)
228*9e94795aSAndroid Build Coastguard Worker		}
229*9e94795aSAndroid Build Coastguard Worker	}
230*9e94795aSAndroid Build Coastguard Worker	if msg.Len() > 0 {
231*9e94795aSAndroid Build Coastguard Worker		return nil, fmt.Errorf("error reading project(s):\n%s", msg.String())
232*9e94795aSAndroid Build Coastguard Worker	}
233*9e94795aSAndroid Build Coastguard Worker	if len(result) == 0 {
234*9e94795aSAndroid Build Coastguard Worker		return nil, nil
235*9e94795aSAndroid Build Coastguard Worker	}
236*9e94795aSAndroid Build Coastguard Worker	return result, nil
237*9e94795aSAndroid Build Coastguard Worker}
238*9e94795aSAndroid Build Coastguard Worker
239*9e94795aSAndroid Build Coastguard Worker// AllMetadataFiles returns the sorted list of all METADATA files read thus far.
240*9e94795aSAndroid Build Coastguard Workerfunc (ix *Index) AllMetadataFiles() []string {
241*9e94795aSAndroid Build Coastguard Worker	var files []string
242*9e94795aSAndroid Build Coastguard Worker	ix.projects.Range(func(key, value any) bool {
243*9e94795aSAndroid Build Coastguard Worker		pi := value.(*projectIndex)
244*9e94795aSAndroid Build Coastguard Worker		if pi.path != "" {
245*9e94795aSAndroid Build Coastguard Worker			files = append(files, pi.path)
246*9e94795aSAndroid Build Coastguard Worker		}
247*9e94795aSAndroid Build Coastguard Worker		return true
248*9e94795aSAndroid Build Coastguard Worker	})
249*9e94795aSAndroid Build Coastguard Worker	return files
250*9e94795aSAndroid Build Coastguard Worker}
251*9e94795aSAndroid Build Coastguard Worker
252*9e94795aSAndroid Build Coastguard Worker// readMetadataFile tries to read and parse a METADATA file at `path` for `project`.
253*9e94795aSAndroid Build Coastguard Workerfunc (ix *Index) readMetadataFile(pi *projectIndex, path string) {
254*9e94795aSAndroid Build Coastguard Worker	f, err := ix.rootFS.Open(path)
255*9e94795aSAndroid Build Coastguard Worker	if err != nil {
256*9e94795aSAndroid Build Coastguard Worker		pi.err = fmt.Errorf("error opening project %q metadata %q: %w", pi.project, path, err)
257*9e94795aSAndroid Build Coastguard Worker		return
258*9e94795aSAndroid Build Coastguard Worker	}
259*9e94795aSAndroid Build Coastguard Worker
260*9e94795aSAndroid Build Coastguard Worker	// read the file
261*9e94795aSAndroid Build Coastguard Worker	data, err := io.ReadAll(f)
262*9e94795aSAndroid Build Coastguard Worker	if err != nil {
263*9e94795aSAndroid Build Coastguard Worker		pi.err = fmt.Errorf("error reading project %q metadata %q: %w", pi.project, path, err)
264*9e94795aSAndroid Build Coastguard Worker		return
265*9e94795aSAndroid Build Coastguard Worker	}
266*9e94795aSAndroid Build Coastguard Worker	f.Close()
267*9e94795aSAndroid Build Coastguard Worker
268*9e94795aSAndroid Build Coastguard Worker	uo := prototext.UnmarshalOptions{DiscardUnknown: true}
269*9e94795aSAndroid Build Coastguard Worker	pm := &ProjectMetadata{project: pi.project}
270*9e94795aSAndroid Build Coastguard Worker	err = uo.Unmarshal(data, &pm.proto)
271*9e94795aSAndroid Build Coastguard Worker	if err != nil {
272*9e94795aSAndroid Build Coastguard Worker		pi.err = fmt.Errorf(`error in project %q METADATA %q: %v
273*9e94795aSAndroid Build Coastguard Worker
274*9e94795aSAndroid Build Coastguard WorkerMETADATA and METADATA.android files must parse as text protobufs
275*9e94795aSAndroid Build Coastguard Workerdefined by
276*9e94795aSAndroid Build Coastguard Worker   build/soong/compliance/project_metadata_proto/project_metadata.proto
277*9e94795aSAndroid Build Coastguard Worker
278*9e94795aSAndroid Build Coastguard Worker* unknown fields don't matter
279*9e94795aSAndroid Build Coastguard Worker* check invalid ENUM names
280*9e94795aSAndroid Build Coastguard Worker* check quoting
281*9e94795aSAndroid Build Coastguard Worker* check unescaped nested quotes
282*9e94795aSAndroid Build Coastguard Worker* check the comment marker for protobuf is '#' not '//'
283*9e94795aSAndroid Build Coastguard Worker
284*9e94795aSAndroid Build Coastguard Workerif importing a library that uses a different sort of METADATA file, add
285*9e94795aSAndroid Build Coastguard Workera METADATA.android file beside it to parse instead
286*9e94795aSAndroid Build Coastguard Worker`, pi.project, path, err)
287*9e94795aSAndroid Build Coastguard Worker		return
288*9e94795aSAndroid Build Coastguard Worker	}
289*9e94795aSAndroid Build Coastguard Worker
290*9e94795aSAndroid Build Coastguard Worker	pi.path = path
291*9e94795aSAndroid Build Coastguard Worker	pi.pm = pm
292*9e94795aSAndroid Build Coastguard Worker}
293