diff --git a/tools/compliance/Android.bp b/tools/compliance/Android.bp index 5684f2f63d..0d7cf10472 100644 --- a/tools/compliance/Android.bp +++ b/tools/compliance/Android.bp @@ -45,6 +45,13 @@ blueprint_go_binary { testSrcs: ["cmd/dumpresolutions_test.go"], } +blueprint_go_binary { + name: "htmlnotice", + srcs: ["cmd/htmlnotice.go"], + deps: ["compliance-module"], + testSrcs: ["cmd/htmlnotice_test.go"], +} + blueprint_go_binary { name: "textnotice", srcs: ["cmd/textnotice.go"], diff --git a/tools/compliance/cmd/htmlnotice.go b/tools/compliance/cmd/htmlnotice.go new file mode 100644 index 0000000000..cff1ff84eb --- /dev/null +++ b/tools/compliance/cmd/htmlnotice.go @@ -0,0 +1,216 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "compliance" + "flag" + "fmt" + "html" + "io" + "io/fs" + "os" + "path/filepath" + "strings" +) + +var ( + outputFile = flag.String("o", "-", "Where to write the NOTICE text file. (default stdout)") + includeTOC = flag.Bool("toc", true, "Whether to include a table of contents.") + stripPrefix = flag.String("strip_prefix", "", "Prefix to remove from paths. i.e. path to root") + title = flag.String("title", "", "The title of the notice file.") + + failNoneRequested = fmt.Errorf("\nNo license metadata files requested") + failNoLicenses = fmt.Errorf("No licenses found") +) + +type context struct { + stdout io.Writer + stderr io.Writer + rootFS fs.FS + includeTOC bool + stripPrefix string + title string +} + +func init() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...} + +Outputs an html NOTICE.html file. + +Options: +`, filepath.Base(os.Args[0])) + flag.PrintDefaults() + } +} + +func main() { + flag.Parse() + + // Must specify at least one root target. + if flag.NArg() == 0 { + flag.Usage() + os.Exit(2) + } + + if len(*outputFile) == 0 { + flag.Usage() + fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n") + os.Exit(2) + } else { + dir, err := filepath.Abs(filepath.Dir(*outputFile)) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot determine path to %q: %w\n", *outputFile, err) + os.Exit(1) + } + fi, err := os.Stat(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %w\n", dir, *outputFile, err) + os.Exit(1) + } + if !fi.IsDir() { + fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile) + os.Exit(1) + } + } + + var ofile io.Writer + ofile = os.Stdout + if *outputFile != "-" { + ofile = &bytes.Buffer{} + } + + ctx := &context{ofile, os.Stderr, os.DirFS("."), *includeTOC, *stripPrefix, *title} + + err := htmlNotice(ctx, flag.Args()...) + if err != nil { + if err == failNoneRequested { + flag.Usage() + } + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + if *outputFile != "-" { + err := os.WriteFile(*outputFile, ofile.(*bytes.Buffer).Bytes(), 0666) + if err != nil { + fmt.Fprintf(os.Stderr, "could not write output to %q: %w\n", *outputFile, err) + os.Exit(1) + } + } + os.Exit(0) +} + +// htmlNotice implements the htmlnotice utility. +func htmlNotice(ctx *context, files ...string) error { + // Must be at least one root file. + if len(files) < 1 { + return failNoneRequested + } + + // Read the license graph from the license metadata files (*.meta_lic). + licenseGraph, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files) + if err != nil { + return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err) + } + if licenseGraph == nil { + return failNoLicenses + } + + // rs contains all notice resolutions. + rs := compliance.ResolveNotices(licenseGraph) + + ni, err := compliance.IndexLicenseTexts(ctx.rootFS, licenseGraph, rs) + if err != nil { + return fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err) + } + + fmt.Fprintln(ctx.stdout, "") + fmt.Fprintln(ctx.stdout, "\n") + fmt.Fprintln(ctx.stdout, "\n") + if 0 < len(ctx.title) { + fmt.Fprintf(ctx.stdout, "%s\n", html.EscapeString(ctx.title)) + } + fmt.Fprintln(ctx.stdout, "") + fmt.Fprintln(ctx.stdout, "") + + if 0 < len(ctx.title) { + fmt.Fprintf(ctx.stdout, "

%s

\n", html.EscapeString(ctx.title)) + } + ids := make(map[string]string) + if ctx.includeTOC { + fmt.Fprintln(ctx.stdout, " ") + } + for h := range ni.Hashes() { + fmt.Fprintln(ctx.stdout, "
") + for _, libName := range ni.HashLibs(h) { + fmt.Fprintf(ctx.stdout, " %s used by:\n \n") + } + fmt.Fprintf(ctx.stdout, " \n
", h.String())
+		fmt.Fprintln(ctx.stdout, html.EscapeString(string(ni.HashText(h))))
+		fmt.Fprintln(ctx.stdout, "  
") + } + fmt.Fprintln(ctx.stdout, "") + + return nil +} diff --git a/tools/compliance/cmd/htmlnotice_test.go b/tools/compliance/cmd/htmlnotice_test.go new file mode 100644 index 0000000000..8d3ea02703 --- /dev/null +++ b/tools/compliance/cmd/htmlnotice_test.go @@ -0,0 +1,812 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "html" + "os" + "regexp" + "strings" + "testing" +) + +var ( + horizontalRule = regexp.MustCompile(`^\s*
\s*$`) + bodyTag = regexp.MustCompile(`^\s*\s*$`) + boilerPlate = regexp.MustCompile(`^\s*(?: