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