1// Copyright 2021 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 main 16 17import ( 18 "bytes" 19 "flag" 20 "fmt" 21 "io" 22 "io/fs" 23 "os" 24 "path/filepath" 25 "sort" 26 "strings" 27 28 "android/soong/response" 29 "android/soong/tools/compliance" 30) 31 32var ( 33 failNoneRequested = fmt.Errorf("\nNo license metadata files requested") 34 failNoLicenses = fmt.Errorf("No licenses found") 35) 36 37type context struct { 38 conditions []compliance.LicenseCondition 39 graphViz bool 40 labelConditions bool 41 stripPrefix []string 42} 43 44func (ctx context) strip(installPath string) string { 45 for _, prefix := range ctx.stripPrefix { 46 if strings.HasPrefix(installPath, prefix) { 47 p := strings.TrimPrefix(installPath, prefix) 48 if 0 == len(p) { 49 continue 50 } 51 return p 52 } 53 } 54 return installPath 55} 56 57// newMultiString creates a flag that allows multiple values in an array. 58func newMultiString(flags *flag.FlagSet, name, usage string) *multiString { 59 var f multiString 60 flags.Var(&f, name, usage) 61 return &f 62} 63 64// multiString implements the flag `Value` interface for multiple strings. 65type multiString []string 66 67func (ms *multiString) String() string { return strings.Join(*ms, ", ") } 68func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil } 69 70func main() { 71 var expandedArgs []string 72 for _, arg := range os.Args[1:] { 73 if strings.HasPrefix(arg, "@") { 74 f, err := os.Open(strings.TrimPrefix(arg, "@")) 75 if err != nil { 76 fmt.Fprintln(os.Stderr, err.Error()) 77 os.Exit(1) 78 } 79 80 respArgs, err := response.ReadRspFile(f) 81 f.Close() 82 if err != nil { 83 fmt.Fprintln(os.Stderr, err.Error()) 84 os.Exit(1) 85 } 86 expandedArgs = append(expandedArgs, respArgs...) 87 } else { 88 expandedArgs = append(expandedArgs, arg) 89 } 90 } 91 92 flags := flag.NewFlagSet("flags", flag.ExitOnError) 93 94 flags.Usage = func() { 95 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...} 96 97Outputs a space-separated Target ActsOn Origin Condition tuple for each 98resolution in the graph. When -dot flag given, outputs nodes and edges 99in graphviz directed graph format. 100 101If one or more '-c condition' conditions are given, outputs the 102resolution for the union of the conditions. Otherwise, outputs the 103resolution for all conditions. 104 105In plain text mode, when '-label_conditions' is requested, the Target 106and Origin have colon-separated license conditions appended: 107i.e. target:condition1:condition2 etc. 108 109Options: 110`, filepath.Base(os.Args[0])) 111 flags.PrintDefaults() 112 } 113 114 conditions := newMultiString(flags, "c", "License condition to resolve. (may be given multiple times)") 115 graphViz := flags.Bool("dot", false, "Whether to output graphviz (i.e. dot) format.") 116 labelConditions := flags.Bool("label_conditions", false, "Whether to label target nodes with conditions.") 117 outputFile := flags.String("o", "-", "Where to write the output. (default stdout)") 118 stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)") 119 120 flags.Parse(expandedArgs) 121 122 // Must specify at least one root target. 123 if flags.NArg() == 0 { 124 flags.Usage() 125 os.Exit(2) 126 } 127 128 if len(*outputFile) == 0 { 129 flags.Usage() 130 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n") 131 os.Exit(2) 132 } else { 133 dir, err := filepath.Abs(filepath.Dir(*outputFile)) 134 if err != nil { 135 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err) 136 os.Exit(1) 137 } 138 fi, err := os.Stat(dir) 139 if err != nil { 140 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err) 141 os.Exit(1) 142 } 143 if !fi.IsDir() { 144 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile) 145 os.Exit(1) 146 } 147 } 148 149 var ofile io.Writer 150 ofile = os.Stdout 151 var obuf *bytes.Buffer 152 if *outputFile != "-" { 153 obuf = &bytes.Buffer{} 154 ofile = obuf 155 } 156 157 lcs := make([]compliance.LicenseCondition, 0, len(*conditions)) 158 for _, name := range *conditions { 159 lcs = append(lcs, compliance.RecognizedConditionNames[name]) 160 } 161 ctx := &context{ 162 conditions: lcs, 163 graphViz: *graphViz, 164 labelConditions: *labelConditions, 165 stripPrefix: *stripPrefix, 166 } 167 _, err := dumpResolutions(ctx, ofile, os.Stderr, compliance.FS, flags.Args()...) 168 if err != nil { 169 if err == failNoneRequested { 170 flags.Usage() 171 } 172 fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 173 os.Exit(1) 174 } 175 if *outputFile != "-" { 176 err := os.WriteFile(*outputFile, obuf.Bytes(), 0666) 177 if err != nil { 178 fmt.Fprintf(os.Stderr, "could not write output to %q from %q: %s\n", *outputFile, os.Getenv("PWD"), err) 179 os.Exit(1) 180 } 181 } 182 os.Exit(0) 183} 184 185// dumpResolutions implements the dumpresolutions utility. 186func dumpResolutions(ctx *context, stdout, stderr io.Writer, rootFS fs.FS, files ...string) (*compliance.LicenseGraph, error) { 187 if len(files) < 1 { 188 return nil, failNoneRequested 189 } 190 191 // Read the license graph from the license metadata files (*.meta_lic). 192 licenseGraph, err := compliance.ReadLicenseGraph(rootFS, stderr, files) 193 if err != nil { 194 return nil, fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err) 195 } 196 if licenseGraph == nil { 197 return nil, failNoLicenses 198 } 199 200 compliance.ResolveTopDownConditions(licenseGraph) 201 cs := compliance.AllLicenseConditions 202 if len(ctx.conditions) > 0 { 203 cs = compliance.NewLicenseConditionSet() 204 for _, c := range ctx.conditions { 205 cs = cs.Plus(c) 206 } 207 } 208 209 resolutions := compliance.WalkResolutionsForCondition(licenseGraph, cs) 210 211 // nodes maps license metadata file names to graphViz node names when graphViz requested. 212 nodes := make(map[string]string) 213 n := 0 214 215 // targetOut calculates the string to output for `target` adding `sep`-separated conditions as needed. 216 targetOut := func(target *compliance.TargetNode, sep string) string { 217 tOut := ctx.strip(target.Name()) 218 if ctx.labelConditions { 219 conditions := target.LicenseConditions().Names() 220 if len(conditions) > 0 { 221 tOut += sep + strings.Join(conditions, sep) 222 } 223 } 224 return tOut 225 } 226 227 // makeNode maps `target` to a graphViz node name. 228 makeNode := func(target *compliance.TargetNode) { 229 tName := target.Name() 230 if _, ok := nodes[tName]; !ok { 231 nodeName := fmt.Sprintf("n%d", n) 232 nodes[tName] = nodeName 233 fmt.Fprintf(stdout, "\t%s [label=\"%s\"];\n", nodeName, targetOut(target, "\\n")) 234 n++ 235 } 236 } 237 238 // outputResolution prints a resolution in the requested format to `stdout`, where one can read 239 // a resolution as `tname` resolves `oname`'s conditions named in `cnames`. 240 // `tname` is the name of the target the resolution applies to. 241 // `cnames` is the list of conditions to resolve. 242 outputResolution := func(tname, aname string, cnames []string) { 243 if ctx.graphViz { 244 // ... one edge per line labelled with \\n-separated annotations. 245 tNode := nodes[tname] 246 aNode := nodes[aname] 247 fmt.Fprintf(stdout, "\t%s -> %s [label=\"%s\"];\n", tNode, aNode, strings.Join(cnames, "\\n")) 248 } else { 249 // ... one edge per line with names in a colon-separated tuple. 250 fmt.Fprintf(stdout, "%s %s %s\n", tname, aname, strings.Join(cnames, ":")) 251 } 252 } 253 254 // Sort the resolutions by targetname for repeatability/stability. 255 targets := resolutions.AttachesTo() 256 sort.Sort(targets) 257 258 // If graphviz output, start the directed graph. 259 if ctx.graphViz { 260 fmt.Fprintf(stdout, "strict digraph {\n\trankdir=LR;\n") 261 for _, target := range targets { 262 makeNode(target) 263 rl := resolutions.Resolutions(target) 264 sort.Sort(rl) 265 for _, r := range rl { 266 makeNode(r.ActsOn()) 267 } 268 } 269 } 270 271 // Output the sorted targets. 272 for _, target := range targets { 273 var tname string 274 if ctx.graphViz { 275 tname = target.Name() 276 } else { 277 tname = targetOut(target, ":") 278 } 279 280 rl := resolutions.Resolutions(target) 281 sort.Sort(rl) 282 for _, r := range rl { 283 var aname string 284 if ctx.graphViz { 285 aname = r.ActsOn().Name() 286 } else { 287 aname = targetOut(r.ActsOn(), ":") 288 } 289 290 // cnames accumulates the list of condition names originating at a single origin that apply to `target`. 291 cnames := r.Resolves().Names() 292 293 // Output 1 line for each attachesTo+actsOn combination. 294 outputResolution(tname, aname, cnames) 295 } 296 } 297 // If graphViz output, rank the root nodes together, and complete the directed graph. 298 if ctx.graphViz { 299 fmt.Fprintf(stdout, "\t{rank=same;") 300 for _, f := range files { 301 fName := f 302 if !strings.HasSuffix(fName, ".meta_lic") { 303 fName += ".meta_lic" 304 } 305 if fNode, ok := nodes[fName]; ok { 306 fmt.Fprintf(stdout, " %s", fNode) 307 } 308 } 309 fmt.Fprintf(stdout, "}\n}\n") 310 } 311 return licenseGraph, nil 312} 313