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