github.com/blockchain-foundry/go-ethereum@v1.5.9/build/update-license.go (about)

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