blob: 36fbb7d8b9f8fa754952fd6c7340f7f71290820b [file] [log] [blame]
Bob Badour1ded0a12021-12-03 17:16:14 -08001// 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 "compliance"
19 "flag"
20 "fmt"
21 "io"
22 "os"
23 "path/filepath"
24 "sort"
25 "strings"
26)
27
28var (
29 conditions = newMultiString("c", "License condition to resolve. (may be given multiple times)")
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.")
32 stripPrefix = flag.String("strip_prefix", "", "Prefix to remove from paths. i.e. path to root")
33
34 failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
35 failNoLicenses = fmt.Errorf("No licenses found")
36)
37
38type context struct {
39 conditions []string
40 graphViz bool
41 labelConditions bool
42 stripPrefix string
43}
44
45func init() {
46 flag.Usage = func() {
47 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
48
49Outputs a space-separated Target ActsOn Origin Condition tuple for each
50resolution in the graph. When -dot flag given, outputs nodes and edges
51in graphviz directed graph format.
52
53If one or more '-c condition' conditions are given, outputs the joined
54set of resolutions for all of the conditions. Otherwise, outputs the
55result of the bottom-up and top-down resolve only.
56
57In plain text mode, when '-label_conditions' is requested, the Target
58and Origin have colon-separated license conditions appended:
59i.e. target:condition1:condition2 etc.
60
61Options:
62`, filepath.Base(os.Args[0]))
63 flag.PrintDefaults()
64 }
65}
66
67// newMultiString creates a flag that allows multiple values in an array.
68func newMultiString(name, usage string) *multiString {
69 var f multiString
70 flag.Var(&f, name, usage)
71 return &f
72}
73
74// multiString implements the flag `Value` interface for multiple strings.
75type multiString []string
76
77func (ms *multiString) String() string { return strings.Join(*ms, ", ") }
78func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
79
80func main() {
81 flag.Parse()
82
83 // Must specify at least one root target.
84 if flag.NArg() == 0 {
85 flag.Usage()
86 os.Exit(2)
87 }
88
89 ctx := &context{
90 conditions: append([]string{}, *conditions...),
91 graphViz: *graphViz,
92 labelConditions: *labelConditions,
93 stripPrefix: *stripPrefix,
94 }
95 err := dumpResolutions(ctx, os.Stdout, os.Stderr, flag.Args()...)
96 if err != nil {
97 if err == failNoneRequested {
98 flag.Usage()
99 }
100 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
101 os.Exit(1)
102 }
103 os.Exit(0)
104}
105
106// dumpResolutions implements the dumpresolutions utility.
107func dumpResolutions(ctx *context, stdout, stderr io.Writer, files ...string) error {
108 if len(files) < 1 {
109 return failNoneRequested
110 }
111
112 // Read the license graph from the license metadata files (*.meta_lic).
113 licenseGraph, err := compliance.ReadLicenseGraph(os.DirFS("."), stderr, files)
114 if err != nil {
115 return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
116 }
117 if licenseGraph == nil {
118 return failNoLicenses
119 }
120
121 // resolutions will contain the requested set of resolutions.
122 var resolutions *compliance.ResolutionSet
123
124 resolutions = compliance.ResolveTopDownConditions(licenseGraph)
125 if len(ctx.conditions) > 0 {
126 rlist := make([]*compliance.ResolutionSet, 0, len(ctx.conditions))
127 for _, c := range ctx.conditions {
128 rlist = append(rlist, compliance.WalkResolutionsForCondition(licenseGraph, resolutions, compliance.ConditionNames{c}))
129 }
130 if len(rlist) == 1 {
131 resolutions = rlist[0]
132 } else {
133 resolutions = compliance.JoinResolutionSets(rlist...)
134 }
135 }
136
137 // nodes maps license metadata file names to graphViz node names when graphViz requested.
138 nodes := make(map[string]string)
139 n := 0
140
141 // targetOut calculates the string to output for `target` adding `sep`-separated conditions as needed.
142 targetOut := func(target *compliance.TargetNode, sep string) string {
143 tOut := strings.TrimPrefix(target.Name(), ctx.stripPrefix)
144 if ctx.labelConditions {
145 conditions := make([]string, 0, target.LicenseConditions().Count())
146 for _, lc := range target.LicenseConditions().AsList() {
147 conditions = append(conditions, lc.Name())
148 }
149 sort.Strings(conditions)
150 if len(conditions) > 0 {
151 tOut += sep + strings.Join(conditions, sep)
152 }
153 }
154 return tOut
155 }
156
157 // makeNode maps `target` to a graphViz node name.
158 makeNode := func(target *compliance.TargetNode) {
159 tName := target.Name()
160 if _, ok := nodes[tName]; !ok {
161 nodeName := fmt.Sprintf("n%d", n)
162 nodes[tName] = nodeName
163 fmt.Fprintf(stdout, "\t%s [label=\"%s\"];\n", nodeName, targetOut(target, "\\n"))
164 n++
165 }
166 }
167
168 // outputResolution prints a resolution in the requested format to `stdout`, where one can read
169 // a resolution as `tname` resolves `oname`'s conditions named in `cnames`.
170 // `tname` is the name of the target the resolution applies to.
171 // `oname` is the name of the target where the conditions originate.
172 // `cnames` is the list of conditions to resolve.
173 outputResolution := func(tname, aname, oname string, cnames []string) {
174 if ctx.graphViz {
175 // ... one edge per line labelled with \\n-separated annotations.
176 tNode := nodes[tname]
177 aNode := nodes[aname]
178 oNode := nodes[oname]
179 fmt.Fprintf(stdout, "\t%s -> %s; %s -> %s [label=\"%s\"];\n", tNode, aNode, aNode, oNode, strings.Join(cnames, "\\n"))
180 } else {
181 // ... one edge per line with names in a colon-separated tuple.
182 fmt.Fprintf(stdout, "%s %s %s %s\n", tname, aname, oname, strings.Join(cnames, ":"))
183 }
184 }
185
186 // outputSingleton prints `tname` to plain text in the unexpected event that `tname` is the name of
187 // a target in `resolutions.AppliesTo()` but has no conditions to resolve.
188 outputSingleton := func(tname, aname string) {
189 if !ctx.graphViz {
190 fmt.Fprintf(stdout, "%s %s\n", tname, aname)
191 }
192 }
193
194 // Sort the resolutions by targetname for repeatability/stability.
195 targets := resolutions.AttachesTo()
196 sort.Sort(targets)
197
198 // If graphviz output, start the directed graph.
199 if ctx.graphViz {
200 fmt.Fprintf(stdout, "strict digraph {\n\trankdir=LR;\n")
201 for _, target := range targets {
202 makeNode(target)
203 rl := compliance.ResolutionList(resolutions.Resolutions(target))
204 sort.Sort(rl)
205 for _, r := range rl {
206 makeNode(r.ActsOn())
207 }
208 conditions := rl.AllConditions().AsList()
209 sort.Sort(conditions)
210 for _, lc := range conditions {
211 makeNode(lc.Origin())
212 }
213 }
214 }
215
216 // Output the sorted targets.
217 for _, target := range targets {
218 var tname string
219 if ctx.graphViz {
220 tname = target.Name()
221 } else {
222 tname = targetOut(target, ":")
223 }
224
225 rl := compliance.ResolutionList(resolutions.Resolutions(target))
226 sort.Sort(rl)
227 for _, r := range rl {
228 var aname string
229 if ctx.graphViz {
230 aname = r.ActsOn().Name()
231 } else {
232 aname = targetOut(r.ActsOn(), ":")
233 }
234
235 conditions := r.Resolves().AsList()
236 sort.Sort(conditions)
237
238 // poname is the previous origin name or "" if no previous
239 poname := ""
240
241 // cnames accumulates the list of condition names originating at a single origin that apply to `target`.
242 cnames := make([]string, 0, len(conditions))
243
244 // Output 1 line for each attachesTo+actsOn+origin combination.
245 for _, condition := range conditions {
246 var oname string
247 if ctx.graphViz {
248 oname = condition.Origin().Name()
249 } else {
250 oname = targetOut(condition.Origin(), ":")
251 }
252
253 // Detect when origin changes and output prior origin's conditions.
254 if poname != oname && poname != "" {
255 outputResolution(tname, aname, poname, cnames)
256 cnames = cnames[:0]
257 }
258 poname = oname
259 cnames = append(cnames, condition.Name())
260 }
261 // Output last origin's conditions or a singleton if no origins.
262 if poname == "" {
263 outputSingleton(tname, aname)
264 } else {
265 outputResolution(tname, aname, poname, cnames)
266 }
267 }
268 }
269 // If graphViz output, rank the root nodes together, and complete the directed graph.
270 if ctx.graphViz {
271 fmt.Fprintf(stdout, "\t{rank=same;")
272 for _, f := range files {
273 fName := f
274 if !strings.HasSuffix(fName, ".meta_lic") {
275 fName += ".meta_lic"
276 }
277 if fNode, ok := nodes[fName]; ok {
278 fmt.Fprintf(stdout, " %s", fNode)
279 }
280 }
281 fmt.Fprintf(stdout, "}\n}\n")
282 }
283 return nil
284}