Skip to content
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func LoadConfig() (Config, error) {
return config, err
}

homeDir, err := homedir.Dir()
homeDir, err := os.UserHomeDir()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are replacing the library - it is being used here as well
https://github.com/asdf-vm/asdf/pull/1970/files#diff-54c7c1af5fa8d5db4dc49f0e8e80e93ba2b1183ba4d5c9e2e5729e6deae6a3cdL207

There is a related issue
#1923

Copy link
Contributor Author

@andrecloutier andrecloutier Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it here since it's a fairly safe change. Nothing else appears to be reading from config.Home prior to this change and the current implementation of install was already relying on os.UserHomeDir(). I think removing the other usages merit its own PR. Seems trivial but I'm not confident enough w/ this code base yet to say there aren't edge cases to consider.

if err != nil {
return config, err
}
Expand Down
128 changes: 96 additions & 32 deletions internal/resolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package resolve

import (
"fmt"
"iter"
"os"
"path"
"strings"
Expand All @@ -16,69 +17,132 @@ import (

// ToolVersions represents a tool along with versions specified for it
type ToolVersions struct {
Name string
Versions []string
Directory string
Source string
}

// AllVersions takes a set of plugins and a directory and resolves all tools to one or more
// versions. This includes tools without a corresponding plugin.
func AllVersions(conf config.Config, plugins []plugins.Plugin, directory string) (versions []ToolVersions, err error) {
resolvedToolVersions := map[string]bool{}
var finalVersions []ToolVersions

// First: Resolve using environment values
for _, plugin := range plugins {
version, envVariableName, found := findVersionsInEnv(plugin.Name)
if found {
resolvedToolVersions[plugin.Name] = true
finalVersions = append(finalVersions, ToolVersions{Name: plugin.Name, Versions: version, Source: envVariableName})
}
}

// Iterate from the current towards the root directory, ending with the user's home.
for iterDir := range iterDirectories(conf, directory) {
// Second: Resolve using the tool versions file
filepath := path.Join(iterDir, conf.DefaultToolVersionsFilename)
if _, err = os.Stat(filepath); err == nil {
allVersions, err := toolversions.GetAllToolsAndVersions(filepath)
if err != nil {
return versions, err
}
for _, version := range allVersions {
if _, isPluginResolved := resolvedToolVersions[version.Name]; !isPluginResolved {
resolvedToolVersions[version.Name] = true
finalVersions = append(finalVersions, ToolVersions{Name: version.Name, Versions: version.Versions, Source: conf.DefaultToolVersionsFilename, Directory: iterDir})
}
}
}

// Third: Resolve using legacy settings
for _, plugin := range plugins {
if _, isPluginResolved := resolvedToolVersions[plugin.Name]; !isPluginResolved {
version, found, err := findLegacyVersionsInDir(conf, plugin, iterDir)
if err != nil {
return versions, err
}
if found {
resolvedToolVersions[plugin.Name] = true
finalVersions = append(finalVersions, version)
}
}
}
}
return finalVersions, nil
}

// Version takes a plugin and a directory and resolves the tool to one or more
// versions.
// versions. Only returns results for the provided plugin.
func Version(conf config.Config, plugin plugins.Plugin, directory string) (versions ToolVersions, found bool, err error) {
version, envVariableName, found := findVersionsInEnv(plugin.Name)
if found {
return ToolVersions{Versions: version, Source: envVariableName}, true, nil
return ToolVersions{Name: plugin.Name, Versions: version, Source: envVariableName}, true, nil
}

for !found {
versions, found, err = findVersionsInDir(conf, plugin, directory)
if err != nil {
return versions, false, err
for iterDir := range iterDirectories(conf, directory) {
versions, found, err = findVersionsInDir(conf, plugin, iterDir)
if found || err != nil {
return versions, found, err
}
}
return versions, found, err
}

nextDir := path.Dir(directory)
// If current dir and next dir are the same it means we've reached `/` and
// have no more parent directories to search.
if nextDir == directory {
// If no version found, try current users home directory. I'd like to
// eventually remove this feature.
homeDir, osErr := os.UserHomeDir()
if osErr != nil {
func iterDirectories(conf config.Config, directory string) iter.Seq[string] {
return func(yield func(string) bool) {
if !yield(directory) {
return
}
iterDir := directory
for {
nextDir := path.Dir(iterDir)
// If current dir and next dir are the same it means we've reached `/` and
// have no more parent directories to search.
if nextDir == iterDir {
break
}

versions, found, err = findVersionsInDir(conf, plugin, homeDir)
break
if !yield(iterDir) {
return
}
iterDir = nextDir
}
// If no version found, try current users home directory. I'd like to
// eventually remove this feature.
homeDir := conf.Home
if homeDir != "" {
if !yield(homeDir) {
return
}
}
directory = nextDir
}

return versions, found, err
}

func findVersionsInDir(conf config.Config, plugin plugins.Plugin, directory string) (versions ToolVersions, found bool, err error) {
filepath := path.Join(directory, conf.DefaultToolVersionsFilename)

if _, err = os.Stat(filepath); err == nil {
versions, found, err := toolversions.FindToolVersions(filepath, plugin.Name)
if found || err != nil {
return ToolVersions{Versions: versions, Source: conf.DefaultToolVersionsFilename, Directory: directory}, found, err
foundVersions, found, err := toolversions.FindToolVersions(filepath, plugin.Name)
if err != nil {
return versions, found, err
}
if found {
return ToolVersions{Name: plugin.Name, Versions: foundVersions, Source: conf.DefaultToolVersionsFilename, Directory: directory}, found, err
}
}

return findLegacyVersionsInDir(conf, plugin, directory)
}

func findLegacyVersionsInDir(conf config.Config, plugin plugins.Plugin, directory string) (versions ToolVersions, found bool, err error) {
legacyFiles, err := conf.LegacyVersionFile()
if err != nil {
return versions, found, err
}

if legacyFiles {
versions, found, err := findVersionsInLegacyFile(plugin, directory)

if found || err != nil {
return versions, found, err
}
return findVersionsInLegacyFile(plugin, directory)
}

return versions, found, nil
return versions, false, nil
}

// findVersionsInEnv returns the version from the environment if present
Expand Down Expand Up @@ -111,7 +175,7 @@ func findVersionsInLegacyFile(plugin plugins.Plugin, directory string) (versions
if len(versionsSlice) == 0 || (len(versionsSlice) == 1 && versionsSlice[0] == "") {
return versions, false, nil
}
return ToolVersions{Versions: versionsSlice, Source: filename, Directory: directory}, err == nil, err
return ToolVersions{Name: plugin.Name, Versions: versionsSlice, Source: filename, Directory: directory}, err == nil, err
}
}

Expand Down
102 changes: 101 additions & 1 deletion internal/resolve/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const testPluginName = "test-plugin"
func TestVersion(t *testing.T) {
testDataDir := t.TempDir()
currentDir := t.TempDir()
conf := config.Config{DataDir: testDataDir, DefaultToolVersionsFilename: ".tool-versions", ConfigFile: "testdata/asdfrc"}
homeDir := t.TempDir()
conf := config.Config{Home: homeDir, DataDir: testDataDir, DefaultToolVersionsFilename: ".tool-versions", ConfigFile: "testdata/asdfrc"}
_, err := repotest.InstallPlugin("dummy_plugin", conf.DataDir, testPluginName)
assert.Nil(t, err)
plugin := plugins.New(conf, testPluginName)
Expand Down Expand Up @@ -72,6 +73,105 @@ func TestVersion(t *testing.T) {
})
}

func TestAllVersions(t *testing.T) {
testDataDir := t.TempDir()
currentDir := t.TempDir()
homeDir := t.TempDir()
conf := config.Config{Home: homeDir, DataDir: testDataDir, DefaultToolVersionsFilename: ".tool-versions", ConfigFile: "testdata/asdfrc"}
_, err := repotest.InstallPlugin("dummy_plugin", conf.DataDir, testPluginName)
assert.Nil(t, err)
allPlugins := []plugins.Plugin{plugins.New(conf, testPluginName)}

t.Run("returns empty slice when non-existent version passed", func(t *testing.T) {
toolVersions, err := AllVersions(conf, allPlugins, t.TempDir())
assert.Nil(t, err)
assert.Empty(t, toolVersions)
})

t.Run("returns single version from .tool-versions file", func(t *testing.T) {
// write a version file
data := []byte(fmt.Sprintf("%s 1.2.3", testPluginName))
err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666)

toolVersions, err := AllVersions(conf, allPlugins, currentDir)
assert.Nil(t, err)
assert.Equal(t, toolVersions[0].Versions, []string{"1.2.3"})
})

t.Run("returns version from env when env variable set", func(t *testing.T) {
// Set env
t.Setenv(fmt.Sprintf("ASDF_%s_VERSION", strings.ToUpper(testPluginName)), "2.3.4")

// write a version file
data := []byte(fmt.Sprintf("%s 1.2.3", testPluginName))
err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666)

// assert env variable takes precedence
toolVersions, err := AllVersions(conf, allPlugins, currentDir)
assert.Nil(t, err)
assert.Equal(t, toolVersions[0].Versions, []string{"2.3.4"})
})

t.Run("returns single version from .tool-versions file in parent directory", func(t *testing.T) {
// write a version file
data := []byte(fmt.Sprintf("%s 1.2.3", testPluginName))
err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666)

subDir := filepath.Join(currentDir, "subdir")
err = os.MkdirAll(subDir, 0o777)
assert.Nil(t, err)

toolVersion, err := AllVersions(conf, allPlugins, subDir)
assert.Nil(t, err)
assert.Equal(t, toolVersion[0].Versions, []string{"1.2.3"})
})

t.Run("returns single version from .tool-versions file in home directory", func(t *testing.T) {
// write a version file
data := []byte(fmt.Sprintf("%s 1.2.3", testPluginName))
err = os.WriteFile(filepath.Join(homeDir, ".tool-versions"), data, 0o666)

toolVersion, err := AllVersions(conf, allPlugins, currentDir)
assert.Nil(t, err)
assert.Equal(t, toolVersion[0].Versions, []string{"1.2.3"})
})

t.Run("returns unknown plugin version from .tool-versions file in parent directory", func(t *testing.T) {
// write a version file
unknownPluginName := "dummy_unknown_plugin"
data := []byte(fmt.Sprintf("%s 1.2.3", unknownPluginName))
err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666)

subDir := filepath.Join(currentDir, "subdir")
err = os.MkdirAll(subDir, 0o777)
assert.Nil(t, err)

toolVersion, err := AllVersions(conf, allPlugins, subDir)
assert.Nil(t, err)
assert.Equal(t, toolVersion[0].Name, unknownPluginName)
assert.Equal(t, toolVersion[0].Versions, []string{"1.2.3"})
})

t.Run("returns results in order from .tool-versions file", func(t *testing.T) {
testPluginTwoName := "dummy_plugin_two"
_, err := repotest.InstallPlugin("dummy_plugin", conf.DataDir, testPluginTwoName)
assert.Nil(t, err)
allPlugins := []plugins.Plugin{plugins.New(conf, testPluginName), plugins.New(conf, testPluginTwoName)}

// write a version file
unknownPluginName := "dummy_unknown_plugin"
data := []byte(fmt.Sprintf("%s 1.2.3\n%s 1.2.3\n%s 1.2.3", testPluginTwoName, testPluginName, unknownPluginName))
err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666)
assert.Nil(t, err)

toolVersion, err := AllVersions(conf, allPlugins, currentDir)
assert.Nil(t, err)
assert.Equal(t, toolVersion[0].Name, testPluginTwoName)
assert.Equal(t, toolVersion[1].Name, testPluginName)
assert.Equal(t, toolVersion[2].Name, unknownPluginName)
})
}

