Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: go.mod

- name: Build
run: go build ./cmd/gobrew
Expand Down
5 changes: 4 additions & 1 deletion gobrew.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,10 @@ func (gb *GoBrew) Uninstall(version string) {
color.Errorf("[Error] Version: %s you are trying to remove is your current version. Please use a different version first before uninstalling the current version\n", version)
os.Exit(1)
}
gb.cleanVersionDir(version)
if err := gb.cleanVersionDir(version); err != nil {
color.Errorf("==> [Error] Failed to uninstall version: %s: %s\n", version, err)
return
}
color.Successf("==> [Success] Version: %s uninstalled\n", version)
}

Expand Down
25 changes: 25 additions & 0 deletions gobrew_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,31 @@ func TestPrune(t *testing.T) {
t.Log("test finished")
}

// TestPruneReadOnlyDir reproduces issue #226: a read-only directory inside an
// installed version (e.g. a Go module-cache style tree with mode 0555) made
// os.RemoveAll fail with "permission denied", so prune reported success while
// the version stayed on disk.
func TestPruneReadOnlyDir(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.FileServer(http.Dir("testdata")))
defer ts.Close()
gb := setupGobrew(t, ts)
gb.Install("1.20")
gb.Install("1.19")
gb.Use("1.19")

// Inject a read-only directory holding a read-only file into version 1.20.
roDir := filepath.Join(gb.getVersionDir("1.20"), "go", "readonly")
assert.NoError(t, os.MkdirAll(roDir, 0o755))
assert.NoError(t, os.WriteFile(filepath.Join(roDir, "f"), []byte("x"), 0o600))
assert.NoError(t, os.Chmod(roDir, 0o555))

gb.Prune()
assert.Equal(t, false, gb.existsVersion("1.20"))
assert.Equal(t, true, gb.existsVersion("1.19"))
t.Log("test finished")
}

func TestGoBrew_CurrentVersion(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.FileServer(http.Dir("testdata")))
Expand Down
6 changes: 3 additions & 3 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ func (gb *GoBrew) existsVersion(version string) bool {
return err == nil
}

func (gb *GoBrew) cleanVersionDir(version string) {
_ = os.RemoveAll(gb.getVersionDir(version))
func (gb *GoBrew) cleanVersionDir(version string) error {
return utils.RemoveAll(gb.getVersionDir(version))
}

func (gb *GoBrew) cleanDownloadsDir() {
Expand Down Expand Up @@ -263,7 +263,7 @@ func (gb *GoBrew) downloadAndExtract(version string) {
err = gb.extract(srcTar, dstDir)
if err != nil {
// clean up dir
gb.cleanVersionDir(version)
_ = gb.cleanVersionDir(version)
color.Errorln("==> [Info] Extract failed:", err)
os.Exit(1)
}
Expand Down
23 changes: 23 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package utils
import (
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"

"github.com/gookit/color"
"github.com/schollz/progressbar/v3"
Expand Down Expand Up @@ -52,6 +54,27 @@ func DownloadWithProgress(url string, tarName string, destFolder string) (err er
return nil
}

// RemoveAll removes path and all its children, making any read-only
// directories writable first. A plain os.RemoveAll fails with
// "permission denied" when the tree contains read-only directories
// (e.g. Go module-cache style trees with mode 0555), leaving the
// directory in place. Making directories writable before removal avoids
// silently failing to delete an installed Go version.
func RemoveAll(path string) error {
_ = filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
// WalkDir does not follow symlinks and d.IsDir() is false for
// symlinked dirs, so p stays within gobrew's version tree.
if d.IsDir() {
_ = os.Chmod(p, 0o755) //nolint:gosec // path is confined to the walked tree
}
return nil
})
return os.RemoveAll(path)
}

func CheckError(err error, format string) {
if err != nil {
color.Errorf(format+": %s", err)
Expand Down
Loading