github.com/jimmyx0x/go-ethereum@v1.10.28/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/ethereum/go-ethereum/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, err := fd.Read(buf) 346 if err != nil { 347 return false 348 } 349 buf = buf[:n] 350 for _, l := range bytes.Split(buf, []byte("\n")) { 351 if bytes.HasPrefix(l, []byte("// Code generated")) { 352 return true 353 } 354 } 355 return false 356 } 357 358 // fileInfo finds the lowest year in which the given file was committed. 359 func fileInfo(file string) (*info, error) { 360 info := &info{file: file, Year: int64(time.Now().Year())} 361 cmd := exec.Command("git", "log", "--follow", "--find-renames=80", "--find-copies=80", "--pretty=format:%ai", "--", file) 362 err := doLines(cmd, func(line string) { 363 y, err := strconv.ParseInt(line[:4], 10, 64) 364 if err != nil { 365 fmt.Printf("cannot parse year: %q", line[:4]) 366 } 367 if y < info.Year { 368 info.Year = y 369 } 370 }) 371 return info, err 372 } 373 374 func writeLicenses(infos <-chan *info) { 375 for i := range infos { 376 writeLicense(i) 377 } 378 } 379 380 func writeLicense(info *info) { 381 fi, err := os.Stat(info.file) 382 if os.IsNotExist(err) { 383 fmt.Println("skipping (does not exist)", info.file) 384 return 385 } 386 if err != nil { 387 log.Fatalf("error stat'ing %s: %v\n", info.file, err) 388 } 389 content, err := os.ReadFile(info.file) 390 if err != nil { 391 log.Fatalf("error reading %s: %v\n", info.file, err) 392 } 393 // Construct new file content. 394 buf := new(bytes.Buffer) 395 licenseT.Execute(buf, info) 396 if m := licenseCommentRE.FindIndex(content); m != nil && m[0] == 0 { 397 buf.Write(content[:m[0]]) 398 buf.Write(content[m[1]:]) 399 } else { 400 buf.Write(content) 401 } 402 // Write it to the file. 403 if bytes.Equal(content, buf.Bytes()) { 404 fmt.Println("skipping (no changes)", info.file) 405 return 406 } 407 fmt.Println("writing", info.ShortLicense(), info.file) 408 if err := os.WriteFile(info.file, buf.Bytes(), fi.Mode()); err != nil { 409 log.Fatalf("error writing %s: %v", info.file, err) 410 } 411 } 412 413 func doLines(cmd *exec.Cmd, f func(string)) error { 414 stdout, err := cmd.StdoutPipe() 415 if err != nil { 416 return err 417 } 418 if err := cmd.Start(); err != nil { 419 return err 420 } 421 s := bufio.NewScanner(stdout) 422 for s.Scan() { 423 f(s.Text()) 424 } 425 if s.Err() != nil { 426 return s.Err() 427 } 428 if err := cmd.Wait(); err != nil { 429 return fmt.Errorf("%v (for %s)", err, strings.Join(cmd.Args, " ")) 430 } 431 return nil 432 }