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 }