diff --git a/cmd/cmd.go b/cmd/cmd.go index b0e53fbb..9f110609 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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) diff --git a/internal/installs/installs.go b/internal/installs/installs.go index fd18553e..ae0299b0 100644 --- a/internal/installs/installs.go +++ b/internal/installs/installs.go @@ -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) diff --git a/internal/installs/installs_test.go b/internal/installs/installs_test.go index 6a9e4cd6..c1c5b88c 100644 --- a/internal/installs/installs_test.go +++ b/internal/installs/installs_test.go @@ -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) } diff --git a/internal/shims/shims.go b/internal/shims/shims.go index cfc9edd0..c4c511a8 100644 --- a/internal/shims/shims.go +++ b/internal/shims/shims.go @@ -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 { diff --git a/internal/shims/shims_test.go b/internal/shims/shims_test.go index bb9ab093..781c3174 100644 --- a/internal/shims/shims_test.go +++ b/internal/shims/shims_test.go @@ -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 diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 3d1c7a3e..b58f5350 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -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 diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go index e9909090..23132ec6 100644 --- a/internal/versions/versions_test.go +++ b/internal/versions/versions_test.go @@ -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() diff --git a/main_test.go b/main_test.go index 916d89d1..cfa09315 100644 --- a/main_test.go +++ b/main_test.go @@ -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") //})