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