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