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