func TestFindVersionsInDir(t *testing.T) {
testDataDir := t.TempDir()
conf := config.Config{DataDir: testDataDir, DefaultToolVersionsFilename: ".tool-versions", ConfigFile: "testdata/asdfrc"}
Expand Down
1 change: 1 addition & 0 deletions internal/shims/shims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ func generateConfig(t *testing.T) (config.Config, plugins.Plugin) {
conf, err := config.LoadConfig()
assert.Nil(t, err)
conf.DataDir = testDataDir
conf.Home = t.TempDir()

return conf, installPlugin(t, conf, "dummy_plugin", testPluginName)
}
Expand Down
44 changes: 37 additions & 7 deletions internal/versions/versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
latestFilterRegex = "(?i)(^Available versions:|-src|-dev|-latest|-stm|[-\\.]rc|-milestone|-alpha|-beta|[-\\.]pre|-next|(a|b|c)[0-9]+|snapshot|master|main)"
numericStartFilterRegex = "^\\s*[0-9]"
noLatestVersionErrMsg = "no latest version found"
missingPluginErrMsg = "missing plugin for %s"
)

// UninstallableVersionError is an error returned if someone tries to install the
Expand All @@ -44,6 +45,16 @@ type NoVersionSetError struct {
toolName string
}

// MissingPluginError is returned whenever an operation expects a plugin,
// but it is not installed.
type MissingPluginError struct {
toolName string
}

