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