github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/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  	"slices"
    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  		"common/bitutil/bitutil",
    68  		"common/prque/",
    69  		"crypto/blake2b/",
    70  		"crypto/bn256/",
    71  		"crypto/bls12381/",
    72  		"crypto/ecies/",
    73  		"graphql/graphiql.go",
    74  		"internal/jsre/deps",
    75  		"log/",
    76  		"metrics/",
    77  		"signer/rules/deps",
    78  		"internal/reexec",
    79  
    80  		// skip special licenses
    81  		"crypto/secp256k1", // Relicensed to BSD-3 via https://github.com/ethereum/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  func main() {
   155  	var (
   156  		files = getFiles()
   157  		filec = make(chan string)
   158  		infoc = make(chan *info, 20)
   159  		wg    sync.WaitGroup
   160  	)
   161  
   162  	writeAuthors(files)
   163  
   164  	go func() {
   165  		for _, f := range files {
   166  			filec <- f
   167  		}
   168  		close(filec)
   169  	}()
   170  	for i := runtime.NumCPU(); i >= 0; i-- {
   171  		// getting file info is slow and needs to be parallel.
   172  		// it traverses git history for each file.
   173  		wg.Add(1)
   174  		go getInfo(filec, infoc, &wg)
   175  	}
   176  	go func() {
   177  		wg.Wait()
   178  		close(infoc)
   179  	}()
   180  	writeLicenses(infoc)
   181  }
   182  
   183  func skipFile(path string) bool {
   184  	if strings.Contains(path, "/testdata/") {
   185  		return true
   186  	}
   187  	for _, p := range skipPrefixes {
   188  		if strings.HasPrefix(path, p) {
   189  			return true
   190  		}
   191  	}
   192  	return false
   193  }
   194  
   195  func getFiles() []string {
   196  	cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "HEAD")
   197  	var files []string
   198  	err := doLines(cmd, func(line string) {
   199  		if skipFile(line) {
   200  			return
   201  		}
   202  		ext := filepath.Ext(line)
   203  		for _, wantExt := range extensions {
   204  			if ext == wantExt {
   205  				goto keep
   206  			}
   207  		}
   208  		return
   209  	keep:
   210  		files = append(files, line)
   211  	})
   212  	if err != nil {
   213  		log.Fatal("error getting files:", err)
   214  	}
   215  	return files
   216  }
   217  
   218  var authorRegexp = regexp.MustCompile(`\s*[0-9]+\s*(.*)`)
   219  
   220  func gitAuthors(files []string) []string {
   221  	cmds := []string{"shortlog", "-s", "-n", "-e", "HEAD", "--"}
   222  	cmds = append(cmds, files...)
   223  	cmd := exec.Command("git", cmds...)
   224  	var authors []string
   225  	err := doLines(cmd, func(line string) {
   226  		m := authorRegexp.FindStringSubmatch(line)
   227  		if len(m) > 1 {
   228  			authors = append(authors, m[1])
   229  		}
   230  	})
   231  	if err != nil {
   232  		log.Fatalln("error getting authors:", err)
   233  	}
   234  	return authors
   235  }
   236  
   237  func readAuthors() []string {
   238  	content, err := os.ReadFile("AUTHORS")
   239  	if err != nil && !os.IsNotExist(err) {
   240  		log.Fatalln("error reading AUTHORS:", err)
   241  	}
   242  	var authors []string
   243  	for _, a := range bytes.Split(content, []byte("\n")) {
   244  		if len(a) > 0 && a[0] != '#' {
   245  			authors = append(authors, string(a))
   246  		}
   247  	}
   248  	// Retranslate existing authors through .mailmap.
   249  	// This should catch email address changes.
   250  	authors = mailmapLookup(authors)
   251  	return authors
   252  }
   253  
   254  func mailmapLookup(authors []string) []string {
   255  	if len(authors) == 0 {
   256  		return nil
   257  	}
   258  	cmds := []string{"check-mailmap", "--"}
   259  	cmds = append(cmds, authors...)
   260  	cmd := exec.Command("git", cmds...)
   261  	var translated []string
   262  	err := doLines(cmd, func(line string) {
   263  		translated = append(translated, line)
   264  	})
   265  	if err != nil {
   266  		log.Fatalln("error translating authors:", err)
   267  	}
   268  	return translated
   269  }
   270  
   271  func writeAuthors(files []string) {
   272  	var (
   273  		dedup = make(map[string]bool)
   274  		list  []string
   275  	)
   276  	// Add authors that Git reports as contributors.
   277  	// This is the primary source of author information.
   278  	for _, a := range gitAuthors(files) {
   279  		if la := strings.ToLower(a); !dedup[la] {
   280  			list = append(list, a)
   281  			dedup[la] = true
   282  		}
   283  	}
   284  	// Add existing authors from the file. This should ensure that we
   285  	// never lose authors, even if Git stops listing them. We can also
   286  	// add authors manually this way.
   287  	for _, a := range readAuthors() {
   288  		if la := strings.ToLower(a); !dedup[la] {
   289  			list = append(list, a)
   290  			dedup[la] = true
   291  		}
   292  	}
   293  	// Write sorted list of authors back to the file.
   294  	slices.SortFunc(list, func(a, b string) int {
   295  		return strings.Compare(strings.ToLower(a), strings.ToLower(b))
   296  	})
   297  	content := new(bytes.Buffer)
   298  	content.WriteString(authorsFileHeader)
   299  	for _, a := range list {
   300  		content.WriteString(a)
   301  		content.WriteString("\n")
   302  	}
   303  	fmt.Println("writing AUTHORS")
   304  	if err := os.WriteFile("AUTHORS", content.Bytes(), 0644); err != nil {
   305  		log.Fatalln(err)
   306  	}
   307  }
   308  
   309  func getInfo(files <-chan string, out chan<- *info, wg *sync.WaitGroup) {
   310  	for file := range files {
   311  		stat, err := os.Lstat(file)
   312  		if err != nil {
   313  			fmt.Printf("ERROR %s: %v\n", file, err)
   314  			continue
   315  		}
   316  		if !stat.Mode().IsRegular() {
   317  			continue
   318  		}
   319  		if isGenerated(file) {
   320  			continue
   321  		}
   322  		info, err := fileInfo(file)
   323  		if err != nil {
   324  			fmt.Printf("ERROR %s: %v\n", file, err)
   325  			continue
   326  		}
   327  		out <- info
   328  	}
   329  	wg.Done()
   330  }
   331  
   332  func isGenerated(file string) bool {
   333  	fd, err := os.Open(file)
   334  	if err != nil {
   335  		return false
   336  	}
   337  	defer fd.Close()
   338  	buf := make([]byte, 2048)
   339  	n, err := fd.Read(buf)
   340  	if err != nil {
   341  		return false
   342  	}
   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 := os.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 := os.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  }