diff options
author | Sasha Smundak <asmundak@google.com> | 2022-11-09 11:57:00 -0800 |
---|---|---|
committer | Sasha Smundak <asmundak@google.com> | 2022-11-10 10:42:32 -0800 |
commit | 70f4e497e611f7886c7ffd029b356112bd48f85f (patch) | |
tree | 445af51a82fd3d4b78e455694fcb488923dc8cd1 /compliance | |
parent | c57db2d820479418968f0bda38531d03976e96d3 (diff) | |
download | bazel-70f4e497e611f7886c7ffd029b356112bd48f85f.tar.gz |
Implement license notice generator for Bazel.
Bug: 190817312
Test: treehugger
Change-Id: I36521c0a91e5c7e09745407b7ab50f8e91c7296d
Diffstat (limited to 'compliance')
-rw-r--r-- | compliance/Android.bp | 24 | ||||
-rw-r--r-- | compliance/cmd/bazel_notice_gen/bazel_notice_gen.go | 174 | ||||
-rw-r--r-- | compliance/cmd/bazel_notice_gen/bazel_notice_gen_test.go | 128 | ||||
-rw-r--r-- | compliance/go.mod | 5 | ||||
-rw-r--r-- | compliance/testdata/NOTICE_LICENSE | 2 |
5 files changed, 333 insertions, 0 deletions
diff --git a/compliance/Android.bp b/compliance/Android.bp new file mode 100644 index 00000000..0e709429 --- /dev/null +++ b/compliance/Android.bp @@ -0,0 +1,24 @@ +// +// Copyright (C) 2022 The Android Open Source Project +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +blueprint_go_binary { + name: "bazel_notice_gen", + srcs: ["cmd/bazel_notice_gen/bazel_notice_gen.go"], + testSrcs: ["cmd/bazel_notice_gen/bazel_notice_gen_test.go"], +} diff --git a/compliance/cmd/bazel_notice_gen/bazel_notice_gen.go b/compliance/cmd/bazel_notice_gen/bazel_notice_gen.go new file mode 100644 index 00000000..75a206cc --- /dev/null +++ b/compliance/cmd/bazel_notice_gen/bazel_notice_gen.go @@ -0,0 +1,174 @@ +// Copyright 2022 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 ( + "compress/gzip" + "crypto/md5" + "encoding/json" + "flag" + "fmt" + "html/template" + "io" + "os" + "strings" +) + +var ( + inputFile string + outputFile = flag.String("o", "", "output file") + listTargets = flag.Bool("list_targets", false, "list targets using each license") +) + +type LicenseKind struct { + Target string `json:"target"` + Name string `json:"name"` + Conditions []string `json:"conditions"` +} + +type License struct { + Rule string `json:"rule"` + CopyrightNotice string `json:"copyright_notice"` + PackageName string `json:"package_name"` + PackageUrl string `json:"package_url"` + PackageVersion string `json:"package_version"` + LicenseFile string `json:"license_text"` + LicenseKinds []LicenseKind `json:"license_kinds"` + Licensees []string `json:"licensees"` +} + +type LicenseTextHash string + +// generator generates the notices for the given set of licenses read from the JSON-encoded string. +// As the contents of the license files is often the same, they are read into the map by their hash. +type generator struct { + Licenses []License + LicenseTextHash map[string]LicenseTextHash // License.rule->hash of license text contents + LicenseTextIndex map[LicenseTextHash]string +} + +func newGenerator(in string) *generator { + g := generator{} + decoder := json.NewDecoder(strings.NewReader(in)) + decoder.DisallowUnknownFields() //useful to detect typos, e.g. in unit tests + err := decoder.Decode(&g.Licenses) + maybeQuit(err) + return &g +} + +func (g *generator) buildLicenseTextIndex() { + g.LicenseTextHash = make(map[string]LicenseTextHash, len(g.Licenses)) + g.LicenseTextIndex = make(map[LicenseTextHash]string) + for _, l := range g.Licenses { + if l.LicenseFile == "" { + continue + } + data, err := os.ReadFile(l.LicenseFile) + if err != nil { + fmt.Fprintf(os.Stderr, "%s: bad license file %s: %s\n", l.Rule, l.LicenseFile, err) + os.Exit(1) + } + h := LicenseTextHash(fmt.Sprintf("%x", md5.Sum(data))) + g.LicenseTextHash[l.Rule] = h + if _, found := g.LicenseTextIndex[h]; !found { + g.LicenseTextIndex[h] = string(data) + } + } +} + +func (g *generator) generate(sink io.Writer, listTargets bool) { + const tpl = `<!DOCTYPE html> +<html> + <head> + <style type="text/css"> + body { padding: 2px; margin: 0; } + .license { background-color: seashell; margin: 1em;} + pre { padding: 1em; }</style></head> + <body> + The following software has been included in this product and contains the license and notice as shown below.<p> + {{- $x := . }} + {{- range .Licenses }} + {{ if .PackageName }}<strong>{{.PackageName}}</strong>{{- else }}Rule: {{.Rule}}{{ end }} + {{- if .CopyrightNotice }}<br>Copyright Notice: {{.CopyrightNotice}}{{ end }} + {{- $v := index $x.LicenseTextHash .Rule }}{{- if $v }}<br><a href=#{{$v}}>License</a>{{- end }}<br> + {{- if list_targets }} + Used by: {{- range .Licensees }} {{.}} {{- end }}<hr> + {{- end }} + {{- end }} + {{ range $k, $v := .LicenseTextIndex }}<div id="{{$k}}" class="license"><pre>{{$v}} + </pre></div> {{- end }} + </body> +</html> +` + funcMap := template.FuncMap{ + "list_targets": func() bool { return listTargets }, + } + t, err := template.New("NoticesPage").Funcs(funcMap).Parse(tpl) + maybeQuit(err) + if g.LicenseTextHash == nil { + g.buildLicenseTextIndex() + } + maybeQuit(t.Execute(sink, g)) +} + +func maybeQuit(err error) { + if err == nil { + return + } + + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func processArgs() { + flag.Usage = func() { + fmt.Fprintln(os.Stderr, `usage: bazelhtmlnotice -o <output> <input>`) + flag.PrintDefaults() + os.Exit(2) + } + flag.Parse() + if len(flag.Args()) != 1 { + flag.Usage() + } + inputFile = flag.Arg(0) +} + +func setupWriting() (io.Writer, io.Closer, *os.File) { + if *outputFile == "" { + return os.Stdout, nil, nil + } + ofile, err := os.Create(*outputFile) + maybeQuit(err) + if !strings.HasSuffix(*outputFile, ".gz") { + return ofile, nil, ofile + } + gz, err := gzip.NewWriterLevel(ofile, gzip.BestCompression) + maybeQuit(err) + return gz, gz, ofile +} + +func main() { + processArgs() + data, err := os.ReadFile(inputFile) + maybeQuit(err) + sink, closer, ofile := setupWriting() + newGenerator(string(data)).generate(sink, *listTargets) + if closer != nil { + maybeQuit(closer.Close()) + } + if ofile != nil { + maybeQuit(ofile.Close()) + } +} diff --git a/compliance/cmd/bazel_notice_gen/bazel_notice_gen_test.go b/compliance/cmd/bazel_notice_gen/bazel_notice_gen_test.go new file mode 100644 index 00000000..31953871 --- /dev/null +++ b/compliance/cmd/bazel_notice_gen/bazel_notice_gen_test.go @@ -0,0 +1,128 @@ +// Copyright 2022 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" + "testing" +) + +func Test_doit(t *testing.T) { + input := ` +[ + { + "rule": "@//build/soong/licenses:Android-Apache-2.0", + "license_kinds": [ + { + "target": "@//build/soong/licenses:SPDX-license-identifier-Apache-2.0", + "name": "SPDX-license-identifier-Apache-2.0", + "conditions": ["notice"] + } + ], + "copyright_notice": "Copyright (C) The Android Open Source Project", + "package_name": "Discombobulator", + "package_url": null, + "package_version": null, + "license_text": "../../testdata/NOTICE_LICENSE", + "licensees": [ + "@//bionic/libc:libc_bionic_ndk", + "@//system/logging/liblog:liblog" + ] + }, + { + "rule": "@//external/scudo:external_scudo_license", + "license_kinds": [ + { + "target": "@//build/soong/licenses:SPDX-license-identifier-Apache-2.0", + "name": "SPDX-license-identifier-Apache-2.0", + "conditions": ["notice"] + } + ], + "copyright_notice": "", + "package_name": "Scudo Standalone", + "package_url": null, + "package_version": null, + "licensees": [ + "@//external/scudo:foo" + ] + } +] +` + tests := []struct { + name string + in string + listTargets bool + want string + }{ + { + name: "ListTargets", + in: input, + listTargets: true, + want: `<!DOCTYPE html> +<html> + <head> + <style type="text/css"> + body { padding: 2px; margin: 0; } + .license { background-color: seashell; margin: 1em;} + pre { padding: 1em; }</style></head> + <body> + The following software has been included in this product and contains the license and notice as shown below.<p> + <strong>Discombobulator</strong><br>Copyright Notice: Copyright (C) The Android Open Source Project<br><a href=#b9835e4a000fb18a4c8970690daa3b95>License</a><br> + Used by: @//bionic/libc:libc_bionic_ndk @//system/logging/liblog:liblog<hr> + <strong>Scudo Standalone</strong><br> + Used by: @//external/scudo:foo<hr> + <div id="b9835e4a000fb18a4c8970690daa3b95" class="license"><pre>neque porro quisquam est qui do- +lorem ipsum + + </pre></div> + </body> +</html> +`, + }, + { + name: "NoTargets", + in: input, + listTargets: false, + want: `<!DOCTYPE html> +<html> + <head> + <style type="text/css"> + body { padding: 2px; margin: 0; } + .license { background-color: seashell; margin: 1em;} + pre { padding: 1em; }</style></head> + <body> + The following software has been included in this product and contains the license and notice as shown below.<p> + <strong>Discombobulator</strong><br>Copyright Notice: Copyright (C) The Android Open Source Project<br><a href=#b9835e4a000fb18a4c8970690daa3b95>License</a><br> + <strong>Scudo Standalone</strong><br> + <div id="b9835e4a000fb18a4c8970690daa3b95" class="license"><pre>neque porro quisquam est qui do- +lorem ipsum + + </pre></div> + </body> +</html> +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.Buffer{} + newGenerator(tt.in).generate(&buf, tt.listTargets) + got := buf.String() + if got != tt.want { + t.Errorf("doit() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/compliance/go.mod b/compliance/go.mod new file mode 100644 index 00000000..336254d2 --- /dev/null +++ b/compliance/go.mod @@ -0,0 +1,5 @@ +module android/bazel/compliance + + + +go 1.19 diff --git a/compliance/testdata/NOTICE_LICENSE b/compliance/testdata/NOTICE_LICENSE new file mode 100644 index 00000000..da74c170 --- /dev/null +++ b/compliance/testdata/NOTICE_LICENSE @@ -0,0 +1,2 @@ +neque porro quisquam est qui do- +lorem ipsum |