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