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