Skip to content

Commit 9b9e8b9

Browse files
committed
fix: restore symlink support for tool versions
Fixes a regression where symlink support for tool versions was inadvertently dropped. Fixes asdf-vm#1873
1 parent eb2eb6b commit 9b9e8b9

File tree

2 files changed

+95
-11
lines changed

2 files changed

+95
-11
lines changed

internal/installs/installs.go

+50-6
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
package installs
55

66
import (
7+
"errors"
78
"io/fs"
89
"os"
10+
"path"
911
"path/filepath"
1012

1113
"github.com/asdf-vm/asdf/internal/config"
@@ -19,19 +21,26 @@ func Installed(conf config.Config, plugin plugins.Plugin) (versions []string, er
1921
installDirectory := data.InstallDirectory(conf.DataDir, plugin.Name)
2022
files, err := os.ReadDir(installDirectory)
2123
if err != nil {
22-
if _, ok := err.(*fs.PathError); ok {
23-
return versions, nil
24+
if isFileNotFoundError(err) {
25+
return nil, nil
2426
}
2527

26-
return versions, err
28+
return nil, err
2729
}
2830

2931
for _, file := range files {
30-
if !file.IsDir() {
31-
continue
32+
IsDir, err := resolveIsDirectory(installDirectory, file)
33+
if err != nil {
34+
if isFileNotFoundError(err) {
35+
continue
36+
}
37+
38+
return nil, err
3239
}
3340

34-
versions = append(versions, file.Name())
41+
if IsDir {
42+
versions = append(versions, file.Name())
43+
}
3544
}
3645

3746
return versions, err
@@ -63,3 +72,38 @@ func IsInstalled(conf config.Config, plugin plugins.Plugin, version toolversions
6372
_, err := os.Stat(installDir)
6473
return !os.IsNotExist(err)
6574
}
75+
76+
// isDirectory checks if a given file is a directory or a symbolic link to a directory.
77+
func resolveIsDirectory(parent string, file os.DirEntry) (bool, error) {
78+
if file.IsDir() {
79+
return true, nil
80+
}
81+
82+
// Check if file is a symbolic link (which is a directory)
83+
if file.Type()&os.ModeSymlink == 0 {
84+
return false, nil
85+
}
86+
87+
// Resolve symbolic link to determine if it points to a directory
88+
linkTarget, err := os.Readlink(filepath.Join(parent, file.Name()))
89+
if err != nil {
90+
return false, err
91+
}
92+
93+
// If the link target is relative, resolve it to an absolute path
94+
if !path.IsAbs(linkTarget) {
95+
linkTarget = filepath.Join(parent, linkTarget)
96+
}
97+
98+
info, err := os.Stat(linkTarget)
99+
if err != nil {
100+
return false, err
101+
}
102+
103+
return info.IsDir(), nil
104+
}
105+
106+
func isFileNotFoundError(err error) bool {
107+
var ferr *fs.PathError
108+
return errors.As(err, &ferr) || errors.Is(err, fs.ErrNotExist)
109+
}

internal/installs/installs_test.go

+45-5
Original file line numberDiff line numberDiff line change
@@ -48,35 +48,67 @@ func TestInstallPath(t *testing.T) {
4848
}
4949

5050
func TestInstalled(t *testing.T) {
51-
conf, plugin := generateConfig(t)
52-
5351
t.Run("returns empty slice for newly installed plugin", func(t *testing.T) {
52+
conf, plugin := generateConfig(t)
5453
installedVersions, err := Installed(conf, plugin)
5554
assert.Nil(t, err)
5655
assert.Empty(t, installedVersions)
5756
})
5857

5958
t.Run("returns slice of all installed versions for a tool", func(t *testing.T) {
59+
conf, plugin := generateConfig(t)
6060
mockInstall(t, conf, plugin, "1.0.0")
6161

6262
installedVersions, err := Installed(conf, plugin)
6363
assert.Nil(t, err)
6464
assert.Equal(t, installedVersions, []string{"1.0.0"})
6565
})
66+
67+
t.Run("returns installed versions including symlinks", func(t *testing.T) {
68+
conf, plugin := generateConfig(t)
69+
mockInstall(t, conf, plugin, "1.0.0")
70+
aliasVersion(t, conf, plugin, "1.0.0", "latest")
71+
72+
installedVersions, err := Installed(conf, plugin)
73+
assert.Nil(t, err)
74+
assert.Equal(t, installedVersions, []string{"1.0.0", "latest"})
75+
})
6676
}
6777

6878
func TestIsInstalled(t *testing.T) {
69-
conf, plugin := generateConfig(t)
70-
installVersion(t, conf, plugin, "1.0.0")
71-
7279
t.Run("returns false when not installed", func(t *testing.T) {
80+
conf, plugin := generateConfig(t)
81+
installVersion(t, conf, plugin, "1.0.0")
82+
7383
version := toolversions.Version{Type: "version", Value: "4.0.0"}
7484
assert.False(t, IsInstalled(conf, plugin, version))
7585
})
86+
7687
t.Run("returns true when installed", func(t *testing.T) {
88+
conf, plugin := generateConfig(t)
89+
installVersion(t, conf, plugin, "1.0.0")
90+
7791
version := toolversions.Version{Type: "version", Value: "1.0.0"}
7892
assert.True(t, IsInstalled(conf, plugin, version))
7993
})
94+
95+
t.Run("returns true when aliased using symlinks", func(t *testing.T) {
96+
conf, plugin := generateConfig(t)
97+
installVersion(t, conf, plugin, "1.0.0")
98+
aliasVersion(t, conf, plugin, "1.0.0", "latest")
99+
100+
version := toolversions.Version{Type: "alias", Value: "latest"}
101+
assert.True(t, IsInstalled(conf, plugin, version))
102+
})
103+
104+
t.Run("returns false when symlink broken", func(t *testing.T) {
105+
conf, plugin := generateConfig(t)
106+
installVersion(t, conf, plugin, "1.0.0")
107+
aliasVersion(t, conf, plugin, "2.0.0", "latest")
108+
109+
version := toolversions.Version{Type: "alias", Value: "latest"}
110+
assert.False(t, IsInstalled(conf, plugin, version))
111+
})
80112
}
81113

82114
// helper functions
@@ -101,6 +133,14 @@ func mockInstall(t *testing.T, conf config.Config, plugin plugins.Plugin, versio
101133
assert.Nil(t, err)
102134
}
103135

136+
func aliasVersion(t *testing.T, conf config.Config, plugin plugins.Plugin, versionStr, aliasStr string) {
137+
t.Helper()
138+
originPath := InstallPath(conf, plugin, toolversions.Version{Type: "version", Value: versionStr})
139+
aliasPath := InstallPath(conf, plugin, toolversions.Version{Type: "version", Value: aliasStr})
140+
err := os.Symlink(originPath, aliasPath)
141+
assert.Nil(t, err)
142+
}
143+
104144
func installVersion(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) {
105145
t.Helper()
106146
err := installtest.InstallOneVersion(conf, plugin, "version", version)

0 commit comments

Comments
 (0)