blob: 1ee63b238c8111500b4267c1e6bcaf20d636dfc1 [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 (
18 "compliance"
19 "flag"
20 "fmt"
21 "io"
22 "os"
23 "path/filepath"
24 "sort"
25 "strings"
26)
27
28var (
29 graphViz = flag.Bool("dot", false, "Whether to output graphviz (i.e. dot) format.")
30 labelConditions = flag.Bool("label_conditions", false, "Whether to label target nodes with conditions.")
31 stripPrefix = flag.String("strip_prefix", "", "Prefix to remove from paths. i.e. path to root")
32
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 init() {
44 flag.Usage = func() {
45 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
46
47Outputs space-separated Target Dependency Annotations tuples for each
48edge in the license graph. When -dot flag given, outputs the nodes and
49edges in graphViz directed graph format.
50
51In plain text mode, multiple values within a field are colon-separated.
52e.g. multiple annotations appear as annotation1:annotation2:annotation3
53or when -label_conditions is requested, Target and Dependency become
54target:condition1:condition2 etc.
55
56Options:
57`, filepath.Base(os.Args[0]))
58 flag.PrintDefaults()
59 }
60}
61
62func main() {
63 flag.Parse()
64
65 // Must specify at least one root target.
66 if flag.NArg() == 0 {
67 flag.Usage()
68 os.Exit(2)
69 }
70
71 ctx := &context{*graphViz, *labelConditions, *stripPrefix}
72
73 err := dumpGraph(ctx, os.Stdout, os.Stderr, flag.Args()...)
74 if err != nil {
75 if err == failNoneRequested {
76 flag.Usage()
77 }
78 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
79 os.Exit(1)
80 }
81 os.Exit(0)
82}
83
84// dumpGraph implements the dumpgraph utility.
85func dumpGraph(ctx *context, stdout, stderr io.Writer, files ...string) error {
86 if len(files) < 1 {
87 return failNoneRequested
88 }
89
90 // Read the license graph from the license metadata files (*.meta_lic).
91 licenseGraph, err := compliance.ReadLicenseGraph(os.DirFS("."), stderr, files)
92 if err != nil {
93 return fmt.Errorf("Unable to read license metadata file(s) %q: %w\n", files, err)
94 }
95 if licenseGraph == nil {
96 return failNoLicenses
97 }
98
99 // Sort the edges of the graph.
100 edges := licenseGraph.Edges()
101 sort.Sort(edges)
102
103 // nodes maps license metadata file names to graphViz node names when ctx.graphViz is true.
104 var nodes map[string]string
105 n := 0
106
107 // targetOut calculates the string to output for `target` separating conditions as needed using `sep`.
108 targetOut := func(target *compliance.TargetNode, sep string) string {
109 tOut := strings.TrimPrefix(target.Name(), ctx.stripPrefix)
110 if ctx.labelConditions {
111 conditions := target.LicenseConditions().Names()
112 sort.Strings(conditions)
113 if len(conditions) > 0 {
114 tOut += sep + strings.Join(conditions, sep)
115 }
116 }
117 return tOut
118 }
119
120 // makeNode maps `target` to a graphViz node name.
121 makeNode := func(target *compliance.TargetNode) {
122 tName := target.Name()
123 if _, ok := nodes[tName]; !ok {
124 nodeName := fmt.Sprintf("n%d", n)
125 nodes[tName] = nodeName
126 fmt.Fprintf(stdout, "\t%s [label=\"%s\"];\n", nodeName, targetOut(target, "\\n"))
127 n++
128 }
129 }
130
131 // If graphviz output, map targets to node names, and start the directed graph.
132 if ctx.graphViz {
133 nodes = make(map[string]string)
134 targets := licenseGraph.Targets()
135 sort.Sort(targets)
136
137 fmt.Fprintf(stdout, "strict digraph {\n\trankdir=RL;\n")
138 for _, target := range targets {
139 makeNode(target)
140 }
141 }
142
143 // Print the sorted edges to stdout ...
144 for _, e := range edges {
145 // sort the annotations for repeatability/stability
146 annotations := e.Annotations().AsList()
147 sort.Strings(annotations)
148
149 tName := e.Target().Name()
150 dName := e.Dependency().Name()
151
152 if ctx.graphViz {
153 // ... one edge per line labelled with \\n-separated annotations.
154 tNode := nodes[tName]
155 dNode := nodes[dName]
156 fmt.Fprintf(stdout, "\t%s -> %s [label=\"%s\"];\n", dNode, tNode, strings.Join(annotations, "\\n"))
157 } else {
158 // ... one edge per line with annotations in a colon-separated tuple.
159 fmt.Fprintf(stdout, "%s %s %s\n", targetOut(e.Target(), ":"), targetOut(e.Dependency(), ":"), strings.Join(annotations, ":"))
160 }
161 }
162
163 // If graphViz output, rank the root nodes together, and complete the directed graph.
164 if ctx.graphViz {
165 fmt.Fprintf(stdout, "\t{rank=same;")
166 for _, f := range files {
167 fName := f
168 if !strings.HasSuffix(fName, ".meta_lic") {
169 fName += ".meta_lic"
170 }
171 if fNode, ok := nodes[fName]; ok {
172 fmt.Fprintf(stdout, " %s", fNode)
173 }
174 }
175 fmt.Fprintf(stdout, "}\n}\n")
176 }
177 return nil
178}