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