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