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