blob: fa16b1bdf0bf961b2b5c261e114eec6e01f58f9a [file] [log] [blame]
Bob Badourfa739da2021-10-25 16:21:00 -07001// 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 (
Bob Badourfa739da2021-10-25 16:21:00 -070018 "flag"
19 "fmt"
20 "io"
21 "os"
22 "path/filepath"
23 "sort"
24 "strings"
Colin Cross38a61932022-01-27 15:26:49 -080025
26 "android/soong/tools/compliance"
Bob Badourfa739da2021-10-25 16:21:00 -070027)
28
29var (
30 graphViz = flag.Bool("dot", false, "Whether to output graphviz (i.e. dot) format.")
31 labelConditions = flag.Bool("label_conditions", false, "Whether to label target nodes with conditions.")
Bob Badour682e1ba2022-02-02 15:15:56 -080032 stripPrefix = newMultiString("strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
Bob Badourfa739da2021-10-25 16:21:00 -070033
34 failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
Colin Cross35f79c32022-01-27 15:18:52 -080035 failNoLicenses = fmt.Errorf("No licenses found")
Bob Badourfa739da2021-10-25 16:21:00 -070036)
37
38type context struct {
39 graphViz bool
40 labelConditions bool
Bob Badour682e1ba2022-02-02 15:15:56 -080041 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
Bob Badourfa739da2021-10-25 16:21:00 -070055}
56
57func init() {
58 flag.Usage = func() {
59 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
60
61Outputs space-separated Target Dependency Annotations tuples for each
62edge in the license graph. When -dot flag given, outputs the nodes and
63edges in graphViz directed graph format.
64
65In plain text mode, multiple values within a field are colon-separated.
66e.g. multiple annotations appear as annotation1:annotation2:annotation3
67or when -label_conditions is requested, Target and Dependency become
68target:condition1:condition2 etc.
69
70Options:
71`, filepath.Base(os.Args[0]))
72 flag.PrintDefaults()
73 }
74}
75
Bob Badour682e1ba2022-02-02 15:15:56 -080076// newMultiString creates a flag that allows multiple values in an array.
77func newMultiString(name, usage string) *multiString {
78 var f multiString
79 flag.Var(&f, name, usage)
80 return &f
81}
82
83// multiString implements the flag `Value` interface for multiple strings.
84type multiString []string
85
86func (ms *multiString) String() string { return strings.Join(*ms, ", ") }
87func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
88
Bob Badourfa739da2021-10-25 16:21:00 -070089func main() {
90 flag.Parse()
91
92 // Must specify at least one root target.
93 if flag.NArg() == 0 {
94 flag.Usage()
95 os.Exit(2)
96 }
97
98 ctx := &context{*graphViz, *labelConditions, *stripPrefix}
99
100 err := dumpGraph(ctx, os.Stdout, os.Stderr, flag.Args()...)
101 if err != nil {
102 if err == failNoneRequested {
103 flag.Usage()
104 }
105 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
106 os.Exit(1)
107 }
108 os.Exit(0)
109}
110
111// dumpGraph implements the dumpgraph utility.
112func dumpGraph(ctx *context, stdout, stderr io.Writer, files ...string) error {
113 if len(files) < 1 {
114 return failNoneRequested
115 }
116
117 // Read the license graph from the license metadata files (*.meta_lic).
118 licenseGraph, err := compliance.ReadLicenseGraph(os.DirFS("."), stderr, files)
119 if err != nil {
120 return fmt.Errorf("Unable to read license metadata file(s) %q: %w\n", files, err)
121 }
122 if licenseGraph == nil {
123 return failNoLicenses
124 }
125
126 // Sort the edges of the graph.
127 edges := licenseGraph.Edges()
128 sort.Sort(edges)
129
130 // nodes maps license metadata file names to graphViz node names when ctx.graphViz is true.
131 var nodes map[string]string
132 n := 0
133
134 // targetOut calculates the string to output for `target` separating conditions as needed using `sep`.
135 targetOut := func(target *compliance.TargetNode, sep string) string {
Bob Badour682e1ba2022-02-02 15:15:56 -0800136 tOut := ctx.strip(target.Name())
Bob Badourfa739da2021-10-25 16:21:00 -0700137 if ctx.labelConditions {
138 conditions := target.LicenseConditions().Names()
139 sort.Strings(conditions)
140 if len(conditions) > 0 {
141 tOut += sep + strings.Join(conditions, sep)
142 }
143 }
144 return tOut
145 }
146
147 // makeNode maps `target` to a graphViz node name.
148 makeNode := func(target *compliance.TargetNode) {
149 tName := target.Name()
150 if _, ok := nodes[tName]; !ok {
151 nodeName := fmt.Sprintf("n%d", n)
152 nodes[tName] = nodeName
153 fmt.Fprintf(stdout, "\t%s [label=\"%s\"];\n", nodeName, targetOut(target, "\\n"))
154 n++
155 }
156 }
157
158 // If graphviz output, map targets to node names, and start the directed graph.
159 if ctx.graphViz {
160 nodes = make(map[string]string)
161 targets := licenseGraph.Targets()
162 sort.Sort(targets)
163
164 fmt.Fprintf(stdout, "strict digraph {\n\trankdir=RL;\n")
165 for _, target := range targets {
166 makeNode(target)
167 }
168 }
169
170 // Print the sorted edges to stdout ...
171 for _, e := range edges {
172 // sort the annotations for repeatability/stability
173 annotations := e.Annotations().AsList()
174 sort.Strings(annotations)
175
176 tName := e.Target().Name()
177 dName := e.Dependency().Name()
178
179 if ctx.graphViz {
180 // ... one edge per line labelled with \\n-separated annotations.
181 tNode := nodes[tName]
182 dNode := nodes[dName]
183 fmt.Fprintf(stdout, "\t%s -> %s [label=\"%s\"];\n", dNode, tNode, strings.Join(annotations, "\\n"))
184 } else {
185 // ... one edge per line with annotations in a colon-separated tuple.
186 fmt.Fprintf(stdout, "%s %s %s\n", targetOut(e.Target(), ":"), targetOut(e.Dependency(), ":"), strings.Join(annotations, ":"))
187 }
188 }
189
190 // If graphViz output, rank the root nodes together, and complete the directed graph.
191 if ctx.graphViz {
192 fmt.Fprintf(stdout, "\t{rank=same;")
193 for _, f := range files {
194 fName := f
195 if !strings.HasSuffix(fName, ".meta_lic") {
196 fName += ".meta_lic"
197 }
198 if fNode, ok := nodes[fName]; ok {
199 fmt.Fprintf(stdout, " %s", fNode)
200 }
201 }
202 fmt.Fprintf(stdout, "}\n}\n")
203 }
204 return nil
205}