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 graphViz bool 39 labelConditions bool 40 stripPrefix []string 41} 42 43func (ctx context) strip(installPath string) string { 44 for _, prefix := range ctx.stripPrefix { 45 if strings.HasPrefix(installPath, prefix) { 46 p := strings.TrimPrefix(installPath, prefix) 47 if 0 == len(p) { 48 continue 49 } 50 return p 51 } 52 } 53 return installPath 54} 55 56// newMultiString creates a flag that allows multiple values in an array. 57func newMultiString(flags *flag.FlagSet, name, usage string) *multiString { 58 var f multiString 59 flags.Var(&f, name, usage) 60 return &f 61} 62 63// multiString implements the flag `Value` interface for multiple strings. 64type multiString []string 65 66func (ms *multiString) String() string { return strings.Join(*ms, ", ") } 67func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil } 68 69func main() { 70 var expandedArgs []string 71 for _, arg := range os.Args[1:] { 72 if strings.HasPrefix(arg, "@") { 73 f, err := os.Open(strings.TrimPrefix(arg, "@")) 74 if err != nil { 75 fmt.Fprintln(os.Stderr, err.Error()) 76 os.Exit(1) 77 } 78 79 respArgs, err := response.ReadRspFile(f) 80 f.Close() 81 if err != nil { 82 fmt.Fprintln(os.Stderr, err.Error()) 83 os.Exit(1) 84 } 85 expandedArgs = append(expandedArgs, respArgs...) 86 } else { 87 expandedArgs = append(expandedArgs, arg) 88 } 89 } 90 91 flags := flag.NewFlagSet("flags", flag.ExitOnError) 92 93 flags.Usage = func() { 94 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...} 95 96Outputs space-separated Target Dependency Annotations tuples for each 97edge in the license graph. When -dot flag given, outputs the nodes and 98edges in graphViz directed graph format. 99 100In plain text mode, multiple values within a field are colon-separated. 101e.g. multiple annotations appear as annotation1:annotation2:annotation3 102or when -label_conditions is requested, Target and Dependency become 103target:condition1:condition2 etc. 104 105Options: 106`, filepath.Base(os.Args[0])) 107 flags.PrintDefaults() 108 } 109 110 graphViz := flags.Bool("dot", false, "Whether to output graphviz (i.e. dot) format.") 111 labelConditions := flags.Bool("label_conditions", false, "Whether to label target nodes with conditions.") 112 outputFile := flags.String("o", "-", "Where to write the output. (default stdout)") 113 stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)") 114 115 flags.Parse(expandedArgs) 116 117 // Must specify at least one root target. 118 if flags.NArg() == 0 { 119 flags.Usage() 120 os.Exit(2) 121 } 122 123 if len(*outputFile) == 0 { 124 flags.Usage() 125 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n") 126 os.Exit(2) 127 } else { 128 dir, err := filepath.Abs(filepath.Dir(*outputFile)) 129 if err != nil { 130 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err) 131 os.Exit(1) 132 } 133 fi, err := os.Stat(dir) 134 if err != nil { 135 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err) 136 os.Exit(1) 137 } 138 if !fi.IsDir() { 139 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile) 140 os.Exit(1) 141 } 142 } 143 144 var ofile io.Writer 145 ofile = os.Stdout 146 var obuf *bytes.Buffer 147 if *outputFile != "-" { 148 obuf = &bytes.Buffer{} 149 ofile = obuf 150 } 151 152 ctx := &context{*graphViz, *labelConditions, *stripPrefix} 153 154 err := dumpGraph(ctx, ofile, os.Stderr, compliance.FS, flags.Args()...) 155 if err != nil { 156 if err == failNoneRequested { 157 flags.Usage() 158 } 159 fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 160 os.Exit(1) 161 } 162 if *outputFile != "-" { 163 err := os.WriteFile(*outputFile, obuf.Bytes(), 0666) 164 if err != nil { 165 fmt.Fprintf(os.Stderr, "could not write output to %q from %q: %s\n", *outputFile, os.Getenv("PWD"), err) 166 os.Exit(1) 167 } 168 } 169 os.Exit(0) 170} 171 172// dumpGraph implements the dumpgraph utility. 173func dumpGraph(ctx *context, stdout, stderr io.Writer, rootFS fs.FS, files ...string) error { 174 if len(files) < 1 { 175 return failNoneRequested 176 } 177 178 // Read the license graph from the license metadata files (*.meta_lic). 179 licenseGraph, err := compliance.ReadLicenseGraph(rootFS, stderr, files) 180 if err != nil { 181 return fmt.Errorf("Unable to read license metadata file(s) %q: %w\n", files, err) 182 } 183 if licenseGraph == nil { 184 return failNoLicenses 185 } 186 187 // Sort the edges of the graph. 188 edges := licenseGraph.Edges() 189 sort.Sort(edges) 190 191 // nodes maps license metadata file names to graphViz node names when ctx.graphViz is true. 192 var nodes map[string]string 193 n := 0 194 195 // targetOut calculates the string to output for `target` separating conditions as needed using `sep`. 196 targetOut := func(target *compliance.TargetNode, sep string) string { 197 tOut := ctx.strip(target.Name()) 198 if ctx.labelConditions { 199 conditions := target.LicenseConditions().Names() 200 sort.Strings(conditions) 201 if len(conditions) > 0 { 202 tOut += sep + strings.Join(conditions, sep) 203 } 204 } 205 return tOut 206 } 207 208 // makeNode maps `target` to a graphViz node name. 209 makeNode := func(target *compliance.TargetNode) { 210 tName := target.Name() 211 if _, ok := nodes[tName]; !ok { 212 nodeName := fmt.Sprintf("n%d", n) 213 nodes[tName] = nodeName 214 fmt.Fprintf(stdout, "\t%s [label=\"%s\"];\n", nodeName, targetOut(target, "\\n")) 215 n++ 216 } 217 } 218 219 // If graphviz output, map targets to node names, and start the directed graph. 220 if ctx.graphViz { 221 nodes = make(map[string]string) 222 targets := licenseGraph.Targets() 223 sort.Sort(targets) 224 225 fmt.Fprintf(stdout, "strict digraph {\n\trankdir=RL;\n") 226 for _, target := range targets { 227 makeNode(target) 228 } 229 } 230 231 // Print the sorted edges to stdout ... 232 for _, e := range edges { 233 // sort the annotations for repeatability/stability 234 annotations := e.Annotations().AsList() 235 sort.Strings(annotations) 236 237 tName := e.Target().Name() 238 dName := e.Dependency().Name() 239 240 if ctx.graphViz { 241 // ... one edge per line labelled with \\n-separated annotations. 242 tNode := nodes[tName] 243 dNode := nodes[dName] 244 fmt.Fprintf(stdout, "\t%s -> %s [label=\"%s\"];\n", dNode, tNode, strings.Join(annotations, "\\n")) 245 } else { 246 // ... one edge per line with annotations in a colon-separated tuple. 247 fmt.Fprintf(stdout, "%s %s %s\n", targetOut(e.Target(), ":"), targetOut(e.Dependency(), ":"), strings.Join(annotations, ":")) 248 } 249 } 250 251 // If graphViz output, rank the root nodes together, and complete the directed graph. 252 if ctx.graphViz { 253 fmt.Fprintf(stdout, "\t{rank=same;") 254 for _, f := range files { 255 fName := f 256 if !strings.HasSuffix(fName, ".meta_lic") { 257 fName += ".meta_lic" 258 } 259 if fNode, ok := nodes[fName]; ok { 260 fmt.Fprintf(stdout, " %s", fNode) 261 } 262 } 263 fmt.Fprintf(stdout, "}\n}\n") 264 } 265 return nil 266} 267