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