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