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