feat(golang-rewrite): use version type when generating shims

* Generate shims after version install
* Enable `reshim_command.bats` tests
* Remove `update_command.bats` tests from main_test.go
* Update `installs` package functions to accept version type in addition to version value
* Update `versions.ParseString` function to handle path version
* Move invocation of hooks for reshim
* Update `asdf reshim` command so shims can be generated for path versions
This commit is contained in:
Trevor Brown 2024-09-05 22:12:08 -04:00
parent ec156d66b9
commit 54552f91bf
8 changed files with 153 additions and 59 deletions

View File

@ -4,6 +4,7 @@ package cmd
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"strings" "strings"
@ -139,8 +140,9 @@ func Execute(version string) {
}, },
{ {
Name: "reshim", Name: "reshim",
Action: func(_ *cli.Context) error { Action: func(cCtx *cli.Context) error {
return reshimCommand(logger) args := cCtx.Args()
return reshimCommand(logger, args.Get(0), args.Get(1))
}, },
}, },
}, },
@ -390,24 +392,32 @@ func latestCommand(logger *log.Logger, all bool, toolName, pattern string) (err
return nil return nil
} }
func reshimCommand(logger *log.Logger) (err error) { func reshimCommand(logger *log.Logger, tool, version string) (err error) {
conf, err := config.LoadConfig() conf, err := config.LoadConfig()
if err != nil { if err != nil {
logger.Printf("error loading config: %s", err) logger.Printf("error loading config: %s", err)
return err return err
} }
err = shims.RemoveAll(conf) // if either tool or version are missing just regenerate all shims. This is
if err != nil { // fast enough now.
return err if tool == "" || version == "" {
err = shims.RemoveAll(conf)
if err != nil {
return err
}
return shims.GenerateAll(conf, os.Stdout, os.Stderr)
} }
err = shims.GenerateAll(conf, os.Stdout, os.Stderr) // If provided a specific version it could be something special like a path
if err != nil { // version so we need to generate it manually
return err return reshimToolVersion(conf, tool, version, os.Stdout, os.Stderr)
} }
return err func reshimToolVersion(conf config.Config, tool, version string, out io.Writer, errOut io.Writer) error {
versionType, version := versions.ParseString(version)
return shims.GenerateForVersion(conf, plugins.New(conf, tool), versionType, version, out, errOut)
} }
func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bool) error { func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bool) error {
@ -426,7 +436,7 @@ func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bo
} }
if showStatus { if showStatus {
installed := installs.IsInstalled(conf, plugin, latest) installed := installs.IsInstalled(conf, plugin, "version", latest)
fmt.Printf("%s\t%s\t%s\n", plugin.Name, latest, installedStatus(installed)) fmt.Printf("%s\t%s\t%s\n", plugin.Name, latest, installedStatus(installed))
} else { } else {
fmt.Printf("%s\n", latest) fmt.Printf("%s\n", latest)

View File

@ -41,18 +41,24 @@ func Installed(conf config.Config, plugin plugins.Plugin) (versions []string, er
} }
// InstallPath returns the path to a tool installation // InstallPath returns the path to a tool installation
func InstallPath(conf config.Config, plugin plugins.Plugin, version string) string { func InstallPath(conf config.Config, plugin plugins.Plugin, versionType, version string) string {
if versionType == "path" {
return version
}
return filepath.Join(pluginInstallPath(conf, plugin), version) return filepath.Join(pluginInstallPath(conf, plugin), version)
} }
// DownloadPath returns the download path for a particular plugin and version // DownloadPath returns the download path for a particular plugin and version
func DownloadPath(conf config.Config, plugin plugins.Plugin, version string) string { func DownloadPath(conf config.Config, plugin plugins.Plugin, versionType, version string) string {
if versionType == "path" {
return ""
}
return filepath.Join(conf.DataDir, dataDirDownloads, plugin.Name, version) return filepath.Join(conf.DataDir, dataDirDownloads, plugin.Name, version)
} }
// IsInstalled checks if a specific version of a tool is installed // IsInstalled checks if a specific version of a tool is installed
func IsInstalled(conf config.Config, plugin plugins.Plugin, version string) bool { func IsInstalled(conf config.Config, plugin plugins.Plugin, versionType, version string) bool {
installDir := InstallPath(conf, plugin, version) installDir := InstallPath(conf, plugin, versionType, version)
// Check if version already installed // Check if version already installed
_, err := os.Stat(installDir) _, err := os.Stat(installDir)

View File

@ -2,6 +2,7 @@ package installs
import ( import (
"os" "os"
"path/filepath"
"testing" "testing"
"asdf/internal/config" "asdf/internal/config"
@ -14,6 +15,34 @@ import (
const testPluginName = "lua" const testPluginName = "lua"
func TestDownloadPath(t *testing.T) {
conf, plugin := generateConfig(t)
t.Run("returns empty string when given path version", func(t *testing.T) {
path := DownloadPath(conf, plugin, "path", "foo/bar")
assert.Empty(t, path)
})
t.Run("returns empty string when given path version", func(t *testing.T) {
path := DownloadPath(conf, plugin, "version", "1.2.3")
assert.Equal(t, path, filepath.Join(conf.DataDir, "downloads", "lua", "1.2.3"))
})
}
func TestInstallPath(t *testing.T) {
conf, plugin := generateConfig(t)
t.Run("returns empty string when given path version", func(t *testing.T) {
path := InstallPath(conf, plugin, "path", "foo/bar")
assert.Equal(t, path, "foo/bar")
})
t.Run("returns install path when given regular version as version", func(t *testing.T) {
path := InstallPath(conf, plugin, "version", "1.2.3")
assert.Equal(t, path, filepath.Join(conf.DataDir, "installs", "lua", "1.2.3"))
})
}
func TestInstalled(t *testing.T) { func TestInstalled(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
@ -37,10 +66,10 @@ func TestIsInstalled(t *testing.T) {
installVersion(t, conf, plugin, "1.0.0") installVersion(t, conf, plugin, "1.0.0")
t.Run("returns false when not installed", func(t *testing.T) { t.Run("returns false when not installed", func(t *testing.T) {
assert.False(t, IsInstalled(conf, plugin, "4.0.0")) assert.False(t, IsInstalled(conf, plugin, "version", "4.0.0"))
}) })
t.Run("returns true when installed", func(t *testing.T) { t.Run("returns true when installed", func(t *testing.T) {
assert.True(t, IsInstalled(conf, plugin, "1.0.0")) assert.True(t, IsInstalled(conf, plugin, "version", "1.0.0"))
}) })
} }
@ -60,7 +89,7 @@ func generateConfig(t *testing.T) (config.Config, plugins.Plugin) {
func mockInstall(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) { func mockInstall(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) {
t.Helper() t.Helper()
path := InstallPath(conf, plugin, version) path := InstallPath(conf, plugin, "version", version)
err := os.MkdirAll(path, os.ModePerm) err := os.MkdirAll(path, os.ModePerm)
assert.Nil(t, err) assert.Nil(t, err)
} }

View File

@ -62,28 +62,26 @@ func GenerateForPluginVersions(conf config.Config, plugin plugins.Plugin, stdOut
} }
for _, version := range installedVersions { for _, version := range installedVersions {
err = hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_reshim_%s", plugin.Name), []string{version}, stdOut, stdErr) GenerateForVersion(conf, plugin, "version", version, stdOut, stdErr)
if err != nil {
return err
}
GenerateForVersion(conf, plugin, version)
err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_reshim_%s", plugin.Name), []string{version}, stdOut, stdErr)
if err != nil {
return err
}
} }
return nil return nil
} }
// GenerateForVersion loops over all the executable files found for a tool and // GenerateForVersion loops over all the executable files found for a tool and
// generates a shim for each one // generates a shim for each one
func GenerateForVersion(conf config.Config, plugin plugins.Plugin, version string) error { func GenerateForVersion(conf config.Config, plugin plugins.Plugin, versionType, version string, stdOut io.Writer, stdErr io.Writer) error {
executables, err := ToolExecutables(conf, plugin, version) err := hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_reshim_%s", plugin.Name), []string{version}, stdOut, stdErr)
if err != nil { if err != nil {
return err return err
} }
executables, err := ToolExecutables(conf, plugin, versionType, version)
if err != nil {
return err
}
if versionType == "path" {
version = fmt.Sprintf("path:%s", version)
}
for _, executablePath := range executables { for _, executablePath := range executables {
err := Write(conf, plugin, version, executablePath) err := Write(conf, plugin, version, executablePath)
@ -92,6 +90,10 @@ func GenerateForVersion(conf config.Config, plugin plugins.Plugin, version strin
} }
} }
err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_reshim_%s", plugin.Name), []string{version}, stdOut, stdErr)
if err != nil {
return err
}
return nil return nil
} }
@ -129,13 +131,13 @@ func ensureShimDirExists(conf config.Config) error {
} }
// ToolExecutables returns a slice of executables for a given tool version // ToolExecutables returns a slice of executables for a given tool version
func ToolExecutables(conf config.Config, plugin plugins.Plugin, version string) (executables []string, err error) { func ToolExecutables(conf config.Config, plugin plugins.Plugin, versionType, version string) (executables []string, err error) {
dirs, err := ExecutableDirs(plugin) dirs, err := ExecutableDirs(plugin)
if err != nil { if err != nil {
return executables, err return executables, err
} }
installPath := installs.InstallPath(conf, plugin, version) installPath := installs.InstallPath(conf, plugin, versionType, version)
paths := dirsToPaths(dirs, installPath) paths := dirsToPaths(dirs, installPath)
for _, path := range paths { for _, path := range paths {

View File

@ -9,6 +9,7 @@ import (
"testing" "testing"
"asdf/internal/config" "asdf/internal/config"
"asdf/internal/installs"
"asdf/internal/installtest" "asdf/internal/installtest"
"asdf/internal/plugins" "asdf/internal/plugins"
"asdf/repotest" "asdf/repotest"
@ -23,7 +24,7 @@ func TestRemoveAll(t *testing.T) {
version := "1.1.0" version := "1.1.0"
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
installVersion(t, conf, plugin, version) installVersion(t, conf, plugin, version)
executables, err := ToolExecutables(conf, plugin, version) executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err) assert.Nil(t, err)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
@ -46,7 +47,7 @@ func TestGenerateAll(t *testing.T) {
installVersion(t, conf, plugin, version) installVersion(t, conf, plugin, version)
installPlugin(t, conf, "dummy_plugin", "ruby") installPlugin(t, conf, "dummy_plugin", "ruby")
installVersion(t, conf, plugin, version2) installVersion(t, conf, plugin, version2)
executables, err := ToolExecutables(conf, plugin, version) executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err) assert.Nil(t, err)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
@ -75,7 +76,7 @@ func TestGenerateForPluginVersions(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
installVersion(t, conf, plugin, version) installVersion(t, conf, plugin, version)
installVersion(t, conf, plugin, version2) installVersion(t, conf, plugin, version2)
executables, err := ToolExecutables(conf, plugin, version) executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err) assert.Nil(t, err)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
@ -112,11 +113,12 @@ func TestGenerateForVersion(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
installVersion(t, conf, plugin, version) installVersion(t, conf, plugin, version)
installVersion(t, conf, plugin, version2) installVersion(t, conf, plugin, version2)
executables, err := ToolExecutables(conf, plugin, version) executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err) assert.Nil(t, err)
t.Run("generates shim script for every executable in version", func(t *testing.T) { t.Run("generates shim script for every executable in version", func(t *testing.T) {
assert.Nil(t, GenerateForVersion(conf, plugin, version)) stdout, stderr := buildOutputs()
assert.Nil(t, GenerateForVersion(conf, plugin, "version", version, &stdout, &stderr))
// check for generated shims // check for generated shims
for _, executable := range executables { for _, executable := range executables {
@ -127,8 +129,9 @@ func TestGenerateForVersion(t *testing.T) {
}) })
t.Run("updates existing shims for every executable in version", func(t *testing.T) { t.Run("updates existing shims for every executable in version", func(t *testing.T) {
assert.Nil(t, GenerateForVersion(conf, plugin, version)) stdout, stderr := buildOutputs()
assert.Nil(t, GenerateForVersion(conf, plugin, version2)) assert.Nil(t, GenerateForVersion(conf, plugin, "version", version, &stdout, &stderr))
assert.Nil(t, GenerateForVersion(conf, plugin, "version", version2, &stdout, &stderr))
// check for generated shims // check for generated shims
for _, executable := range executables { for _, executable := range executables {
@ -145,7 +148,7 @@ func TestWrite(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
installVersion(t, conf, plugin, version) installVersion(t, conf, plugin, version)
installVersion(t, conf, plugin, version2) installVersion(t, conf, plugin, version2)
executables, err := ToolExecutables(conf, plugin, version) executables, err := ToolExecutables(conf, plugin, "version", version)
executable := executables[0] executable := executables[0]
assert.Nil(t, err) assert.Nil(t, err)
@ -205,7 +208,22 @@ func TestToolExecutables(t *testing.T) {
installVersion(t, conf, plugin, version) installVersion(t, conf, plugin, version)
t.Run("returns list of executables for plugin", func(t *testing.T) { t.Run("returns list of executables for plugin", func(t *testing.T) {
executables, err := ToolExecutables(conf, plugin, version) executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err)
var filenames []string
for _, executablePath := range executables {
assert.True(t, strings.HasPrefix(executablePath, conf.DataDir))
filenames = append(filenames, filepath.Base(executablePath))
}
assert.Equal(t, filenames, []string{"dummy"})
})
t.Run("returns list of executables for version installed in arbitrary directory", func(t *testing.T) {
// Reference regular install by path to validate this behavior
path := installs.InstallPath(conf, plugin, "version", version)
executables, err := ToolExecutables(conf, plugin, "path", path)
assert.Nil(t, err) assert.Nil(t, err)
var filenames []string var filenames []string

View File

@ -15,22 +15,25 @@ import (
"asdf/internal/installs" "asdf/internal/installs"
"asdf/internal/plugins" "asdf/internal/plugins"
"asdf/internal/resolve" "asdf/internal/resolve"
"asdf/internal/shims"
) )
const ( const (
systemVersion = "system" systemVersion = "system"
latestVersion = "latest" latestVersion = "latest"
uninstallableVersionMsg = "uninstallable version: system" uninstallableVersionMsg = "uninstallable version: %s"
latestFilterRegex = "(?i)(^Available versions:|-src|-dev|-latest|-stm|[-\\.]rc|-milestone|-alpha|-beta|[-\\.]pre|-next|(a|b|c)[0-9]+|snapshot|master)" latestFilterRegex = "(?i)(^Available versions:|-src|-dev|-latest|-stm|[-\\.]rc|-milestone|-alpha|-beta|[-\\.]pre|-next|(a|b|c)[0-9]+|snapshot|master)"
noLatestVersionErrMsg = "no latest version found" noLatestVersionErrMsg = "no latest version found"
) )
// UninstallableVersionError is an error returned if someone tries to install the // UninstallableVersionError is an error returned if someone tries to install the
// system version. // system version.
type UninstallableVersionError struct{} type UninstallableVersionError struct {
versionType string
}
func (e UninstallableVersionError) Error() string { func (e UninstallableVersionError) Error() string {
return fmt.Sprint(uninstallableVersionMsg) return fmt.Sprintf(uninstallableVersionMsg, e.versionType)
} }
// NoVersionSetError is returned whenever an operation that requires a version // NoVersionSetError is returned whenever an operation that requires a version
@ -125,14 +128,18 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string
} }
if version == systemVersion { if version == systemVersion {
return UninstallableVersionError{} return UninstallableVersionError{versionType: "system"}
} }
downloadDir := installs.DownloadPath(conf, plugin, version)
installDir := installs.InstallPath(conf, plugin, version)
versionType, version := ParseString(version) versionType, version := ParseString(version)
if installs.IsInstalled(conf, plugin, version) { if versionType == "path" {
return UninstallableVersionError{versionType: "path"}
}
downloadDir := installs.DownloadPath(conf, plugin, versionType, version)
installDir := installs.InstallPath(conf, plugin, versionType, version)
if installs.IsInstalled(conf, plugin, versionType, version) {
return fmt.Errorf("version %s of %s is already installed", version, plugin.Name) return fmt.Errorf("version %s of %s is already installed", version, plugin.Name)
} }
@ -174,6 +181,12 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string
return fmt.Errorf("failed to run install callback: %w", err) return fmt.Errorf("failed to run install callback: %w", err)
} }
// Reshim
err = shims.GenerateAll(conf, stdOut, stdErr)
if err != nil {
return fmt.Errorf("unable to generate shims post-install: %w", err)
}
err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_install_%s", plugin.Name), []string{version}, stdOut, stdErr) err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_install_%s", plugin.Name), []string{version}, stdOut, stdErr)
if err != nil { if err != nil {
return fmt.Errorf("failed to run post-install hook: %w", err) return fmt.Errorf("failed to run post-install hook: %w", err)
@ -297,8 +310,20 @@ func parseVersions(rawVersions string) []string {
// ParseString parses a version string into versionType and version components // ParseString parses a version string into versionType and version components
func ParseString(version string) (string, string) { func ParseString(version string) (string, string) {
segments := strings.Split(version, ":") segments := strings.Split(version, ":")
if len(segments) >= 1 && segments[0] == "ref" { if len(segments) >= 1 {
return "ref", strings.Join(segments[1:], ":") remainder := strings.Join(segments[1:], ":")
switch segments[0] {
case "ref":
return "ref", remainder
case "path":
// This is for people who have the local source already compiled
// Like those who work on the language, etc
// We'll allow specifying path:/foo/bar/project in .tool-versions
// And then use the binaries there
return "path", remainder
default:
return "version", version
}
} }
return "version", version return "version", version

View File

@ -164,6 +164,14 @@ func TestInstallOneVersion(t *testing.T) {
assert.IsType(t, plugins.PluginMissing{}, err) assert.IsType(t, plugins.PluginMissing{}, err)
}) })
t.Run("returns error when passed a path version", func(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, "path:/foo/bar", &stdout, &stderr)
assert.ErrorContains(t, err, "uninstallable version: path")
})
t.Run("returns error when plugin version is 'system'", func(t *testing.T) { t.Run("returns error when plugin version is 'system'", func(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()

View File

@ -71,9 +71,9 @@ func TestBatsTests(t *testing.T) {
// runBatsFile(t, dir, "remove_command.bats") // runBatsFile(t, dir, "remove_command.bats")
//}) //})
//t.Run("reshim_command", func(t *testing.T) { t.Run("reshim_command", func(t *testing.T) {
// runBatsFile(t, dir, "reshim_command.bats") runBatsFile(t, dir, "reshim_command.bats")
//}) })
//t.Run("shim_env_command", func(t *testing.T) { //t.Run("shim_env_command", func(t *testing.T) {
// runBatsFile(t, dir, "shim_env_command.bats") // runBatsFile(t, dir, "shim_env_command.bats")
@ -91,10 +91,6 @@ func TestBatsTests(t *testing.T) {
// runBatsFile(t, dir, "uninstall_command.bats") // runBatsFile(t, dir, "uninstall_command.bats")
//}) //})
//t.Run("update_command", func(t *testing.T) {
// runBatsFile(t, dir, "update_command.bats")
//})
//t.Run("version_commands", func(t *testing.T) { //t.Run("version_commands", func(t *testing.T) {
// runBatsFile(t, dir, "version_commands.bats") // runBatsFile(t, dir, "version_commands.bats")
//}) //})