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