blob: e98b27222aa7220e8018b38cc96f9c265e747298 [file] [log] [blame]
Bob Badour6ea14572022-01-23 17:15:46 -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 "bytes"
Bob Badour608bdff2022-02-01 12:02:30 -080019 "compress/gzip"
Bob Badour6ea14572022-01-23 17:15:46 -080020 "flag"
21 "fmt"
22 "html"
23 "io"
24 "io/fs"
25 "os"
26 "path/filepath"
27 "strings"
Colin Cross38a61932022-01-27 15:26:49 -080028
29 "android/soong/tools/compliance"
Colin Crossbb45f8c2022-01-28 15:18:19 -080030
31 "github.com/google/blueprint/deptools"
Bob Badour6ea14572022-01-23 17:15:46 -080032)
33
34var (
35 outputFile = flag.String("o", "-", "Where to write the NOTICE text file. (default stdout)")
Colin Crossbb45f8c2022-01-28 15:18:19 -080036 depsFile = flag.String("d", "", "Where to write the deps file")
Bob Badour6ea14572022-01-23 17:15:46 -080037 includeTOC = flag.Bool("toc", true, "Whether to include a table of contents.")
Bob Badour49dd4f72022-02-04 14:49:01 -080038 product = flag.String("product", "", "The name of the product for which the notice is generated.")
Bob Badour682e1ba2022-02-02 15:15:56 -080039 stripPrefix = newMultiString("strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
Bob Badour6ea14572022-01-23 17:15:46 -080040 title = flag.String("title", "", "The title of the notice file.")
41
42 failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
43 failNoLicenses = fmt.Errorf("No licenses found")
44)
45
46type context struct {
47 stdout io.Writer
48 stderr io.Writer
49 rootFS fs.FS
50 includeTOC bool
Bob Badour49dd4f72022-02-04 14:49:01 -080051 product string
Bob Badour682e1ba2022-02-02 15:15:56 -080052 stripPrefix []string
Bob Badour6ea14572022-01-23 17:15:46 -080053 title string
Colin Crossbb45f8c2022-01-28 15:18:19 -080054 deps *[]string
Bob Badour6ea14572022-01-23 17:15:46 -080055}
56
Bob Badour682e1ba2022-02-02 15:15:56 -080057func (ctx context) strip(installPath string) string {
58 for _, prefix := range ctx.stripPrefix {
59 if strings.HasPrefix(installPath, prefix) {
60 p := strings.TrimPrefix(installPath, prefix)
61 if 0 == len(p) {
Bob Badour49dd4f72022-02-04 14:49:01 -080062 p = ctx.product
Bob Badour682e1ba2022-02-02 15:15:56 -080063 }
64 if 0 == len(p) {
65 continue
66 }
67 return p
68 }
69 }
70 return installPath
71}
72
Bob Badour6ea14572022-01-23 17:15:46 -080073func init() {
74 flag.Usage = func() {
75 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
76
Bob Badour608bdff2022-02-01 12:02:30 -080077Outputs an html NOTICE.html or gzipped NOTICE.html.gz file if the -o filename
78ends with ".gz".
Bob Badour6ea14572022-01-23 17:15:46 -080079
80Options:
81`, filepath.Base(os.Args[0]))
82 flag.PrintDefaults()
83 }
84}
85
Bob Badour682e1ba2022-02-02 15:15:56 -080086// newMultiString creates a flag that allows multiple values in an array.
87func newMultiString(name, usage string) *multiString {
88 var f multiString
89 flag.Var(&f, name, usage)
90 return &f
91}
92
93// multiString implements the flag `Value` interface for multiple strings.
94type multiString []string
95
96func (ms *multiString) String() string { return strings.Join(*ms, ", ") }
97func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
98
Bob Badour6ea14572022-01-23 17:15:46 -080099func main() {
100 flag.Parse()
101
102 // Must specify at least one root target.
103 if flag.NArg() == 0 {
104 flag.Usage()
105 os.Exit(2)
106 }
107
108 if len(*outputFile) == 0 {
109 flag.Usage()
110 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
111 os.Exit(2)
112 } else {
113 dir, err := filepath.Abs(filepath.Dir(*outputFile))
114 if err != nil {
Colin Cross179ec3e2022-01-27 15:47:09 -0800115 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
Bob Badour6ea14572022-01-23 17:15:46 -0800116 os.Exit(1)
117 }
118 fi, err := os.Stat(dir)
119 if err != nil {
Colin Cross179ec3e2022-01-27 15:47:09 -0800120 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
Bob Badour6ea14572022-01-23 17:15:46 -0800121 os.Exit(1)
122 }
123 if !fi.IsDir() {
124 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
125 os.Exit(1)
126 }
127 }
128
129 var ofile io.Writer
Bob Badour608bdff2022-02-01 12:02:30 -0800130 var closer io.Closer
Bob Badour6ea14572022-01-23 17:15:46 -0800131 ofile = os.Stdout
Bob Badour608bdff2022-02-01 12:02:30 -0800132 var obuf *bytes.Buffer
Bob Badour6ea14572022-01-23 17:15:46 -0800133 if *outputFile != "-" {
Bob Badour608bdff2022-02-01 12:02:30 -0800134 obuf = &bytes.Buffer{}
135 ofile = obuf
136 }
137 if strings.HasSuffix(*outputFile, ".gz") {
138 ofile, _ = gzip.NewWriterLevel(obuf, gzip.BestCompression)
139 closer = ofile.(io.Closer)
Bob Badour6ea14572022-01-23 17:15:46 -0800140 }
141
Colin Crossbb45f8c2022-01-28 15:18:19 -0800142 var deps []string
143
Bob Badourc778e4c2022-03-22 13:05:19 -0700144 ctx := &context{ofile, os.Stderr, compliance.FS, *includeTOC, *product, *stripPrefix, *title, &deps}
Bob Badour6ea14572022-01-23 17:15:46 -0800145
146 err := htmlNotice(ctx, flag.Args()...)
147 if err != nil {
148 if err == failNoneRequested {
149 flag.Usage()
150 }
151 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
152 os.Exit(1)
153 }
Bob Badour608bdff2022-02-01 12:02:30 -0800154 if closer != nil {
155 closer.Close()
156 }
157
Bob Badour6ea14572022-01-23 17:15:46 -0800158 if *outputFile != "-" {
Bob Badour608bdff2022-02-01 12:02:30 -0800159 err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
Bob Badour6ea14572022-01-23 17:15:46 -0800160 if err != nil {
Colin Cross179ec3e2022-01-27 15:47:09 -0800161 fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
Bob Badour6ea14572022-01-23 17:15:46 -0800162 os.Exit(1)
163 }
164 }
Colin Crossbb45f8c2022-01-28 15:18:19 -0800165 if *depsFile != "" {
166 err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
167 if err != nil {
168 fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
169 os.Exit(1)
170 }
171 }
Bob Badour6ea14572022-01-23 17:15:46 -0800172 os.Exit(0)
173}
174
175// htmlNotice implements the htmlnotice utility.
176func htmlNotice(ctx *context, files ...string) error {
177 // Must be at least one root file.
178 if len(files) < 1 {
179 return failNoneRequested
180 }
181
182 // Read the license graph from the license metadata files (*.meta_lic).
183 licenseGraph, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
184 if err != nil {
185 return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
186 }
187 if licenseGraph == nil {
188 return failNoLicenses
189 }
190
191 // rs contains all notice resolutions.
192 rs := compliance.ResolveNotices(licenseGraph)
193
194 ni, err := compliance.IndexLicenseTexts(ctx.rootFS, licenseGraph, rs)
195 if err != nil {
196 return fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err)
197 }
198
199 fmt.Fprintln(ctx.stdout, "<!DOCTYPE html>")
Colin Cross179ec3e2022-01-27 15:47:09 -0800200 fmt.Fprintln(ctx.stdout, "<html><head>")
Bob Badour6ea14572022-01-23 17:15:46 -0800201 fmt.Fprintln(ctx.stdout, "<style type=\"text/css\">")
202 fmt.Fprintln(ctx.stdout, "body { padding: 2px; margin: 0; }")
203 fmt.Fprintln(ctx.stdout, "ul { list-style-type: none; margin: 0; padding: 0; }")
204 fmt.Fprintln(ctx.stdout, "li { padding-left: 1em; }")
205 fmt.Fprintln(ctx.stdout, ".file-list { margin-left: 1em; }")
Colin Cross179ec3e2022-01-27 15:47:09 -0800206 fmt.Fprintln(ctx.stdout, "</style>")
Bob Badoure9b38c12022-02-09 15:56:59 -0800207 if len(ctx.title) > 0 {
Bob Badour6ea14572022-01-23 17:15:46 -0800208 fmt.Fprintf(ctx.stdout, "<title>%s</title>\n", html.EscapeString(ctx.title))
Bob Badoure9b38c12022-02-09 15:56:59 -0800209 } else if len(ctx.product) > 0 {
Bob Badour49dd4f72022-02-04 14:49:01 -0800210 fmt.Fprintf(ctx.stdout, "<title>%s</title>\n", html.EscapeString(ctx.product))
Bob Badour6ea14572022-01-23 17:15:46 -0800211 }
212 fmt.Fprintln(ctx.stdout, "</head>")
213 fmt.Fprintln(ctx.stdout, "<body>")
214
Bob Badoure9b38c12022-02-09 15:56:59 -0800215 if len(ctx.title) > 0 {
Bob Badour6ea14572022-01-23 17:15:46 -0800216 fmt.Fprintf(ctx.stdout, " <h1>%s</h1>\n", html.EscapeString(ctx.title))
Bob Badoure9b38c12022-02-09 15:56:59 -0800217 } else if len(ctx.product) > 0 {
Bob Badour49dd4f72022-02-04 14:49:01 -0800218 fmt.Fprintf(ctx.stdout, " <h1>%s</h1>\n", html.EscapeString(ctx.product))
Bob Badour6ea14572022-01-23 17:15:46 -0800219 }
220 ids := make(map[string]string)
221 if ctx.includeTOC {
222 fmt.Fprintln(ctx.stdout, " <ul class=\"toc\">")
223 i := 0
224 for installPath := range ni.InstallPaths() {
225 id := fmt.Sprintf("id%d", i)
226 i++
227 ids[installPath] = id
Bob Badour682e1ba2022-02-02 15:15:56 -0800228 fmt.Fprintf(ctx.stdout, " <li id=\"%s\"><strong>%s</strong>\n <ul>\n", id, html.EscapeString(ctx.strip(installPath)))
Bob Badour6ea14572022-01-23 17:15:46 -0800229 for _, h := range ni.InstallHashes(installPath) {
230 libs := ni.InstallHashLibs(installPath, h)
231 fmt.Fprintf(ctx.stdout, " <li><a href=\"#%s\">%s</a>\n", h.String(), html.EscapeString(strings.Join(libs, ", ")))
232 }
233 fmt.Fprintln(ctx.stdout, " </ul>")
234 }
235 fmt.Fprintln(ctx.stdout, " </ul><!-- toc -->")
236 }
237 for h := range ni.Hashes() {
238 fmt.Fprintln(ctx.stdout, " <hr>")
239 for _, libName := range ni.HashLibs(h) {
240 fmt.Fprintf(ctx.stdout, " <strong>%s</strong> used by:\n <ul class=\"file-list\">\n", html.EscapeString(libName))
241 for _, installPath := range ni.HashLibInstalls(h, libName) {
242 if id, ok := ids[installPath]; ok {
Bob Badour682e1ba2022-02-02 15:15:56 -0800243 fmt.Fprintf(ctx.stdout, " <li><a href=\"#%s\">%s</a>\n", id, html.EscapeString(ctx.strip(installPath)))
Bob Badour6ea14572022-01-23 17:15:46 -0800244 } else {
Bob Badour682e1ba2022-02-02 15:15:56 -0800245 fmt.Fprintf(ctx.stdout, " <li>%s\n", html.EscapeString(ctx.strip(installPath)))
Bob Badour6ea14572022-01-23 17:15:46 -0800246 }
247 }
248 fmt.Fprintf(ctx.stdout, " </ul>\n")
249 }
250 fmt.Fprintf(ctx.stdout, " </ul>\n <a id=\"%s\"/><pre class=\"license-text\">", h.String())
251 fmt.Fprintln(ctx.stdout, html.EscapeString(string(ni.HashText(h))))
252 fmt.Fprintln(ctx.stdout, " </pre><!-- license-text -->")
253 }
254 fmt.Fprintln(ctx.stdout, "</body></html>")
255
Colin Crossbb45f8c2022-01-28 15:18:19 -0800256 *ctx.deps = ni.InputNoticeFiles()
257
Bob Badour6ea14572022-01-23 17:15:46 -0800258 return nil
259}