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