Skip to content

Commit db761f4

Browse files
authored
Toolprovider: asdf integration (#1105)
1 parent a1eaeed commit db761f4

File tree

24 files changed

+2584
-11
lines changed

24 files changed

+2584
-11
lines changed

go.mod

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.0
55
toolchain go1.23.10 // https://github.com/golang/go/issues/72877
66

77
require (
8+
al.essio.dev/pkg/shellescape v1.6.0
89
github.com/bitrise-io/colorstring v0.0.0-20180614154802-a8cd70115192
910
github.com/bitrise-io/envman/v2 v2.5.3
1011
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.19
@@ -36,13 +37,17 @@ require (
3637
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
3738
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
3839
github.com/davecgh/go-spew v1.1.1 // indirect
40+
github.com/distribution/reference v0.5.0 // indirect
41+
github.com/docker/docker v28.0.0+incompatible
42+
github.com/docker/go-connections v0.5.0 // indirect
3943
github.com/docker/go-units v0.4.0 // indirect
4044
github.com/emirpasic/gods v1.18.1 // indirect
4145
github.com/felixge/httpsnoop v1.0.4 // indirect
4246
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
4347
github.com/go-git/go-billy/v5 v5.6.0 // indirect
4448
github.com/go-logr/logr v1.4.2 // indirect
4549
github.com/go-logr/stdr v1.2.2 // indirect
50+
github.com/gogo/protobuf v1.3.2 // indirect
4651
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
4752
github.com/google/uuid v1.6.0 // indirect
4853
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@@ -53,6 +58,7 @@ require (
5358
github.com/moby/term v0.5.0 // indirect
5459
github.com/morikuni/aec v1.0.0 // indirect
5560
github.com/opencontainers/go-digest v1.0.0 // indirect
61+
github.com/opencontainers/image-spec v1.0.2 // indirect
5662
github.com/pjbgf/sha1cd v0.3.0 // indirect
5763
github.com/pkg/errors v0.9.1 // indirect
5864
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -77,11 +83,3 @@ require (
7783
gopkg.in/warnings.v0 v0.1.2 // indirect
7884
gotest.tools/v3 v3.5.1 // indirect
7985
)
80-
81-
require (
82-
github.com/distribution/reference v0.5.0 // indirect
83-
github.com/docker/docker v28.0.0+incompatible
84-
github.com/docker/go-connections v0.5.0 // indirect
85-
github.com/gogo/protobuf v1.3.2 // indirect
86-
github.com/opencontainers/image-spec v1.0.2 // indirect
87-
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
2+
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
13
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
24
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
35
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@@ -80,6 +82,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
8082
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
8183
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
8284
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
85+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
86+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
8387
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
8488
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8589
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

toolprovider/asdf/activate.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package asdf
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/bitrise-io/bitrise/v2/toolprovider"
8+
)
9+
10+
func (a AsdfToolProvider) ActivateEnv(result toolprovider.ToolInstallResult) (toolprovider.EnvironmentActivation, error) {
11+
envKey := fmt.Sprint("ASDF_", strings.ToUpper(string(result.ToolName)), "_VERSION")
12+
return toolprovider.EnvironmentActivation{
13+
ContributedEnvVars: map[string]string{
14+
envKey: result.ConcreteVersion,
15+
},
16+
ContributedPaths: []string{}, // TODO: shims dir?
17+
}, nil
18+
}

toolprovider/asdf/asdf.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package asdf
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"slices"
7+
"strings"
8+
9+
"github.com/bitrise-io/bitrise/v2/log"
10+
"github.com/bitrise-io/bitrise/v2/toolprovider"
11+
"github.com/bitrise-io/bitrise/v2/toolprovider/asdf/execenv"
12+
)
13+
14+
type ProviderOptions struct {
15+
AsdfVersion string
16+
}
17+
18+
type AsdfToolProvider struct {
19+
ExecEnv execenv.ExecEnv
20+
}
21+
22+
func (a AsdfToolProvider) ID() string {
23+
return "asdf"
24+
}
25+
26+
func (a AsdfToolProvider) Bootstrap() error {
27+
// TODO:
28+
// Check if asdf is installed
29+
// Check if asdf version satisfies the supported version range
30+
31+
return nil
32+
}
33+
34+
func (a AsdfToolProvider) InstallTool(tool toolprovider.ToolRequest) (toolprovider.ToolInstallResult, error) {
35+
err := a.InstallPlugin(tool)
36+
if err != nil {
37+
return toolprovider.ToolInstallResult{}, fmt.Errorf("install tool plugin %s: %w", tool.ToolName, err)
38+
}
39+
40+
installedVersions, err := a.listInstalled(tool.ToolName)
41+
if err != nil {
42+
return toolprovider.ToolInstallResult{}, fmt.Errorf("list installed versions: %w", err)
43+
}
44+
45+
// Short-circuit for exact version match among installed versions.
46+
// Fetching released versions is a slow operation that we want to avoid.
47+
v := strings.TrimSpace(tool.UnparsedVersion)
48+
if tool.ResolutionStrategy == toolprovider.ResolutionStrategyStrict && slices.Contains(installedVersions, v) {
49+
return toolprovider.ToolInstallResult{
50+
ToolName: tool.ToolName,
51+
IsAlreadyInstalled: true,
52+
ConcreteVersion: v,
53+
}, nil
54+
}
55+
56+
releasedVersions, err := a.listReleased(tool.ToolName)
57+
if err != nil {
58+
return toolprovider.ToolInstallResult{}, fmt.Errorf("list released versions: %w", err)
59+
}
60+
61+
if len(releasedVersions) == 0 && len(installedVersions) == 0 {
62+
return toolprovider.ToolInstallResult{}, &ErrNoMatchingVersion{
63+
RequestedVersion: tool.UnparsedVersion,
64+
AvailableVersions: releasedVersions,
65+
}
66+
}
67+
68+
resolution, err := ResolveVersion(tool, releasedVersions, installedVersions)
69+
if err != nil {
70+
var nomatchErr *ErrNoMatchingVersion
71+
if errors.As(err, &nomatchErr) {
72+
log.Warn("No matching version found, updating asdf-%s plugin and retrying...", tool.ToolName)
73+
// Some asdf plugins hardcode the list of installable versions and need a new plugin release to support new versions.
74+
_, err = a.ExecEnv.RunAsdf("plugin", "update", string(tool.ToolName))
75+
if err != nil {
76+
return toolprovider.ToolInstallResult{}, fmt.Errorf("update plugin: %w", err)
77+
}
78+
releasedVersions, err = a.listReleased(tool.ToolName)
79+
if err != nil {
80+
return toolprovider.ToolInstallResult{}, fmt.Errorf("list released versions after plugin update: %w", err)
81+
}
82+
resolution, err = ResolveVersion(tool, releasedVersions, installedVersions)
83+
if err != nil {
84+
if errors.As(err, &nomatchErr) {
85+
errorDetails := toolprovider.ToolInstallError{
86+
ToolName: string(tool.ToolName),
87+
RequestedVersion: tool.UnparsedVersion,
88+
Cause: nomatchErr.Error(),
89+
Recommendation: fmt.Sprintf("You might want to use `%s:installed` or `%s:latest` to install the latest installed or latest released version of %s %s.", tool.UnparsedVersion, tool.UnparsedVersion, tool.ToolName, tool.UnparsedVersion),
90+
}
91+
return toolprovider.ToolInstallResult{}, errorDetails
92+
}
93+
return toolprovider.ToolInstallResult{}, fmt.Errorf("resolve version: %w", err)
94+
}
95+
}
96+
97+
return toolprovider.ToolInstallResult{}, fmt.Errorf("resolve version: %w", err)
98+
}
99+
100+
if resolution.IsInstalled {
101+
return toolprovider.ToolInstallResult{
102+
ToolName: tool.ToolName,
103+
IsAlreadyInstalled: true,
104+
ConcreteVersion: resolution.VersionString,
105+
}, nil
106+
} else {
107+
err = a.installToolVersion(tool.ToolName, resolution.VersionString)
108+
if err != nil {
109+
return toolprovider.ToolInstallResult{}, err
110+
}
111+
112+
return toolprovider.ToolInstallResult{
113+
ToolName: tool.ToolName,
114+
IsAlreadyInstalled: false,
115+
ConcreteVersion: resolution.VersionString,
116+
}, nil
117+
}
118+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package execenv
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"strings"
8+
9+
"al.essio.dev/pkg/shellescape"
10+
)
11+
12+
// ExecEnv contains everything needed to run asdf commands in a specific environment
13+
// that is installed and pre-configured.
14+
type ExecEnv struct {
15+
// Env vars that confiure asdf and are required for its operation.
16+
EnvVars map[string]string
17+
18+
// When set to true, env vars inherited from the parent process are cleared for maximum isolation.
19+
ClearInheritedEnvs bool
20+
21+
// ShellInit is a shell command that initializes asdf in the shell session.
22+
// This is required because classic asdf is written in bash and we can't assume that
23+
// its init command is sourced in .bashrc or similar (and we don't want to modify
24+
// anything system-wide).
25+
ShellInit string
26+
}
27+
28+
func (e *ExecEnv) RunAsdf(args ...string) (string, error) {
29+
cmdWithArgs := append([]string{"asdf"}, args...)
30+
return e.RunCommand(nil, cmdWithArgs...)
31+
}
32+
33+
func (e *ExecEnv) RunAsdfPlugin(args ...string) (string, error) {
34+
cmdWithArgs := append([]string{"asdf", "plugin"}, args...)
35+
return e.RunCommand(nil, cmdWithArgs...)
36+
}
37+
38+
func (e *ExecEnv) RunCommand(extraEnvs map[string]string, args ...string) (string, error) {
39+
innerShellCmd := []string{}
40+
if e.ShellInit != "" {
41+
innerShellCmd = append(innerShellCmd, e.ShellInit+" &&")
42+
}
43+
innerShellCmd = append(innerShellCmd, shellescape.QuoteCommand(args))
44+
45+
// We need to spawn a sub-shell because classic asdf is implemented in bash and
46+
// relies on shell features.
47+
bashArgs := []string{"-c", strings.Join(innerShellCmd, " ")}
48+
bashCmd := exec.Command("bash", bashArgs...)
49+
if !e.ClearInheritedEnvs {
50+
bashCmd.Env = os.Environ()
51+
}
52+
for k, v := range e.EnvVars {
53+
bashCmd.Env = append(bashCmd.Env, fmt.Sprintf("%s=%s", k, v))
54+
}
55+
for k, v := range extraEnvs {
56+
bashCmd.Env = append(bashCmd.Env, fmt.Sprintf("%s=%s", k, v))
57+
}
58+
59+
output, err := bashCmd.CombinedOutput()
60+
if err != nil {
61+
return "", fmt.Errorf("%s %v: %w\n\nOutput:\n%s", "bash", bashArgs, err, output)
62+
}
63+
64+
return string(output), nil
65+
}

toolprovider/asdf/install.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package asdf
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/bitrise-io/bitrise/v2/toolprovider"
7+
"github.com/bitrise-io/bitrise/v2/toolprovider/asdf/workarounds"
8+
)
9+
10+
func (a *AsdfToolProvider) installToolVersion(
11+
toolName toolprovider.ToolID,
12+
versionString string,
13+
) error {
14+
if toolName == "" || versionString == "" {
15+
return fmt.Errorf("toolName and versionString must not be empty")
16+
}
17+
18+
out, err := a.ExecEnv.RunAsdf("install", string(toolName), versionString)
19+
if err != nil {
20+
return toolprovider.ToolInstallError{
21+
ToolName: string(toolName),
22+
RequestedVersion: versionString,
23+
Cause: fmt.Sprintf("asdf install %s %s: %s", string(toolName), versionString, err),
24+
RawOutput: out,
25+
}
26+
}
27+
28+
if toolName == "nodejs" {
29+
err = workarounds.SetupCorepack(a.ExecEnv, versionString)
30+
if err != nil {
31+
return fmt.Errorf("setup corepack for %s %s: %w", string(toolName), versionString, err)
32+
}
33+
}
34+
return nil
35+
}

0 commit comments

Comments
 (0)