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	"flag"
20	"fmt"
21	"io"
22	"io/fs"
23	"os"
24	"path/filepath"
25	"strings"
26
27	"android/soong/response"
28	"android/soong/tools/compliance"
29)
30
31var (
32	failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
33	failNoLicenses    = fmt.Errorf("No licenses found")
34)
35
36type context struct {
37	stdout      io.Writer
38	stderr      io.Writer
39	rootFS      fs.FS
40	stripPrefix []string
41}
42
43func (ctx context) strip(installPath string) string {
44	for _, prefix := range ctx.stripPrefix {
45		if strings.HasPrefix(installPath, prefix) {
46			p := strings.TrimPrefix(installPath, prefix)
47			if 0 == len(p) {
48				continue
49			}
50			return p
51		}
52	}
53	return installPath
54}
55
56// newMultiString creates a flag that allows multiple values in an array.
57func newMultiString(flags *flag.FlagSet, name, usage string) *multiString {
58	var f multiString
59	flags.Var(&f, name, usage)
60	return &f
61}
62
63// multiString implements the flag `Value` interface for multiple strings.
64type multiString []string
65
66func (ms *multiString) String() string     { return strings.Join(*ms, ", ") }
67func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
68
69func main() {
70	var expandedArgs []string
71	for _, arg := range os.Args[1:] {
72		if strings.HasPrefix(arg, "@") {
73			f, err := os.Open(strings.TrimPrefix(arg, "@"))
74			if err != nil {
75				fmt.Fprintln(os.Stderr, err.Error())
76				os.Exit(1)
77			}
78
79			respArgs, err := response.ReadRspFile(f)
80			f.Close()
81			if err != nil {
82				fmt.Fprintln(os.Stderr, err.Error())
83				os.Exit(1)
84			}
85			expandedArgs = append(expandedArgs, respArgs...)
86		} else {
87			expandedArgs = append(expandedArgs, arg)
88		}
89	}
90
91	flags := flag.NewFlagSet("flags", flag.ExitOnError)
92
93	flags.Usage = func() {
94		fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
95
96Outputs a bill of materials. i.e. the list of installed paths.
97
98Options:
99`, filepath.Base(os.Args[0]))
100		flags.PrintDefaults()
101	}
102
103	outputFile := flags.String("o", "-", "Where to write the bill of materials. (default stdout)")
104	stripPrefix := newMultiString(flags, "strip_prefix", "Prefix to remove from paths. i.e. path to root (multiple allowed)")
105
106	flags.Parse(expandedArgs)
107
108	// Must specify at least one root target.
109	if flags.NArg() == 0 {
110		flags.Usage()
111		os.Exit(2)
112	}
113
114	if len(*outputFile) == 0 {
115		flags.Usage()
116		fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
117		os.Exit(2)
118	} else {
119		dir, err := filepath.Abs(filepath.Dir(*outputFile))
120		if err != nil {
121			fmt.Fprintf(os.Stderr, "cannot determine path to %q: %s\n", *outputFile, err)
122			os.Exit(1)
123		}
124		fi, err := os.Stat(dir)
125		if err != nil {
126			fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %s\n", dir, *outputFile, err)
127			os.Exit(1)
128		}
129		if !fi.IsDir() {
130			fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
131			os.Exit(1)
132		}
133	}
134
135	var ofile io.Writer
136	ofile = os.Stdout
137	if *outputFile != "-" {
138		ofile = &bytes.Buffer{}
139	}
140
141	ctx := &context{ofile, os.Stderr, compliance.FS, *stripPrefix}
142
143	err := billOfMaterials(ctx, flags.Args()...)
144	if err != nil {
145		if err == failNoneRequested {
146			flags.Usage()
147		}
148		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
149		os.Exit(1)
150	}
151	if *outputFile != "-" {
152		err := os.WriteFile(*outputFile, ofile.(*bytes.Buffer).Bytes(), 0666)
153		if err != nil {
154			fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
155			os.Exit(1)
156		}
157	}
158	os.Exit(0)
159}
160
161// billOfMaterials implements the bom utility.
162func billOfMaterials(ctx *context, files ...string) error {
163	// Must be at least one root file.
164	if len(files) < 1 {
165		return failNoneRequested
166	}
167
168	// Read the license graph from the license metadata files (*.meta_lic).
169	licenseGraph, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
170	if err != nil {
171		return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
172	}
173	if licenseGraph == nil {
174		return failNoLicenses
175	}
176
177	// rs contains all notice resolutions.
178	rs := compliance.ResolveNotices(licenseGraph)
179
180	ni, err := compliance.IndexLicenseTexts(ctx.rootFS, licenseGraph, rs)
181	if err != nil {
182		return fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err)
183	}
184
185	for path := range ni.InstallPaths() {
186		fmt.Fprintln(ctx.stdout, ctx.strip(path))
187	}
188	return nil
189}
190