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