├── uggboot.png ├── go.mod ├── main.go ├── LICENSE ├── internal ├── tool │ ├── LICENSE │ └── tool.go ├── browser │ ├── LICENSE │ └── browser.go └── modrepo │ ├── LICENSE │ └── repo.go ├── go.sum ├── README.md └── cmd.go /uggboot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kortschak/ugbt/HEAD/uggboot.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kortschak/ugbt 2 | 3 | go 1.17 4 | 5 | require ( 6 | golang.org/x/mod v0.5.1 7 | golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 8 | ) 9 | 10 | require golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect 11 | 12 | retract v1.0.0 // Unsafe use of os/exec. 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2021 Dan Kortschak. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Ugg boot provides a simple way to update Go executables and list 6 | // available versions using module version information embedded in 7 | // the executable. 8 | // 9 | // Available commands are: 10 | // list: return a list of available versions for a Go executable. 11 | // install: install an executable from source based on source location 12 | // information stored in the executable. 13 | // update: update an executable to the latest release if it is newer 14 | // than the installed version. 15 | // repo: print the source code repository for the executable. 16 | // bugs: print the issues link for the executable. 17 | // version: print the ugbt version information 18 | // help: output ugbt help information 19 | // 20 | package main 21 | 22 | import ( 23 | "context" 24 | "os" 25 | 26 | "github.com/kortschak/ugbt/internal/tool" 27 | ) 28 | 29 | func main() { 30 | tool.Main(context.Background(), newUggboot(os.Args[0], "", nil), os.Args[1:]) 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright ©2021 Dan Kortschak. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | * The name of the author may not be used to endorse or promote products 11 | derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /internal/tool/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /internal/browser/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /internal/modrepo/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 2 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 3 | golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= 4 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 5 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 6 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 7 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 8 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 9 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= 11 | golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 12 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 13 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 14 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 15 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= 16 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 17 | -------------------------------------------------------------------------------- /internal/browser/browser.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package browser provides utilities for interacting with users' browsers. 6 | package browser 7 | 8 | import ( 9 | "os" 10 | "runtime" 11 | "time" 12 | 13 | exec "golang.org/x/sys/execabs" 14 | ) 15 | 16 | // Commands returns a list of possible commands to use to open a url. 17 | func Commands() [][]string { 18 | var cmds [][]string 19 | if exe := os.Getenv("BROWSER"); exe != "" { 20 | cmds = append(cmds, []string{exe}) 21 | } 22 | switch runtime.GOOS { 23 | case "darwin": 24 | cmds = append(cmds, []string{"/usr/bin/open"}) 25 | case "windows": 26 | cmds = append(cmds, []string{"cmd", "/c", "start"}) 27 | default: 28 | if os.Getenv("DISPLAY") != "" { 29 | // xdg-open is only for use in a desktop environment. 30 | cmds = append(cmds, []string{"xdg-open"}) 31 | } 32 | } 33 | cmds = append(cmds, 34 | []string{"firefox"}, 35 | []string{"chromium"}, 36 | []string{"chromium-browser"}, 37 | []string{"chrome"}, 38 | []string{"google-chrome"}, 39 | ) 40 | return cmds 41 | } 42 | 43 | // Open tries to open url in a browser and reports whether it succeeded. 44 | func Open(url string) bool { 45 | for _, args := range Commands() { 46 | cmd := exec.Command(args[0], append(args[1:], url)...) 47 | if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) { 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | 54 | // appearsSuccessful reports whether the command appears to have run successfully. 55 | // If the command runs longer than the timeout, it's deemed successful. 56 | // If the command runs within the timeout, it's deemed successful if it exited cleanly. 57 | func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool { 58 | errc := make(chan error, 1) 59 | go func() { 60 | errc <- cmd.Wait() 61 | }() 62 | 63 | select { 64 | case <-time.After(timeout): 65 | return true 66 | case err := <-errc: 67 | return err == nil 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ugg Boot 2 | 3 | ![Ugg boot](uggboot.png) 4 | 5 | Ugg boot is a tool for people wanting to have some comfort in their lives. 6 | 7 | It provides a simple way to update Go executables and list available versions using module version information embedded in the executable. 8 | 9 | - list: print a list of available versions for a Go executable. 10 | - install: reinstall or update an executable from source. 11 | - update: update an executable to latest release if it is newer than the installed version. 12 | - repo: print the source code repository for the executable. 13 | - bugs: print the issues link for the executable. 14 | 15 | ## Installation 16 | 17 | Ugg boot can be installed by `go install github.com/kortschak/ugbt@latest`. 18 | 19 | ## Example Use 20 | 21 | ### Go executable: 22 | 23 | Show the repo for an executable. 24 | ``` 25 | $ ugbt repo goimports 26 | https://cs.opensource.google/go/x/tools 27 | ``` 28 | 29 | List all available released versions. 30 | ``` 31 | $ ugbt list -all goimports 32 | v0.1.7 28 Sep 2021 22:34 33 | v0.1.6 17 Sep 2021 17:58 34 | v0.1.5 13 Jul 2021 20:15 35 | v0.1.4 23 Jun 2021 15:16 36 | v0.1.3 9 Jun 2021 21:40 37 | v0.1.2 25 May 2021 19:05 38 | v0.1.1 11 May 2021 17:48 39 | v0.1.0 19 Jan 2021 22:25 40 | ``` 41 | 42 | Upgrade to the latest version. 43 | ``` 44 | $ go version -m $(which goimports) | grep -v dep 45 | $GOBIN/goimports: go1.17.3 46 | path golang.org/x/tools/cmd/goimports 47 | mod golang.org/x/tools v0.1.6 h1:SIasE1FVIQOWz2GEAHFOmoW7xchJcqlucjSULTL0Ag4= 48 | $ ugbt install goimports latest 49 | $ go version -m $(which goimports) | grep -v dep 50 | $GOBIN/goimports: go1.17.3 51 | path golang.org/x/tools/cmd/goimports 52 | mod golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= 53 | ``` 54 | 55 | ### Go toolchain: 56 | 57 | Show the repo for the tool chain. 58 | ``` 59 | $ ugbt repo go 60 | https://cs.opensource.google/go/go 61 | ``` 62 | 63 | Show the bugs page for the tool chain. 64 | ``` 65 | $ ugbt bugs go 66 | https://github.com/golang/go/issues 67 | ``` 68 | 69 | Install gotip. 70 | ``` 71 | $ ugbt install go gotip 72 | go tool available as gotip 73 | $ gotip version 74 | go version devel go1.18-a142d65 Sat Nov 27 19:49:32 2021 +0000 linux/amd64 75 | ``` 76 | --- 77 | 78 | Ugg boot is a product of Australia. 79 | -------------------------------------------------------------------------------- /internal/tool/tool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package tool is a harness for writing Go tools. 6 | package tool 7 | 8 | import ( 9 | "context" 10 | "flag" 11 | "fmt" 12 | "log" 13 | "os" 14 | "reflect" 15 | "runtime" 16 | "runtime/pprof" 17 | "runtime/trace" 18 | "time" 19 | ) 20 | 21 | // This file is a harness for writing your main function. 22 | // The original version of the file is in golang.org/x/tools/internal/tool. 23 | // 24 | // It adds a method to the Application type 25 | // Main(name, usage string, args []string) 26 | // which should normally be invoked from a true main as follows: 27 | // func main() { 28 | // (&Application{}).Main("myapp", "non-flag-command-line-arg-help", os.Args[1:]) 29 | // } 30 | // It recursively scans the application object for fields with a tag containing 31 | // `flag:"flagname" help:"short help text"`` 32 | // uses all those fields to build command line flags. 33 | // It expects the Application type to have a method 34 | // Run(context.Context, args...string) error 35 | // which it invokes only after all command line flag processing has been finished. 36 | // If Run returns an error, the error will be printed to stderr and the 37 | // application will quit with a non zero exit status. 38 | 39 | // Profile can be embedded in your application struct to automatically 40 | // add command line arguments and handling for the common profiling methods. 41 | type Profile struct { 42 | CPU string `flag:"profile.cpu" help:"write CPU profile to this file"` 43 | Memory string `flag:"profile.mem" help:"write memory profile to this file"` 44 | Trace string `flag:"profile.trace" help:"write trace log to this file"` 45 | } 46 | 47 | // Application is the interface that must be satisfied by an object passed to Main. 48 | type Application interface { 49 | // Name returns the application's name. It is used in help and error messages. 50 | Name() string 51 | // Most of the help usage is automatically generated, this string should only 52 | // describe the contents of non flag arguments. 53 | Usage() string 54 | // ShortHelp returns the one line overview of the command. 55 | ShortHelp() string 56 | // DetailedHelp should print a detailed help message. It will only ever be shown 57 | // when the ShortHelp is also printed, so there is no need to duplicate 58 | // anything from there. 59 | // It is passed the flag set so it can print the default values of the flags. 60 | // It should use the flag sets configured Output to write the help to. 61 | DetailedHelp(*flag.FlagSet) 62 | // Run is invoked after all flag processing, and inside the profiling and 63 | // error handling harness. 64 | Run(ctx context.Context, args ...string) error 65 | } 66 | 67 | // This is the type returned by CommandLineErrorf, which causes the outer main 68 | // to trigger printing of the command line help. 69 | type commandLineError string 70 | 71 | func (e commandLineError) Error() string { return string(e) } 72 | 73 | // CommandLineErrorf is like fmt.Errorf except that it returns a value that 74 | // triggers printing of the command line help. 75 | // In general you should use this when generating command line validation errors. 76 | func CommandLineErrorf(message string, args ...interface{}) error { 77 | return commandLineError(fmt.Sprintf(message, args...)) 78 | } 79 | 80 | // Main should be invoked directly by main function. 81 | // It will only return if there was no error. If an error 82 | // was encountered it is printed to standard error and the 83 | // application exits with an exit code of 2. 84 | func Main(ctx context.Context, app Application, args []string) { 85 | s := flag.NewFlagSet(app.Name(), flag.ExitOnError) 86 | s.Usage = func() { 87 | fmt.Fprint(s.Output(), app.ShortHelp()) 88 | fmt.Fprintf(s.Output(), "\n\nUsage: %v [flags] %v\n", app.Name(), app.Usage()) 89 | app.DetailedHelp(s) 90 | } 91 | addFlags(s, reflect.StructField{}, reflect.ValueOf(app)) 92 | if err := Run(ctx, app, args); err != nil { 93 | fmt.Fprintf(s.Output(), "%s: %v\n", app.Name(), err) 94 | if _, printHelp := err.(commandLineError); printHelp { 95 | s.Usage() 96 | } 97 | os.Exit(2) 98 | } 99 | } 100 | 101 | // Run is the inner loop for Main; invoked by Main, recursively by 102 | // Run, and by various tests. It runs the application and returns an 103 | // error. 104 | func Run(ctx context.Context, app Application, args []string) error { 105 | s := flag.NewFlagSet(app.Name(), flag.ExitOnError) 106 | s.Usage = func() { 107 | fmt.Fprint(s.Output(), app.ShortHelp()) 108 | fmt.Fprintf(s.Output(), "\n\nUsage: %v [flags] %v\n", app.Name(), app.Usage()) 109 | app.DetailedHelp(s) 110 | } 111 | p := addFlags(s, reflect.StructField{}, reflect.ValueOf(app)) 112 | s.Parse(args) 113 | 114 | if p != nil && p.CPU != "" { 115 | f, err := os.Create(p.CPU) 116 | if err != nil { 117 | return err 118 | } 119 | if err := pprof.StartCPUProfile(f); err != nil { 120 | return err 121 | } 122 | defer pprof.StopCPUProfile() 123 | } 124 | 125 | if p != nil && p.Trace != "" { 126 | f, err := os.Create(p.Trace) 127 | if err != nil { 128 | return err 129 | } 130 | if err := trace.Start(f); err != nil { 131 | return err 132 | } 133 | defer func() { 134 | trace.Stop() 135 | log.Printf("To view the trace, run:\n$ go tool trace view %s", p.Trace) 136 | }() 137 | } 138 | 139 | if p != nil && p.Memory != "" { 140 | f, err := os.Create(p.Memory) 141 | if err != nil { 142 | return err 143 | } 144 | defer func() { 145 | runtime.GC() // get up-to-date statistics 146 | if err := pprof.WriteHeapProfile(f); err != nil { 147 | log.Printf("Writing memory profile: %v", err) 148 | } 149 | f.Close() 150 | }() 151 | } 152 | 153 | return app.Run(ctx, s.Args()...) 154 | } 155 | 156 | // addFlags scans fields of structs recursively to find things with flag tags 157 | // and add them to the flag set. 158 | func addFlags(f *flag.FlagSet, field reflect.StructField, value reflect.Value) *Profile { 159 | // is it a field we are allowed to reflect on? 160 | if field.PkgPath != "" { 161 | return nil 162 | } 163 | // now see if is actually a flag 164 | flagName, isFlag := field.Tag.Lookup("flag") 165 | help := field.Tag.Get("help") 166 | if !isFlag { 167 | // not a flag, but it might be a struct with flags in it 168 | if value.Elem().Kind() != reflect.Struct { 169 | return nil 170 | } 171 | p, _ := value.Interface().(*Profile) 172 | // go through all the fields of the struct 173 | sv := value.Elem() 174 | for i := 0; i < sv.Type().NumField(); i++ { 175 | child := sv.Type().Field(i) 176 | v := sv.Field(i) 177 | // make sure we have a pointer 178 | if v.Kind() != reflect.Ptr { 179 | v = v.Addr() 180 | } 181 | // check if that field is a flag or contains flags 182 | if fp := addFlags(f, child, v); fp != nil { 183 | p = fp 184 | } 185 | } 186 | return p 187 | } 188 | switch v := value.Interface().(type) { 189 | case flag.Value: 190 | f.Var(v, flagName, help) 191 | case *bool: 192 | f.BoolVar(v, flagName, *v, help) 193 | case *time.Duration: 194 | f.DurationVar(v, flagName, *v, help) 195 | case *float64: 196 | f.Float64Var(v, flagName, *v, help) 197 | case *int64: 198 | f.Int64Var(v, flagName, *v, help) 199 | case *int: 200 | f.IntVar(v, flagName, *v, help) 201 | case *string: 202 | f.StringVar(v, flagName, *v, help) 203 | case *uint: 204 | f.UintVar(v, flagName, *v, help) 205 | case *uint64: 206 | f.Uint64Var(v, flagName, *v, help) 207 | default: 208 | log.Fatalf("Cannot understand flag of type %T", v) 209 | } 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /internal/modrepo/repo.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2021 Dan Kortschak. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Copyright 2019 The Go Authors. All rights reserved. 6 | // Use of this source code is governed by a BSD-style 7 | // license that can be found in the LICENSE file. 8 | 9 | // Package modrepo provide a function to obtain the repo URL for a module 10 | // path. It is a cut down version of golang.org/x/pkgsite/internal/source. 11 | package modrepo 12 | 13 | import ( 14 | "context" 15 | "encoding/xml" 16 | "errors" 17 | "fmt" 18 | "io" 19 | "net/http" 20 | "regexp" 21 | "strings" 22 | ) 23 | 24 | const ( 25 | goSourceRepoURL = "https://cs.opensource.google/go/go" 26 | goIssuesURL = "https://github.com/golang/go/issues" 27 | ) 28 | 29 | // URL returns the repository corresponding to the module path. 30 | func URL(ctx context.Context, mod string) (repo, bugs string, _ error) { 31 | // The example.com domain can never be real; it is reserved for testing 32 | // (https://en.wikipedia.org/wiki/Example.com). Treat it as if it used 33 | // GitHub templates. 34 | if strings.HasPrefix(mod, "example.com/") { 35 | repo = trimVCSSuffix("https://" + mod) 36 | return repo, repo, nil 37 | } 38 | 39 | // standard is the name of the module for the standard library. 40 | const standard = "std" 41 | if mod == standard { 42 | return goSourceRepoURL, goIssuesURL, nil 43 | } 44 | 45 | repo, bugsFor, err := matchStatic(mod) 46 | if err != nil { 47 | meta, err := fetchMeta(ctx, mod) 48 | if err != nil { 49 | return "", "", err 50 | } 51 | repo = strings.TrimSuffix(meta.repoURL, "/") 52 | _, bugsFor, _ = matchStatic(removeHTTPScheme(meta.repoURL)) 53 | } else { 54 | repo = trimVCSSuffix("https://" + repo) 55 | } 56 | if strings.HasPrefix(mod, "golang.org/") { 57 | repo, bugs = adjustGoRepoInfo(repo, mod) 58 | return repo, bugs, nil 59 | } 60 | return repo, bugsFor(repo), nil 61 | } 62 | 63 | // csNonXRepos is a set of repos hosted at https://cs.opensource.google/go, 64 | // that are not an x/repo. 65 | var csNonXRepos = map[string]bool{ 66 | "dl": true, 67 | "proposal": true, 68 | "vscode-go": true, 69 | } 70 | 71 | // csXRepos is the set of repos hosted at https://cs.opensource.google/go, 72 | // that have a x/ prefix. 73 | // 74 | // x/scratch is not included. 75 | var csXRepos = map[string]bool{ 76 | "x/arch": true, 77 | "x/benchmarks": true, 78 | "x/blog": true, 79 | "x/build": true, 80 | "x/crypto": true, 81 | "x/debug": true, 82 | "x/example": true, 83 | "x/exp": true, 84 | "x/image": true, 85 | "x/mobile": true, 86 | "x/mod": true, 87 | "x/net": true, 88 | "x/oauth2": true, 89 | "x/perf": true, 90 | "x/pkgsite": true, 91 | "x/playground": true, 92 | "x/review": true, 93 | "x/sync": true, 94 | "x/sys": true, 95 | "x/talks": true, 96 | "x/term": true, 97 | "x/text": true, 98 | "x/time": true, 99 | "x/tools": true, 100 | "x/tour": true, 101 | "x/vgo": true, 102 | "x/website": true, 103 | "x/xerrors": true, 104 | } 105 | 106 | func adjustGoRepoInfo(repo string, modulePath string) (src, bugs string) { 107 | suffix := strings.TrimPrefix(modulePath, "golang.org/") 108 | 109 | // Validate that this is a repo that exists on 110 | // https://cs.opensource.google/go. Otherwise, default to the existing 111 | // info. 112 | parts := strings.Split(suffix, "/") 113 | if len(parts) >= 2 { 114 | suffix = parts[0] + "/" + parts[1] 115 | } 116 | if strings.HasPrefix(suffix, "x/") { 117 | if !csXRepos[suffix] { 118 | return repo, repo 119 | } 120 | } else if !csNonXRepos[suffix] { 121 | return repo, repo 122 | } 123 | 124 | return fmt.Sprintf("https://cs.opensource.google/go/%s", suffix), goIssuesURL 125 | } 126 | 127 | // matchStatic matches the given module or repo path against a list of known 128 | // patterns. It returns the repo name if there is a match. 129 | func matchStatic(moduleOrRepoPath string) (repo string, bugs func(string) string, _ error) { 130 | for _, pat := range patterns { 131 | matches := pat.re.FindStringSubmatch(moduleOrRepoPath) 132 | if matches == nil { 133 | continue 134 | } 135 | var repo string 136 | for i, n := range pat.re.SubexpNames() { 137 | if n == "repo" { 138 | repo = matches[i] 139 | break 140 | } 141 | } 142 | // Special case: git.apache.org has a go-import tag that points to 143 | // github.com/apache, but it's not quite right (the repo prefix is 144 | // missing a ".git"), so handle it here. 145 | const apacheDomain = "git.apache.org/" 146 | if strings.HasPrefix(repo, apacheDomain) { 147 | repo = strings.Replace(repo, apacheDomain, "github.com/apache/", 1) 148 | } 149 | // Special case: module paths are blitiri.com.ar/go/..., but repos are blitiri.com.ar/git/r/... 150 | if strings.HasPrefix(repo, "blitiri.com.ar/") { 151 | repo = strings.Replace(repo, "/go/", "/git/r/", 1) 152 | } 153 | return repo, pat.issues, nil 154 | } 155 | noop := func(s string) string { return s } 156 | return "", noop, errors.New("not found") 157 | } 158 | 159 | // Patterns for determining repo and URL transformation from module paths or repo 160 | // URLs. Each regexp must match a prefix of the target string, and must have a 161 | // group named "repo". 162 | var patterns = []struct { 163 | pattern string // uncompiled regexp 164 | re *regexp.Regexp 165 | issues func(repo string) string 166 | }{ 167 | { 168 | pattern: `^(?Pgithub\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`, 169 | issues: func(repo string) string { return fmt.Sprintf("%s/issues", repo) }, 170 | }, 171 | { 172 | // Assume that any site beginning with "github." works like github.com. 173 | pattern: `^(?Pgithub\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`, 174 | issues: func(repo string) string { return fmt.Sprintf("%s/issues", repo) }, 175 | }, 176 | { 177 | pattern: `^(?Pbitbucket\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`, 178 | issues: func(repo string) string { return fmt.Sprintf("%s/issues", repo) }, 179 | }, 180 | { 181 | pattern: `^(?Pgitlab\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`, 182 | issues: func(repo string) string { return fmt.Sprintf("%s/-/issues", repo) }, 183 | }, 184 | { 185 | // Assume that any site beginning with "gitlab." works like gitlab.com. 186 | pattern: `^(?Pgitlab\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`, 187 | issues: func(repo string) string { return fmt.Sprintf("%s/-/issues", repo) }, 188 | }, 189 | { 190 | pattern: `^(?Pgitee\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`, 191 | issues: func(repo string) string { return fmt.Sprintf("%s/issues", repo) }, 192 | }, 193 | { 194 | pattern: `^(?Pgit\.sr\.ht/~[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`, 195 | issues: func(repo string) string { return strings.Replace(repo, "git.sr.ht", "todo.sr.ht", 1) }, 196 | }, 197 | { 198 | pattern: `^(?Pgit\.fd\.io/[a-z0-9A-Z_.\-]+)`, 199 | issues: func(repo string) string { return repo }, 200 | }, 201 | { 202 | pattern: `^(?Pgit\.pirl\.io/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`, 203 | issues: func(repo string) string { return repo }, 204 | }, 205 | { 206 | pattern: `^(?Pgitea\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`, 207 | issues: func(repo string) string { return fmt.Sprintf("%s/issues", repo) }, 208 | }, 209 | { 210 | // Assume that any site beginning with "gitea." works like gitea.com. 211 | pattern: `^(?Pgitea\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`, 212 | issues: func(repo string) string { return fmt.Sprintf("%s/issues", repo) }, 213 | }, 214 | { 215 | pattern: `^(?Pgo\.isomorphicgo\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`, 216 | issues: func(repo string) string { return fmt.Sprintf("%s/issues", repo) }, 217 | }, 218 | { 219 | pattern: `^(?Pgit\.openprivacy\.ca/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`, 220 | issues: func(repo string) string { return fmt.Sprintf("%s/issues", repo) }, 221 | }, 222 | { 223 | pattern: `^(?Pgogs\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`, 224 | issues: func(repo string) string { return repo }, 225 | }, 226 | { 227 | pattern: `^(?Pdmitri\.shuralyov\.com\/.+)$`, 228 | issues: func(repo string) string { return fmt.Sprintf("%s$issues", repo) }, 229 | }, 230 | { 231 | pattern: `^(?Pblitiri\.com\.ar/go/.+)$`, 232 | issues: func(repo string) string { return "mailto:albertito@blitiri.com.ar" }, 233 | }, 234 | 235 | // Patterns that match the general go command pattern, where they must have 236 | // a ".git" repo suffix in an import path. If matching a repo URL from a meta tag, 237 | // there is no ".git". 238 | { 239 | pattern: `^(?P[^.]+\.googlesource\.com/[^.]+)(\.git|$)`, 240 | issues: func(repo string) string { return repo }, 241 | }, 242 | { 243 | pattern: `^(?Pgit\.apache\.org/[^.]+)(\.git|$)`, 244 | issues: func(repo string) string { return repo }, 245 | }, 246 | // General syntax for the go command. We can extract the repo and directory, but 247 | // we don't know the URL templates. 248 | // Must be last in this list. 249 | { 250 | pattern: `(?P([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?(/~?[A-Za-z0-9_.\-]+)+?)\.(bzr|fossil|git|hg|svn)`, 251 | issues: func(repo string) string { return repo }, 252 | }, 253 | } 254 | 255 | func init() { 256 | for i := range patterns { 257 | re := regexp.MustCompile(patterns[i].pattern) 258 | // The pattern regexp must contain a group named "repo". 259 | found := false 260 | for _, n := range re.SubexpNames() { 261 | if n == "repo" { 262 | found = true 263 | break 264 | } 265 | } 266 | if !found { 267 | panic(fmt.Sprintf("pattern %s missing group", patterns[i].pattern)) 268 | } 269 | patterns[i].re = re 270 | } 271 | } 272 | 273 | // trimVCSSuffix removes a VCS suffix from a repo URL in selected cases. 274 | // 275 | // The Go command allows a VCS suffix on a repo, like github.com/foo/bar.git. But 276 | // some code hosting sites don't support all paths constructed from such URLs. 277 | // For example, GitHub will redirect github.com/foo/bar.git to github.com/foo/bar, 278 | // but will 404 on github.com/goo/bar.git/tree/master and any other URL with a 279 | // non-empty path. 280 | // 281 | // To be conservative, we remove the suffix only in cases where we know it's 282 | // wrong. 283 | func trimVCSSuffix(repoURL string) string { 284 | if !strings.HasSuffix(repoURL, ".git") { 285 | return repoURL 286 | } 287 | if strings.HasPrefix(repoURL, "https://github.com/") || strings.HasPrefix(repoURL, "https://gitlab.com/") { 288 | return strings.TrimSuffix(repoURL, ".git") 289 | } 290 | return repoURL 291 | } 292 | 293 | // removeHTTPScheme removes an initial "http://" or "https://" from url. 294 | // The result can be used to match against our static patterns. 295 | // If the URL uses a different scheme, it won't be removed and it won't 296 | // match any patterns, as intended. 297 | func removeHTTPScheme(url string) string { 298 | for _, prefix := range []string{"https://", "http://"} { 299 | if strings.HasPrefix(url, prefix) { 300 | return url[len(prefix):] 301 | } 302 | } 303 | return url 304 | } 305 | 306 | // sourceMeta represents the values in a go-source meta tag, or as a fallback, 307 | // values from a go-import meta tag. 308 | // The go-source spec is at https://github.com/golang/gddo/wiki/Source-Code-Links. 309 | // The go-import spec is in "go help importpath". 310 | type sourceMeta struct { 311 | repoRootPrefix string // import path prefix corresponding to repo root 312 | repoURL string // URL of the repo root 313 | } 314 | 315 | // fetchMeta retrieves go-import and go-source meta tag information, using the import path to construct 316 | // a URL as described in "go help importpath". 317 | // 318 | // The importPath argument, as the name suggests, could be any package import 319 | // path. But we only pass module paths. 320 | // 321 | // The discovery site only cares about linking to source, not fetching it (we 322 | // already have it in the module zip file). So we merge the go-import and 323 | // go-source meta tag information, preferring the latter. 324 | func fetchMeta(ctx context.Context, importPath string) (_ *sourceMeta, err error) { 325 | uri := importPath 326 | if !strings.Contains(uri, "/") { 327 | // Add slash for root of domain. 328 | uri = uri + "/" 329 | } 330 | uri = uri + "?go-get=1" 331 | 332 | var client http.Client 333 | resp, err := doURL(ctx, &client, "GET", "https://"+uri, true) 334 | if err != nil { 335 | resp, err = doURL(ctx, &client, "GET", "http://"+uri, false) 336 | if err != nil { 337 | return nil, err 338 | } 339 | } 340 | defer resp.Body.Close() 341 | return parseMeta(importPath, resp.Body) 342 | } 343 | 344 | // doURL makes an HTTP request using the given url and method. It returns an 345 | // error if the request returns an error. If only200 is true, it also returns an 346 | // error if any status code other than 200 is returned. 347 | func doURL(ctx context.Context, client *http.Client, method, url string, only200 bool) (_ *http.Response, err error) { 348 | req, err := http.NewRequestWithContext(ctx, method, url, nil) 349 | if err != nil { 350 | return nil, err 351 | } 352 | resp, err := client.Do(req) 353 | if err != nil { 354 | return nil, err 355 | } 356 | if only200 && resp.StatusCode != 200 { 357 | resp.Body.Close() 358 | return nil, fmt.Errorf("status %s", resp.Status) 359 | } 360 | return resp, nil 361 | } 362 | 363 | func parseMeta(importPath string, r io.Reader) (sm *sourceMeta, err error) { 364 | errorMessage := "go-import and go-source meta tags not found" 365 | // gddo uses an xml parser, and this code is adapted from it. 366 | d := xml.NewDecoder(r) 367 | d.Strict = false 368 | metaScan: 369 | for { 370 | t, tokenErr := d.Token() 371 | if tokenErr != nil { 372 | break metaScan 373 | } 374 | switch t := t.(type) { 375 | case xml.EndElement: 376 | if strings.EqualFold(t.Name.Local, "head") { 377 | break metaScan 378 | } 379 | case xml.StartElement: 380 | if strings.EqualFold(t.Name.Local, "body") { 381 | break metaScan 382 | } 383 | if !strings.EqualFold(t.Name.Local, "meta") { 384 | continue metaScan 385 | } 386 | nameAttr := attrValue(t.Attr, "name") 387 | if nameAttr != "go-import" && nameAttr != "go-source" { 388 | continue metaScan 389 | } 390 | fields := strings.Fields(attrValue(t.Attr, "content")) 391 | if len(fields) < 1 { 392 | continue metaScan 393 | } 394 | repoRootPrefix := fields[0] 395 | if !strings.HasPrefix(importPath, repoRootPrefix) || (len(importPath) != len(repoRootPrefix) && importPath[len(repoRootPrefix)] != '/') { 396 | // Ignore if root is not a prefix of the path. This allows a 397 | // site to use a single error page for multiple repositories. 398 | continue metaScan 399 | } 400 | switch nameAttr { 401 | case "go-import": 402 | if len(fields) != 3 { 403 | errorMessage = "go-import meta tag content attribute does not have three fields" 404 | continue metaScan 405 | } 406 | if fields[1] == "mod" { 407 | // We can't make source links from a "mod" vcs type, so skip it. 408 | continue 409 | } 410 | if sm != nil { 411 | sm = nil 412 | errorMessage = "more than one go-import meta tag found" 413 | break metaScan 414 | } 415 | sm = &sourceMeta{ 416 | repoRootPrefix: repoRootPrefix, 417 | repoURL: fields[2], 418 | } 419 | // Keep going in the hope of finding a go-source tag. 420 | case "go-source": 421 | if len(fields) != 4 { 422 | errorMessage = "go-source meta tag content attribute does not have four fields" 423 | continue metaScan 424 | } 425 | if sm != nil && sm.repoRootPrefix != repoRootPrefix { 426 | errorMessage = fmt.Sprintf("import path prefixes %q for go-import and %q for go-source disagree", sm.repoRootPrefix, repoRootPrefix) 427 | sm = nil 428 | break metaScan 429 | } 430 | // If go-source repo is "_", then default to the go-import repo. 431 | repoURL := fields[1] 432 | if repoURL == "_" { 433 | if sm == nil { 434 | errorMessage = `go-source repo is "_", but no previous go-import tag` 435 | break metaScan 436 | } 437 | repoURL = sm.repoURL 438 | } 439 | sm = &sourceMeta{ 440 | repoRootPrefix: repoRootPrefix, 441 | repoURL: repoURL, 442 | } 443 | break metaScan 444 | } 445 | } 446 | } 447 | if sm == nil { 448 | return nil, fmt.Errorf("%s: %w", errorMessage, errors.New("not found")) 449 | } 450 | return sm, nil 451 | } 452 | 453 | func attrValue(attrs []xml.Attr, name string) string { 454 | for _, a := range attrs { 455 | if strings.EqualFold(a.Name.Local, name) { 456 | return a.Value 457 | } 458 | } 459 | return "" 460 | } 461 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2021 Dan Kortschak. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bufio" 9 | "bytes" 10 | "context" 11 | "encoding/json" 12 | "errors" 13 | "flag" 14 | "fmt" 15 | "io" 16 | "net/http" 17 | "net/url" 18 | "os" 19 | "os/exec" 20 | "path" 21 | "regexp" 22 | "runtime/debug" 23 | "sort" 24 | "strings" 25 | "text/tabwriter" 26 | "time" 27 | 28 | "golang.org/x/mod/modfile" 29 | "golang.org/x/mod/module" 30 | "golang.org/x/mod/semver" 31 | "golang.org/x/sys/execabs" 32 | 33 | "github.com/kortschak/ugbt/internal/browser" 34 | "github.com/kortschak/ugbt/internal/modrepo" 35 | "github.com/kortschak/ugbt/internal/tool" 36 | ) 37 | 38 | // ugbt is the main application as passed to tool.Main 39 | // It handles the main command line parsing and dispatch to the sub commands. 40 | type ugbt struct { 41 | // Core application flags 42 | Timeout time.Duration `flag:"timeout" help:"set timeout for operations (0 for no timeout)."` 43 | tool.Profile 44 | 45 | // The name of the binary, used in help and telemetry. 46 | name string 47 | 48 | // The working directory to run commands in. 49 | wd string 50 | 51 | // The environment variables to use. 52 | env []string 53 | } 54 | 55 | // newUggboot returns a new ugbt ready to run. 56 | func newUggboot(name, wd string, env []string) *ugbt { 57 | if wd == "" { 58 | wd, _ = os.Getwd() 59 | } 60 | return &ugbt{ 61 | name: name, 62 | wd: wd, 63 | env: env, 64 | Timeout: 10 * time.Minute, 65 | } 66 | } 67 | 68 | // Name implements tool.Application returning the binary name. 69 | func (u *ugbt) Name() string { return u.name } 70 | 71 | // Usage implements tool.Application returning empty extra argument usage. 72 | func (*ugbt) Usage() string { return " [command-flags] [command-args]" } 73 | 74 | // ShortHelp implements tool.Application returning the main binary help. 75 | func (*ugbt) ShortHelp() string { 76 | return "The Ugg boot tool." 77 | } 78 | 79 | // DetailedHelp implements tool.Application returning the main binary help. 80 | // This includes the short help for all the sub commands. 81 | func (u *ugbt) DetailedHelp(f *flag.FlagSet) { 82 | fmt.Fprint(f.Output(), ` 83 | Available commands are: 84 | `) 85 | for _, c := range u.commands() { 86 | fmt.Fprintf(f.Output(), " %s: %v\n", c.Name(), c.ShortHelp()) 87 | } 88 | fmt.Fprint(f.Output(), ` 89 | ugbt flags are: 90 | `) 91 | f.PrintDefaults() 92 | } 93 | 94 | // Run takes the args after top level flag processing, and invokes the correct 95 | // sub command as specified by the first argument. 96 | // If no arguments are passed it will invoke the server sub command, as a 97 | // temporary measure for compatibility. 98 | func (u *ugbt) Run(ctx context.Context, args ...string) error { 99 | if len(args) == 0 { 100 | return tool.Run(ctx, &help{}, args) 101 | } 102 | if u.Timeout > 0 { 103 | var cancel context.CancelFunc 104 | ctx, cancel = context.WithTimeout(ctx, u.Timeout) 105 | defer cancel() 106 | } 107 | command, args := args[0], args[1:] 108 | for _, c := range u.commands() { 109 | if c.Name() == command { 110 | return tool.Run(ctx, c, args) 111 | } 112 | } 113 | return tool.CommandLineErrorf("Unknown command %v", command) 114 | } 115 | 116 | // commands returns the set of commands supported by the ugbt tool on the 117 | // command line. 118 | // The command is specified by the first non flag argument. 119 | func (u *ugbt) commands() []tool.Application { 120 | return []tool.Application{ 121 | &list{ugbt: u}, 122 | &install{ugbt: u}, 123 | &update{ugbt: u, PreRelease: "^$"}, 124 | &repo{ugbt: u}, 125 | &bugs{ugbt: u}, 126 | &version{ugbt: u}, 127 | &help{}, 128 | } 129 | } 130 | 131 | // list implements the list command. 132 | type list struct { 133 | *ugbt 134 | 135 | All bool `flag:"all" help:"list all versions not just unretracted and newer than the installed executable"` 136 | PreRelease string `flag:"suffix" help:"only print versions with a pre-release matching the regexp pattern"` 137 | } 138 | 139 | func (*list) Name() string { return "list" } 140 | func (*list) Usage() string { return "[/path/to/go/executable]" } 141 | func (*list) ShortHelp() string { return "runs the ugbt list command" } 142 | func (*list) DetailedHelp(f *flag.FlagSet) { 143 | fmt.Fprint(f.Output(), ` 144 | The list command prints a list of available versions for the queried 145 | executable including any retraction details. If the -all flag is given, 146 | all versions including versions older that the current executable are 147 | printed. If an executable path is not provided, ugbt will print ugbt 148 | version information. 149 | Executables in modules matching GOPRIVATE or GONOPROXY are not handled. 150 | 151 | `) 152 | f.PrintDefaults() 153 | } 154 | 155 | // Run runs the ugbt list command. 156 | func (l *list) Run(ctx context.Context, args ...string) error { 157 | var exe string 158 | switch len(args) { 159 | case 0: 160 | // Work on ugbt. 161 | case 1: 162 | exe = args[0] 163 | default: 164 | return errors.New("list requires zero or one argument") 165 | } 166 | 167 | suffix, err := regexp.Compile(l.PreRelease) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | const defaultFormat = "_2 Jan 2006 15:04" 173 | format := defaultFormat 174 | 175 | _, mod, current, err := l.version(ctx, exe) 176 | if err != nil { 177 | return err 178 | } 179 | versions, err := l.availableVersions(ctx, mod, current, l.All) 180 | if err != nil { 181 | return err 182 | } 183 | w := tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', tabwriter.DiscardEmptyColumns) 184 | var n int 185 | for _, v := range versions { 186 | if !l.All && semverCompare(v.Version, current) <= 0 { 187 | break 188 | } 189 | if !l.All && v.isRetracted { 190 | continue 191 | } 192 | if !suffix.MatchString(semver.Prerelease(v.Version)) { 193 | continue 194 | } 195 | fmt.Fprintf(w, "%s", v.Version) 196 | if !v.Time.IsZero() { 197 | fmt.Fprintf(w, "\t%s", v.Time.Format(format)) 198 | } 199 | if v.isRetracted { 200 | if v.retractionRationale != "" { 201 | fmt.Fprintf(w, "\tretracted: %s", v.retractionRationale) 202 | } else { 203 | fmt.Fprint(w, "\tretracted") 204 | } 205 | } 206 | fmt.Fprintln(w) 207 | n++ 208 | } 209 | if n == 0 { 210 | fmt.Fprintln(os.Stderr, "no new version") 211 | } 212 | return w.Flush() 213 | } 214 | 215 | // update implements the update command. 216 | type update struct { 217 | *ugbt 218 | 219 | PreRelease string `flag:"suffix" help:"only update to versions with a pre-release matching the regexp pattern"` 220 | Verbose bool `flag:"v" help:"print the names of packages as they are compiled."` 221 | Commands bool `flag:"x" help:"print the commands run by the go tool."` 222 | DryRun bool `flag:"dry-run" help:"don't install anything, just print what would be installed."` 223 | } 224 | 225 | func (*update) Name() string { return "update" } 226 | func (*update) Usage() string { return "[/path/to/go/executable]" } 227 | func (*update) ShortHelp() string { return "runs the ugbt update command" } 228 | func (*update) DetailedHelp(f *flag.FlagSet) { 229 | fmt.Fprint(f.Output(), ` 230 | The update command updates the executable to the latest version matching 231 | the pre-release suffix pattern. If no newer version is available update 232 | is a no-op. By default it will update to the latest release. If no 233 | executable is specified ugbt will be updated. 234 | 235 | `) 236 | f.PrintDefaults() 237 | } 238 | 239 | // Run runs the ugbt update command. 240 | func (u *update) Run(ctx context.Context, args ...string) error { 241 | var exe string 242 | switch len(args) { 243 | case 0: 244 | // Work on ugbt. 245 | case 1: 246 | exe = args[0] 247 | default: 248 | return errors.New("update requires zero or one argument") 249 | } 250 | 251 | suffix, err := regexp.Compile(u.PreRelease) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | path, mod, current, err := u.version(ctx, exe) 257 | if err != nil { 258 | return err 259 | } 260 | versions, err := u.availableVersions(ctx, mod, current, false) 261 | if err != nil { 262 | return err 263 | } 264 | for _, v := range versions { 265 | if semverCompare(v.Version, current) <= 0 { 266 | break 267 | } 268 | if v.isRetracted { 269 | continue 270 | } 271 | if !suffix.MatchString(semver.Prerelease(v.Version)) { 272 | continue 273 | } 274 | if exe == "" { 275 | exe = "ugbt" 276 | } 277 | fmt.Fprintf(os.Stderr, "update %s to %s\n", exe, v.Version) 278 | if u.DryRun { 279 | return nil 280 | } 281 | return u.install(ctx, path, mod, v.Version, u.Verbose, u.Commands) 282 | } 283 | fmt.Fprintln(os.Stderr, "no new version") 284 | return nil 285 | } 286 | 287 | func semverCompare(v, w string) int { 288 | return semver.Compare(replacePrefix(v, "go", "v"), replacePrefix(w, "go", "v")) 289 | } 290 | 291 | func replacePrefix(s, old, new string) string { 292 | if !strings.HasPrefix(s, old) { 293 | return s 294 | } 295 | return new + strings.TrimPrefix(s, old) 296 | } 297 | 298 | // install implements the install command. 299 | type install struct { 300 | *ugbt 301 | 302 | Verbose bool `flag:"v" help:"print the names of packages as they are compiled."` 303 | Commands bool `flag:"x" help:"print the commands run by the go tool."` 304 | } 305 | 306 | func (*install) Name() string { return "install" } 307 | func (*install) Usage() string { return "[/path/to/go/executable] " } 308 | func (*install) ShortHelp() string { return "runs the ugbt install command" } 309 | func (*install) DetailedHelp(f *flag.FlagSet) { 310 | fmt.Fprint(f.Output(), ` 311 | The install command reinstalls the executable at the provided path using 312 | go install. Any valid version may be used including "latest". See 'go help get'. 313 | If an executable path is not provided, ugbt will install the ugbt command 314 | at the requested version. 315 | 316 | If the executable is in the standard library, a golang.org/x/dl tool will 317 | be used to download the SDK. When installing the SDK, "latest" refers to the 318 | latest release. The "gotip" version will install the current development tip. 319 | 320 | `) 321 | f.PrintDefaults() 322 | } 323 | 324 | // Run runs the ugbt install command. 325 | func (i *install) Run(ctx context.Context, args ...string) error { 326 | var exe, version string 327 | switch len(args) { 328 | case 1: 329 | version = args[0] 330 | case 2: 331 | exe = args[0] 332 | version = args[1] 333 | default: 334 | return errors.New("install requires one or two arguments") 335 | } 336 | 337 | path, mod, _, err := i.version(ctx, exe) 338 | if err != nil { 339 | return err 340 | } 341 | return i.install(ctx, path, mod, version, i.Verbose, i.Commands) 342 | } 343 | 344 | // repo implements the repo command. 345 | type repo struct { 346 | *ugbt 347 | 348 | Open bool `flag:"o" help:"open the repo url in a browser instead of printing it."` 349 | } 350 | 351 | func (*repo) Name() string { return "repo" } 352 | func (*repo) Usage() string { return "[/path/to/go/executable]" } 353 | func (*repo) ShortHelp() string { return "runs the ugbt repo command" } 354 | func (*repo) DetailedHelp(f *flag.FlagSet) { 355 | fmt.Fprint(f.Output(), ` 356 | The repo command prints the source repo URL for the executable. If an 357 | executable path is not provided, ugbt will print the ugbt repo. 358 | 359 | `) 360 | f.PrintDefaults() 361 | } 362 | 363 | // Run runs the ugbt repo command. 364 | func (r *repo) Run(ctx context.Context, args ...string) error { 365 | var exe string 366 | switch len(args) { 367 | case 0: 368 | // Work on ugbt. 369 | case 1: 370 | exe = args[0] 371 | default: 372 | return errors.New("repo requires zero or one argument") 373 | } 374 | 375 | _, mod, _, err := r.version(ctx, exe) 376 | if err != nil { 377 | return err 378 | } 379 | url, _, err := modrepo.URL(ctx, mod) 380 | if err != nil { 381 | return err 382 | } 383 | if !r.Open || !browser.Open(url) { 384 | fmt.Println(url) 385 | } 386 | return nil 387 | } 388 | 389 | // bugs implements the bugs command. 390 | type bugs struct { 391 | *ugbt 392 | 393 | Open bool `flag:"o" help:"open the issues url in a browser instead of printing it."` 394 | } 395 | 396 | func (*bugs) Name() string { return "bugs" } 397 | func (*bugs) Usage() string { return "[/path/to/go/executable]" } 398 | func (*bugs) ShortHelp() string { return "runs the ugbt bugs command" } 399 | func (*bugs) DetailedHelp(f *flag.FlagSet) { 400 | fmt.Fprint(f.Output(), ` 401 | The bugs command prints the URL for issues for the executable. If an executable 402 | path is not provided, ugbt will print the ugbt bugs. If the issues URL is not 403 | known, the source repo URL is printed. 404 | 405 | `) 406 | f.PrintDefaults() 407 | } 408 | 409 | // Run runs the ugbt bugs command. 410 | func (b *bugs) Run(ctx context.Context, args ...string) error { 411 | var exe string 412 | switch len(args) { 413 | case 0: 414 | // Work on ugbt. 415 | case 1: 416 | exe = args[0] 417 | default: 418 | return errors.New("bugs requires zero or one argument") 419 | } 420 | 421 | _, mod, _, err := b.version(ctx, exe) 422 | if err != nil { 423 | return err 424 | } 425 | _, url, err := modrepo.URL(ctx, mod) 426 | if err != nil { 427 | return err 428 | } 429 | if !b.Open || !browser.Open(url) { 430 | fmt.Println(url) 431 | } 432 | return nil 433 | } 434 | 435 | // version implements the version command. 436 | type version struct { 437 | *ugbt 438 | 439 | // Enable verbose logging 440 | Verbose bool `flag:"v" help:"verbose output"` 441 | } 442 | 443 | func (*version) Name() string { return "version" } 444 | func (*version) Usage() string { return "" } 445 | func (*version) ShortHelp() string { return "print the ugbt version information" } 446 | func (*version) DetailedHelp(f *flag.FlagSet) { 447 | f.PrintDefaults() 448 | } 449 | 450 | // Run prints ugbt version information. 451 | func (v *version) Run(ctx context.Context, args ...string) error { 452 | printBuildInfo(os.Stdout, v.Verbose) 453 | return nil 454 | } 455 | 456 | func printBuildInfo(w io.Writer, verbose bool) { 457 | if info, ok := debug.ReadBuildInfo(); ok { 458 | fmt.Fprintf(w, "%v %v\n", info.Path, info.Main.Version) 459 | if verbose { 460 | for _, dep := range info.Deps { 461 | printModuleInfo(w, dep) 462 | } 463 | } 464 | } else { 465 | fmt.Fprintf(w, "version unknown, built in $GOPATH mode\n") 466 | } 467 | } 468 | 469 | func printModuleInfo(w io.Writer, m *debug.Module) { 470 | fmt.Fprintf(w, " %s@%s", m.Path, m.Version) 471 | if m.Sum != "" { 472 | fmt.Fprintf(w, " %s", m.Sum) 473 | } 474 | if m.Replace != nil { 475 | fmt.Fprintf(w, " => %v", m.Replace.Path) 476 | } 477 | fmt.Fprintf(w, "\n") 478 | } 479 | 480 | // help implements the help command. 481 | type help struct{} 482 | 483 | func (*help) Name() string { return "help" } 484 | func (*help) Usage() string { return "" } 485 | func (*help) ShortHelp() string { return "output ugbt help information" } 486 | func (*help) DetailedHelp(f *flag.FlagSet) { 487 | f.PrintDefaults() 488 | } 489 | 490 | // Run outputs the help text. 491 | func (*help) Run(ctx context.Context, args ...string) error { 492 | fmt.Fprintf(os.Stdout, "%s", helpText) 493 | return nil 494 | } 495 | 496 | const helpText = ` 497 | The Ugg boot tool. 498 | 499 | Usage: ugbt [flags] [command-flags] [command-args] 500 | 501 | Ugg boot provides a simple way to update Go executables and list 502 | available versions using module version information embedded in 503 | the executable. 504 | 505 | Available commands: 506 | 507 | list: print a list of available versions for a Go executable 508 | 509 | install: install an executable from source based on source location 510 | information stored in the executable 511 | 512 | update: update an executable to the latest release if it is newer 513 | than the installed version 514 | 515 | repo: print the source code repository URL for the executable 516 | 517 | bugs: print the issues URL for the executable 518 | 519 | version: print the ugbt version information 520 | 521 | help: output ugbt help information 522 | 523 | Help for each command is provided with the -h flag. 524 | ` 525 | 526 | // version returns the Go package path, mod path and version of the an 527 | // executable. 528 | func (u *ugbt) version(ctx context.Context, exepath string) (pth, mod, version string, err error) { 529 | if exepath == "" { 530 | info, ok := debug.ReadBuildInfo() 531 | if !ok { 532 | return "", "", "", errors.New("could not read build info") 533 | } 534 | // info.Path is being abused here, but it will work if the ugbt 535 | // command always lives at the root of the module. 536 | return info.Path, info.Main.Path, info.Main.Version, nil 537 | } 538 | 539 | exepath, err = exec.LookPath(exepath) 540 | if err != nil { 541 | return "", "", "", err 542 | } 543 | 544 | var stdout bytes.Buffer 545 | err = u.cmd(ctx, &stdout, nil, "version", "-m", exepath).Run() 546 | if err != nil { 547 | return "", "", "", err 548 | } 549 | var main string 550 | sc := bufio.NewScanner(&stdout) 551 | for sc.Scan() { 552 | if len(sc.Bytes()) == 0 { 553 | continue 554 | } 555 | if m := bytes.Split(sc.Bytes(), []byte(": ")); len(m) == 2 { 556 | main = string(m[0]) 557 | version = string(m[1]) 558 | } 559 | f := bytes.Fields(sc.Bytes()) 560 | switch { 561 | case bytes.Equal(f[0], []byte("path")): 562 | if len(f) < 2 { 563 | return "", "", "", fmt.Errorf("unexpected path information format: %q", sc.Bytes()) 564 | } 565 | pth = string(f[1]) 566 | case bytes.Equal(f[0], []byte("mod")): 567 | if len(f) < 3 { 568 | return "", "", "", fmt.Errorf("unexpected module information format: %q", sc.Bytes()) 569 | } 570 | mod = string(f[1]) 571 | version = string(f[2]) 572 | } 573 | if pth != "" && mod != "" && version != "" { 574 | return pth, mod, version, nil 575 | } 576 | } 577 | if sc.Err() != nil { 578 | return "", "", "", sc.Err() 579 | } 580 | if strings.HasPrefix(version, "go") { 581 | return path.Join("cmd", path.Base(main)), "std", version, nil 582 | } 583 | return "", "", "", errors.New("not a go binary or no module information") 584 | } 585 | 586 | // install installs the package at the given path at the given version. 587 | func (u *ugbt) install(ctx context.Context, path, mod, version string, verbose, commands bool) error { 588 | if mod == "std" { 589 | return u.installStd(ctx, path, version, verbose, commands) 590 | } 591 | 592 | args := []string{"install"} 593 | if verbose { 594 | args = append(args, "-v") 595 | } 596 | if commands { 597 | args = append(args, "-x") 598 | } 599 | args = append(args, path+"@"+version) 600 | var buf bytes.Buffer 601 | stderr := io.Writer(&buf) 602 | if verbose || commands { 603 | stderr = io.MultiWriter(os.Stderr, stderr) 604 | } 605 | err := u.cmd(ctx, nil, stderr, args...).Run() 606 | if err != nil { 607 | if verbose || commands { 608 | return fmt.Errorf("go install: %w", err) 609 | } 610 | return errors.New(strings.TrimSpace(buf.String())) 611 | } 612 | return nil 613 | } 614 | 615 | // installStd installs the go tool chain and standard library. 616 | func (u *ugbt) installStd(ctx context.Context, path, version string, verbose, commands bool) error { 617 | if version == "latest" { 618 | versions, err := u.stdInfo(ctx) 619 | if err != nil { 620 | return err 621 | } 622 | if len(versions) == 0 { 623 | return errors.New("not found") 624 | } 625 | version = versions[0].Version 626 | } 627 | err := u.install(ctx, "golang.org/dl/"+version, "", "latest", verbose, commands) 628 | if err != nil { 629 | return err 630 | } 631 | stderr := io.Discard 632 | if verbose { 633 | stderr = os.Stderr 634 | } 635 | cmd := execabs.CommandContext(ctx, version, "download") 636 | cmd.Dir = u.wd 637 | cmd.Stderr = stderr 638 | err = cmd.Run() 639 | if err != nil { 640 | return err 641 | } 642 | if !verbose { 643 | fmt.Fprintf(os.Stderr, "go tool available as %s\n", version) 644 | } 645 | return nil 646 | } 647 | 648 | type info struct { 649 | Version string 650 | Time time.Time 651 | isRetracted bool 652 | retractionRationale string 653 | } 654 | 655 | // availableVersions returns the available semver versions from the 656 | // $GOPROXY version database. Only versions at or after the current 657 | // version are returned unless all is true. 658 | func (t *ugbt) availableVersions(ctx context.Context, mod, current string, all bool) ([]info, error) { 659 | if mod == "std" { 660 | return t.stdInfo(ctx) 661 | } 662 | 663 | mod, err := module.EscapePath(mod) 664 | if err != nil { 665 | return nil, err 666 | } 667 | 668 | for _, reason := range []string{ 669 | "GOPRIVATE", 670 | "GONOPROXY", 671 | } { 672 | private, err := t.isPrivate(ctx, mod, reason) 673 | if err != nil { 674 | return nil, err 675 | } 676 | if private { 677 | return nil, fmt.Errorf("module %s matches %s", mod, reason) 678 | } 679 | } 680 | 681 | proxies, err := t.proxies(ctx) 682 | if err != nil { 683 | return nil, err 684 | } 685 | 686 | var ( 687 | versions []info 688 | cli http.Client 689 | retractions []*modfile.Retract 690 | ) 691 | for _, p := range proxies { 692 | u, err := url.Parse(p) 693 | if err != nil { 694 | return nil, err 695 | } 696 | u.Path = path.Join(mod, "@v", "list") 697 | req, err := http.NewRequest("GET", u.String(), nil) 698 | if err != nil { 699 | return nil, err 700 | } 701 | resp, err := cli.Do(req) 702 | if err != nil { 703 | return nil, err 704 | } 705 | defer resp.Body.Close() 706 | 707 | sc := bufio.NewScanner(resp.Body) 708 | var list []string 709 | for sc.Scan() { 710 | version := sc.Text() 711 | if all || semverCompare(version, current) >= 0 { 712 | list = append(list, version) 713 | } 714 | } 715 | for _, version := range list { 716 | u.Path = path.Join(mod, "@v", version) 717 | url := u.String() 718 | 719 | i, err := t.info(ctx, url) 720 | if err != nil { 721 | var status statusError 722 | if errors.As(err, &status) { 723 | switch status.code { 724 | case http.StatusNotFound, http.StatusGone: 725 | continue 726 | } 727 | } 728 | return nil, err 729 | } 730 | versions = append(versions, i) 731 | 732 | r, err := t.retractions(ctx, url) 733 | if err != nil { 734 | return nil, err 735 | } 736 | retractions = append(retractions, r...) 737 | } 738 | } 739 | versions = unique(versions) 740 | for i, v := range versions { 741 | for _, r := range retractions { 742 | if semver.Compare(v.Version, r.Low) >= 0 && semver.Compare(v.Version, r.High) <= 0 { 743 | versions[i].isRetracted = true 744 | versions[i].retractionRationale = r.Rationale 745 | } 746 | } 747 | } 748 | return versions, nil 749 | } 750 | 751 | // stdInfo returns the information for a Go standard library versions. 752 | func (u *ugbt) stdInfo(ctx context.Context) ([]info, error) { 753 | buf, err := get(ctx, "https://go.dev/dl/?mode=json&include=all") 754 | if err != nil { 755 | return nil, fmt.Errorf("query proxy: %w", err) 756 | } 757 | var versions []info 758 | err = json.Unmarshal(buf, &versions) 759 | if err != nil { 760 | return nil, fmt.Errorf("invalid version information: %w", err) 761 | } 762 | sort.Slice(versions, func(i, j int) bool { 763 | return semverCompare(versions[i].Version, versions[j].Version) > 0 764 | }) 765 | return versions, nil 766 | } 767 | 768 | // info returns the information for a version recorded by a Go proxy. 769 | func (u *ugbt) info(ctx context.Context, version string) (info, error) { 770 | buf, err := get(ctx, version+".info") 771 | if err != nil { 772 | return info{}, fmt.Errorf("query proxy: %w", err) 773 | } 774 | var i info 775 | err = json.Unmarshal(buf, &i) 776 | if err != nil { 777 | return info{}, fmt.Errorf("invalid version information: %w", err) 778 | } 779 | return i, nil 780 | } 781 | 782 | // retractions returns any retractions noted in the version's modfile. 783 | func (u *ugbt) retractions(ctx context.Context, version string) ([]*modfile.Retract, error) { 784 | buf, err := get(ctx, version+".mod") 785 | if err != nil { 786 | return nil, fmt.Errorf("query proxy: %w", err) 787 | } 788 | f, err := modfile.Parse(version+".mod", buf, nil) 789 | if err != nil { 790 | return nil, fmt.Errorf("invalid modfile: %w", err) 791 | } 792 | return f.Retract, nil 793 | } 794 | 795 | // get returns the body of a GET request to the provided URL. Any non 200 796 | // response status is returned as an error. 797 | func get(ctx context.Context, url string) ([]byte, error) { 798 | req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 799 | if err != nil { 800 | return nil, err 801 | } 802 | var cli http.Client 803 | resp, err := cli.Do(req) 804 | if err != nil { 805 | return nil, err 806 | } 807 | defer resp.Body.Close() 808 | if resp.StatusCode != http.StatusOK { 809 | io.Copy(io.Discard, resp.Body) 810 | return nil, statusError{status: resp.Status, code: resp.StatusCode} 811 | } 812 | var buf bytes.Buffer 813 | _, err = io.Copy(&buf, resp.Body) 814 | if err != nil { 815 | return nil, err 816 | } 817 | return buf.Bytes(), nil 818 | } 819 | 820 | // statusError is an HTTP status error. 821 | type statusError struct { 822 | status string 823 | code int 824 | } 825 | 826 | func (e statusError) Error() string { return e.status } 827 | 828 | // unique returns version lexically sorted in descending version order 829 | // and with repeated elements omitted. 830 | func unique(versions []info) []info { 831 | if len(versions) < 2 { 832 | return versions 833 | } 834 | sort.Slice(versions, func(i, j int) bool { 835 | return semver.Compare(versions[i].Version, versions[j].Version) > 0 836 | }) 837 | curr := 0 838 | for i, addr := range versions { 839 | if addr == versions[curr] { 840 | continue 841 | } 842 | curr++ 843 | if curr < i { 844 | versions[curr], versions[i] = versions[i], info{} 845 | } 846 | } 847 | return versions[:curr+1] 848 | } 849 | 850 | // proxies returns the list of GOPROXY proxies in go env. 851 | func (u *ugbt) proxies(ctx context.Context) ([]string, error) { 852 | goproxy, err := u.goenv(ctx, "GOPROXY") 853 | if err != nil { 854 | return nil, err 855 | } 856 | var proxies []string 857 | for _, p := range strings.Split(goproxy, ",") { 858 | if p == "off" || p == "direct" { 859 | continue 860 | } 861 | proxies = append(proxies, p) 862 | } 863 | return proxies, nil 864 | } 865 | 866 | // isPrivate returns whether the module matches any GOPRIVATE or GONOPROXY pattern. 867 | func (u *ugbt) isPrivate(ctx context.Context, mod, reason string) (bool, error) { 868 | patterns, err := u.goenv(ctx, reason) 869 | if err != nil { 870 | return false, err 871 | } 872 | return module.MatchPrefixPatterns(patterns, mod), nil 873 | } 874 | 875 | // goenv returns the requested go env variable. 876 | func (u *ugbt) goenv(ctx context.Context, name string) (string, error) { 877 | var stdout, stderr bytes.Buffer 878 | err := u.cmd(ctx, &stdout, &stderr, "env", name).Run() 879 | if err != nil { 880 | return "", fmt.Errorf("%s: %w", &stderr, err) 881 | } 882 | return strings.TrimSpace(stdout.String()), nil 883 | } 884 | 885 | // cmd is a go command runner helper. 886 | func (u *ugbt) cmd(ctx context.Context, stdout, stderr io.Writer, args ...string) *execabs.Cmd { 887 | cmd := execabs.CommandContext(ctx, "go", args...) 888 | cmd.Dir = u.wd 889 | cmd.Stdout = stdout 890 | cmd.Stderr = stderr 891 | return cmd 892 | } 893 | --------------------------------------------------------------------------------