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