blob: b31413d03a51a2f107c9e4c38123f837cbde2f3f [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
43// String returns a string representation of the metadata for error messages.
44func (pm *ProjectMetadata) String() string {
45 return fmt.Sprintf("project: %q\n%s", pm.project, pm.proto.String())
46}
47
48// VersionedName returns the name of the project including the version if any.
49func (pm *ProjectMetadata) VersionedName() string {
50 name := pm.proto.GetName()
51 if name != "" {
52 tp := pm.proto.GetThirdParty()
53 if tp != nil {
54 version := tp.GetVersion()
55 if version != "" {
56 if version[0] == 'v' || version[0] == 'V' {
57 return name + "_" + version
58 } else {
59 return name + "_v_" + version
60 }
61 }
62 }
63 return name
64 }
65 return pm.proto.GetDescription()
66}
67
68// projectIndex describes a project to be read; after `wg.Wait()`, will contain either
69// a `ProjectMetadata`, pm (can be nil even without error), or a non-nil `err`.
70type projectIndex struct {
71 project string
72 pm *ProjectMetadata
73 err error
74 done chan struct{}
75}
76
77// finish marks the task to read the `projectIndex` completed.
78func (pi *projectIndex) finish() {
79 close(pi.done)
80}
81
82// wait suspends execution until the `projectIndex` task completes.
83func (pi *projectIndex) wait() {
84 <-pi.done
85}
86
87// Index reads and caches ProjectMetadata (thread safe)
88type Index struct {
89 // projecs maps project name to a wait group if read has already started, and
90 // to a `ProjectMetadata` or to an `error` after the read completes.
91 projects sync.Map
92
93 // task provides a fixed-size task pool to limit concurrent open files etc.
94 task chan bool
95
96 // rootFS locates the root of the file system from which to read the files.
97 rootFS fs.FS
98}
99
100// NewIndex constructs a project metadata `Index` for the given file system.
101func NewIndex(rootFS fs.FS) *Index {
102 ix := &Index{task: make(chan bool, ConcurrentReaders), rootFS: rootFS}
103 for i := 0; i < ConcurrentReaders; i++ {
104 ix.task <- true
105 }
106 return ix
107}
108
109// MetadataForProjects returns 0..n ProjectMetadata for n `projects`, or an error.
110// Each project that has a METADATA.android or a METADATA file in the root of the project will have
111// a corresponding ProjectMetadata in the result. Projects with neither file get skipped. A nil
112// result with no error indicates none of the given `projects` has a METADATA file.
113// (thread safe -- can be called concurrently from multiple goroutines)
114func (ix *Index) MetadataForProjects(projects ...string) ([]*ProjectMetadata, error) {
115 if ConcurrentReaders < 1 {
116 return nil, fmt.Errorf("need at least one task in project metadata pool")
117 }
118 if len(projects) == 0 {
119 return nil, nil
120 }
121 // Identify the projects that have never been read
122 projectsToRead := make([]*projectIndex, 0, len(projects))
123 projectIndexes := make([]*projectIndex, 0, len(projects))
124 for _, p := range projects {
125 pi, loaded := ix.projects.LoadOrStore(p, &projectIndex{project: p, done: make(chan struct{})})
126 if !loaded {
127 projectsToRead = append(projectsToRead, pi.(*projectIndex))
128 }
129 projectIndexes = append(projectIndexes, pi.(*projectIndex))
130 }
131 // findMeta locates and reads the appropriate METADATA file, if any.
132 findMeta := func(pi *projectIndex) {
133 <-ix.task
134 defer func() {
135 ix.task <- true
136 pi.finish()
137 }()
138
139 // Support METADATA.android for projects that already have a different sort of METADATA file.
140 path := filepath.Join(pi.project, "METADATA.android")
141 fi, err := fs.Stat(ix.rootFS, path)
142 if err == nil {
143 if fi.Mode().IsRegular() {
144 ix.readMetadataFile(pi, path)
145 return
146 }
147 }
148 // No METADATA.android try METADATA file.
149 path = filepath.Join(pi.project, "METADATA")
150 fi, err = fs.Stat(ix.rootFS, path)
151 if err == nil {
152 if fi.Mode().IsRegular() {
153 ix.readMetadataFile(pi, path)
154 return
155 }
156 }
157 // no METADATA file exists -- leave nil and finish
158 }
159 // Look for the METADATA files to read, and record any missing.
160 for _, p := range projectsToRead {
161 go findMeta(p)
162 }
163 // Wait until all of the projects have been read.
164 var msg strings.Builder
165 result := make([]*ProjectMetadata, 0, len(projects))
166 for _, pi := range projectIndexes {
167 pi.wait()
168 // Combine any errors into a single error.
169 if pi.err != nil {
170 fmt.Fprintf(&msg, " %v\n", pi.err)
171 } else if pi.pm != nil {
172 result = append(result, pi.pm)
173 }
174 }
175 if msg.Len() > 0 {
176 return nil, fmt.Errorf("error reading project(s):\n%s", msg.String())
177 }
178 if len(result) == 0 {
179 return nil, nil
180 }
181 return result, nil
182}
183
184// readMetadataFile tries to read and parse a METADATA file at `path` for `project`.
185func (ix *Index) readMetadataFile(pi *projectIndex, path string) {
186 f, err := ix.rootFS.Open(path)
187 if err != nil {
188 pi.err = fmt.Errorf("error opening project %q metadata %q: %w", pi.project, path, err)
189 return
190 }
191
192 // read the file
193 data, err := io.ReadAll(f)
194 if err != nil {
195 pi.err = fmt.Errorf("error reading project %q metadata %q: %w", pi.project, path, err)
196 return
197 }
198 f.Close()
199
200 uo := prototext.UnmarshalOptions{DiscardUnknown: true}
201 pm := &ProjectMetadata{project: pi.project}
202 err = uo.Unmarshal(data, &pm.proto)
203 if err != nil {
204 pi.err = fmt.Errorf("error in project %q metadata %q: %w", pi.project, path, err)
205 return
206 }
207
208 pi.pm = pm
209}