blob: 1f555467b36c339dadd6488e60e265b0eb35ebd7 [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 Badour6ea14572022-01-23 17:15:46 -080019 "flag"
20 "fmt"
21 "html"
22 "io"
23 "io/fs"
24 "os"
25 "path/filepath"
26 "strings"
Colin Cross38a61932022-01-27 15:26:49 -080027
28 "android/soong/tools/compliance"
Colin Crossbb45f8c2022-01-28 15:18:19 -080029
30 "github.com/google/blueprint/deptools"
Bob Badour6ea14572022-01-23 17:15:46 -080031)
32
33var (
34 outputFile = flag.String("o", "-", "Where to write the NOTICE text file. (default stdout)")
Colin Crossbb45f8c2022-01-28 15:18:19 -080035 depsFile = flag.String("d", "", "Where to write the deps file")
Bob Badour6ea14572022-01-23 17:15:46 -080036 includeTOC = flag.Bool("toc", true, "Whether to include a table of contents.")
37 stripPrefix = flag.String("strip_prefix", "", "Prefix to remove from paths. i.e. path to root")
38 title = flag.String("title", "", "The title of the notice file.")
39
40 failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
41 failNoLicenses = fmt.Errorf("No licenses found")
42)
43
44type context struct {
45 stdout io.Writer
46 stderr io.Writer
47 rootFS fs.FS
48 includeTOC bool
49 stripPrefix string
50 title string
Colin Crossbb45f8c2022-01-28 15:18:19 -080051 deps *[]string
Bob Badour6ea14572022-01-23 17:15:46 -080052}
53
54func init() {
55 flag.Usage = func() {
56 fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
57
58Outputs an html NOTICE.html file.
59
60Options:
61`, filepath.Base(os.Args[0]))
62 flag.PrintDefaults()
63 }
64}
65
66func main() {
67 flag.Parse()
68
69 // Must specify at least one root target.
70 if flag.NArg() == 0 {
71 flag.Usage()
72 os.Exit(2)
73 }
74
75 if len(*outputFile) == 0 {
76 flag.Usage()
77 fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
78 os.Exit(2)
79 } else {
80 dir, err := filepath.Abs(filepath.Dir(*outputFile))
81 if err != nil {
Colin Cross179ec3e2022-01-27 15:47:09 -080082 fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
Bob Badour6ea14572022-01-23 17:15:46 -080083 os.Exit(1)
84 }
85 fi, err := os.Stat(dir)
86 if err != nil {
Colin Cross179ec3e2022-01-27 15:47:09 -080087 fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
Bob Badour6ea14572022-01-23 17:15:46 -080088 os.Exit(1)
89 }
90 if !fi.IsDir() {
91 fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
92 os.Exit(1)
93 }
94 }
95
96 var ofile io.Writer
97 ofile = os.Stdout
98 if *outputFile != "-" {
99 ofile = &bytes.Buffer{}
100 }
101
Colin Crossbb45f8c2022-01-28 15:18:19 -0800102 var deps []string
103
104 ctx := &context{ofile, os.Stderr, os.DirFS("."), *includeTOC, *stripPrefix, *title, &deps}
Bob Badour6ea14572022-01-23 17:15:46 -0800105
106 err := htmlNotice(ctx, flag.Args()...)
107 if err != nil {
108 if err == failNoneRequested {
109 flag.Usage()
110 }
111 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
112 os.Exit(1)
113 }
114 if *outputFile != "-" {
115 err := os.WriteFile(*outputFile, ofile.(*bytes.Buffer).Bytes(), 0666)
116 if err != nil {
Colin Cross179ec3e2022-01-27 15:47:09 -0800117 fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
Bob Badour6ea14572022-01-23 17:15:46 -0800118 os.Exit(1)
119 }
120 }
Colin Crossbb45f8c2022-01-28 15:18:19 -0800121 if *depsFile != "" {
122 err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
123 if err != nil {
124 fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
125 os.Exit(1)
126 }
127 }
Bob Badour6ea14572022-01-23 17:15:46 -0800128 os.Exit(0)
129}
130
131// htmlNotice implements the htmlnotice utility.
132func htmlNotice(ctx *context, files ...string) error {
133 // Must be at least one root file.
134 if len(files) < 1 {
135 return failNoneRequested
136 }
137
138 // Read the license graph from the license metadata files (*.meta_lic).
139 licenseGraph, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
140 if err != nil {
141 return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
142 }
143 if licenseGraph == nil {
144 return failNoLicenses
145 }
146
147 // rs contains all notice resolutions.
148 rs := compliance.ResolveNotices(licenseGraph)
149
150 ni, err := compliance.IndexLicenseTexts(ctx.rootFS, licenseGraph, rs)
151 if err != nil {
152 return fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err)
153 }
154
155 fmt.Fprintln(ctx.stdout, "<!DOCTYPE html>")
Colin Cross179ec3e2022-01-27 15:47:09 -0800156 fmt.Fprintln(ctx.stdout, "<html><head>")
Bob Badour6ea14572022-01-23 17:15:46 -0800157 fmt.Fprintln(ctx.stdout, "<style type=\"text/css\">")
158 fmt.Fprintln(ctx.stdout, "body { padding: 2px; margin: 0; }")
159 fmt.Fprintln(ctx.stdout, "ul { list-style-type: none; margin: 0; padding: 0; }")
160 fmt.Fprintln(ctx.stdout, "li { padding-left: 1em; }")
161 fmt.Fprintln(ctx.stdout, ".file-list { margin-left: 1em; }")
Colin Cross179ec3e2022-01-27 15:47:09 -0800162 fmt.Fprintln(ctx.stdout, "</style>")
Bob Badour6ea14572022-01-23 17:15:46 -0800163 if 0 < len(ctx.title) {
164 fmt.Fprintf(ctx.stdout, "<title>%s</title>\n", html.EscapeString(ctx.title))
165 }
166 fmt.Fprintln(ctx.stdout, "</head>")
167 fmt.Fprintln(ctx.stdout, "<body>")
168
169 if 0 < len(ctx.title) {
170 fmt.Fprintf(ctx.stdout, " <h1>%s</h1>\n", html.EscapeString(ctx.title))
171 }
172 ids := make(map[string]string)
173 if ctx.includeTOC {
174 fmt.Fprintln(ctx.stdout, " <ul class=\"toc\">")
175 i := 0
176 for installPath := range ni.InstallPaths() {
177 id := fmt.Sprintf("id%d", i)
178 i++
179 ids[installPath] = id
180 var p string
181 if 0 < len(ctx.stripPrefix) && strings.HasPrefix(installPath, ctx.stripPrefix) {
182 p = installPath[len(ctx.stripPrefix):]
183 if 0 == len(p) {
184 if 0 < len(ctx.title) {
185 p = ctx.title
186 } else {
187 p = "root"
188 }
189 }
190 } else {
191 p = installPath
192 }
193 fmt.Fprintf(ctx.stdout, " <li id=\"%s\"><strong>%s</strong>\n <ul>\n", id, html.EscapeString(p))
194 for _, h := range ni.InstallHashes(installPath) {
195 libs := ni.InstallHashLibs(installPath, h)
196 fmt.Fprintf(ctx.stdout, " <li><a href=\"#%s\">%s</a>\n", h.String(), html.EscapeString(strings.Join(libs, ", ")))
197 }
198 fmt.Fprintln(ctx.stdout, " </ul>")
199 }
200 fmt.Fprintln(ctx.stdout, " </ul><!-- toc -->")
201 }
202 for h := range ni.Hashes() {
203 fmt.Fprintln(ctx.stdout, " <hr>")
204 for _, libName := range ni.HashLibs(h) {
205 fmt.Fprintf(ctx.stdout, " <strong>%s</strong> used by:\n <ul class=\"file-list\">\n", html.EscapeString(libName))
206 for _, installPath := range ni.HashLibInstalls(h, libName) {
207 if id, ok := ids[installPath]; ok {
208 if 0 < len(ctx.stripPrefix) && strings.HasPrefix(installPath, ctx.stripPrefix) {
209 fmt.Fprintf(ctx.stdout, " <li><a href=\"#%s\">%s</a>\n", id, html.EscapeString(installPath[len(ctx.stripPrefix):]))
210 } else {
211 fmt.Fprintf(ctx.stdout, " <li><a href=\"#%s\">%s</a>\n", id, html.EscapeString(installPath))
212 }
213 } else {
214 if 0 < len(ctx.stripPrefix) && strings.HasPrefix(installPath, ctx.stripPrefix) {
215 fmt.Fprintf(ctx.stdout, " <li>%s\n", html.EscapeString(installPath[len(ctx.stripPrefix):]))
216 } else {
217 fmt.Fprintf(ctx.stdout, " <li>%s\n", html.EscapeString(installPath))
218 }
219 }
220 }
221 fmt.Fprintf(ctx.stdout, " </ul>\n")
222 }
223 fmt.Fprintf(ctx.stdout, " </ul>\n <a id=\"%s\"/><pre class=\"license-text\">", h.String())
224 fmt.Fprintln(ctx.stdout, html.EscapeString(string(ni.HashText(h))))
225 fmt.Fprintln(ctx.stdout, " </pre><!-- license-text -->")
226 }
227 fmt.Fprintln(ctx.stdout, "</body></html>")
228
Colin Crossbb45f8c2022-01-28 15:18:19 -0800229 *ctx.deps = ni.InputNoticeFiles()
230
Bob Badour6ea14572022-01-23 17:15:46 -0800231 return nil
232}