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