xref: /aosp_15_r20/build/make/tools/compliance/cmd/htmlnotice/htmlnotice.go (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
1// 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"
19	"compress/gzip"
20	"flag"
21	"fmt"
22	"html"
23	"io"
24	"io/fs"
25	"os"
26	"path/filepath"
27	"sort"
28	"strings"
29
30	"android/soong/response"
31	"android/soong/tools/compliance"
32
33	"github.com/google/blueprint/deptools"
34)
35
36var (
37	failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
38	failNoLicenses    = fmt.Errorf("No licenses found")
39)
40
41type context struct {
42	stdout      io.Writer
43	stderr      io.Writer
44	rootFS      fs.FS
45	includeTOC  bool
46	product     string
47	stripPrefix []string
48	title       string
49	deps        *[]string
50}
51
52func (ctx context) strip(installPath string) string {
53	for _, prefix := range ctx.stripPrefix {
54		if strings.HasPrefix(installPath, prefix) {
55			p := strings.TrimPrefix(installPath, prefix)
56			if 0 == len(p) {
57				p = ctx.product
58			}
59			if 0 == len(p) {
60				continue
61			}
62			return p
63		}
64	}
65	return installPath
66}
67
68// newMultiString creates a flag that allows multiple values in an array.
69func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
70	var f multiString
71	flags.Var(&f, name, usage)
72	return &f
73}
74
75// multiString implements the flag `Value` interface for multiple strings.
76type multiString []string
77
78func (ms *multiString) String() string     { return strings.Join(*ms, ", ") }
79func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
80
81func main() {
82	var expandedArgs []string
83	for _, arg := range os.Args[1:] {
84		if strings.HasPrefix(arg, "@") {
85			f, err := os.Open(strings.TrimPrefix(arg, "@"))
86			if err != nil {
87				fmt.Fprintln(os.Stderr, err.Error())
88				os.Exit(1)
89			}
90
91			respArgs, err := response.ReadRspFile(f)
92			f.Close()
93			if err != nil {
94				fmt.Fprintln(os.Stderr, err.Error())
95				os.Exit(1)
96			}
97			expandedArgs = append(expandedArgs, respArgs...)
98		} else {
99			expandedArgs = append(expandedArgs, arg)
100		}
101	}
102
103	flags := flag.NewFlagSet("flags", flag.ExitOnError)
104
105	flags.Usage = func() {
106		fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
107
108Outputs an html NOTICE.html or gzipped NOTICE.html.gz file if the -o filename
109ends with ".gz".
110
111Options:
112`, filepath.Base(os.Args[0]))
113		flags.PrintDefaults()
114	}
115
116	outputFile := flags.String("o", "-", "Where to write the NOTICE text file. (default stdout)")
117	depsFile := flags.String("d", "", "Where to write the deps file")
118	includeTOC := flags.Bool("toc", true, "Whether to include a table of contents.")
119	product := flags.String("product", "", "The name of the product for which the notice is generated.")
120	stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
121	title := flags.String("title", "", "The title of the notice file.")
122
123	flags.Parse(expandedArgs)
124
125	// Must specify at least one root target.
126	if flags.NArg() == 0 {
127		flags.Usage()
128		os.Exit(2)
129	}
130
131	if len(*outputFile) == 0 {
132		flags.Usage()
133		fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
134		os.Exit(2)
135	} else {
136		dir, err := filepath.Abs(filepath.Dir(*outputFile))
137		if err != nil {
138			fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
139			os.Exit(1)
140		}
141		fi, err := os.Stat(dir)
142		if err != nil {
143			fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
144			os.Exit(1)
145		}
146		if !fi.IsDir() {
147			fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
148			os.Exit(1)
149		}
150	}
151
152	var ofile io.Writer
153	var closer io.Closer
154	ofile = os.Stdout
155	var obuf *bytes.Buffer
156	if *outputFile != "-" {
157		obuf = &bytes.Buffer{}
158		ofile = obuf
159	}
160	if strings.HasSuffix(*outputFile, ".gz") {
161		ofile, _ = gzip.NewWriterLevel(obuf, gzip.BestCompression)
162		closer = ofile.(io.Closer)
163	}
164
165	var deps []string
166
167	ctx := &context{ofile, os.Stderr, compliance.FS, *includeTOC, *product, *stripPrefix, *title, &deps}
168
169	err := htmlNotice(ctx, flags.Args()...)
170	if err != nil {
171		if err == failNoneRequested {
172			flags.Usage()
173		}
174		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
175		os.Exit(1)
176	}
177	if closer != nil {
178		closer.Close()
179	}
180
181	if *outputFile != "-" {
182		err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
183		if err != nil {
184			fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
185			os.Exit(1)
186		}
187	}
188	if *depsFile != "" {
189		err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
190		if err != nil {
191			fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
192			os.Exit(1)
193		}
194	}
195	os.Exit(0)
196}
197
198// htmlNotice implements the htmlnotice utility.
199func htmlNotice(ctx *context, files ...string) error {
200	// Must be at least one root file.
201	if len(files) < 1 {
202		return failNoneRequested
203	}
204
205	// Read the license graph from the license metadata files (*.meta_lic).
206	licenseGraph, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
207	if err != nil {
208		return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
209	}
210	if licenseGraph == nil {
211		return failNoLicenses
212	}
213
214	// rs contains all notice resolutions.
215	rs := compliance.ResolveNotices(licenseGraph)
216
217	ni, err := compliance.IndexLicenseTexts(ctx.rootFS, licenseGraph, rs)
218	if err != nil {
219		return fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err)
220	}
221
222	fmt.Fprintln(ctx.stdout, "<!DOCTYPE html>")
223	fmt.Fprintln(ctx.stdout, "<html><head>")
224	fmt.Fprintln(ctx.stdout, "<style type=\"text/css\">")
225	fmt.Fprintln(ctx.stdout, "body { padding: 2px; margin: 0; }")
226	fmt.Fprintln(ctx.stdout, "ul { list-style-type: none; margin: 0; padding: 0; }")
227	fmt.Fprintln(ctx.stdout, "li { padding-left: 1em; }")
228	fmt.Fprintln(ctx.stdout, ".file-list { margin-left: 1em; }")
229	fmt.Fprintln(ctx.stdout, "</style>")
230	if len(ctx.title) > 0 {
231		fmt.Fprintf(ctx.stdout, "<title>%s</title>\n", html.EscapeString(ctx.title))
232	} else if len(ctx.product) > 0 {
233		fmt.Fprintf(ctx.stdout, "<title>%s</title>\n", html.EscapeString(ctx.product))
234	}
235	fmt.Fprintln(ctx.stdout, "</head>")
236	fmt.Fprintln(ctx.stdout, "<body>")
237
238	if len(ctx.title) > 0 {
239		fmt.Fprintf(ctx.stdout, "  <h1>%s</h1>\n", html.EscapeString(ctx.title))
240	} else if len(ctx.product) > 0 {
241		fmt.Fprintf(ctx.stdout, "  <h1>%s</h1>\n", html.EscapeString(ctx.product))
242	}
243	ids := make(map[string]string)
244	if ctx.includeTOC {
245		fmt.Fprintln(ctx.stdout, "  <ul class=\"toc\">")
246		i := 0
247		for installPath := range ni.InstallPaths() {
248			id := fmt.Sprintf("id%d", i)
249			i++
250			ids[installPath] = id
251			fmt.Fprintf(ctx.stdout, "    <li id=\"%s\"><strong>%s</strong>\n      <ul>\n", id, html.EscapeString(ctx.strip(installPath)))
252			for _, h := range ni.InstallHashes(installPath) {
253				libs := ni.InstallHashLibs(installPath, h)
254				fmt.Fprintf(ctx.stdout, "        <li><a href=\"#%s\">%s</a>\n", h.String(), html.EscapeString(strings.Join(libs, ", ")))
255			}
256			fmt.Fprintln(ctx.stdout, "      </ul>")
257		}
258		fmt.Fprintln(ctx.stdout, "  </ul><!-- toc -->")
259	}
260	for h := range ni.Hashes() {
261		fmt.Fprintln(ctx.stdout, "  <hr>")
262		for _, libName := range ni.HashLibs(h) {
263			fmt.Fprintf(ctx.stdout, "  <strong>%s</strong> used by:\n    <ul class=\"file-list\">\n", html.EscapeString(libName))
264			for _, installPath := range ni.HashLibInstalls(h, libName) {
265				if id, ok := ids[installPath]; ok {
266					fmt.Fprintf(ctx.stdout, "      <li><a href=\"#%s\">%s</a>\n", id, html.EscapeString(ctx.strip(installPath)))
267				} else {
268					fmt.Fprintf(ctx.stdout, "      <li>%s\n", html.EscapeString(ctx.strip(installPath)))
269				}
270			}
271			fmt.Fprintf(ctx.stdout, "    </ul>\n")
272		}
273		fmt.Fprintf(ctx.stdout, "  </ul>\n  <a id=\"%s\"/><pre class=\"license-text\">", h.String())
274		fmt.Fprintln(ctx.stdout, html.EscapeString(string(ni.HashText(h))))
275		fmt.Fprintln(ctx.stdout, "  </pre><!-- license-text -->")
276	}
277	fmt.Fprintln(ctx.stdout, "</body></html>")
278
279	*ctx.deps = ni.InputFiles()
280	sort.Strings(*ctx.deps)
281
282	return nil
283}
284