github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/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 "slices" 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 "common/bitutil/bitutil", 68 "common/prque/", 69 "crypto/blake2b/", 70 "crypto/bn256/", 71 "crypto/bls12381/", 72 "crypto/ecies/", 73 "graphql/graphiql.go", 74 "internal/jsre/deps", 75 "log/", 76 "metrics/", 77 "signer/rules/deps", 78 "internal/reexec", 79 80 // skip special licenses 81 "crypto/secp256k1", // Relicensed to BSD-3 via https://github.com/ethereum/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 func main() { 155 var ( 156 files = getFiles() 157 filec = make(chan string) 158 infoc = make(chan *info, 20) 159 wg sync.WaitGroup 160 ) 161 162 writeAuthors(files) 163 164 go func() { 165 for _, f := range files { 166 filec <- f 167 } 168 close(filec) 169 }() 170 for i := runtime.NumCPU(); i >= 0; i-- { 171 // getting file info is slow and needs to be parallel. 172 // it traverses git history for each file. 173 wg.Add(1) 174 go getInfo(filec, infoc, &wg) 175 } 176 go func() { 177 wg.Wait() 178 close(infoc) 179 }() 180 writeLicenses(infoc) 181 } 182 183 func skipFile(path string) bool { 184 if strings.Contains(path, "/testdata/") { 185 return true 186 } 187 for _, p := range skipPrefixes { 188 if strings.HasPrefix(path, p) { 189 return true 190 } 191 } 192 return false 193 } 194 195 func getFiles() []string { 196 cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "HEAD") 197 var files []string 198 err := doLines(cmd, func(line string) { 199 if skipFile(line) { 200 return 201 } 202 ext := filepath.Ext(line) 203 for _, wantExt := range extensions { 204 if ext == wantExt { 205 goto keep 206 } 207 } 208 return 209 keep: 210 files = append(files, line) 211 }) 212 if err != nil { 213 log.Fatal("error getting files:", err) 214 } 215 return files 216 } 217 218 var authorRegexp = regexp.MustCompile(`\s*[0-9]+\s*(.*)`) 219 220 func gitAuthors(files []string) []string { 221 cmds := []string{"shortlog", "-s", "-n", "-e", "HEAD", "--"} 222 cmds = append(cmds, files...) 223 cmd := exec.Command("git", cmds...) 224 var authors []string 225 err := doLines(cmd, func(line string) { 226 m := authorRegexp.FindStringSubmatch(line) 227 if len(m) > 1 { 228 authors = append(authors, m[1]) 229 } 230 }) 231 if err != nil { 232 log.Fatalln("error getting authors:", err) 233 } 234 return authors 235 } 236 237 func readAuthors() []string { 238 content, err := os.ReadFile("AUTHORS") 239 if err != nil && !os.IsNotExist(err) { 240 log.Fatalln("error reading AUTHORS:", err) 241 } 242 var authors []string 243 for _, a := range bytes.Split(content, []byte("\n")) { 244 if len(a) > 0 && a[0] != '#' { 245 authors = append(authors, string(a)) 246 } 247 } 248 // Retranslate existing authors through .mailmap. 249 // This should catch email address changes. 250 authors = mailmapLookup(authors) 251 return authors 252 } 253 254 func mailmapLookup(authors []string) []string { 255 if len(authors) == 0 { 256 return nil 257 } 258 cmds := []string{"check-mailmap", "--"} 259 cmds = append(cmds, authors...) 260 cmd := exec.Command("git", cmds...) 261 var translated []string 262 err := doLines(cmd, func(line string) { 263 translated = append(translated, line) 264 }) 265 if err != nil { 266 log.Fatalln("error translating authors:", err) 267 } 268 return translated 269 } 270 271 func writeAuthors(files []string) { 272 var ( 273 dedup = make(map[string]bool) 274 list []string 275 ) 276 // Add authors that Git reports as contributors. 277 // This is the primary source of author information. 278 for _, a := range gitAuthors(files) { 279 if la := strings.ToLower(a); !dedup[la] { 280 list = append(list, a) 281 dedup[la] = true 282 } 283 } 284 // Add existing authors from the file. This should ensure that we 285 // never lose authors, even if Git stops listing them. We can also 286 // add authors manually this way. 287 for _, a := range readAuthors() { 288 if la := strings.ToLower(a); !dedup[la] { 289 list = append(list, a) 290 dedup[la] = true 291 } 292 } 293 // Write sorted list of authors back to the file. 294 slices.SortFunc(list, func(a, b string) int { 295 return strings.Compare(strings.ToLower(a), strings.ToLower(b)) 296 }) 297 content := new(bytes.Buffer) 298 content.WriteString(authorsFileHeader) 299 for _, a := range list { 300 content.WriteString(a) 301 content.WriteString("\n") 302 } 303 fmt.Println("writing AUTHORS") 304 if err := os.WriteFile("AUTHORS", content.Bytes(), 0644); err != nil { 305 log.Fatalln(err) 306 } 307 } 308 309 func getInfo(files <-chan string, out chan<- *info, wg *sync.WaitGroup) { 310 for file := range files { 311 stat, err := os.Lstat(file) 312 if err != nil { 313 fmt.Printf("ERROR %s: %v\n", file, err) 314 continue 315 } 316 if !stat.Mode().IsRegular() { 317 continue 318 } 319 if isGenerated(file) { 320 continue 321 } 322 info, err := fileInfo(file) 323 if err != nil { 324 fmt.Printf("ERROR %s: %v\n", file, err) 325 continue 326 } 327 out <- info 328 } 329 wg.Done() 330 } 331 332 func isGenerated(file string) bool { 333 fd, err := os.Open(file) 334 if err != nil { 335 return false 336 } 337 defer fd.Close() 338 buf := make([]byte, 2048) 339 n, err := fd.Read(buf) 340 if err != nil { 341 return false 342 } 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 := os.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 := os.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 }