Documentation
¶
Overview ¶
Package gotool provides a runner to install and run compiled executables of Go module-based "main" packages.
Go Executable Installation ¶
As of Go 1.16 `go install` supports the `pkg@version` syntax [1] which allows to install commands without "polluting" a projects `go.mod` file. The resulting executables are placed in the Go executable search path that is defined by the "GOBIN" environment variable [2] (see the "go env" command [3] to show or modify the Go toolchain environment). The problem is that installed executables will overwrite any previously installed executable of the same module/package regardless of the version. Therefore only one version of an executable can be installed at a time which makes it impossible to work on different projects that make use of the same executable but require different versions.
UX Before Go 1.16 ¶
The installation concept for "main" package executables was always a somewhat controversial point which unfortunately, partly for historical reasons, did not offer an optimal and user-friendly solution until Go 1.16. The "go" command [4] is a fantastic toolchain that provides many great features one would expect to be provided out-of-the-box from a modern and well designed programming language without the requirement to use a third-party solution: from compiling code, running unit/integration/benchmark tests, quality and error analysis, debugging utilities and many more. This did not apply for the "go install" command [1] of Go versions less than 1.16.
The general problem of tool dependencies was a long-time known issue/weak point of the Go toolchain and was a highly rated change request from the Go community with discussions like https://github.com/golang/go/issues/30515, https://github.com/golang/go/issues/25922 and https://github.com/golang/go/issues/27653 to improve this essential feature. They have been around for quite a long time without a solution that worked without introducing breaking changes and most users and the Go team agree on. Luckily, this topic was finally resolved in the Go release version 1.16 [5] and https://github.com/golang/go/issues/40276 introduced a way to install executables in module mode outside a module.
The Leftover Drawback ¶
Even though the "go install" command works totally fine to globally install executables, the problem that only a single version can be installed at a time is still left. The executable is placed in the path defined by "go env GOBIN" so the previously installed executable will be overridden. It is not possible to install multiple versions of the same package and "go install" still messes up the local user environment.
The Workaround ¶
To work around the leftover drawback, this package provides a runner that uses "go install" under the hood, but allows to place the compiled executable in a custom cache directory instead of `go env GOBIN`. It checks if the executable already exists, installs it if not so, and executes it afterwards.
The concept of storing dependencies locally on a per-project basis is well-known from the "node_modules" directory [6] of the Node [7] package manager npm [8]. Storing executables in a cache directory within the repository (not tracked by Git) allows to use "go install" mechanisms while not affect the global user environment and executables stored in "go env GOBIN". The runner achieves this by temporarily changing the "GOBIN" environment variable to the custom cache directory during the execution of "go install".
The only known disadvantage is the increased usage of storage disk space, but since most Go executables are small in size anyway, this is perfectly acceptable compared to the clearly outweighing advantages.
Note that the runner dynamically runs executables based on the given task so the "Validate" method is a NOOP.
Future Changes ¶
The provided runner is still not a clean solution that uses the Go toolchain without any special logic so as soon as the following changes are made to the Go toolchain (Go 1.17 or later), the runner will be removed again:
- https://github.com/golang/go/issues/42088 tracks the process of adding support for the Go module syntax to the "go run" command. This will allow to let the Go toolchain handle the way how compiled executable are stored, located and executed.
- https://github.com/golang/go/issues/44469#issuecomment-784534876 tracks the process of making "go install" aware of the "-o" flag like the "go build" command which is the only reason why the provided runner exists.
References
[1]: https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies [2]: https://pkg.go.dev/cmd/go/#hdr-Environment_variables [3]: https://pkg.go.dev/cmd/go/#hdr-Print_Go_environment_information [4]: https://pkg.go.dev/cmd/go [5]: https://golang.org/doc/go1.16#modules [6]: https://docs.npmjs.com/cli/v7/configuring-npm/folders#node-modules [7]: https://nodejs.org [8]: https://www.npmjs.com
Index ¶
Constants ¶
const (
// RunnerName is the name of the runner.
RunnerName = "gotool"
)
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner is runner to install and run compiled executables of Go module-based "main" packages.
Go Executable Installation ¶
As of Go 1.16 `go install` supports the `pkg@version` syntax [1] which allows to install commands without "polluting" a projects `go.mod` file. The resulting executables are placed in the Go executable search path that is defined by the "GOBIN" environment variable [2] (see the "go env" command [3] to show or modify the Go toolchain environment). The problem is that installed executables will overwrite any previously installed executable of the same module/package regardless of the version. Therefore only one version of an executable can be installed at a time which makes it impossible to work on different projects that make use of the same executable but require different versions.
UX Before Go 1.16 ¶
The installation concept for "main" package executables was always a somewhat controversial point which unfortunately, partly for historical reasons, did not offer an optimal and user-friendly solution until Go 1.16. The "go" command [4] is a fantastic toolchain that provides many great features one would expect to be provided out-of-the-box from a modern and well designed programming language without the requirement to use a third-party solution: from compiling code, running unit/integration/benchmark tests, quality and error analysis, debugging utilities and many more. This did not apply for the "go install" command [1] of Go versions less than 1.16.
The general problem of tool dependencies was a long-time known issue/weak point of the Go toolchain and was a highly rated change request from the Go community with discussions like https://github.com/golang/go/issues/30515, https://github.com/golang/go/issues/25922 and https://github.com/golang/go/issues/27653 to improve this essential feature. They have been around for quite a long time without a solution that worked without introducing breaking changes and most users and the Go team agree on. Luckily, this topic was finally resolved in the Go release version 1.16 [5] and https://github.com/golang/go/issues/40276 introduced a way to install executables in module mode outside a module.
The Leftover Drawback ¶
Even though the "go install" command works totally fine to globally install executables, the problem that only a single version can be installed at a time is still left. The executable is placed in the path defined by "go env GOBIN" so the previously installed executable will be overridden. It is not possible to install multiple versions of the same package and "go install" still messes up the local user environment.
The Workaround ¶
To work around the leftover drawback, this runner uses "go install" under the hood, but allows to place the compiled executable in a custom cache directory instead of `go env GOBIN`. It checks if the executable already exists, installs it if not so, and executes it afterwards.
The concept of storing dependencies locally on a per-project basis is well-known from the "node_modules" directory [6] of the Node [7] package manager npm [8]. Storing executables in a cache directory within the repository (not tracked by Git) allows to use "go install" mechanisms while not affect the global user environment and executables stored in "go env GOBIN". The runner achieves this by temporarily changing the "GOBIN" environment variable to the custom cache directory during the execution of "go install".
The only known disadvantage is the increased usage of storage disk space, but since most Go executables are small in size anyway, this is perfectly acceptable compared to the clearly outweighing advantages.
Note that the runner dynamically runs executables based on the given task so the "Validate" method is a NOOP.
Future Changes ¶
This runner is still not a clean solution that uses the Go toolchain without any special logic so as soon as the following changes are made to the Go toolchain (Go 1.17 or later), the runner will be removed again:
- https://github.com/golang/go/issues/42088 tracks the process of adding support for the Go module syntax to the "go run" command. This will allow to let the Go toolchain handle the way how compiled executable are stored, located and executed.
- https://github.com/golang/go/issues/44469#issuecomment-784534876 tracks the process of making "go install" aware of the "-o" flag like the "go build" command which is the only reason why this runner exists.
References
[1]: https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies [2]: https://pkg.go.dev/cmd/go/#hdr-Environment_variables [3]: https://pkg.go.dev/cmd/go/#hdr-Print_Go_environment_information [4]: https://pkg.go.dev/cmd/go [5]: https://golang.org/doc/go1.16#modules [6]: https://docs.npmjs.com/cli/v7/configuring-npm/folders#node-modules [7]: https://nodejs.org [8]: https://www.npmjs.com
func NewRunner ¶
func NewRunner(goRunner *taskGo.Runner, opts ...RunnerOption) (*Runner, error)
NewRunner creates a new command runner for Go module-based tools. It returns an error of type *task.ErrRunner when any error occurs during the creation.
func (*Runner) Install ¶
func (r *Runner) Install(goModule *project.GoModuleID) error
Install installs the executable of the given Go module. It returns an error of type *task.ErrRunner when any error occurs during the command execution.
// See https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies for more details.
func (*Runner) Run ¶
Run runs the command. It returns an error of type *task.ErrRunner when any error occurs during the command execution.
type RunnerOption ¶
type RunnerOption func(*RunnerOptions)
RunnerOption is a runner option.
func WithEnv ¶
func WithEnv(env map[string]string) RunnerOption
WithEnv sets the runner specific environment.
func WithQuiet ¶
func WithQuiet(quiet bool) RunnerOption
WithQuiet indicates whether the runner output should be minimal.
func WithToolsBinDir ¶
func WithToolsBinDir(toolsBinDir string) RunnerOption
WithToolsBinDir sets the path to the directory where compiled binaries of Go module-based tools are placed. Defaults to DefaultToolsBinDir.
type RunnerOptions ¶
type RunnerOptions struct { // Env is the runner specific environment. Env map[string]string // Quiet indicates whether the runner output should be minimal. Quiet bool // contains filtered or unexported fields }
RunnerOptions are runner options.
func NewRunnerOptions ¶
func NewRunnerOptions(opts ...RunnerOption) (*RunnerOptions, error)
NewRunnerOptions creates new runner options.