From ba86ee02c3b412ea171b6485808b7512ea120782 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Thu, 31 Oct 2024 09:30:48 -0400 Subject: [PATCH] feat(golang-rewrite): introduce `Version` struct, get some `shim_exec.bats` tests passing * Get more shim_exec.bats tests passing by adding shebang lines to test scripts * Disable shim_exec test case for scenario that is no longer possible * Add documentation on another breaking change * Create toolversions.Version struct and update code to use new struct --- cmd/cmd.go | 94 +++++++++++++++------ docs/guide/upgrading-from-v0-14-to-v0-15.md | 13 +++ internal/help/help.go | 6 +- internal/installs/installs.go | 18 ++-- internal/installs/installs_test.go | 24 ++++-- internal/shims/shims.go | 4 +- internal/shims/shims_test.go | 28 +++--- internal/toolversions/toolversions.go | 33 +++++--- internal/toolversions/toolversions_test.go | 80 ++++++++++-------- internal/versions/versions.go | 42 ++++----- test/fixtures/dummy_plugin/bin/install | 2 + test/shim_exec.bats | 38 +++++---- 12 files changed, 239 insertions(+), 143 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index d8dc1830..98365b32 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -299,8 +299,8 @@ func getVersionInfo(conf config.Config, plugin plugins.Plugin, currentDir string installed := false if found { firstVersion := toolversion.Versions[0] - versionType, version := toolversions.Parse(firstVersion) - installed = installs.IsInstalled(conf, plugin, versionType, version) + version := toolversions.Parse(firstVersion) + installed = installs.IsInstalled(conf, plugin, version) } return toolversion, found, installed } @@ -354,13 +354,41 @@ func execCommand(logger *log.Logger, command string, args []string) error { } executable, found, err := shims.FindExecutable(conf, command, currentDir) + if err != nil { - logger.Printf("executable not found due to reason: %s", err.Error()) + + shimPath := shims.Path(conf, command) + toolVersions, _ := shims.GetToolsAndVersionsFromShimFile(shimPath) + + if len(toolVersions) > 0 { + if anyInstalled(conf, toolVersions) { + logger.Printf("No version is set for command %s", command) + logger.Printf("Consider adding one of the following versions in your config file at %s/.tool-versions\n", currentDir) + } else { + logger.Printf("No preset version installed for command %s", command) + for _, toolVersion := range toolVersions { + for _, version := range toolVersion.Versions { + fmt.Printf("asdf install %s %s\n", toolVersion.Name, version) + } + } + + fmt.Printf("or add one of the following versions in your config file at %s/.tool-versions\n", currentDir) + } + + for _, toolVersion := range toolVersions { + for _, version := range toolVersion.Versions { + fmt.Printf("%s %s", toolVersion.Name, version) + } + } + } + + os.Exit(126) return err } if !found { logger.Print("executable not found") + os.Exit(126) return fmt.Errorf("executable not found") } if len(args) > 1 { @@ -372,6 +400,19 @@ func execCommand(logger *log.Logger, command string, args []string) error { return exec.Exec(executable, args, os.Environ()) } +func anyInstalled(conf config.Config, toolVersions []toolversions.ToolVersions) bool { + for _, toolVersion := range toolVersions { + for _, version := range toolVersion.Versions { + version := toolversions.Parse(version) + plugin := plugins.New(conf, toolVersion.Name) + if installs.IsInstalled(conf, plugin, version) { + return true + } + } + } + return false +} + func pluginAddCommand(_ *cli.Context, conf config.Config, logger *log.Logger, pluginName, pluginRepo string) error { if pluginName == "" { // Invalid arguments @@ -577,10 +618,10 @@ func installCommand(logger *log.Logger, toolName, version string) error { return err } } else { - parsedVersion, query := toolversions.ParseFromCliArg(version) + parsedVersion := toolversions.ParseFromCliArg(version) - if parsedVersion == "latest" { - err = versions.InstallVersion(conf, plugin, version, query, os.Stdout, os.Stderr) + if parsedVersion.Type == "latest" { + err = versions.InstallVersion(conf, plugin, version, parsedVersion.Value, os.Stdout, os.Stderr) } else { err = versions.InstallOneVersion(conf, plugin, version, os.Stdout, os.Stderr) } @@ -890,7 +931,7 @@ func uninstallCommand(logger *log.Logger, tool, version string) error { return shims.GenerateAll(conf, os.Stdout, os.Stderr) } -func whereCommand(logger *log.Logger, tool, version string) error { +func whereCommand(logger *log.Logger, tool, versionStr string) error { conf, err := config.LoadConfig() if err != nil { logger.Printf("error loading config: %s", err) @@ -912,20 +953,28 @@ func whereCommand(logger *log.Logger, tool, version string) error { return err } - versionType, parsedVersion := toolversions.Parse(version) + version := toolversions.Parse(versionStr) - if version == "" { + if version.Type == "system" { + logger.Printf("System version is selected") + return errors.New("System version is selected") + } + + if version.Value == "" { // resolve version - toolversions, found, err := resolve.Version(conf, plugin, currentDir) + versions, found, err := resolve.Version(conf, plugin, currentDir) if err != nil { fmt.Printf("err %#+v\n", err) return err } - if found && len(toolversions.Versions) > 0 && installs.IsInstalled(conf, plugin, "version", toolversions.Versions[0]) { - installPath := installs.InstallPath(conf, plugin, "version", toolversions.Versions[0]) - logger.Printf("%s", installPath) - return nil + if found && len(versions.Versions) > 0 { + versionStruct := toolversions.Version{Type: "version", Value: versions.Versions[0]} + if installs.IsInstalled(conf, plugin, versionStruct) { + installPath := installs.InstallPath(conf, plugin, versionStruct) + logger.Printf("%s", installPath) + return nil + } } // not found @@ -934,25 +983,20 @@ func whereCommand(logger *log.Logger, tool, version string) error { return errors.New(msg) } - if version == "system" { - logger.Printf("System version is selected") - return errors.New("System version is selected") - } - - if !installs.IsInstalled(conf, plugin, versionType, parsedVersion) { + if !installs.IsInstalled(conf, plugin, version) { logger.Printf("Version not installed") return errors.New("Version not installed") } - installPath := installs.InstallPath(conf, plugin, versionType, parsedVersion) + installPath := installs.InstallPath(conf, plugin, version) logger.Printf("%s", installPath) return nil } -func reshimToolVersion(conf config.Config, tool, version string, out io.Writer, errOut io.Writer) error { - versionType, version := toolversions.Parse(version) - return shims.GenerateForVersion(conf, plugins.New(conf, tool), versionType, version, out, errOut) +func reshimToolVersion(conf config.Config, tool, versionStr string, out io.Writer, errOut io.Writer) error { + version := toolversions.Parse(versionStr) + return shims.GenerateForVersion(conf, plugins.New(conf, tool), version.Type, version.Value, out, errOut) } func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bool) error { @@ -971,7 +1015,7 @@ func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bo } if showStatus { - installed := installs.IsInstalled(conf, plugin, "version", latest) + installed := installs.IsInstalled(conf, plugin, toolversions.Version{Type: "version", Value: latest}) fmt.Printf("%s\t%s\t%s\n", plugin.Name, latest, installedStatus(installed)) } else { fmt.Printf("%s\n", latest) diff --git a/docs/guide/upgrading-from-v0-14-to-v0-15.md b/docs/guide/upgrading-from-v0-14-to-v0-15.md index 7a3140f9..2335147b 100644 --- a/docs/guide/upgrading-from-v0-14-to-v0-15.md +++ b/docs/guide/upgrading-from-v0-14-to-v0-15.md @@ -43,6 +43,19 @@ not an executable. The new rewrite removes all shell code from asdf, and it is now a binary rather than a shell function, so setting environment variables directly in the shell is no longer possible. +### Executables Shims Resolve to Must Runnable by `syscall.Exec` + +The most obvious example of this breaking change are scripts that lack a proper +shebang line. asdf 0.14.1 and older were implemented in Bash, so as long it was +an executable that could be executed with Bash it would run. This mean that +scripts lacking a shebang could still be run by `asdf exec`. With asdf 0.15.x +implemented in Go we now invoke executables via Go's `syscall.Exec` function, +which cannot handle scripts lacking a shebang. + +In practice this isn't much of a problem. Most shell scripts DO contain a +shebang line. If a tool managed by asdf provides scripts that don't have a +shebang line one will need to be added to them. + ## Installation Installation of version 0.15.0 is much simpler than previous versions of asdf. It's just three steps: diff --git a/internal/help/help.go b/internal/help/help.go index 27076e1a..5911dcd9 100644 --- a/internal/help/help.go +++ b/internal/help/help.go @@ -80,9 +80,9 @@ func writePluginHelp(conf config.Config, toolName, toolVersion string, writer io } if toolVersion != "" { - versionType, version := toolversions.Parse(toolVersion) - env["ASDF_INSTALL_VERSION"] = version - env["ASDF_INSTALL_TYPE"] = versionType + version := toolversions.Parse(toolVersion) + env["ASDF_INSTALL_VERSION"] = version.Value + env["ASDF_INSTALL_TYPE"] = version.Type } if err := plugin.Exists(); err != nil { diff --git a/internal/installs/installs.go b/internal/installs/installs.go index 4a2a1a18..c0604614 100644 --- a/internal/installs/installs.go +++ b/internal/installs/installs.go @@ -38,26 +38,26 @@ 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, versionType, version string) string { - if versionType == "path" { - return version +func InstallPath(conf config.Config, plugin plugins.Plugin, version toolversions.Version) string { + if version.Type == "path" { + return version.Value } - return filepath.Join(data.InstallDirectory(conf.DataDir, plugin.Name), toolversions.FormatForFS(versionType, version)) + return filepath.Join(data.InstallDirectory(conf.DataDir, plugin.Name), toolversions.FormatForFS(version)) } // DownloadPath returns the download path for a particular plugin and version -func DownloadPath(conf config.Config, plugin plugins.Plugin, versionType, version string) string { - if versionType == "path" { +func DownloadPath(conf config.Config, plugin plugins.Plugin, version toolversions.Version) string { + if version.Type == "path" { return "" } - return filepath.Join(data.DownloadDirectory(conf.DataDir, plugin.Name), toolversions.FormatForFS(versionType, version)) + return filepath.Join(data.DownloadDirectory(conf.DataDir, plugin.Name), toolversions.FormatForFS(version)) } // IsInstalled checks if a specific version of a tool is installed -func IsInstalled(conf config.Config, plugin plugins.Plugin, versionType, version string) bool { - installDir := InstallPath(conf, plugin, versionType, version) +func IsInstalled(conf config.Config, plugin plugins.Plugin, version toolversions.Version) bool { + installDir := InstallPath(conf, plugin, version) // Check if version already installed _, err := os.Stat(installDir) diff --git a/internal/installs/installs_test.go b/internal/installs/installs_test.go index c1c5b88c..b41883f1 100644 --- a/internal/installs/installs_test.go +++ b/internal/installs/installs_test.go @@ -8,6 +8,7 @@ import ( "asdf/internal/config" "asdf/internal/installtest" "asdf/internal/plugins" + "asdf/internal/toolversions" "asdf/repotest" "github.com/stretchr/testify/assert" @@ -19,12 +20,14 @@ 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") + version := toolversions.Version{Type: "path", Value: "foo/bar"} + path := DownloadPath(conf, plugin, version) 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") + version := toolversions.Version{Type: "version", Value: "1.2.3"} + path := DownloadPath(conf, plugin, version) assert.Equal(t, path, filepath.Join(conf.DataDir, "downloads", "lua", "1.2.3")) }) } @@ -33,12 +36,14 @@ 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") + version := toolversions.Version{Type: "path", Value: "foo/bar"} + path := InstallPath(conf, plugin, version) 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") + version := toolversions.Version{Type: "version", Value: "1.2.3"} + path := InstallPath(conf, plugin, version) assert.Equal(t, path, filepath.Join(conf.DataDir, "installs", "lua", "1.2.3")) }) } @@ -66,10 +71,12 @@ 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, "version", "4.0.0")) + version := toolversions.Version{Type: "version", Value: "4.0.0"} + assert.False(t, IsInstalled(conf, plugin, version)) }) t.Run("returns true when installed", func(t *testing.T) { - assert.True(t, IsInstalled(conf, plugin, "version", "1.0.0")) + version := toolversions.Version{Type: "version", Value: "1.0.0"} + assert.True(t, IsInstalled(conf, plugin, version)) }) } @@ -87,9 +94,10 @@ func generateConfig(t *testing.T) (config.Config, plugins.Plugin) { return conf, plugins.New(conf, testPluginName) } -func mockInstall(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) { +func mockInstall(t *testing.T, conf config.Config, plugin plugins.Plugin, versionStr string) { t.Helper() - path := InstallPath(conf, plugin, "version", version) + version := toolversions.Version{Type: "version", Value: versionStr} + path := InstallPath(conf, plugin, version) err := os.MkdirAll(path, os.ModePerm) assert.Nil(t, err) } diff --git a/internal/shims/shims.go b/internal/shims/shims.go index a59c6ccc..e4dfea5b 100644 --- a/internal/shims/shims.go +++ b/internal/shims/shims.go @@ -173,7 +173,7 @@ func getCustomExecutablePath(conf config.Config, plugin plugins.Plugin, shimName var stdOut strings.Builder var stdErr strings.Builder - installPath := installs.InstallPath(conf, plugin, "version", version) + installPath := installs.InstallPath(conf, plugin, toolversions.Version{Type: "version", Value: version}) env := map[string]string{"ASDF_INSTALL_TYPE": "version"} err := plugin.RunCallback("exec-path", []string{installPath, shimName}, env, &stdOut, &stdErr) @@ -303,7 +303,7 @@ func ToolExecutables(conf config.Config, plugin plugins.Plugin, versionType, ver return executables, err } - installPath := installs.InstallPath(conf, plugin, versionType, version) + installPath := installs.InstallPath(conf, plugin, toolversions.Version{Type: versionType, Value: version}) paths := dirsToPaths(dirs, installPath) for _, path := range paths { diff --git a/internal/shims/shims_test.go b/internal/shims/shims_test.go index 9877a818..abf7ce40 100644 --- a/internal/shims/shims_test.go +++ b/internal/shims/shims_test.go @@ -12,6 +12,7 @@ import ( "asdf/internal/installs" "asdf/internal/installtest" "asdf/internal/plugins" + "asdf/internal/toolversions" "asdf/repotest" "github.com/stretchr/testify/assert" @@ -56,7 +57,8 @@ func TestFindExecutable(t *testing.T) { t.Run("returns string containing path to system executable when system version set", func(t *testing.T) { // Create dummy `ls` executable - path := filepath.Join(installs.InstallPath(conf, plugin, "version", version), "bin", "ls") + versionStruct := toolversions.Version{Type: "version", Value: version} + path := filepath.Join(installs.InstallPath(conf, plugin, versionStruct), "bin", "ls") assert.Nil(t, os.WriteFile(path, []byte("echo 'I'm ls'"), 0o777)) // write system version to version file @@ -75,19 +77,19 @@ func TestFindExecutable(t *testing.T) { } func TestGetExecutablePath(t *testing.T) { - version := "1.1.0" + version := toolversions.Version{Type: "version", Value: "1.1.0"} conf, plugin := generateConfig(t) - installVersion(t, conf, plugin, version) + installVersion(t, conf, plugin, version.Value) t.Run("returns path to executable", func(t *testing.T) { - path, err := GetExecutablePath(conf, plugin, "dummy", version) + path, err := GetExecutablePath(conf, plugin, "dummy", version.Value) assert.Nil(t, err) assert.Equal(t, filepath.Base(path), "dummy") - assert.Equal(t, filepath.Base(filepath.Dir(filepath.Dir(path))), version) + assert.Equal(t, filepath.Base(filepath.Dir(filepath.Dir(path))), version.Value) }) t.Run("returns error when executable with name not found", func(t *testing.T) { - path, err := GetExecutablePath(conf, plugin, "foo", version) + path, err := GetExecutablePath(conf, plugin, "foo", version.Value) assert.ErrorContains(t, err, "executable not found") assert.Equal(t, path, "") }) @@ -96,7 +98,7 @@ func TestGetExecutablePath(t *testing.T) { // Create exec-path callback installDummyExecPathScript(t, conf, plugin, version, "dummy") - path, err := GetExecutablePath(conf, plugin, "dummy", version) + path, err := GetExecutablePath(conf, plugin, "dummy", version.Value) assert.Nil(t, err) assert.Equal(t, filepath.Base(filepath.Dir(path)), "custom") }) @@ -285,12 +287,12 @@ func TestWrite(t *testing.T) { } func TestToolExecutables(t *testing.T) { - version := "1.1.0" + version := toolversions.Version{Type: "version", Value: "1.1.0"} conf, plugin := generateConfig(t) - installVersion(t, conf, plugin, version) + installVersion(t, conf, plugin, version.Value) t.Run("returns list of executables for plugin", func(t *testing.T) { - executables, err := ToolExecutables(conf, plugin, "version", version) + executables, err := ToolExecutables(conf, plugin, "version", version.Value) assert.Nil(t, err) var filenames []string @@ -304,7 +306,7 @@ func TestToolExecutables(t *testing.T) { 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) + path := installs.InstallPath(conf, plugin, version) executables, err := ToolExecutables(conf, plugin, "path", path) assert.Nil(t, err) @@ -357,14 +359,14 @@ func generateConfig(t *testing.T) (config.Config, plugins.Plugin) { return conf, installPlugin(t, conf, "dummy_plugin", testPluginName) } -func installDummyExecPathScript(t *testing.T, conf config.Config, plugin plugins.Plugin, version, name string) { +func installDummyExecPathScript(t *testing.T, conf config.Config, plugin plugins.Plugin, version toolversions.Version, name string) { t.Helper() execPath := filepath.Join(plugin.Dir, "bin", "exec-path") contents := fmt.Sprintf("#!/usr/bin/env bash\necho 'bin/custom/%s'", name) err := os.WriteFile(execPath, []byte(contents), 0o777) assert.Nil(t, err) - installPath := installs.InstallPath(conf, plugin, "version", version) + installPath := installs.InstallPath(conf, plugin, version) err = os.MkdirAll(filepath.Join(installPath, "bin", "custom"), 0o777) assert.Nil(t, err) diff --git a/internal/toolversions/toolversions.go b/internal/toolversions/toolversions.go index 2929c695..8319fd4b 100644 --- a/internal/toolversions/toolversions.go +++ b/internal/toolversions/toolversions.go @@ -10,6 +10,12 @@ import ( "strings" ) +// Version struct represents a single version in asdf. +type Version struct { + Type string // Must be one of: version, ref, path, system, latest + Value string // Any string +} + // ToolVersions represents a tool along with versions specified for it type ToolVersions struct { Name string @@ -86,49 +92,52 @@ func Unique(versions []ToolVersions) (uniques []ToolVersions) { // ParseFromCliArg parses a string that is passed in as an argument to one of // the asdf subcommands. Some subcommands allow the special version `latest` to // be used, with an optional filter string. -func ParseFromCliArg(version string) (string, string) { +func ParseFromCliArg(version string) Version { segments := strings.Split(version, ":") if len(segments) > 0 && segments[0] == "latest" { if len(segments) > 1 { // Must be latest with filter - return "latest", segments[1] + return Version{Type: "latest", Value: segments[1]} } - return "latest", "" + return Version{Type: "latest", Value: ""} } return Parse(version) } // Parse parses a version string into versionType and version components -func Parse(version string) (string, string) { +func Parse(version string) Version { segments := strings.Split(version, ":") if len(segments) >= 1 { remainder := strings.Join(segments[1:], ":") switch segments[0] { case "ref": - return "ref", remainder + return Version{Type: "ref", Value: 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 + return Version{Type: "path", Value: remainder} default: - return "version", version } } - return "version", version + if version == "system" { + return Version{Type: "system"} + } + + return Version{Type: "version", Value: version} } // FormatForFS takes a versionType and version strings and generate a version // string suitable for the file system -func FormatForFS(versionType, version string) string { - switch versionType { +func FormatForFS(version Version) string { + switch version.Type { case "ref": - return fmt.Sprintf("ref-%s", version) + return fmt.Sprintf("ref-%s", version.Value) default: - return version + return version.Value } } diff --git a/internal/toolversions/toolversions_test.go b/internal/toolversions/toolversions_test.go index 2179dc5d..5de32b49 100644 --- a/internal/toolversions/toolversions_test.go +++ b/internal/toolversions/toolversions_test.go @@ -159,63 +159,75 @@ func TestgetAllToolsAndVersionsInContent(t *testing.T) { } func TestParse(t *testing.T) { - t.Run("returns 'version', and unmodified version when passed semantic version", func(t *testing.T) { - versionType, version := Parse("1.2.3") - assert.Equal(t, versionType, "version") - assert.Equal(t, version, "1.2.3") + t.Run("when passed version string returns struct with type of 'version' and version as value", func(t *testing.T) { + version := Parse("1.2.3") + assert.Equal(t, version.Type, "version") + assert.Equal(t, version.Value, "1.2.3") }) - t.Run("returns 'ref' and reference version when passed a ref version", func(t *testing.T) { - versionType, version := Parse("ref:abc123") - assert.Equal(t, versionType, "ref") - assert.Equal(t, version, "abc123") + t.Run("when passed ref and version returns struct with type of 'ref' and version as value", func(t *testing.T) { + version := Parse("ref:abc123") + assert.Equal(t, version.Type, "ref") + assert.Equal(t, version.Value, "abc123") }) - t.Run("returns 'ref' and empty string when passed 'ref:'", func(t *testing.T) { - versionType, version := Parse("ref:") - assert.Equal(t, versionType, "ref") - assert.Equal(t, version, "") + t.Run("when passed 'ref:' returns struct with type of 'ref' and empty value", func(t *testing.T) { + version := Parse("ref:") + assert.Equal(t, version.Type, "ref") + assert.Equal(t, version.Value, "") + }) + + t.Run("when passed 'system' returns struct with type of 'system'", func(t *testing.T) { + version := Parse("system") + assert.Equal(t, version.Type, "system") + assert.Equal(t, version.Value, "") }) } func TestParseFromCliArg(t *testing.T) { - t.Run("returns 'latest' as version type when passed string 'latest'", func(t *testing.T) { - versionType, version := ParseFromCliArg("latest") - assert.Equal(t, versionType, "latest") - assert.Equal(t, version, "") + t.Run("when passed 'latest' returns struct with type of 'latest'", func(t *testing.T) { + version := ParseFromCliArg("latest") + assert.Equal(t, version.Type, "latest") + assert.Equal(t, version.Value, "") + }) + + t.Run("when passed latest with filter returns struct with type of 'latest' and unmodified filter string as value", func(t *testing.T) { + version := ParseFromCliArg("latest:1.2") + assert.Equal(t, version.Type, "latest") + assert.Equal(t, version.Value, "1.2") }) - t.Run("returns 'latest' and unmodified filter string when passed a latest version", func(t *testing.T) { - versionType, version := ParseFromCliArg("latest:1.2") - assert.Equal(t, versionType, "latest") - assert.Equal(t, version, "1.2") + t.Run("when passed version string returns struct with type of 'version' and version as value", func(t *testing.T) { + version := ParseFromCliArg("1.2.3") + assert.Equal(t, version.Type, "version") + assert.Equal(t, version.Value, "1.2.3") }) - t.Run("returns 'version', and unmodified version when passed semantic version", func(t *testing.T) { - versionType, version := ParseFromCliArg("1.2.3") - assert.Equal(t, versionType, "version") - assert.Equal(t, version, "1.2.3") + t.Run("when passed ref and version returns struct with type of 'ref' and version as value", func(t *testing.T) { + version := ParseFromCliArg("ref:abc123") + assert.Equal(t, version.Type, "ref") + assert.Equal(t, version.Value, "abc123") }) - t.Run("returns 'ref' and reference version when passed a ref version", func(t *testing.T) { - versionType, version := ParseFromCliArg("ref:abc123") - assert.Equal(t, versionType, "ref") - assert.Equal(t, version, "abc123") + t.Run("when passed 'ref:' returns struct with type of 'ref' and empty value", func(t *testing.T) { + version := ParseFromCliArg("ref:") + assert.Equal(t, version.Type, "ref") + assert.Equal(t, version.Value, "") }) - t.Run("returns 'ref' and empty string when passed 'ref:'", func(t *testing.T) { - versionType, version := ParseFromCliArg("ref:") - assert.Equal(t, versionType, "ref") - assert.Equal(t, version, "") + t.Run("when passed 'system' returns struct with type of 'system'", func(t *testing.T) { + version := ParseFromCliArg("system") + assert.Equal(t, version.Type, "system") + assert.Equal(t, version.Value, "") }) } func TestFormatForFS(t *testing.T) { t.Run("returns version when version type is not ref", func(t *testing.T) { - assert.Equal(t, FormatForFS("version", "foobar"), "foobar") + assert.Equal(t, FormatForFS(Version{Type: "version", Value: "foobar"}), "foobar") }) t.Run("returns version prefixed with 'ref-' when version type is ref", func(t *testing.T) { - assert.Equal(t, FormatForFS("ref", "foobar"), "ref-foobar") + assert.Equal(t, FormatForFS(Version{Type: "ref", Value: "foobar"}), "ref-foobar") }) } diff --git a/internal/versions/versions.go b/internal/versions/versions.go index f920d6f2..7e589ced 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -122,31 +122,31 @@ func InstallVersion(conf config.Config, plugin plugins.Plugin, version string, p } // InstallOneVersion installs a specific version of a specific tool -func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string, stdOut io.Writer, stdErr io.Writer) error { +func InstallOneVersion(conf config.Config, plugin plugins.Plugin, versionStr string, stdOut io.Writer, stdErr io.Writer) error { err := plugin.Exists() if err != nil { return err } - if version == systemVersion { - return UninstallableVersionError{versionType: "system"} + if versionStr == systemVersion { + return UninstallableVersionError{versionType: systemVersion} } - versionType, version := toolversions.Parse(version) + version := toolversions.Parse(versionStr) - if versionType == "path" { + if version.Type == "path" { return UninstallableVersionError{versionType: "path"} } - downloadDir := installs.DownloadPath(conf, plugin, versionType, version) - installDir := installs.InstallPath(conf, plugin, versionType, version) + downloadDir := installs.DownloadPath(conf, plugin, version) + installDir := installs.InstallPath(conf, plugin, version) - if installs.IsInstalled(conf, plugin, versionType, version) { + if installs.IsInstalled(conf, plugin, version) { return fmt.Errorf("version %s of %s is already installed", version, plugin.Name) } env := map[string]string{ - "ASDF_INSTALL_TYPE": versionType, - "ASDF_INSTALL_VERSION": version, + "ASDF_INSTALL_TYPE": version.Type, + "ASDF_INSTALL_VERSION": version.Value, "ASDF_INSTALL_PATH": installDir, "ASDF_DOWNLOAD_PATH": downloadDir, "ASDF_CONCURRENCY": asdfConcurrency(conf), @@ -157,7 +157,7 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string return fmt.Errorf("unable to create download dir: %w", err) } - err = hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_download_%s", plugin.Name), []string{version}, stdOut, stdErr) + err = hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_download_%s", plugin.Name), []string{version.Value}, stdOut, stdErr) if err != nil { return fmt.Errorf("failed to run pre-download hook: %w", err) } @@ -167,7 +167,7 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string return fmt.Errorf("failed to run download callback: %w", err) } - err = hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_install_%s", plugin.Name), []string{version}, stdOut, stdErr) + err = hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_install_%s", plugin.Name), []string{version.Value}, stdOut, stdErr) if err != nil { return fmt.Errorf("failed to run pre-install hook: %w", err) } @@ -188,7 +188,7 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string 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.Value}, stdOut, stdErr) if err != nil { return fmt.Errorf("failed to run post-install hook: %w", err) } @@ -278,26 +278,26 @@ func AllVersionsFiltered(plugin plugins.Plugin, query string) (versions []string // post-uninstall hooks if set, and runs the plugin's uninstall callback if // defined. func Uninstall(conf config.Config, plugin plugins.Plugin, rawVersion string, stdout, stderr io.Writer) error { - versionType, version := toolversions.ParseFromCliArg(rawVersion) + version := toolversions.ParseFromCliArg(rawVersion) - if versionType == "latest" { + if version.Type == "latest" { return errors.New("'latest' is a special version value that cannot be used for uninstall command") } - if !installs.IsInstalled(conf, plugin, versionType, version) { + if !installs.IsInstalled(conf, plugin, version) { return errors.New("No such version") } - err := hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_uninstall_%s", plugin.Name), []string{version}, stdout, stderr) + err := hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_uninstall_%s", plugin.Name), []string{version.Value}, stdout, stderr) if err != nil { return err } // invoke uninstall callback if available - installDir := installs.InstallPath(conf, plugin, versionType, version) + installDir := installs.InstallPath(conf, plugin, version) env := map[string]string{ - "ASDF_INSTALL_TYPE": versionType, - "ASDF_INSTALL_VERSION": version, + "ASDF_INSTALL_TYPE": version.Type, + "ASDF_INSTALL_VERSION": version.Value, "ASDF_INSTALL_PATH": installDir, } err = plugin.RunCallback("uninstall", []string{}, env, stdout, stderr) @@ -310,7 +310,7 @@ func Uninstall(conf config.Config, plugin plugins.Plugin, rawVersion string, std return err } - err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_uninstall_%s", plugin.Name), []string{version}, stdout, stderr) + err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_uninstall_%s", plugin.Name), []string{version.Value}, stdout, stderr) if err != nil { return err } diff --git a/test/fixtures/dummy_plugin/bin/install b/test/fixtures/dummy_plugin/bin/install index 645f6ec6..1d511daf 100755 --- a/test/fixtures/dummy_plugin/bin/install +++ b/test/fixtures/dummy_plugin/bin/install @@ -17,11 +17,13 @@ echo "$ASDF_INSTALL_VERSION" >"$ASDF_INSTALL_PATH/version" # create the dummy executable mkdir -p "$ASDF_INSTALL_PATH/bin" cat <"$ASDF_INSTALL_PATH/bin/dummy" +#!/usr/bin/env bash echo This is Dummy ${ASDF_INSTALL_VERSION}! \$2 \$1 EOF chmod +x "$ASDF_INSTALL_PATH/bin/dummy" mkdir -p "$ASDF_INSTALL_PATH/bin/subdir" cat <"$ASDF_INSTALL_PATH/bin/subdir/other_bin" +#!/usr/bin/env bash echo This is Other Bin ${ASDF_INSTALL_VERSION}! \$2 \$1 EOF chmod +x "$ASDF_INSTALL_PATH/bin/subdir/other_bin" diff --git a/test/shim_exec.bats b/test/shim_exec.bats index 5c6d0673..284782c5 100644 --- a/test/shim_exec.bats +++ b/test/shim_exec.bats @@ -61,7 +61,8 @@ teardown() { echo "dummy 1.0" >"$PROJECT_DIR/.tool-versions" run asdf install - echo "tr [:lower:] [:upper:]" >"$ASDF_DIR/installs/dummy/1.0/bin/upper" + echo "#!/usr/bin/env bash + tr [:lower:] [:upper:]" >"$ASDF_DIR/installs/dummy/1.0/bin/upper" chmod +x "$ASDF_DIR/installs/dummy/1.0/bin/upper" run asdf reshim dummy 1.0 @@ -114,20 +115,22 @@ teardown() { echo "$output" | grep -q "mummy 3.0" 2>/dev/null } -@test "shim exec should suggest to install missing version" { - run asdf install dummy 1.0 +# No longer possible for shim to specify version that isn't installed because +# shims are re-generated after every install and uninstall. +#@test "shim exec should suggest to install missing version" { +# run asdf install dummy 1.0 - echo "dummy 2.0.0 1.3" >"$PROJECT_DIR/.tool-versions" +# echo "dummy 2.0.0 1.3" >"$PROJECT_DIR/.tool-versions" - run "$ASDF_DIR/shims/dummy" world hello - [ "$status" -eq 126 ] - echo "$output" | grep -q "No preset version installed for command dummy" 2>/dev/null - echo "$output" | grep -q "Please install a version by running one of the following:" 2>/dev/null - echo "$output" | grep -q "asdf install dummy 2.0.0" 2>/dev/null - echo "$output" | grep -q "asdf install dummy 1.3" 2>/dev/null - echo "$output" | grep -q "or add one of the following versions in your config file at $PROJECT_DIR/.tool-versions" 2>/dev/null - echo "$output" | grep -q "dummy 1.0" 2>/dev/null -} +# run "$ASDF_DIR/shims/dummy" world hello +# [ "$status" -eq 126 ] +# echo "$output" | grep -q "No preset version installed for command dummy" 2>/dev/null +# echo "$output" | grep -q "Please install a version by running one of the following:" 2>/dev/null +# echo "$output" | grep -q "asdf install dummy 2.0.0" 2>/dev/null +# echo "$output" | grep -q "asdf install dummy 1.3" 2>/dev/null +# echo "$output" | grep -q "or add one of the following versions in your config file at $PROJECT_DIR/.tool-versions" 2>/dev/null +# echo "$output" | grep -q "dummy 1.0" 2>/dev/null +#} @test "shim exec should execute first plugin that is installed and set" { run asdf install dummy 2.0.0 @@ -199,7 +202,8 @@ teardown() { echo "dummy system" >"$PROJECT_DIR/.tool-versions" mkdir "$PROJECT_DIR/foo/" - echo "echo System" >"$PROJECT_DIR/foo/dummy" + echo "#!/usr/bin/env bash + echo System" >"$PROJECT_DIR/foo/dummy" chmod +x "$PROJECT_DIR/foo/dummy" run env "PATH=$PATH:$PROJECT_DIR/foo" "$ASDF_DIR/shims/dummy" hello @@ -214,7 +218,8 @@ teardown() { CUSTOM_DUMMY_PATH="$PROJECT_DIR/foo" CUSTOM_DUMMY_BIN_PATH="$CUSTOM_DUMMY_PATH/bin" mkdir -p "$CUSTOM_DUMMY_BIN_PATH" - echo "echo System" >"$CUSTOM_DUMMY_BIN_PATH/dummy" + echo "#!/usr/bin/env bash + echo System" >"$CUSTOM_DUMMY_BIN_PATH/dummy" chmod +x "$CUSTOM_DUMMY_BIN_PATH/dummy" echo "dummy path:$CUSTOM_DUMMY_PATH" >"$PROJECT_DIR/.tool-versions" @@ -230,7 +235,8 @@ teardown() { echo "dummy 2.0.0" >>"$PROJECT_DIR/.tool-versions" mkdir "$PROJECT_DIR/foo/" - echo "echo System" >"$PROJECT_DIR/foo/dummy" + echo "#!/usr/bin/env bash + echo System" >"$PROJECT_DIR/foo/dummy" chmod +x "$PROJECT_DIR/foo/dummy" run env "PATH=$PATH:$PROJECT_DIR/foo" "$ASDF_DIR/shims/dummy" hello