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