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