func (e MissingPluginError) Error() string {
return fmt.Sprintf(missingPluginErrMsg, e.toolName)
}

func (e NoVersionSetError) Error() string {
// Eventually switch this to a more friendly error message, BATS tests fail
// with this improvement
Expand All @@ -67,21 +78,40 @@ func (e VersionAlreadyInstalledError) Error() string {
// installed, but it may be multiple versions if multiple versions for the tool
// are specified in the .tool-versions file.
func InstallAll(conf config.Config, dir string, stdOut io.Writer, stdErr io.Writer) (failures []error) {
plugins, err := plugins.List(conf, false, false)
installedPlugins, err := plugins.List(conf, false, false)
if err != nil {
return []error{fmt.Errorf("unable to list plugins: %w", err)}
}
pluginsMap := map[string]plugins.Plugin{}
for _, plugin := range installedPlugins {
pluginsMap[plugin.Name] = plugin
}

// Ideally we should install these in the order they are specified in the
// closest .tool-versions file, but for now that is too complicated to
// implement.
for _, plugin := range plugins {
err := Install(conf, plugin, dir, stdOut, stdErr)
if err != nil {
toolVersions, err := resolve.AllVersions(conf, installedPlugins, dir)
if err != nil {
return []error{fmt.Errorf("unable to resolve versions: %w", err)}
}

for _, toolVersion := range toolVersions {
if plugin, isPluginResolved := pluginsMap[toolVersion.Name]; isPluginResolved {
delete(pluginsMap, plugin.Name)
for _, version := range toolVersion.Versions {
err := InstallOneVersion(conf, plugin, version, false, stdOut, stdErr)
if err != nil {
failures = append(failures, err)
}
}
} else {
err = MissingPluginError{toolName: toolVersion.Name}
failures = append(failures, err)
}
}

for _, plugin := range pluginsMap {
err := NoVersionSetError{toolName: plugin.Name}
failures = append(failures, err)
}

return failures
}

Expand Down
Loading