blob: 56257794a4a6ed3f29b94d385fce13ee71035adf [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 Badour986a8392022-06-03 10:42:27 -070018 "bytes"
Bob Badourfa739da2021-10-25 16:21:00 -070019 "flag"
20 "fmt"
21 "io"
Bob Badourc778e4c2022-03-22 13:05:19 -070022 "io/fs"
Bob Badourfa739da2021-10-25 16:21:00 -070023 "os"
24 "path/filepath"
25 "sort"
26 "strings"
Colin Cross38a61932022-01-27 15:26:49 -080027
Bob Badour986a8392022-06-03 10:42:27 -070028 "android/soong/response"
Colin Cross38a61932022-01-27 15:26:49 -080029 "android/soong/tools/compliance"
Bob Badourfa739da2021-10-25 16:21:00 -070030)
31
32var (
Bob Badourfa739da2021-10-25 16:21:00 -070033 failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
Colin Cross35f79c32022-01-27 15:18:52 -080034 failNoLicenses = fmt.Errorf("No licenses found")
Bob Badourfa739da2021-10-25 16:21:00 -070035)
36
37type context struct {
38 graphViz bool
39 labelConditions bool
Bob Badour682e1ba2022-02-02 15:15:56 -080040 stripPrefix []string
41}
42
43func (ctx context) strip(installPath string) string {
44 for _, prefix := range ctx.stripPrefix {
45 if strings.HasPrefix(installPath, prefix) {
46 p := strings.TrimPrefix(installPath, prefix)
47 if 0 == len(p) {
48 continue
49 }
50 return p
51 }
52 }
53 return installPath
Bob Badourfa739da2021-10-25 16:21:00 -070054}
55
Bob Badour986a8392022-06-03 10:42:27 -070056// newMultiString creates a flag that allows multiple values in an array.
57func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
58 var f multiString
59 flags.Var(&f, name, usage)
60 return &f
61}
62
63// multiString implements the flag `Value` interface for multiple strings.
64type multiString []string
65
66func (ms *multiString) String() string { return strings.Join(*ms, ", ") }
67func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
68
69func main() {
70 var expandedArgs []string
71 for _, arg := range os.Args[1:] {
72 if strings.HasPrefix(arg, "@") {
73 f, err := os.Open(strings.TrimPrefix(arg, "@"))
74 if err != nil {
75 fmt.Fprintln(os.Stderr, err.Error())
76 os.Exit(1)
77 }
78
79 respArgs, err := response.ReadRspFile(f)
80 f.Close()
81 if err != nil {
82 fmt.Fprintln(os.Stderr, err.Error())
83 os.Exit(1)
84 }
85 expandedArgs = append(expandedArgs, respArgs...)
86 } else {
87 expandedArgs = append(expandedArgs, arg)
88 }
89 }
90
91 flags := flag.NewFlagSet("flags", flag.ExitOnError)
92
93 flags.Usage = func() {
Bob Badourfa739da2021-10-25 16:21:00 -070094 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
95
96Outputs space-separated Target Dependency Annotations tuples for each
97edge in the license graph. When -dot flag given, outputs the nodes and
98edges in graphViz directed graph format.
99
100In plain text mode, multiple values within a field are colon-separated.
101e.g. multiple annotations appear as annotation1:annotation2:annotation3
102or when -label_conditions is requested, Target and Dependency become
103target:condition1:condition2 etc.
104
105Options:
106`, filepath.Base(os.Args[0]))
Bob Badour986a8392022-06-03 10:42:27 -0700107 flags.PrintDefaults()
Bob Badourfa739da2021-10-25 16:21:00 -0700108 }
Bob Badourfa739da2021-10-25 16:21:00 -0700109
Bob Badour986a8392022-06-03 10:42:27 -0700110 graphViz := flags.Bool("dot", false, "Whether to output graphviz (i.e. dot) format.")
111 labelConditions := flags.Bool("label_conditions", false, "Whether to label target nodes with conditions.")
112 outputFile := flags.String("o", "-", "Where to write the output. (default stdout)")
113 stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
Bob Badour682e1ba2022-02-02 15:15:56 -0800114
Bob Badour986a8392022-06-03 10:42:27 -0700115 flags.Parse(expandedArgs)
Bob Badourfa739da2021-10-25 16:21:00 -0700116
117 // Must specify at least one root target.
Bob Badour986a8392022-06-03 10:42:27 -0700118 if flags.NArg() == 0 {
119 flags.Usage()
Bob Badourfa739da2021-10-25 16:21:00 -0700120 os.Exit(2)
121 }
122
Bob Badour986a8392022-06-03 10:42:27 -0700123 if len(*outputFile) == 0 {
124 flags.Usage()
125 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
126 os.Exit(2)
127 } else {
128 dir, err := filepath.Abs(filepath.Dir(*outputFile))
129 if err != nil {
130 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
131 os.Exit(1)
132 }
133 fi, err := os.Stat(dir)
134 if err != nil {
135 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
136 os.Exit(1)
137 }
138 if !fi.IsDir() {
139 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
140 os.Exit(1)
141 }
142 }
143
144 var ofile io.Writer
145 ofile = os.Stdout
146 var obuf *bytes.Buffer
147 if *outputFile != "-" {
148 obuf = &bytes.Buffer{}
149 ofile = obuf
150 }
151
Bob Badourfa739da2021-10-25 16:21:00 -0700152 ctx := &context{*graphViz, *labelConditions, *stripPrefix}
153
Bob Badour986a8392022-06-03 10:42:27 -0700154 err := dumpGraph(ctx, ofile, os.Stderr, compliance.FS, flags.Args()...)
Bob Badourfa739da2021-10-25 16:21:00 -0700155 if err != nil {
156 if err == failNoneRequested {
Bob Badour986a8392022-06-03 10:42:27 -0700157 flags.Usage()
Bob Badourfa739da2021-10-25 16:21:00 -0700158 }
159 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
160 os.Exit(1)
161 }
Bob Badour986a8392022-06-03 10:42:27 -0700162 if *outputFile != "-" {
163 err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
164 if err != nil {
165 fmt.Fprintf(os.Stderr, "could not write output to %q from %q: %s\n", *outputFile, os.Getenv("PWD"), err)
166 os.Exit(1)
167 }
168 }
Bob Badourfa739da2021-10-25 16:21:00 -0700169 os.Exit(0)
170}
171
172// dumpGraph implements the dumpgraph utility.
Bob Badourc778e4c2022-03-22 13:05:19 -0700173func dumpGraph(ctx *context, stdout, stderr io.Writer, rootFS fs.FS, files ...string) error {
Bob Badourfa739da2021-10-25 16:21:00 -0700174 if len(files) < 1 {
175 return failNoneRequested
176 }
177
178 // Read the license graph from the license metadata files (*.meta_lic).
Bob Badourc778e4c2022-03-22 13:05:19 -0700179 licenseGraph, err := compliance.ReadLicenseGraph(rootFS, stderr, files)
Bob Badourfa739da2021-10-25 16:21:00 -0700180 if err != nil {
181 return fmt.Errorf("Unable to read license metadata file(s) %q: %w\n", files, err)
182 }
183 if licenseGraph == nil {
184 return failNoLicenses
185 }
186
187 // Sort the edges of the graph.
188 edges := licenseGraph.Edges()
189 sort.Sort(edges)
190
191 // nodes maps license metadata file names to graphViz node names when ctx.graphViz is true.
192 var nodes map[string]string
193 n := 0
194
195 // targetOut calculates the string to output for `target` separating conditions as needed using `sep`.
196 targetOut := func(target *compliance.TargetNode, sep string) string {
Bob Badour682e1ba2022-02-02 15:15:56 -0800197 tOut := ctx.strip(target.Name())
Bob Badourfa739da2021-10-25 16:21:00 -0700198 if ctx.labelConditions {
199 conditions := target.LicenseConditions().Names()
200 sort.Strings(conditions)
201 if len(conditions) > 0 {
202 tOut += sep + strings.Join(conditions, sep)
203 }
204 }
205 return tOut
206 }
207
208 // makeNode maps `target` to a graphViz node name.
209 makeNode := func(target *compliance.TargetNode) {
210 tName := target.Name()
211 if _, ok := nodes[tName]; !ok {
212 nodeName := fmt.Sprintf("n%d", n)
213 nodes[tName] = nodeName
214 fmt.Fprintf(stdout, "\t%s [label=\"%s\"];\n", nodeName, targetOut(target, "\\n"))
215 n++
216 }
217 }
218
219 // If graphviz output, map targets to node names, and start the directed graph.
220 if ctx.graphViz {
221 nodes = make(map[string]string)
222 targets := licenseGraph.Targets()
223 sort.Sort(targets)
224
225 fmt.Fprintf(stdout, "strict digraph {\n\trankdir=RL;\n")
226 for _, target := range targets {
227 makeNode(target)
228 }
229 }
230
231 // Print the sorted edges to stdout ...
232 for _, e := range edges {
233 // sort the annotations for repeatability/stability
234 annotations := e.Annotations().AsList()
235 sort.Strings(annotations)
236
237 tName := e.Target().Name()
238 dName := e.Dependency().Name()
239
240 if ctx.graphViz {
241 // ... one edge per line labelled with \\n-separated annotations.
242 tNode := nodes[tName]
243 dNode := nodes[dName]
244 fmt.Fprintf(stdout, "\t%s -> %s [label=\"%s\"];\n", dNode, tNode, strings.Join(annotations, "\\n"))
245 } else {
246 // ... one edge per line with annotations in a colon-separated tuple.
247 fmt.Fprintf(stdout, "%s %s %s\n", targetOut(e.Target(), ":"), targetOut(e.Dependency(), ":"), strings.Join(annotations, ":"))
248 }
249 }
250
251 // If graphViz output, rank the root nodes together, and complete the directed graph.
252 if ctx.graphViz {
253 fmt.Fprintf(stdout, "\t{rank=same;")
254 for _, f := range files {
255 fName := f
256 if !strings.HasSuffix(fName, ".meta_lic") {
257 fName += ".meta_lic"
258 }
259 if fNode, ok := nodes[fName]; ok {
260 fmt.Fprintf(stdout, " %s", fNode)
261 }
262 }
263 fmt.Fprintf(stdout, "}\n}\n")
264 }
265 return nil
266}