github.com/luckypickle/go-ethereum-vet@v1.14.2/build/update-license.go (about)

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