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