github.com/Songmu/gocredits@v0.3.1-0.20231111084238-af961788d757/gocredits.go (about)

     1  package gocredits
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"log"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"text/template"
    16  	"unicode/utf8"
    17  )
    18  
    19  const (
    20  	cmdName     = "gocredits"
    21  	defaultTmpl = `{{range $_, $elm := .Licenses -}}
    22  {{$elm.Name}}
    23  {{$elm.URL}}
    24  ----------------------------------------------------------------
    25  {{$elm.Content}}
    26  ================================================================
    27  
    28  {{end}}`
    29  )
    30  
    31  // Run the gocredits
    32  func Run(argv []string, outStream, errStream io.Writer) error {
    33  	log.SetOutput(errStream)
    34  	fs := flag.NewFlagSet(
    35  		fmt.Sprintf("%s (v%s rev:%s)", cmdName, version, revision), flag.ContinueOnError)
    36  	fs.SetOutput(errStream)
    37  	ver := fs.Bool("version", false, "display version")
    38  	var (
    39  		format      = fs.String("f", "", "format")
    40  		write       = fs.Bool("w", false, "write result to CREDITS file instead of stdout")
    41  		printJSON   = fs.Bool("json", false, "data to be printed in JSON format")
    42  		skipMissing = fs.Bool("skip-missing", false, "skip when gocredits can't find the license")
    43  	)
    44  	if err := fs.Parse(argv); err != nil {
    45  		return err
    46  	}
    47  	if *ver {
    48  		return printVersion(outStream)
    49  	}
    50  	modPath := fs.Arg(0)
    51  	if modPath == "" {
    52  		modPath = "."
    53  	}
    54  	licenses, err := takeCredits(modPath, *skipMissing)
    55  	if err != nil {
    56  		return err
    57  	}
    58  	data := struct {
    59  		Licenses []*license
    60  	}{
    61  		Licenses: licenses,
    62  	}
    63  	if *printJSON {
    64  		return json.NewEncoder(outStream).Encode(data)
    65  	}
    66  
    67  	tmplStr := *format
    68  	if tmplStr == "" {
    69  		tmplStr = defaultTmpl
    70  	}
    71  	tmpl, err := template.New(cmdName).Parse(tmplStr)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	out := outStream
    76  	if *write {
    77  		f, err := os.OpenFile(filepath.Join(modPath, "CREDITS"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
    78  		if err != nil {
    79  			return err
    80  		}
    81  		defer f.Close()
    82  		out = f
    83  	}
    84  	return tmpl.Execute(out, data)
    85  }
    86  
    87  func printVersion(out io.Writer) error {
    88  	_, err := fmt.Fprintf(out, "%s v%s (rev:%s)\n", cmdName, version, revision)
    89  	return err
    90  }
    91  
    92  type license struct {
    93  	Name, URL, FilePath, Content string
    94  }
    95  
    96  type licenseDir struct {
    97  	name, version string
    98  }
    99  
   100  type licenseDirs struct {
   101  	names []string
   102  	dirs  map[string][]*licenseDir
   103  }
   104  
   105  func (ld *licenseDirs) set(l *licenseDir) {
   106  	if ld.dirs == nil {
   107  		ld.dirs = make(map[string][]*licenseDir)
   108  	}
   109  	dirs, ok := ld.dirs[l.name]
   110  	if !ok {
   111  		ld.names = append(ld.names, l.name)
   112  	}
   113  	dirs = append(dirs, l)
   114  	ld.dirs[l.name] = dirs
   115  }
   116  
   117  func takeCredits(dir string, skipMissing bool) ([]*license, error) {
   118  	goroot, err := run("go", "env", "GOROOT")
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	var (
   123  		bs    []byte
   124  		lpath string
   125  	)
   126  	for _, lpath = range []string{"LICENSE", "../LICENSE"} {
   127  		bs, err = ioutil.ReadFile(filepath.Join(goroot, lpath))
   128  		if err == nil {
   129  			break
   130  		}
   131  	}
   132  	if err != nil {
   133  		resp, err := http.Get("https://golang.org/LICENSE?m=text")
   134  		if err != nil {
   135  			return nil, err
   136  		}
   137  		defer resp.Body.Close()
   138  		if resp.StatusCode != http.StatusOK {
   139  			return nil, fmt.Errorf("failed to fetch LICENSE of Go")
   140  		}
   141  		bs, err = ioutil.ReadAll(resp.Body)
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  	}
   146  	ret := []*license{{
   147  		Name:     "Go (the standard library)",
   148  		URL:      "https://golang.org/",
   149  		FilePath: lpath,
   150  		Content:  string(bs),
   151  	}}
   152  	gopath, err := run("go", "env", "GOPATH")
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	gopkgmod := filepath.Join(gopath, "pkg", "mod")
   157  	gosum := filepath.Join(dir, "go.sum")
   158  	f, err := os.Open(gosum)
   159  	if err != nil {
   160  		if os.IsNotExist(err) {
   161  			if _, err := os.Stat(filepath.Join(dir, "go.mod")); err != nil {
   162  				return nil, fmt.Errorf("use go modules")
   163  			}
   164  			return ret, nil
   165  		}
   166  		return nil, err
   167  	}
   168  	defer f.Close()
   169  
   170  	ld := &licenseDirs{}
   171  	scr := bufio.NewScanner(f)
   172  	for scr.Scan() {
   173  		stuff := strings.Fields(scr.Text())
   174  		if len(stuff) != 3 {
   175  			continue
   176  		}
   177  		if strings.HasSuffix(stuff[1], "/go.mod") {
   178  			continue
   179  		}
   180  		ld.set(&licenseDir{
   181  			name:    stuff[0],
   182  			version: stuff[1],
   183  		})
   184  	}
   185  	if err := scr.Err(); err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	for _, packageName := range ld.names {
   190  		encodedPath, err := encodeString(packageName)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  		var found bool
   195  		dirs := ld.dirs[packageName]
   196  		for i := len(dirs) - 1; i >= 0; i-- {
   197  			dirInfo := dirs[i]
   198  			dir := filepath.Join(gopkgmod, encodedPath+"@"+dirInfo.version)
   199  			licenseFile, content, err := findLicense(dir)
   200  			if err != nil {
   201  				if os.IsNotExist(err) {
   202  					continue
   203  				}
   204  				return nil, err
   205  			}
   206  			ret = append(ret, &license{
   207  				Name:     packageName,
   208  				URL:      fmt.Sprintf("https://%s", packageName),
   209  				FilePath: filepath.Join(dir, licenseFile),
   210  				Content:  content,
   211  			})
   212  			found = true
   213  			break
   214  		}
   215  		if !found {
   216  			if skipMissing {
   217  				log.Printf("could not find the license for %q", packageName)
   218  				continue
   219  			}
   220  			return nil, fmt.Errorf("could not find the license for %q", packageName)
   221  		}
   222  	}
   223  	return ret, nil
   224  }
   225  
   226  func findLicense(dir string) (string, string, error) {
   227  	files, err := ioutil.ReadDir(dir)
   228  	if err != nil {
   229  		return "", "", err
   230  	}
   231  	var (
   232  		bestScore = 0.0
   233  		fileName  = ""
   234  	)
   235  	for _, f := range files {
   236  		if f.IsDir() {
   237  			continue
   238  		}
   239  		n := f.Name()
   240  		score := scoreLicenseName(n)
   241  		if score > bestScore {
   242  			bestScore = score
   243  			fileName = n
   244  		}
   245  	}
   246  	if fileName == "" {
   247  		return "", "", os.ErrNotExist
   248  	}
   249  	bs, err := ioutil.ReadFile(filepath.Join(dir, fileName))
   250  	if err != nil {
   251  		return "", "", err
   252  	}
   253  	return fileName, string(bs), nil
   254  }
   255  
   256  // copied from cmd/go/internal/module/module.go
   257  func encodeString(s string) (encoding string, err error) {
   258  	haveUpper := false
   259  	for _, r := range s {
   260  		if r == '!' || r >= utf8.RuneSelf {
   261  			// This should be disallowed by CheckPath, but diagnose anyway.
   262  			// The correctness of the encoding loop below depends on it.
   263  			return "", fmt.Errorf("internal error: inconsistency in EncodePath")
   264  		}
   265  		if 'A' <= r && r <= 'Z' {
   266  			haveUpper = true
   267  		}
   268  	}
   269  
   270  	if !haveUpper {
   271  		return s, nil
   272  	}
   273  
   274  	var buf []byte
   275  	for _, r := range s {
   276  		if 'A' <= r && r <= 'Z' {
   277  			buf = append(buf, '!', byte(r+'a'-'A'))
   278  		} else {
   279  			buf = append(buf, byte(r))
   280  		}
   281  	}
   282  	return string(buf), nil
   283  }