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