github.com/dominant-strategies/go-quai@v0.28.2/build/update-license.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 //go:build none 18 // +build none 19 20 /* 21 This command generates GPL license headers on top of all source files. 22 You can run it once per month, before cutting a release or just 23 whenever you feel like it. 24 25 go run update-license.go 26 27 All authors (people who have contributed code) are listed in the 28 AUTHORS file. The author names are mapped and deduplicated using the 29 .mailmap file. You can use .mailmap to set the canonical name and 30 address for each author. See git-shortlog(1) for an explanation of the 31 .mailmap format. 32 33 Please review the resulting diff to check whether the correct 34 copyright assignments are performed. 35 */ 36 37 package main 38 39 import ( 40 "bufio" 41 "bytes" 42 "fmt" 43 "io/ioutil" 44 "log" 45 "os" 46 "os/exec" 47 "path/filepath" 48 "regexp" 49 "runtime" 50 "sort" 51 "strconv" 52 "strings" 53 "sync" 54 "text/template" 55 "time" 56 ) 57 58 var ( 59 // only files with these extensions will be considered 60 extensions = []string{".go", ".js", ".qml"} 61 62 // paths with any of these prefixes will be skipped 63 skipPrefixes = []string{ 64 // boring stuff 65 "vendor/", "tests/testdata/", "build/", 66 67 // don't relicense vendored sources 68 "cmd/internal/browser", 69 "common/bitutil/bitutil", 70 "common/prque/", 71 "consensus/progpow/xor.go", 72 "crypto/bn256/", 73 "crypto/ecies/", 74 "log/", 75 "metrics/", 76 "signer/rules/deps", 77 78 // skip special licenses 79 "crypto/secp256k1", // Relicensed to BSD-3 80 } 81 82 // paths with this prefix are licensed as GPL. all other files are LGPL. 83 gplPrefixes = []string{"cmd/"} 84 85 // this regexp must match the entire license comment at the 86 // beginning of each file. 87 licenseCommentRE = regexp.MustCompile(`^//\s*(Copyright|This file is part of).*?\n(?://.*?\n)*\n*`) 88 89 // this text appears at the start of AUTHORS 90 authorsFileHeader = "# This is the official list of go-ethereum authors for copyright purposes.\n\n" 91 ) 92 93 // this template generates the license comment. 94 // its input is an info structure. 95 var licenseT = template.Must(template.New("").Parse(` 96 // Copyright {{.Year}} The go-quai Authors 97 // This file is part of {{.Whole false}}. 98 // 99 // {{.Whole true}} is free software: you can redistribute it and/or modify 100 // it under the terms of the GNU {{.License}} as published by 101 // the Free Software Foundation, either version 3 of the License, or 102 // (at your option) any later version. 103 // 104 // {{.Whole true}} is distributed in the hope that it will be useful, 105 // but WITHOUT ANY WARRANTY; without even the implied warranty of 106 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 107 // GNU {{.License}} for more details. 108 // 109 // You should have received a copy of the GNU {{.License}} 110 // along with {{.Whole false}}. If not, see <http://www.gnu.org/licenses/>. 111 112 `[1:])) 113 114 type info struct { 115 file string 116 Year int64 117 } 118 119 func (i info) License() string { 120 if i.gpl() { 121 return "General Public License" 122 } 123 return "Lesser General Public License" 124 } 125 126 func (i info) ShortLicense() string { 127 if i.gpl() { 128 return "GPL" 129 } 130 return "LGPL" 131 } 132 133 func (i info) Whole(startOfSentence bool) string { 134 if i.gpl() { 135 return "go-quai" 136 } 137 if startOfSentence { 138 return "The go-quai library" 139 } 140 return "the go-quai library" 141 } 142 143 func (i info) gpl() bool { 144 for _, p := range gplPrefixes { 145 if strings.HasPrefix(i.file, p) { 146 return true 147 } 148 } 149 return false 150 } 151 152 // authors implements the sort.Interface for strings in case-insensitive mode. 153 type authors []string 154 155 func (as authors) Len() int { return len(as) } 156 func (as authors) Less(i, j int) bool { return strings.ToLower(as[i]) < strings.ToLower(as[j]) } 157 func (as authors) Swap(i, j int) { as[i], as[j] = as[j], as[i] } 158 159 func main() { 160 var ( 161 files = getFiles() 162 filec = make(chan string) 163 infoc = make(chan *info, 20) 164 wg sync.WaitGroup 165 ) 166 167 writeAuthors(files) 168 169 go func() { 170 for _, f := range files { 171 filec <- f 172 } 173 close(filec) 174 }() 175 for i := runtime.NumCPU(); i >= 0; i-- { 176 // getting file info is slow and needs to be parallel. 177 // it traverses git history for each file. 178 wg.Add(1) 179 go getInfo(filec, infoc, &wg) 180 } 181 go func() { 182 wg.Wait() 183 close(infoc) 184 }() 185 writeLicenses(infoc) 186 } 187 188 func skipFile(path string) bool { 189 if strings.Contains(path, "/testdata/") { 190 return true 191 } 192 for _, p := range skipPrefixes { 193 if strings.HasPrefix(path, p) { 194 return true 195 } 196 } 197 return false 198 } 199 200 func getFiles() []string { 201 cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "HEAD") 202 var files []string 203 err := doLines(cmd, func(line string) { 204 if skipFile(line) { 205 return 206 } 207 ext := filepath.Ext(line) 208 for _, wantExt := range extensions { 209 if ext == wantExt { 210 goto keep 211 } 212 } 213 return 214 keep: 215 files = append(files, line) 216 }) 217 if err != nil { 218 log.Fatal("error getting files:", err) 219 } 220 return files 221 } 222 223 var authorRegexp = regexp.MustCompile(`\s*[0-9]+\s*(.*)`) 224 225 func gitAuthors(files []string) []string { 226 cmds := []string{"shortlog", "-s", "-n", "-e", "HEAD", "--"} 227 cmds = append(cmds, files...) 228 cmd := exec.Command("git", cmds...) 229 var authors []string 230 err := doLines(cmd, func(line string) { 231 m := authorRegexp.FindStringSubmatch(line) 232 if len(m) > 1 { 233 authors = append(authors, m[1]) 234 } 235 }) 236 if err != nil { 237 log.Fatalln("error getting authors:", err) 238 } 239 return authors 240 } 241 242 func readAuthors() []string { 243 content, err := ioutil.ReadFile("AUTHORS") 244 if err != nil && !os.IsNotExist(err) { 245 log.Fatalln("error reading AUTHORS:", err) 246 } 247 var authors []string 248 for _, a := range bytes.Split(content, []byte("\n")) { 249 if len(a) > 0 && a[0] != '#' { 250 authors = append(authors, string(a)) 251 } 252 } 253 // Retranslate existing authors through .mailmap. 254 // This should catch email address changes. 255 authors = mailmapLookup(authors) 256 return authors 257 } 258 259 func mailmapLookup(authors []string) []string { 260 if len(authors) == 0 { 261 return nil 262 } 263 cmds := []string{"check-mailmap", "--"} 264 cmds = append(cmds, authors...) 265 cmd := exec.Command("git", cmds...) 266 var translated []string 267 err := doLines(cmd, func(line string) { 268 translated = append(translated, line) 269 }) 270 if err != nil { 271 log.Fatalln("error translating authors:", err) 272 } 273 return translated 274 } 275 276 func writeAuthors(files []string) { 277 var ( 278 dedup = make(map[string]bool) 279 list []string 280 ) 281 // Add authors that Git reports as contributors. 282 // This is the primary source of author information. 283 for _, a := range gitAuthors(files) { 284 if la := strings.ToLower(a); !dedup[la] { 285 list = append(list, a) 286 dedup[la] = true 287 } 288 } 289 // Add existing authors from the file. This should ensure that we 290 // never lose authors, even if Git stops listing them. We can also 291 // add authors manually this way. 292 for _, a := range readAuthors() { 293 if la := strings.ToLower(a); !dedup[la] { 294 list = append(list, a) 295 dedup[la] = true 296 } 297 } 298 // Write sorted list of authors back to the file. 299 sort.Sort(authors(list)) 300 content := new(bytes.Buffer) 301 content.WriteString(authorsFileHeader) 302 for _, a := range list { 303 content.WriteString(a) 304 content.WriteString("\n") 305 } 306 fmt.Println("writing AUTHORS") 307 if err := ioutil.WriteFile("AUTHORS", content.Bytes(), 0644); err != nil { 308 log.Fatalln(err) 309 } 310 } 311 312 func getInfo(files <-chan string, out chan<- *info, wg *sync.WaitGroup) { 313 for file := range files { 314 stat, err := os.Lstat(file) 315 if err != nil { 316 fmt.Printf("ERROR %s: %v\n", file, err) 317 continue 318 } 319 if !stat.Mode().IsRegular() { 320 continue 321 } 322 if isGenerated(file) { 323 continue 324 } 325 info, err := fileInfo(file) 326 if err != nil { 327 fmt.Printf("ERROR %s: %v\n", file, err) 328 continue 329 } 330 out <- info 331 } 332 wg.Done() 333 } 334 335 func isGenerated(file string) bool { 336 fd, err := os.Open(file) 337 if err != nil { 338 return false 339 } 340 defer fd.Close() 341 buf := make([]byte, 2048) 342 n, _ := fd.Read(buf) 343 buf = buf[:n] 344 for _, l := range bytes.Split(buf, []byte("\n")) { 345 if bytes.HasPrefix(l, []byte("// Code generated")) { 346 return true 347 } 348 } 349 return false 350 } 351 352 // fileInfo finds the lowest year in which the given file was committed. 353 func fileInfo(file string) (*info, error) { 354 info := &info{file: file, Year: int64(time.Now().Year())} 355 cmd := exec.Command("git", "log", "--follow", "--find-renames=80", "--find-copies=80", "--pretty=format:%ai", "--", file) 356 err := doLines(cmd, func(line string) { 357 y, err := strconv.ParseInt(line[:4], 10, 64) 358 if err != nil { 359 fmt.Printf("cannot parse year: %q", line[:4]) 360 } 361 if y < info.Year { 362 info.Year = y 363 } 364 }) 365 return info, err 366 } 367 368 func writeLicenses(infos <-chan *info) { 369 for i := range infos { 370 writeLicense(i) 371 } 372 } 373 374 func writeLicense(info *info) { 375 fi, err := os.Stat(info.file) 376 if os.IsNotExist(err) { 377 fmt.Println("skipping (does not exist)", info.file) 378 return 379 } 380 if err != nil { 381 log.Fatalf("error stat'ing %s: %v\n", info.file, err) 382 } 383 content, err := ioutil.ReadFile(info.file) 384 if err != nil { 385 log.Fatalf("error reading %s: %v\n", info.file, err) 386 } 387 // Construct new file content. 388 buf := new(bytes.Buffer) 389 licenseT.Execute(buf, info) 390 if m := licenseCommentRE.FindIndex(content); m != nil && m[0] == 0 { 391 buf.Write(content[:m[0]]) 392 buf.Write(content[m[1]:]) 393 } else { 394 buf.Write(content) 395 } 396 // Write it to the file. 397 if bytes.Equal(content, buf.Bytes()) { 398 fmt.Println("skipping (no changes)", info.file) 399 return 400 } 401 fmt.Println("writing", info.ShortLicense(), info.file) 402 if err := ioutil.WriteFile(info.file, buf.Bytes(), fi.Mode()); err != nil { 403 log.Fatalf("error writing %s: %v", info.file, err) 404 } 405 } 406 407 func doLines(cmd *exec.Cmd, f func(string)) error { 408 stdout, err := cmd.StdoutPipe() 409 if err != nil { 410 return err 411 } 412 if err := cmd.Start(); err != nil { 413 return err 414 } 415 s := bufio.NewScanner(stdout) 416 for s.Scan() { 417 f(s.Text()) 418 } 419 if s.Err() != nil { 420 return s.Err() 421 } 422 if err := cmd.Wait(); err != nil { 423 return fmt.Errorf("%v (for %s)", err, strings.Join(cmd.Args, " ")) 424 } 425 return nil 426 }