github.com/cryptotooltop/go-ethereum@v0.0.0-20231103184714-151d1922f3e5/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/ethash/xor.go",
    72  		"crypto/bn256/",
    73  		"crypto/ecies/",
    74  		"graphql/graphiql.go",
    75  		"internal/jsre/deps",
    76  		"log/",
    77  		"metrics/",
    78  		"signer/rules/deps",
    79  
    80  		// skip special licenses
    81  		"crypto/secp256k1", // Relicensed to BSD-3 via https://github.com/scroll-tech/go-ethereum/pull/17225
    82  	}
    83  
    84  	// paths with this prefix are licensed as GPL. all other files are LGPL.
    85  	gplPrefixes = []string{"cmd/"}
    86  
    87  	// this regexp must match the entire license comment at the
    88  	// beginning of each file.
    89  	licenseCommentRE = regexp.MustCompile(`^//\s*(Copyright|This file is part of).*?\n(?://.*?\n)*\n*`)
    90  
    91  	// this text appears at the start of AUTHORS
    92  	authorsFileHeader = "# This is the official list of go-ethereum authors for copyright purposes.\n\n"
    93  )
    94  
    95  // this template generates the license comment.
    96  // its input is an info structure.
    97  var licenseT = template.Must(template.New("").Parse(`
    98  // Copyright {{.Year}} The go-ethereum Authors
    99  // This file is part of {{.Whole false}}.
   100  //
   101  // {{.Whole true}} is free software: you can redistribute it and/or modify
   102  // it under the terms of the GNU {{.License}} as published by
   103  // the Free Software Foundation, either version 3 of the License, or
   104  // (at your option) any later version.
   105  //
   106  // {{.Whole true}} is distributed in the hope that it will be useful,
   107  // but WITHOUT ANY WARRANTY; without even the implied warranty of
   108  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
   109  // GNU {{.License}} for more details.
   110  //
   111  // You should have received a copy of the GNU {{.License}}
   112  // along with {{.Whole false}}. If not, see <http://www.gnu.org/licenses/>.
   113  
   114  `[1:]))
   115  
   116  type info struct {
   117  	file string
   118  	Year int64
   119  }
   120  
   121  func (i info) License() string {
   122  	if i.gpl() {
   123  		return "General Public License"
   124  	}
   125  	return "Lesser General Public License"
   126  }
   127  
   128  func (i info) ShortLicense() string {
   129  	if i.gpl() {
   130  		return "GPL"
   131  	}
   132  	return "LGPL"
   133  }
   134  
   135  func (i info) Whole(startOfSentence bool) string {
   136  	if i.gpl() {
   137  		return "go-ethereum"
   138  	}
   139  	if startOfSentence {
   140  		return "The go-ethereum library"
   141  	}
   142  	return "the go-ethereum library"
   143  }
   144  
   145  func (i info) gpl() bool {
   146  	for _, p := range gplPrefixes {
   147  		if strings.HasPrefix(i.file, p) {
   148  			return true
   149  		}
   150  	}
   151  	return false
   152  }
   153  
   154  // authors implements the sort.Interface for strings in case-insensitive mode.
   155  type authors []string
   156  
   157  func (as authors) Len() int           { return len(as) }
   158  func (as authors) Less(i, j int) bool { return strings.ToLower(as[i]) < strings.ToLower(as[j]) }
   159  func (as authors) Swap(i, j int)      { as[i], as[j] = as[j], as[i] }
   160  
   161  func main() {
   162  	var (
   163  		files = getFiles()
   164  		filec = make(chan string)
   165  		infoc = make(chan *info, 20)
   166  		wg    sync.WaitGroup
   167  	)
   168  
   169  	writeAuthors(files)
   170  
   171  	go func() {
   172  		for _, f := range files {
   173  			filec <- f
   174  		}
   175  		close(filec)
   176  	}()
   177  	for i := runtime.NumCPU(); i >= 0; i-- {
   178  		// getting file info is slow and needs to be parallel.
   179  		// it traverses git history for each file.
   180  		wg.Add(1)
   181  		go getInfo(filec, infoc, &wg)
   182  	}
   183  	go func() {
   184  		wg.Wait()
   185  		close(infoc)
   186  	}()
   187  	writeLicenses(infoc)
   188  }
   189  
   190  func skipFile(path string) bool {
   191  	if strings.Contains(path, "/testdata/") {
   192  		return true
   193  	}
   194  	for _, p := range skipPrefixes {
   195  		if strings.HasPrefix(path, p) {
   196  			return true
   197  		}
   198  	}
   199  	return false
   200  }
   201  
   202  func getFiles() []string {
   203  	cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "HEAD")
   204  	var files []string
   205  	err := doLines(cmd, func(line string) {
   206  		if skipFile(line) {
   207  			return
   208  		}
   209  		ext := filepath.Ext(line)
   210  		for _, wantExt := range extensions {
   211  			if ext == wantExt {
   212  				goto keep
   213  			}
   214  		}
   215  		return
   216  	keep:
   217  		files = append(files, line)
   218  	})
   219  	if err != nil {
   220  		log.Fatal("error getting files:", err)
   221  	}
   222  	return files
   223  }
   224  
   225  var authorRegexp = regexp.MustCompile(`\s*[0-9]+\s*(.*)`)
   226  
   227  func gitAuthors(files []string) []string {
   228  	cmds := []string{"shortlog", "-s", "-n", "-e", "HEAD", "--"}
   229  	cmds = append(cmds, files...)
   230  	cmd := exec.Command("git", cmds...)
   231  	var authors []string
   232  	err := doLines(cmd, func(line string) {
   233  		m := authorRegexp.FindStringSubmatch(line)
   234  		if len(m) > 1 {
   235  			authors = append(authors, m[1])
   236  		}
   237  	})
   238  	if err != nil {
   239  		log.Fatalln("error getting authors:", err)
   240  	}
   241  	return authors
   242  }
   243  
   244  func readAuthors() []string {
   245  	content, err := ioutil.ReadFile("AUTHORS")
   246  	if err != nil && !os.IsNotExist(err) {
   247  		log.Fatalln("error reading AUTHORS:", err)
   248  	}
   249  	var authors []string
   250  	for _, a := range bytes.Split(content, []byte("\n")) {
   251  		if len(a) > 0 && a[0] != '#' {
   252  			authors = append(authors, string(a))
   253  		}
   254  	}
   255  	// Retranslate existing authors through .mailmap.
   256  	// This should catch email address changes.
   257  	authors = mailmapLookup(authors)
   258  	return authors
   259  }
   260  
   261  func mailmapLookup(authors []string) []string {
   262  	if len(authors) == 0 {
   263  		return nil
   264  	}
   265  	cmds := []string{"check-mailmap", "--"}
   266  	cmds = append(cmds, authors...)
   267  	cmd := exec.Command("git", cmds...)
   268  	var translated []string
   269  	err := doLines(cmd, func(line string) {
   270  		translated = append(translated, line)
   271  	})
   272  	if err != nil {
   273  		log.Fatalln("error translating authors:", err)
   274  	}
   275  	return translated
   276  }
   277  
   278  func writeAuthors(files []string) {
   279  	var (
   280  		dedup = make(map[string]bool)
   281  		list  []string
   282  	)
   283  	// Add authors that Git reports as contributors.
   284  	// This is the primary source of author information.
   285  	for _, a := range gitAuthors(files) {
   286  		if la := strings.ToLower(a); !dedup[la] {
   287  			list = append(list, a)
   288  			dedup[la] = true
   289  		}
   290  	}
   291  	// Add existing authors from the file. This should ensure that we
   292  	// never lose authors, even if Git stops listing them. We can also
   293  	// add authors manually this way.
   294  	for _, a := range readAuthors() {
   295  		if la := strings.ToLower(a); !dedup[la] {
   296  			list = append(list, a)
   297  			dedup[la] = true
   298  		}
   299  	}
   300  	// Write sorted list of authors back to the file.
   301  	sort.Sort(authors(list))
   302  	content := new(bytes.Buffer)
   303  	content.WriteString(authorsFileHeader)
   304  	for _, a := range list {
   305  		content.WriteString(a)
   306  		content.WriteString("\n")
   307  	}
   308  	fmt.Println("writing AUTHORS")
   309  	if err := ioutil.WriteFile("AUTHORS", content.Bytes(), 0644); err != nil {
   310  		log.Fatalln(err)
   311  	}
   312  }
   313  
   314  func getInfo(files <-chan string, out chan<- *info, wg *sync.WaitGroup) {
   315  	for file := range files {
   316  		stat, err := os.Lstat(file)
   317  		if err != nil {
   318  			fmt.Printf("ERROR %s: %v\n", file, err)
   319  			continue
   320  		}
   321  		if !stat.Mode().IsRegular() {
   322  			continue
   323  		}
   324  		if isGenerated(file) {
   325  			continue
   326  		}
   327  		info, err := fileInfo(file)
   328  		if err != nil {
   329  			fmt.Printf("ERROR %s: %v\n", file, err)
   330  			continue
   331  		}
   332  		out <- info
   333  	}
   334  	wg.Done()
   335  }
   336  
   337  func isGenerated(file string) bool {
   338  	fd, err := os.Open(file)
   339  	if err != nil {
   340  		return false
   341  	}
   342  	defer fd.Close()
   343  	buf := make([]byte, 2048)
   344  	n, _ := fd.Read(buf)
   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 := ioutil.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 := ioutil.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  }