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