blob: 1861b471e2f25c60ab4f23cf286d945b54c2ca01 [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 Kanouche776ad802022-10-21 20:47:42 +000061// ProjectName returns the name of the project.
62func (pm *ProjectMetadata) Name() string {
63 return pm.proto.GetName()
64}
65
66// ProjectVersion returns the version of the project if available.
67func (pm *ProjectMetadata) Version() string {
68 tp := pm.proto.GetThirdParty()
69 if tp != nil {
70 version := tp.GetVersion()
71 return version
72 }
73 return ""
74}
75
Bob Badourdc62de42022-10-12 20:10:17 -070076// VersionedName returns the name of the project including the version if any.
77func (pm *ProjectMetadata) VersionedName() string {
78 name := pm.proto.GetName()
79 if name != "" {
80 tp := pm.proto.GetThirdParty()
81 if tp != nil {
82 version := tp.GetVersion()
83 if version != "" {
84 if version[0] == 'v' || version[0] == 'V' {
85 return name + "_" + version
86 } else {
87 return name + "_v_" + version
88 }
89 }
90 }
91 return name
92 }
93 return pm.proto.GetDescription()
94}
95
Ibrahim Kanouche776ad802022-10-21 20:47:42 +000096// UrlsByTypeName returns a map of URLs by Type Name
97func (pm *ProjectMetadata) UrlsByTypeName() ProjectUrlMap {
98 tp := pm.proto.GetThirdParty()
99 if tp == nil {
100 return nil
101 }
102 if len(tp.Url) == 0 {
103 return nil
104 }
105 urls := make(ProjectUrlMap)
106
107 for _, url := range tp.Url {
108 uri := url.GetValue()
109 if uri == "" {
110 continue
111 }
112 urls[project_metadata_proto.URL_Type_name[int32(url.GetType())]] = uri
113 }
114 return urls
115}
116
Bob Badourdc62de42022-10-12 20:10:17 -0700117// projectIndex describes a project to be read; after `wg.Wait()`, will contain either
118// a `ProjectMetadata`, pm (can be nil even without error), or a non-nil `err`.
119type projectIndex struct {
120 project string
Ibrahim Kanouche776ad802022-10-21 20:47:42 +0000121 pm *ProjectMetadata
122 err error
123 done chan struct{}
Bob Badourdc62de42022-10-12 20:10:17 -0700124}
125
126// finish marks the task to read the `projectIndex` completed.
127func (pi *projectIndex) finish() {
128 close(pi.done)
129}
130
131// wait suspends execution until the `projectIndex` task completes.
132func (pi *projectIndex) wait() {
133 <-pi.done
134}
135
136// Index reads and caches ProjectMetadata (thread safe)
137type Index struct {
138 // projecs maps project name to a wait group if read has already started, and
139 // to a `ProjectMetadata` or to an `error` after the read completes.
140 projects sync.Map
141
142 // task provides a fixed-size task pool to limit concurrent open files etc.
143 task chan bool
144
145 // rootFS locates the root of the file system from which to read the files.
146 rootFS fs.FS
147}
148
149// NewIndex constructs a project metadata `Index` for the given file system.
150func NewIndex(rootFS fs.FS) *Index {
151 ix := &Index{task: make(chan bool, ConcurrentReaders), rootFS: rootFS}
152 for i := 0; i < ConcurrentReaders; i++ {
153 ix.task <- true
154 }
155 return ix
156}
157
158// MetadataForProjects returns 0..n ProjectMetadata for n `projects`, or an error.
159// Each project that has a METADATA.android or a METADATA file in the root of the project will have
160// a corresponding ProjectMetadata in the result. Projects with neither file get skipped. A nil
161// result with no error indicates none of the given `projects` has a METADATA file.
162// (thread safe -- can be called concurrently from multiple goroutines)
163func (ix *Index) MetadataForProjects(projects ...string) ([]*ProjectMetadata, error) {
164 if ConcurrentReaders < 1 {
165 return nil, fmt.Errorf("need at least one task in project metadata pool")
166 }
167 if len(projects) == 0 {
168 return nil, nil
169 }
170 // Identify the projects that have never been read
171 projectsToRead := make([]*projectIndex, 0, len(projects))
172 projectIndexes := make([]*projectIndex, 0, len(projects))
173 for _, p := range projects {
174 pi, loaded := ix.projects.LoadOrStore(p, &projectIndex{project: p, done: make(chan struct{})})
175 if !loaded {
176 projectsToRead = append(projectsToRead, pi.(*projectIndex))
177 }
178 projectIndexes = append(projectIndexes, pi.(*projectIndex))
179 }
180 // findMeta locates and reads the appropriate METADATA file, if any.
181 findMeta := func(pi *projectIndex) {
182 <-ix.task
183 defer func() {
184 ix.task <- true
185 pi.finish()
186 }()
187
188 // Support METADATA.android for projects that already have a different sort of METADATA file.
189 path := filepath.Join(pi.project, "METADATA.android")
190 fi, err := fs.Stat(ix.rootFS, path)
191 if err == nil {
192 if fi.Mode().IsRegular() {
193 ix.readMetadataFile(pi, path)
194 return
195 }
196 }
197 // No METADATA.android try METADATA file.
198 path = filepath.Join(pi.project, "METADATA")
199 fi, err = fs.Stat(ix.rootFS, path)
200 if err == nil {
201 if fi.Mode().IsRegular() {
202 ix.readMetadataFile(pi, path)
203 return
204 }
205 }
206 // no METADATA file exists -- leave nil and finish
207 }
208 // Look for the METADATA files to read, and record any missing.
209 for _, p := range projectsToRead {
210 go findMeta(p)
211 }
212 // Wait until all of the projects have been read.
213 var msg strings.Builder
214 result := make([]*ProjectMetadata, 0, len(projects))
215 for _, pi := range projectIndexes {
216 pi.wait()
217 // Combine any errors into a single error.
218 if pi.err != nil {
219 fmt.Fprintf(&msg, " %v\n", pi.err)
220 } else if pi.pm != nil {
221 result = append(result, pi.pm)
222 }
223 }
224 if msg.Len() > 0 {
225 return nil, fmt.Errorf("error reading project(s):\n%s", msg.String())
226 }
227 if len(result) == 0 {
228 return nil, nil
229 }
230 return result, nil
231}
232
233// readMetadataFile tries to read and parse a METADATA file at `path` for `project`.
234func (ix *Index) readMetadataFile(pi *projectIndex, path string) {
235 f, err := ix.rootFS.Open(path)
236 if err != nil {
237 pi.err = fmt.Errorf("error opening project %q metadata %q: %w", pi.project, path, err)
238 return
239 }
240
241 // read the file
242 data, err := io.ReadAll(f)
243 if err != nil {
244 pi.err = fmt.Errorf("error reading project %q metadata %q: %w", pi.project, path, err)
245 return
246 }
247 f.Close()
248
249 uo := prototext.UnmarshalOptions{DiscardUnknown: true}
250 pm := &ProjectMetadata{project: pi.project}
251 err = uo.Unmarshal(data, &pm.proto)
252 if err != nil {
253 pi.err = fmt.Errorf("error in project %q metadata %q: %w", pi.project, path, err)
254 return
255 }
256
257 pi.pm = pm
258}