Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Report missing plugins and install plugins in defined order #1970

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ repository
.vagrant
keyrings
/tmp
.idea

dist/

Expand Down
3 changes: 2 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package config
import (
"context"
"io/fs"
"os"
"strconv"
"strings"

Expand Down Expand Up @@ -100,7 +101,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.

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
1 change: 0 additions & 1 deletion internal/execenv/execenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ func TestMergeEnv(t *testing.T) {
}

func TestGenerate(t *testing.T) {

t.Run("returns map of environment variables", func(t *testing.T) {
testDataDir := t.TempDir()
conf := config.Config{DataDir: testDataDir}
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
Loading