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	"io"
23	"io/fs"
24	"os"
25	"path/filepath"
26	"sort"
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	product     string
45	stripPrefix []string
46	title       string
47	deps        *[]string
48}
49
50func (ctx context) strip(installPath string) string {
51	for _, prefix := range ctx.stripPrefix {
52		if strings.HasPrefix(installPath, prefix) {
53			p := strings.TrimPrefix(installPath, prefix)
54			if 0 == len(p) {
55				p = ctx.product
56			}
57			if 0 == len(p) {
58				continue
59			}
60			return p
61		}
62	}
63	return installPath
64}
65
66// newMultiString creates a flag that allows multiple values in an array.
67func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
68	var f multiString
69	flags.Var(&f, name, usage)
70	return &f
71}
72
73// multiString implements the flag `Value` interface for multiple strings.
74type multiString []string
75
76func (ms *multiString) String() string     { return strings.Join(*ms, ", ") }
77func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
78
79func main() {
80	var expandedArgs []string
81	for _, arg := range os.Args[1:] {
82		if strings.HasPrefix(arg, "@") {
83			f, err := os.Open(strings.TrimPrefix(arg, "@"))
84			if err != nil {
85				fmt.Fprintln(os.Stderr, err.Error())
86				os.Exit(1)
87			}
88
89			respArgs, err := response.ReadRspFile(f)
90			f.Close()
91			if err != nil {
92				fmt.Fprintln(os.Stderr, err.Error())
93				os.Exit(1)
94			}
95			expandedArgs = append(expandedArgs, respArgs...)
96		} else {
97			expandedArgs = append(expandedArgs, arg)
98		}
99	}
100
101	flags := flag.NewFlagSet("flags", flag.ExitOnError)
102
103	flags.Usage = func() {
104		fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
105
106Outputs a text NOTICE file.
107
108Options:
109`, filepath.Base(os.Args[0]))
110		flags.PrintDefaults()
111	}
112
113	outputFile := flags.String("o", "-", "Where to write the NOTICE text file. (default stdout)")
114	depsFile := flags.String("d", "", "Where to write the deps file")
115	product := flags.String("product", "", "The name of the product for which the notice is generated.")
116	stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
117	title := flags.String("title", "", "The title of the notice file.")
118
119	flags.Parse(expandedArgs)
120
121	// Must specify at least one root target.
122	if flags.NArg() == 0 {
123		flags.Usage()
124		os.Exit(2)
125	}
126
127	if len(*outputFile) == 0 {
128		flags.Usage()
129		fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
130		os.Exit(2)
131	} else {
132		dir, err := filepath.Abs(filepath.Dir(*outputFile))
133		if err != nil {
134			fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
135			os.Exit(1)
136		}
137		fi, err := os.Stat(dir)
138		if err != nil {
139			fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
140			os.Exit(1)
141		}
142		if !fi.IsDir() {
143			fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
144			os.Exit(1)
145		}
146	}
147
148	var ofile io.Writer
149	var closer io.Closer
150	ofile = os.Stdout
151	var obuf *bytes.Buffer
152	if *outputFile != "-" {
153		obuf = &bytes.Buffer{}
154		ofile = obuf
155	}
156	if strings.HasSuffix(*outputFile, ".gz") {
157		ofile, _ = gzip.NewWriterLevel(obuf, gzip.BestCompression)
158		closer = ofile.(io.Closer)
159	}
160
161	var deps []string
162
163	ctx := &context{ofile, os.Stderr, compliance.FS, *product, *stripPrefix, *title, &deps}
164
165	err := textNotice(ctx, flags.Args()...)
166	if err != nil {
167		if err == failNoneRequested {
168			flags.Usage()
169		}
170		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
171		os.Exit(1)
172	}
173	if closer != nil {
174		closer.Close()
175	}
176
177	if *outputFile != "-" {
178		err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
179		if err != nil {
180			fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
181			os.Exit(1)
182		}
183	}
184	if *depsFile != "" {
185		err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
186		if err != nil {
187			fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
188			os.Exit(1)
189		}
190	}
191	os.Exit(0)
192}
193
194// textNotice implements the textNotice utility.
195func textNotice(ctx *context, files ...string) error {
196	// Must be at least one root file.
197	if len(files) < 1 {
198		return failNoneRequested
199	}
200
201	// Read the license graph from the license metadata files (*.meta_lic).
202	licenseGraph, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
203	if err != nil {
204		return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
205	}
206	if licenseGraph == nil {
207		return failNoLicenses
208	}
209
210	// rs contains all notice resolutions.
211	rs := compliance.ResolveNotices(licenseGraph)
212
213	ni, err := compliance.IndexLicenseTexts(ctx.rootFS, licenseGraph, rs)
214	if err != nil {
215		return fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err)
216	}
217
218	if len(ctx.title) > 0 {
219		fmt.Fprintf(ctx.stdout, "%s\n\n", ctx.title)
220	}
221	for h := range ni.Hashes() {
222		fmt.Fprintln(ctx.stdout, "==============================================================================")
223		for _, libName := range ni.HashLibs(h) {
224			fmt.Fprintf(ctx.stdout, "%s used by:\n", libName)
225			for _, installPath := range ni.HashLibInstalls(h, libName) {
226				fmt.Fprintf(ctx.stdout, "  %s\n", ctx.strip(installPath))
227			}
228			fmt.Fprintln(ctx.stdout)
229		}
230		ctx.stdout.Write(ni.HashText(h))
231		fmt.Fprintln(ctx.stdout)
232	}
233
234	*ctx.deps = ni.InputFiles()
235	sort.Strings(*ctx.deps)
236
237	return nil
238}
239