From a8733359955a2d1186c5e08f1b687a57e53be5eb Mon Sep 17 00:00:00 2001 From: Aleksey Reuk Date: Mon, 24 Nov 2025 14:56:01 +0400 Subject: [PATCH 1/4] reorganize excluded Go environment variables list - Excluded from list `GOCACHE`, `GOCACHEPROG`. - Moved commented-out environment variables to the bottom of the exclusion list for better readability. - Adjusted formatting for consistency. --- .../workdir/runtimes/runtime-go/private.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/internal/workdir/runtimes/runtime-go/private.go b/internal/workdir/runtimes/runtime-go/private.go index 3986f6c..3d0492a 100644 --- a/internal/workdir/runtimes/runtime-go/private.go +++ b/internal/workdir/runtimes/runtime-go/private.go @@ -268,12 +268,9 @@ func envAllOverride(envs [][2]string) []string { "GOARM64", "GOAUTH", "GOBIN", - "GOCACHE", - "GOCACHEPROG", "GODEBUG", "GOENV", "GOEXE", - //"GOEXPERIMENT", "GOFIPS140", "GOFLAGS", "GOGCCFLAGS", @@ -282,15 +279,9 @@ func envAllOverride(envs [][2]string) []string { "GOINSECURE", "GOMOD", "GOMODCACHE", - //"GONOPROXY", - //"GONOSUMDB", "GOOS", "GOPATH", - //"GOPRIVATE", - //"GOPROXY", "GOROOT", - //"GOSUMDB", - //"GOTELEMETRY", "GOTELEMETRYDIR", "GOTMPDIR", "GOTOOLCHAIN", @@ -299,6 +290,16 @@ func envAllOverride(envs [][2]string) []string { "GOVERSION", "GOWORK", "PKG_CONFIG", + + // "GOCACHE", + // "GOCACHEPROG", + // "GOEXPERIMENT", + // "GONOPROXY", + // "GONOSUMDB", + // "GOPRIVATE", + // "GOPROXY", + // "GOSUMDB", + // "GOTELEMETRY", } for _, env := range excluded { From 26f5f6725d3364a4a2dfdc22c8f10fd2eb8898da Mon Sep 17 00:00:00 2001 From: Aleksey Reuk Date: Mon, 24 Nov 2025 16:06:34 +0400 Subject: [PATCH 2/4] refactor(runtime-go): introduce `Runtime`-based parsing and module fetching - Replaced global `parse` and `fetchModule` functions with `Runtime` methods. - Added `goEnv` helper to manage and override Go environment variables. - Introduced `goCacheDir` for versioned Go cache management. - Updated tests to use the new `Runtime` helpers. --- .../workdir/runtimes/runtime-go/private.go | 35 ++++++++-------- .../runtimes/runtime-go/private_test.go | 30 +++++++++---- .../workdir/runtimes/runtime-go/runtime_go.go | 42 +++++++++++++++---- internal/workdir/workdir_test.go | 3 ++ 4 files changed, 76 insertions(+), 34 deletions(-) diff --git a/internal/workdir/runtimes/runtime-go/private.go b/internal/workdir/runtimes/runtime-go/private.go index 3d0492a..87e54e3 100644 --- a/internal/workdir/runtimes/runtime-go/private.go +++ b/internal/workdir/runtimes/runtime-go/private.go @@ -14,7 +14,6 @@ import ( "regexp" "strings" - "github.com/kazhuravlev/toolset/internal/fsh" "github.com/kazhuravlev/toolset/internal/prog" "github.com/spf13/afero" "golang.org/x/mod/modfile" @@ -31,7 +30,7 @@ type moduleInfo struct { } // parse will parse source string and try to extract all details about mentioned golang program. -func parse(ctx context.Context, goBin, str string) (*moduleInfo, error) { +func (r *Runtime) parse(ctx context.Context, str string) (*moduleInfo, error) { var mod, version, program string { @@ -64,8 +63,8 @@ func parse(ctx context.Context, goBin, str string) (*moduleInfo, error) { buf := bytes.NewBuffer(nil) { - cmd := exec.CommandContext(ctx, goBin, "env", "GOPRIVATE") - cmd.Env = envAllOverride([][2]string{{"GOTOOLCHAIN", "local"}}) + cmd := exec.CommandContext(ctx, r.goBin, "env", "GOPRIVATE") + cmd.Env = r.goEnv() cmd.Stdout = buf cmd.Stderr = io.Discard if err := cmd.Run(); err != nil { @@ -97,19 +96,19 @@ type fetchedMod struct { // @ => @latest // @latest => @vX.X.X // @vX.X.X => @vX.X.X -func fetchModule(ctx context.Context, fs fsh.FS, goBin, link string) (*moduleInfo, error) { - mod, err := parse(ctx, goBin, link) +func (r *Runtime) fetchModule(ctx context.Context, link string) (*moduleInfo, error) { + mod, err := r.parse(ctx, link) if err != nil { return nil, fmt.Errorf("parse module (%s) string: %w", link, err) } if mod.IsPrivate { - privateMod, err := fetchPrivate(ctx, fs, goBin, *mod) + privateMod, err := r.fetchPrivate(ctx, *mod) if err != nil { return nil, fmt.Errorf("fetch private module: %w", err) } - return parse(ctx, goBin, mod.Mod.Name()+at+privateMod.Mod.Version()) + return r.parse(ctx, mod.Mod.Name()+at+privateMod.Mod.Version()) } link = mod.Mod.Name() @@ -153,7 +152,7 @@ func fetchModule(ctx context.Context, fs fsh.FS, goBin, link string) (*moduleInf return nil, fmt.Errorf("unable to decode module: %w", err) } - mod2, err := parse(ctx, goBin, mod.Mod.Name()+at+fMod.Version) + mod2, err := r.parse(ctx, mod.Mod.Name()+at+fMod.Version) if err != nil { return nil, fmt.Errorf("parse fetched module: %w", err) } @@ -170,16 +169,16 @@ func fetchModule(ctx context.Context, fs fsh.FS, goBin, link string) (*moduleInf // - Add dependency // - Get dep info // - Remove temp dir -func fetchPrivate(ctx context.Context, fSys fsh.FS, goBin string, mod moduleInfo) (*moduleInfo, error) { - tmpDir, err := afero.TempDir(fSys, "", "gomodtemp") +func (r *Runtime) fetchPrivate(ctx context.Context, mod moduleInfo) (*moduleInfo, error) { + tmpDir, err := afero.TempDir(r.fs, "", "gomodtemp") if err != nil { return nil, fmt.Errorf("failed to create temp directory: %v", err) } - defer fSys.RemoveAll(tmpDir) //nolint:errcheck + defer r.fs.RemoveAll(tmpDir) //nolint:errcheck { - cmd := exec.CommandContext(ctx, goBin, "mod", "init", "sample") - cmd.Env = envAllOverride([][2]string{{"GOTOOLCHAIN", "local"}}) + cmd := exec.CommandContext(ctx, r.goBin, "mod", "init", "sample") + cmd.Env = r.goEnv() cmd.Dir = tmpDir cmd.Stdout = io.Discard cmd.Stderr = io.Discard @@ -189,8 +188,8 @@ func fetchPrivate(ctx context.Context, fSys fsh.FS, goBin string, mod moduleInfo } { - cmd := exec.CommandContext(ctx, goBin, "get", mod.Mod.S()) - cmd.Env = envAllOverride([][2]string{{"GOTOOLCHAIN", "local"}}) + cmd := exec.CommandContext(ctx, r.goBin, "get", mod.Mod.S()) + cmd.Env = r.goEnv() cmd.Dir = tmpDir cmd.Stdout = io.Discard cmd.Stderr = io.Discard @@ -200,7 +199,7 @@ func fetchPrivate(ctx context.Context, fSys fsh.FS, goBin string, mod moduleInfo } goModFilename := filepath.Join(tmpDir, "go.mod") - bb, err := afero.ReadFile(fSys, goModFilename) + bb, err := afero.ReadFile(r.fs, goModFilename) if err != nil { return nil, fmt.Errorf("failed to read go.mod: %w", err) } @@ -212,7 +211,7 @@ func fetchPrivate(ctx context.Context, fSys fsh.FS, goBin string, mod moduleInfo for _, require := range modFile.Require { if strings.HasPrefix(mod.Mod.Name(), require.Mod.Path) { - return parse(ctx, goBin, mod.Mod.Name()+at+require.Mod.Version) + return r.parse(ctx, mod.Mod.Name()+at+require.Mod.Version) } } diff --git a/internal/workdir/runtimes/runtime-go/private_test.go b/internal/workdir/runtimes/runtime-go/private_test.go index 26d980e..3885639 100644 --- a/internal/workdir/runtimes/runtime-go/private_test.go +++ b/internal/workdir/runtimes/runtime-go/private_test.go @@ -13,13 +13,12 @@ import ( ) func Test_parse(t *testing.T) { - goBin, err := exec.LookPath("go") - require.NoError(t, err, "install go") + rt := newTestRuntime(t) f := func(name, in string, exp moduleInfo) { t.Run(name, func(t *testing.T) { ctx := context.Background() - mod, err := parse(ctx, goBin, in) + mod, err := rt.parse(ctx, in) require.NoError(t, err) require.NotEmpty(t, mod) require.Equal(t, exp, *mod) @@ -49,14 +48,12 @@ func Test_parse(t *testing.T) { } func Test_fetchModule(t *testing.T) { - fs := fsh.NewRealFS() - goBin, err := exec.LookPath("go") - require.NoError(t, err, "install go") + rt := newTestRuntime(t) f := func(name, link string, exp moduleInfo) { t.Run(name, func(t *testing.T) { ctx := context.Background() - mod, err := fetchModule(ctx, fs, goBin, link) + mod, err := rt.fetchModule(ctx, link) require.NoError(t, err) require.NotEmpty(t, mod) require.Equal(t, exp, *mod) @@ -85,3 +82,22 @@ func Test_getGoVersion(t *testing.T) { require.NotEmpty(t, goVersion) require.True(t, strings.HasPrefix(goVersion, "1.")) // NOTE(zhuravlev): should looks like 1.23.4 } + +func newTestRuntime(t *testing.T) *Runtime { + t.Helper() + + fs := fsh.NewRealFS() + goBin, err := exec.LookPath("go") + require.NoError(t, err, "install go") + + ctx := context.Background() + goVersion, err := getGoVersion(ctx, goBin) + require.NoError(t, err) + + binDir := t.TempDir() + + rt, err := New(fs, binDir, goBin, goVersion) + require.NoError(t, err) + + return rt +} diff --git a/internal/workdir/runtimes/runtime-go/runtime_go.go b/internal/workdir/runtimes/runtime-go/runtime_go.go index 5c88fa7..8793d70 100644 --- a/internal/workdir/runtimes/runtime-go/runtime_go.go +++ b/internal/workdir/runtimes/runtime-go/runtime_go.go @@ -28,15 +28,22 @@ type Runtime struct { isGlobal bool goVersion string // ex: 1.23 binToolDir string + goCacheDir string } -func New(fs fsh.FS, binToolDir, goBin, goVer string) *Runtime { +func New(fs fsh.FS, binToolDir, goBin, goVer string) (*Runtime, error) { + goCacheDir := filepath.Join(binToolDir, "go", goVer) + if err := fs.MkdirAll(goCacheDir, 0o755); err != nil { + return nil, fmt.Errorf("create go cache dir (%s): %w", goCacheDir, err) + } + return &Runtime{ fs: fs, goBin: goBin, goVersion: goVer, binToolDir: binToolDir, - } + goCacheDir: goCacheDir, + }, nil } // Parse will parse string to normal version. @@ -48,7 +55,7 @@ func (r *Runtime) Parse(ctx context.Context, str string) (string, error) { return "", errors.New("program name not provided") } - goModule, err := fetchModule(ctx, r.fs, r.goBin, str) + goModule, err := r.fetchModule(ctx, str) if err != nil { return "", fmt.Errorf("get go module version: %w", err) } @@ -57,7 +64,7 @@ func (r *Runtime) Parse(ctx context.Context, str string) (string, error) { } func (r *Runtime) GetModule(ctx context.Context, module string) (*structs.ModuleInfo, error) { - mod, err := parse(ctx, r.goBin, module) + mod, err := r.parse(ctx, module) if err != nil { return nil, fmt.Errorf("parse module (%s): %w", module, err) } @@ -86,7 +93,7 @@ func (r *Runtime) Install(ctx context.Context, program string) error { } cmd := exec.CommandContext(ctx, r.goBin, "install", program) - cmd.Env = envAllOverride([][2]string{{"GOTOOLCHAIN", "local"}, {"GOBIN", mod.BinDir}}) + cmd.Env = r.goEnv([2]string{"GOBIN", mod.BinDir}) var stdout bytes.Buffer cmd.Stderr = &stdout @@ -127,13 +134,13 @@ func (r *Runtime) Run(ctx context.Context, program string, args ...string) error } func (r *Runtime) GetLatest(ctx context.Context, moduleReq string) (string, bool, error) { - mod, err := parse(ctx, r.goBin, moduleReq) + mod, err := r.parse(ctx, moduleReq) if err != nil { return "", false, fmt.Errorf("parse module (%s): %w", moduleReq, err) } latestStr := mod.Mod.AsLatest().S() - latestMod, err := fetchModule(ctx, r.fs, r.goBin, latestStr) + latestMod, err := r.fetchModule(ctx, latestStr) if err != nil { return "", false, fmt.Errorf("get go module: %w", err) } @@ -162,6 +169,15 @@ func (r *Runtime) Remove(ctx context.Context, tool structs.Tool) error { return nil } +func (r *Runtime) goEnv(overrides ...[2]string) []string { + base := make([][2]string, 0, len(overrides)+2) + base = append(base, [2]string{"GOCACHE", r.goCacheDir}) + base = append(base, [2]string{"GOTOOLCHAIN", "local"}) + base = append(base, overrides...) + + return envAllOverride(base) +} + func (r *Runtime) Version() string { if r.isGlobal { return "go" @@ -190,7 +206,10 @@ func Discover(ctx context.Context, fSys fsh.FS, binToolDir string) ([]*Runtime, return res, fmt.Errorf("get go version: %w", err) } - rt := New(fSys, binToolDir, lp, ver) + rt, err := New(fSys, binToolDir, lp, ver) + if err != nil { + return res, fmt.Errorf("init go runtime (%s): %w", ver, err) + } rt.isGlobal = true res = append(res, rt) } @@ -224,7 +243,12 @@ func Discover(ctx context.Context, fSys fsh.FS, binToolDir string) ([]*Runtime, return res, fmt.Errorf("get go version for (%s): %w", goBin, err) } - res = append(res, New(fSys, binToolDir, goBin, goVer)) + rt, err := New(fSys, binToolDir, goBin, goVer) + if err != nil { + return res, fmt.Errorf("init go runtime (%s): %w", goVer, err) + } + + res = append(res, rt) } } diff --git a/internal/workdir/workdir_test.go b/internal/workdir/workdir_test.go index 132e7ed..0318dac 100644 --- a/internal/workdir/workdir_test.go +++ b/internal/workdir/workdir_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "runtime" + "strings" "testing" "github.com/kazhuravlev/toolset/internal/fsh" @@ -89,6 +90,8 @@ func TestCustomDir(t *testing.T) { require.Equal(t, []string{ "/", "/cache", + "/cache/go", + "/cache/go/" + strings.TrimPrefix(runtime.Version(), "go"), // FIXME: maybe it's not a good way? "/cache/stats.json", "/project", "/project/.some-local-dir", From a61e56560600dbb75531b6df84ee68c1e0e16d62 Mon Sep 17 00:00:00 2001 From: Aleksey Reuk Date: Mon, 24 Nov 2025 16:57:09 +0400 Subject: [PATCH 3/4] refactor(runtime-go): use `optional.Val` for `goCacheDir` configuration - Replaced `goCacheDir` string with `optional.Val` to handle optional cache directory paths. - Updated `New` constructor to accept an `optional.Val` for `goCacheDir`. - Adjusted `goEnv` logic to include `GOCACHE` only if `goCacheDir` is set. - Simplified tests with `optional.Empty` and `optional.New` usage. --- .../runtimes/runtime-go/private_test.go | 3 +- .../workdir/runtimes/runtime-go/runtime_go.go | 28 +++++++++++++------ internal/workdir/workdir_test.go | 3 -- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/internal/workdir/runtimes/runtime-go/private_test.go b/internal/workdir/runtimes/runtime-go/private_test.go index 3885639..ecbaf7e 100644 --- a/internal/workdir/runtimes/runtime-go/private_test.go +++ b/internal/workdir/runtimes/runtime-go/private_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/kazhuravlev/optional" "github.com/kazhuravlev/toolset/internal/fsh" "github.com/kazhuravlev/toolset/internal/prog" @@ -96,7 +97,7 @@ func newTestRuntime(t *testing.T) *Runtime { binDir := t.TempDir() - rt, err := New(fs, binDir, goBin, goVersion) + rt, err := New(fs, binDir, goBin, goVersion, optional.Empty[string]()) require.NoError(t, err) return rt diff --git a/internal/workdir/runtimes/runtime-go/runtime_go.go b/internal/workdir/runtimes/runtime-go/runtime_go.go index 8793d70..0291ba9 100644 --- a/internal/workdir/runtimes/runtime-go/runtime_go.go +++ b/internal/workdir/runtimes/runtime-go/runtime_go.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/kazhuravlev/optional" "github.com/spf13/afero" "github.com/kazhuravlev/toolset/internal/fsh" @@ -28,13 +29,15 @@ type Runtime struct { isGlobal bool goVersion string // ex: 1.23 binToolDir string - goCacheDir string + goCacheDir optional.Val[string] } -func New(fs fsh.FS, binToolDir, goBin, goVer string) (*Runtime, error) { - goCacheDir := filepath.Join(binToolDir, "go", goVer) - if err := fs.MkdirAll(goCacheDir, 0o755); err != nil { - return nil, fmt.Errorf("create go cache dir (%s): %w", goCacheDir, err) +func New(fs fsh.FS, binToolDir, goBin, goVer string, goCache optional.Val[string]) (*Runtime, error) { + goCacheVal, ok := goCache.Get() + if ok { + if err := fs.MkdirAll(goCacheVal, 0o755); err != nil { + return nil, fmt.Errorf("create go cache dir (%s): %w", goCacheVal, err) + } } return &Runtime{ @@ -42,7 +45,7 @@ func New(fs fsh.FS, binToolDir, goBin, goVer string) (*Runtime, error) { goBin: goBin, goVersion: goVer, binToolDir: binToolDir, - goCacheDir: goCacheDir, + goCacheDir: goCache, }, nil } @@ -171,7 +174,11 @@ func (r *Runtime) Remove(ctx context.Context, tool structs.Tool) error { func (r *Runtime) goEnv(overrides ...[2]string) []string { base := make([][2]string, 0, len(overrides)+2) - base = append(base, [2]string{"GOCACHE", r.goCacheDir}) + + if val, ok := r.goCacheDir.Get(); ok { + base = append(base, [2]string{"GOCACHE", val}) + } + base = append(base, [2]string{"GOTOOLCHAIN", "local"}) base = append(base, overrides...) @@ -206,10 +213,11 @@ func Discover(ctx context.Context, fSys fsh.FS, binToolDir string) ([]*Runtime, return res, fmt.Errorf("get go version: %w", err) } - rt, err := New(fSys, binToolDir, lp, ver) + rt, err := New(fSys, binToolDir, lp, ver, optional.Empty[string]()) if err != nil { return res, fmt.Errorf("init go runtime (%s): %w", ver, err) } + rt.isGlobal = true res = append(res, rt) } @@ -243,7 +251,9 @@ func Discover(ctx context.Context, fSys fsh.FS, binToolDir string) ([]*Runtime, return res, fmt.Errorf("get go version for (%s): %w", goBin, err) } - rt, err := New(fSys, binToolDir, goBin, goVer) + goCache := filepath.Join(binToolDir, e.Name(), "gocache") + + rt, err := New(fSys, binToolDir, goBin, goVer, optional.New(goCache)) if err != nil { return res, fmt.Errorf("init go runtime (%s): %w", goVer, err) } diff --git a/internal/workdir/workdir_test.go b/internal/workdir/workdir_test.go index 0318dac..132e7ed 100644 --- a/internal/workdir/workdir_test.go +++ b/internal/workdir/workdir_test.go @@ -4,7 +4,6 @@ import ( "context" "os" "runtime" - "strings" "testing" "github.com/kazhuravlev/toolset/internal/fsh" @@ -90,8 +89,6 @@ func TestCustomDir(t *testing.T) { require.Equal(t, []string{ "/", "/cache", - "/cache/go", - "/cache/go/" + strings.TrimPrefix(runtime.Version(), "go"), // FIXME: maybe it's not a good way? "/cache/stats.json", "/project", "/project/.some-local-dir", From 8b06c83f9ce8fe78d8e6454cb618fb9e96219c7b Mon Sep 17 00:00:00 2001 From: Aleksey Reuk Date: Mon, 24 Nov 2025 17:17:20 +0400 Subject: [PATCH 4/4] refactor(runtime-go): simplify `goCache` directory creation logic - Consolidated `goCache.Get` and directory creation steps into a single conditional. - Replaced explicit permissions with `fsh.DefaultDirPerm` for consistency. --- internal/workdir/runtimes/runtime-go/runtime_go.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/workdir/runtimes/runtime-go/runtime_go.go b/internal/workdir/runtimes/runtime-go/runtime_go.go index 0291ba9..08515f1 100644 --- a/internal/workdir/runtimes/runtime-go/runtime_go.go +++ b/internal/workdir/runtimes/runtime-go/runtime_go.go @@ -33,9 +33,8 @@ type Runtime struct { } func New(fs fsh.FS, binToolDir, goBin, goVer string, goCache optional.Val[string]) (*Runtime, error) { - goCacheVal, ok := goCache.Get() - if ok { - if err := fs.MkdirAll(goCacheVal, 0o755); err != nil { + if goCacheVal, ok := goCache.Get(); ok { + if err := fs.MkdirAll(goCacheVal, fsh.DefaultDirPerm); err != nil { return nil, fmt.Errorf("create go cache dir (%s): %w", goCacheVal, err) } }