diff --git a/README.md b/README.md index 17bc118..a9abb36 100644 --- a/README.md +++ b/README.md @@ -84,15 +84,15 @@ with: # arbitrary key-value pairs for the install method ``` -| Option | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | The name of the tool to install. This is used to determine the installation directory and the name of the binary. | -| `version.want` | The version of the tool to install. This can be a specific version, or a version range. | -| `version.constraint` | A constraint on the version of the tool to install. This is used to determine the latest version of the tool to update to. | -| `version.method` | The method to use to determine the latest version of the tool. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | -| `version.with` | The configuration options for the version method. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | -| `method` | The method to use to install the tool. See the [Install Methods](#install-methods) section for more details. | -| `with` | The configuration options for the install method. See the [Install Methods](#install-methods) section for more details. | +| Option | Description | +|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name` | The name of the tool to install. This is used to determine the installation directory and the name of the binary. | +| `version.want` | The version of the tool to install. This can be a specific version, or a version range. | +| `version.constraint` | A constraint on the version of the tool to install. This is used to determine the latest version of the tool to update to. | +| `version.method` | The method to use to determine the latest version of the tool. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | +| `version.with` | The configuration options for the version method. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | +| `method` | The method to use to install the tool. See the [Install Methods](#install-methods) section for more details. | +| `with` | The configuration options for the install method. See the [Install Methods](#install-methods) section for more details. | ### Install Methods @@ -115,20 +115,22 @@ The default version resolver for this method is `github-release`. The `go-install` install method uses `go install` to install a tool. It requires the following configuration options: -| Option | Description | -|-------------------------|-----------------------------------------------------------------------------| -| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | -| `entrypoint` (optional) | The path within the repo to the main package for the tool (e.g. `cmd/syft`) | -| `ldflags` (optional) | A list of ldflags to pass to `go install` (e.g. `-X main.version={{ .Version }}`) | +| Option | Description | +|-------------------------|--------------------------------------------------------------------------------------| +| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | +| `entrypoint` (optional) | The path within the repo to the main package for the tool (e.g. `cmd/syft`) | +| `ldflags` (optional) | A list of ldflags to pass to `go install` (e.g. `-X main.version={{ .Version }}`) | +| `args` (optional) | A list of args/flags to pass to `go install` (e.g. `-tags containers_image_openpgp`) | +| `env` (optional) | A list key=value environment variables to use when running `go install` | The `module` option allows for a special entry: - `.` or `path/to/module/on/disk` The `ldflags` allow for templating with the following variables: -| Variable | Description | -|--------|--------------------------------------------------------------------------------------------| -| `{{ .Version }}` | The resolved version of the tool (which may differe from that of the `version.want` value) | +| Variable | Description | +|--------|-------------------------------------------------------------------------------------------------------| +| `{{ .Version }}` | The resolved version of the tool (which may differe from that of the `version.want` value) | In addition to these variables, [sprig functions](http://masterminds.github.io/sprig/) are allowed; for example: ```yaml @@ -145,10 +147,10 @@ The default version resolver for this method is `go-proxy`. The `hosted-shell` install method uses a hosted shell script to install a tool. It requires the following configuration options: -| Option | Description | -|--------|------------------------------------------------------------| -| `url` | The URL to the hosted shell script (e.g. `https://raw.githubusercontent.com/anchore/syft/main/install.sh`) | -| `args` (optional) | Arguments to pass to the shell script (as a single string) | +| Option | Description | +|--------|------------------------------------------------------------------------------------------------------------| +| `url` | The URL to the hosted shell script (e.g. `https://raw.githubusercontent.com/anchore/syft/main/install.sh`) | +| `args` (optional) | Arguments to pass to the shell script (as a single string) | If the URL refers to either `github.com` or `raw.githubusercontent.com` then the default version resolver is `github-release`. Otherwise, the version resolver must be specified manually. @@ -208,9 +210,9 @@ is a version constraint used. The `go-proxy` version method reaches out to `proxy.golang.org` to determine the latest version of a Go module. It requires the following configuration options: -| Option | Description | -|--------|----------------------------------------------------------------------------------------------------------------------| -| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | +| Option | Description | +|--------|------------------------------------------------------------------------------------------------------------------------------------------| +| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | | `allow-unresolved-version` | If the latest version cannot be found by the proxy allow for "latest" as a valid value (which `go install` supports) | The `version.want` option allows a special entry: diff --git a/cmd/binny/cli/command/add_go_install.go b/cmd/binny/cli/command/add_go_install.go index 1457eae..011dd8c 100644 --- a/cmd/binny/cli/command/add_go_install.go +++ b/cmd/binny/cli/command/add_go_install.go @@ -75,10 +75,16 @@ func runAddGoInstallConfig(cmdCfg AddGoInstallConfig, nameVersion string) error return fmt.Errorf("invalid ldflags: %w", err) } + if err := validateEnvSlice(iCfg.Env); err != nil { + return err + } + coreInstallParams := goinstall.InstallerParameters{ Module: iCfg.Module, Entrypoint: iCfg.Entrypoint, LDFlags: ldFlagsList, + Args: iCfg.Args, + Env: iCfg.Env, } installParamMap, err := toMap(coreInstallParams) @@ -103,3 +109,12 @@ func runAddGoInstallConfig(cmdCfg AddGoInstallConfig, nameVersion string) error return updateConfiguration(cmdCfg.Config, toolCfg) } + +func validateEnvSlice(env []string) error { + for _, e := range env { + if !strings.Contains(e, "=") { + return fmt.Errorf("invalid env format: %q", e) + } + } + return nil +} diff --git a/cmd/binny/cli/option/go_install.go b/cmd/binny/cli/option/go_install.go index 025b0fd..edef733 100644 --- a/cmd/binny/cli/option/go_install.go +++ b/cmd/binny/cli/option/go_install.go @@ -3,13 +3,17 @@ package option import "github.com/anchore/clio" type GoInstall struct { - Module string `json:"module" yaml:"module" mapstructure:"module"` - Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` - LDFlags string `json:"ld-flags" yaml:"ld-flags" mapstructure:"ld-flags"` + Module string `json:"module" yaml:"module" mapstructure:"module"` + Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` + LDFlags string `json:"ld-flags" yaml:"ld-flags" mapstructure:"ld-flags"` + Args []string `json:"args" yaml:"args" mapstructure:"args"` + Env []string `json:"env" yaml:"env" mapstructure:"env"` } func (o *GoInstall) AddFlags(flags clio.FlagSet) { flags.StringVarP(&o.Module, "module", "m", "Go module (e.g. github.com/anchore/syft)") flags.StringVarP(&o.Entrypoint, "entrypoint", "e", "Entrypoint within the go module (e.g. cmd/syft)") flags.StringVarP(&o.LDFlags, "ld-flags", "l", "LD flags to pass to the go install command (e.g. -ldflags \"-X main.version=1.0.0\")") + flags.StringArrayVarP(&o.Args, "args", "a", "Additional arguments to pass to the go install command") + flags.StringArrayVarP(&o.Env, "env", "", "Environment variables to pass to the go install command") } diff --git a/test/cli/trait_assertions_test.go b/test/cli/trait_assertions_test.go index 61c9188..54eb698 100644 --- a/test/cli/trait_assertions_test.go +++ b/test/cli/trait_assertions_test.go @@ -2,7 +2,6 @@ package cli import ( "encoding/json" - "github.com/google/go-cmp/cmp" "os" "os/exec" "path/filepath" @@ -11,6 +10,7 @@ import ( "testing" "github.com/acarl005/stripansi" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" ) diff --git a/tool/goinstall/installer.go b/tool/goinstall/installer.go index 7863323..7e69288 100644 --- a/tool/goinstall/installer.go +++ b/tool/goinstall/installer.go @@ -21,11 +21,13 @@ type InstallerParameters struct { Module string `json:"module" yaml:"module" mapstructure:"module"` Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` LDFlags []string `json:"ldflags" yaml:"ldflags" mapstructure:"ldflags"` + Args []string `json:"args" yaml:"args" mapstructure:"args"` + Env []string `json:"env" yaml:"env" mapstructure:"env"` } type Installer struct { config InstallerParameters - goInstallRunner func(spec, ldflags, destDir string) error + goInstallRunner func(spec, ldflags string, args []string, env []string, destDir string) error } func NewInstaller(cfg InstallerParameters) Installer { @@ -57,17 +59,57 @@ func (i Installer) InstallTo(version, destDir string) (string, error) { return "", fmt.Errorf("failed to template ldflags: %v", err) } - if err := i.goInstallRunner(spec, ldflags, destDir); err != nil { + args, err := templateSlice(i.config.Args, version) + if err != nil { + return "", fmt.Errorf("failed to template args: %v", err) + } + + if err := validateEnvSlice(i.config.Env); err != nil { + return "", err + } + + env, err := templateSlice(i.config.Env, version) + if err != nil { + return "", fmt.Errorf("failed to template env: %v", err) + } + + if err := i.goInstallRunner(spec, ldflags, args, env, destDir); err != nil { return "", fmt.Errorf("failed to install: %v", err) } return binPath, nil } +func validateEnvSlice(env []string) error { + for _, e := range env { + if !strings.Contains(e, "=") { + return fmt.Errorf("invalid env format: %q", e) + } + } + return nil +} + +func templateSlice(in []string, version string) ([]string, error) { + ret := make([]string, len(in)) + for i, arg := range in { + rendered, err := templateString(arg, version) + if err != nil { + return nil, err + } + + ret[i] = rendered + } + return ret, nil +} + func templateFlags(ldFlags []string, version string) (string, error) { flags := strings.Join(ldFlags, " ") - tmpl, err := template.New("ldflags").Funcs(sprig.FuncMap()).Parse(flags) + return templateString(flags, version) +} + +func templateString(in string, version string) (string, error) { + tmpl, err := template.New("ldflags").Funcs(sprig.FuncMap()).Parse(in) if err != nil { return "", err } @@ -84,17 +126,25 @@ func templateFlags(ldFlags []string, version string) (string, error) { return buf.String(), nil } -func runGoInstall(spec, ldflags, destDir string) error { +func runGoInstall(spec, ldflags string, userArgs, userEnv []string, destDir string) error { args := []string{"install"} + args = append(args, userArgs...) + if ldflags != "" { args = append(args, fmt.Sprintf("-ldflags=%s", ldflags)) } args = append(args, spec) - log.Trace("running: go " + strings.Join(args, " ")) + log.WithFields("env-vars", len(userEnv)).Trace("running: go " + strings.Join(args, " ")) cmd := exec.Command("go", args...) - cmd.Env = append(os.Environ(), "GOBIN="+destDir) + + // set env vars... + env := os.Environ() + env = append(env, userEnv...) + // always override any conflicting env vars + env = append(env, "GOBIN="+destDir) + cmd.Env = env output, err := cmd.CombinedOutput() if err != nil { diff --git a/tool/goinstall/installer_test.go b/tool/goinstall/installer_test.go index 689423b..cf79b7c 100644 --- a/tool/goinstall/installer_test.go +++ b/tool/goinstall/installer_test.go @@ -66,7 +66,7 @@ func Test_templateFlags(t *testing.T) { func TestInstaller_InstallTo(t *testing.T) { type fields struct { config InstallerParameters - goInstallRunner func(spec, ldflags, destDir string) error + goInstallRunner func(spec, ldflags string, args, env []string, destDir string) error } type args struct { version string @@ -88,11 +88,21 @@ func TestInstaller_InstallTo(t *testing.T) { LDFlags: []string{ "-X github.com/anchore/binny/internal/version.Version={{.Version}}", }, + Args: []string{ + "-tags", + "containers_image_openpgp", + }, + Env: []string{ + "FOO=BAR", + "BAZ=0", + }, }, - goInstallRunner: func(spec, ldflags, destDir string) error { + goInstallRunner: func(spec, ldflags string, userArgs, userEnv []string, destDir string) error { assert.Equal(t, "github.com/anchore/binny/cmd/binny@1.2.3", spec) assert.Equal(t, "-X github.com/anchore/binny/internal/version.Version=1.2.3", ldflags) assert.Equal(t, "/tmp/to/place", destDir) + assert.Equal(t, []string{"-tags", "containers_image_openpgp"}, userArgs) + assert.Equal(t, []string{"FOO=BAR", "BAZ=0"}, userEnv) return nil }, }, diff --git a/tool/install.go b/tool/install.go index c6c4457..60d3371 100644 --- a/tool/install.go +++ b/tool/install.go @@ -91,6 +91,13 @@ func makeStagingArea(store *binny.Store, toolName string) (string, func(), error if err != nil { return "", cleanup, fmt.Errorf("failed to get absolute path for store root: %w", err) } + + if _, err := os.Stat(absRoot); os.IsNotExist(err) { + if err := os.MkdirAll(absRoot, 0755); err != nil { + return "", cleanup, fmt.Errorf("failed to create store root: %w", err) + } + } + tmpdir, err := os.MkdirTemp(absRoot, fmt.Sprintf("binny-install-%s-", toolName)) if err != nil { return "", cleanup, fmt.Errorf("failed to create temp directory: %w", err)