Merge pull request #64 from asdf-vm/tb/shim-generation-4

feat(golang-rewrite): use version type when generating shims
This commit is contained in:
Trevor Brown 2024-09-07 16:29:09 -04:00 committed by Trevor Brown
commit 631b0d8793
8 changed files with 153 additions and 59 deletions

View File

@ -4,6 +4,7 @@ package cmd
import (
"errors"
"fmt"
"io"
"log"
"os"
"strings"
@ -139,8 +140,9 @@ func Execute(version string) {
},
{
Name: "reshim",
Action: func(_ *cli.Context) error {
return reshimCommand(logger)
Action: func(cCtx *cli.Context) error {
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
}
func reshimCommand(logger *log.Logger) (err error) {
func reshimCommand(logger *log.Logger, tool, version string) (err error) {
conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
return err
}
err = shims.RemoveAll(conf)
if err != nil {
return err
// if either tool or version are missing just regenerate all shims. This is
// fast enough now.
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 err != nil {
return err
}
// If provided a specific version it could be something special like a path
// version so we need to generate it manually
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 {
@ -426,7 +436,7 @@ func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bo
}
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))
} else {
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
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)
}
// 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)
}
// IsInstalled checks if a specific version of a tool is installed
func IsInstalled(conf config.Config, plugin plugins.Plugin, version string) bool {
installDir := InstallPath(conf, plugin, version)
func IsInstalled(conf config.Config, plugin plugins.Plugin, versionType, version string) bool {
installDir := InstallPath(conf, plugin, versionType, version)
// Check if version already installed
_, err := os.Stat(installDir)

View File

@ -2,6 +2,7 @@ package installs
import (
"os"
"path/filepath"
"testing"
"asdf/internal/config"
@ -14,6 +15,34 @@ import (
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) {
conf, plugin := generateConfig(t)
@ -37,10 +66,10 @@ func TestIsInstalled(t *testing.T) {
installVersion(t, conf, plugin, "1.0.0")
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) {
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) {
t.Helper()
path := InstallPath(conf, plugin, version)
path := InstallPath(conf, plugin, "version", version)
err := os.MkdirAll(path, os.ModePerm)
assert.Nil(t, err)
}

View File

@ -62,28 +62,26 @@ func GenerateForPluginVersions(conf config.Config, plugin plugins.Plugin, stdOut
}
for _, version := range installedVersions {
err = hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_reshim_%s", plugin.Name), []string{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
}
GenerateForVersion(conf, plugin, "version", version, stdOut, stdErr)
}
return nil
}
// GenerateForVersion loops over all the executable files found for a tool and
// generates a shim for each one
func GenerateForVersion(conf config.Config, plugin plugins.Plugin, version string) error {
executables, err := ToolExecutables(conf, plugin, version)
func GenerateForVersion(conf config.Config, plugin plugins.Plugin, versionType, version string, stdOut io.Writer, stdErr io.Writer) error {
err := hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_reshim_%s", plugin.Name), []string{version}, stdOut, stdErr)
if err != nil {
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 {
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
}
@ -129,13 +131,13 @@ func ensureShimDirExists(conf config.Config) error {
}
// 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)
if err != nil {
return executables, err
}
installPath := installs.InstallPath(conf, plugin, version)
installPath := installs.InstallPath(conf, plugin, versionType, version)
paths := dirsToPaths(dirs, installPath)
for _, path := range paths {

View File

@ -9,6 +9,7 @@ import (
"testing"
"asdf/internal/config"
"asdf/internal/installs"
"asdf/internal/installtest"
"asdf/internal/plugins"
"asdf/repotest"
@ -23,7 +24,7 @@ func TestRemoveAll(t *testing.T) {
version := "1.1.0"
conf, plugin := generateConfig(t)
installVersion(t, conf, plugin, version)
executables, err := ToolExecutables(conf, plugin, version)
executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err)
stdout, stderr := buildOutputs()
@ -46,7 +47,7 @@ func TestGenerateAll(t *testing.T) {
installVersion(t, conf, plugin, version)
installPlugin(t, conf, "dummy_plugin", "ruby")
installVersion(t, conf, plugin, version2)
executables, err := ToolExecutables(conf, plugin, version)
executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err)
stdout, stderr := buildOutputs()
@ -75,7 +76,7 @@ func TestGenerateForPluginVersions(t *testing.T) {
conf, plugin := generateConfig(t)
installVersion(t, conf, plugin, version)
installVersion(t, conf, plugin, version2)
executables, err := ToolExecutables(conf, plugin, version)
executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err)
stdout, stderr := buildOutputs()
@ -112,11 +113,12 @@ func TestGenerateForVersion(t *testing.T) {
conf, plugin := generateConfig(t)
installVersion(t, conf, plugin, version)
installVersion(t, conf, plugin, version2)
executables, err := ToolExecutables(conf, plugin, version)
executables, err := ToolExecutables(conf, plugin, "version", version)
assert.Nil(t, err)
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
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) {
assert.Nil(t, GenerateForVersion(conf, plugin, version))
assert.Nil(t, GenerateForVersion(conf, plugin, version2))
stdout, stderr := buildOutputs()
assert.Nil(t, GenerateForVersion(conf, plugin, "version", version, &stdout, &stderr))
assert.Nil(t, GenerateForVersion(conf, plugin, "version", version2, &stdout, &stderr))
// check for generated shims
for _, executable := range executables {
@ -145,7 +148,7 @@ func TestWrite(t *testing.T) {
conf, plugin := generateConfig(t)
installVersion(t, conf, plugin, version)
installVersion(t, conf, plugin, version2)
executables, err := ToolExecutables(conf, plugin, version)
executables, err := ToolExecutables(conf, plugin, "version", version)
executable := executables[0]
assert.Nil(t, err)
@ -205,7 +208,22 @@ func TestToolExecutables(t *testing.T) {
installVersion(t, conf, plugin, version)
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)
var filenames []string

View File

@ -15,22 +15,25 @@ import (
"asdf/internal/installs"
"asdf/internal/plugins"
"asdf/internal/resolve"
"asdf/internal/shims"
)
const (
systemVersion = "system"
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)"
noLatestVersionErrMsg = "no latest version found"
)
// UninstallableVersionError is an error returned if someone tries to install the
// system version.
type UninstallableVersionError struct{}
type UninstallableVersionError struct {
versionType 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
@ -125,14 +128,18 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string
}
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)
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)
}
@ -174,6 +181,12 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string
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)
if err != nil {
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
func ParseString(version string) (string, string) {
segments := strings.Split(version, ":")
if len(segments) >= 1 && segments[0] == "ref" {
return "ref", strings.Join(segments[1:], ":")
if len(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

View File

@ -164,6 +164,14 @@ func TestInstallOneVersion(t *testing.T) {
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) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()

View File

@ -71,9 +71,9 @@ func TestBatsTests(t *testing.T) {
// runBatsFile(t, dir, "remove_command.bats")
//})
//t.Run("reshim_command", func(t *testing.T) {
// runBatsFile(t, dir, "reshim_command.bats")
//})
t.Run("reshim_command", func(t *testing.T) {
runBatsFile(t, dir, "reshim_command.bats")
})
//t.Run("shim_env_command", func(t *testing.T) {
// runBatsFile(t, dir, "shim_env_command.bats")
@ -91,10 +91,6 @@ func TestBatsTests(t *testing.T) {
// 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) {
// runBatsFile(t, dir, "version_commands.bats")
//})