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