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