• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2022 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	"compress/gzip"
19	"crypto/md5"
20	"encoding/json"
21	"flag"
22	"fmt"
23	"html/template"
24	"io"
25	"os"
26	"strings"
27)
28
29var (
30	inputFile   string
31	outputFile  = flag.String("o", "", "output file")
32	listTargets = flag.Bool("list_targets", false, "list targets using each license")
33)
34
35type LicenseKind struct {
36	Target     string   `json:"target"`
37	Name       string   `json:"name"`
38	Conditions []string `json:"conditions"`
39}
40
41type License struct {
42	Rule            string        `json:"rule"`
43	CopyrightNotice string        `json:"copyright_notice"`
44	PackageName     string        `json:"package_name"`
45	PackageUrl      string        `json:"package_url"`
46	PackageVersion  string        `json:"package_version"`
47	LicenseFile     string        `json:"license_text"`
48	LicenseKinds    []LicenseKind `json:"license_kinds"`
49	Licensees       []string      `json:"licensees"`
50}
51
52type LicenseTextHash string
53
54// generator generates the notices for the given set of licenses read from the JSON-encoded string.
55// As the contents of the license files is often the same, they are read into the map by their hash.
56type generator struct {
57	Licenses         []License
58	LicenseTextHash  map[string]LicenseTextHash // License.rule->hash of license text contents
59	LicenseTextIndex map[LicenseTextHash]string
60}
61
62func newGenerator(in string) *generator {
63	g := generator{}
64	decoder := json.NewDecoder(strings.NewReader(in))
65	decoder.DisallowUnknownFields() //useful to detect typos, e.g. in unit tests
66	err := decoder.Decode(&g.Licenses)
67	maybeQuit(err)
68	return &g
69}
70
71func (g *generator) buildLicenseTextIndex() {
72	g.LicenseTextHash = make(map[string]LicenseTextHash, len(g.Licenses))
73	g.LicenseTextIndex = make(map[LicenseTextHash]string)
74	for _, l := range g.Licenses {
75		if l.LicenseFile == "" {
76			continue
77		}
78		data, err := os.ReadFile(l.LicenseFile)
79		if err != nil {
80			fmt.Fprintf(os.Stderr, "%s: bad license file %s: %s\n", l.Rule, l.LicenseFile, err)
81			os.Exit(1)
82		}
83		h := LicenseTextHash(fmt.Sprintf("%x", md5.Sum(data)))
84		g.LicenseTextHash[l.Rule] = h
85		if _, found := g.LicenseTextIndex[h]; !found {
86			g.LicenseTextIndex[h] = string(data)
87		}
88	}
89}
90
91func (g *generator) generate(sink io.Writer, listTargets bool) {
92	const tpl = `<!DOCTYPE html>
93<html>
94  <head>
95    <style type="text/css">
96      body { padding: 2px; margin: 0; }
97      .license { background-color: seashell; margin: 1em;}
98      pre { padding: 1em; }</style></head>
99  <body>
100    The following software has been included in this product and contains the license and notice as shown below.<p>
101    {{- $x := . }}
102	{{- range .Licenses }}
103    {{ if .PackageName  }}<strong>{{.PackageName}}</strong>{{- else }}Rule: {{.Rule}}{{ end }}
104    {{- if .CopyrightNotice }}<br>Copyright Notice: {{.CopyrightNotice}}{{ end }}
105    {{- $v := index $x.LicenseTextHash .Rule }}{{- if $v }}<br><a href=#{{$v}}>License</a>{{- end }}<br>
106    {{- if list_targets }}
107    Used by: {{- range .Licensees }} {{.}} {{- end }}<hr>
108    {{- end }}
109    {{- end }}
110    {{ range $k, $v := .LicenseTextIndex }}<div id="{{$k}}" class="license"><pre>{{$v}}
111    </pre></div> {{- end }}
112  </body>
113</html>
114`
115	funcMap := template.FuncMap{
116		"list_targets": func() bool { return listTargets },
117	}
118	t, err := template.New("NoticesPage").Funcs(funcMap).Parse(tpl)
119	maybeQuit(err)
120	if g.LicenseTextHash == nil {
121		g.buildLicenseTextIndex()
122	}
123	maybeQuit(t.Execute(sink, g))
124}
125
126func maybeQuit(err error) {
127	if err == nil {
128		return
129	}
130
131	fmt.Fprintln(os.Stderr, err)
132	os.Exit(1)
133}
134
135func processArgs() {
136	flag.Usage = func() {
137		fmt.Fprintln(os.Stderr, `usage: bazelhtmlnotice -o <output> <input>`)
138		flag.PrintDefaults()
139		os.Exit(2)
140	}
141	flag.Parse()
142	if len(flag.Args()) != 1 {
143		flag.Usage()
144	}
145	inputFile = flag.Arg(0)
146}
147
148func setupWriting() (io.Writer, io.Closer, *os.File) {
149	if *outputFile == "" {
150		return os.Stdout, nil, nil
151	}
152	ofile, err := os.Create(*outputFile)
153	maybeQuit(err)
154	if !strings.HasSuffix(*outputFile, ".gz") {
155		return ofile, nil, ofile
156	}
157	gz, err := gzip.NewWriterLevel(ofile, gzip.BestCompression)
158	maybeQuit(err)
159	return gz, gz, ofile
160}
161
162func main() {
163	processArgs()
164	data, err := os.ReadFile(inputFile)
165	maybeQuit(err)
166	sink, closer, ofile := setupWriting()
167	newGenerator(string(data)).generate(sink, *listTargets)
168	if closer != nil {
169		maybeQuit(closer.Close())
170	}
171	if ofile != nil {
172		maybeQuit(ofile.Close())
173	}
174}
175