From d2afb85eb80bce85e79c9c0d91ad3103a7f985f0 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Mon, 16 Sep 2024 21:11:39 -0400 Subject: [PATCH] feat(golang-rewrite): create `asdf which` command * create asdf which command * enable which_command.bats tests * add more info to NoExecutableForPluginError * Write tests for shims.GetExecutablePath function * Use plugin `exec-path` callback when present to compute executable path --- cmd/cmd.go | 47 ++++++++++++++++++++++++++++++++++++ internal/exec/exec_test.go | 1 - internal/shims/shims.go | 37 +++++++++++++++++++++++++--- internal/shims/shims_test.go | 43 +++++++++++++++++++++++++++++++++ main_test.go | 6 ++--- 5 files changed, 126 insertions(+), 8 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index bf3df123..567f1cb9 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -166,6 +166,14 @@ func Execute(version string) { return reshimCommand(logger, args.Get(0), args.Get(1)) }, }, + { + Name: "which", + Action: func(cCtx *cli.Context) error { + tool := cCtx.Args().Get(0) + + return whichCommand(logger, tool) + }, + }, }, Action: func(_ *cli.Context) error { // TODO: flesh this out @@ -577,6 +585,45 @@ func reshimCommand(logger *log.Logger, tool, version string) (err error) { return reshimToolVersion(conf, tool, version, os.Stdout, os.Stderr) } +// This function is a whole mess and needs to be refactored +func whichCommand(logger *log.Logger, command string) error { + conf, err := config.LoadConfig() + if err != nil { + logger.Printf("error loading config: %s", err) + return err + } + + currentDir, err := os.Getwd() + if err != nil { + logger.Printf("unable to get current directory: %s", err) + return err + } + + if command == "" { + fmt.Println("usage: asdf which ") + return errors.New("must provide command") + } + + path, _, err := shims.FindExecutable(conf, command, currentDir) + if _, ok := err.(shims.UnknownCommandError); ok { + logger.Printf("unknown command: %s. Perhaps you have to reshim?", command) + return errors.New("command not found") + } + + if _, ok := err.(shims.NoExecutableForPluginError); ok { + logger.Printf("%s", err.Error()) + return errors.New("no executable for tool version") + } + + if err != nil { + fmt.Printf("unexpected error: %s\n", err.Error()) + return err + } + + fmt.Printf("%s\n", path) + return nil +} + 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) diff --git a/internal/exec/exec_test.go b/internal/exec/exec_test.go index 31b063ea..50ba550f 100644 --- a/internal/exec/exec_test.go +++ b/internal/exec/exec_test.go @@ -13,7 +13,6 @@ func execit() int { // Exec only works with absolute path cmdPath, _ := exec.LookPath(os.Args[1]) err := Exec(cmdPath, os.Args[2:], os.Environ()) - if err != nil { fmt.Printf("Err: %#+v\n", err.Error()) } diff --git a/internal/shims/shims.go b/internal/shims/shims.go index 78cf88f6..3504d159 100644 --- a/internal/shims/shims.go +++ b/internal/shims/shims.go @@ -45,11 +45,13 @@ func (e NoVersionSetError) Error() string { // NoExecutableForPluginError is returned when a compatible version is found // but no executable matching the name is located. type NoExecutableForPluginError struct { - shim string + shim string + tools []string + versions []string } func (e NoExecutableForPluginError) Error() string { - return fmt.Sprintf("no %s executable for plugin %s", e.shim, "") + return fmt.Sprintf("No %s executable found for %s %s", e.shim, strings.Join(e.tools, ", "), strings.Join(e.versions, ", ")) } // FindExecutable takes a shim name and a current directory and returns the path @@ -110,7 +112,14 @@ func FindExecutable(conf config.Config, shimName, currentDirectory string) (stri } } - return "", false, NoExecutableForPluginError{shim: shimName} + tools := []string{} + versions := []string{} + for plugin := range existingPluginToolVersions { + tools = append(tools, plugin.Name) + versions = append(versions, existingPluginToolVersions[plugin].Versions...) + } + + return "", false, NoExecutableForPluginError{shim: shimName, tools: tools, versions: versions} } // FindSystemExecutable returns the path to the system @@ -125,6 +134,11 @@ func FindSystemExecutable(conf config.Config, executableName string) (string, bo // GetExecutablePath returns the path of the executable func GetExecutablePath(conf config.Config, plugin plugins.Plugin, shimName, version string) (string, error) { + path, err := getCustomExecutablePath(conf, plugin, shimName, version) + if err == nil { + return path, err + } + executables, err := ToolExecutables(conf, plugin, "version", version) if err != nil { return "", err @@ -140,6 +154,21 @@ func GetExecutablePath(conf config.Config, plugin plugins.Plugin, shimName, vers return "", fmt.Errorf("executable not found") } +func getCustomExecutablePath(conf config.Config, plugin plugins.Plugin, shimName, version string) (string, error) { + var stdOut strings.Builder + var stdErr strings.Builder + + installPath := installs.InstallPath(conf, plugin, "version", version) + env := map[string]string{"ASDF_INSTALL_TYPE": "version"} + + err := plugin.RunCallback("exec-path", []string{installPath, shimName}, env, &stdOut, &stdErr) + if err != nil { + return "", err + } + + return filepath.Join(installPath, stdOut.String()), err +} + func getToolsAndVersionsFromShimFile(shimPath string) (versions []toolversions.ToolVersions, err error) { contents, err := os.ReadFile(shimPath) if err != nil { @@ -283,7 +312,7 @@ func ToolExecutables(conf config.Config, plugin plugins.Plugin, versionType, ver // If entry is dir or cannot be executed by the current user ignore it filePath := filepath.Join(path, entry.Name()) if entry.IsDir() || unix.Access(filePath, unix.X_OK) != nil { - return executables, nil + continue } executables = append(executables, filePath) diff --git a/internal/shims/shims_test.go b/internal/shims/shims_test.go index c13f6047..7041e2eb 100644 --- a/internal/shims/shims_test.go +++ b/internal/shims/shims_test.go @@ -74,6 +74,34 @@ func TestFindExecutable(t *testing.T) { }) } +func TestGetExecutablePath(t *testing.T) { + version := "1.1.0" + conf, plugin := generateConfig(t) + installVersion(t, conf, plugin, version) + + t.Run("returns path to executable", func(t *testing.T) { + path, err := GetExecutablePath(conf, plugin, "dummy", version) + assert.Nil(t, err) + assert.Equal(t, filepath.Base(path), "dummy") + assert.Equal(t, filepath.Base(filepath.Dir(filepath.Dir(path))), version) + }) + + t.Run("returns error when executable with name not found", func(t *testing.T) { + path, err := GetExecutablePath(conf, plugin, "foo", version) + assert.ErrorContains(t, err, "executable not found") + assert.Equal(t, path, "") + }) + + t.Run("returns custom path when plugin has exec-path callback", func(t *testing.T) { + // Create exec-path callback + installDummyExecPathScript(t, conf, plugin, version, "dummy") + + path, err := GetExecutablePath(conf, plugin, "dummy", version) + assert.Nil(t, err) + assert.Equal(t, filepath.Base(filepath.Dir(path)), "custom") + }) +} + func TestRemoveAll(t *testing.T) { version := "1.1.0" conf, plugin := generateConfig(t) @@ -329,6 +357,21 @@ 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) { + 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) + err = os.MkdirAll(filepath.Join(installPath, "bin", "custom"), 0o777) + assert.Nil(t, err) + + err = os.WriteFile(filepath.Join(installPath, "bin", "custom", name), []byte{}, 0o777) + assert.Nil(t, err) +} + func installPlugin(t *testing.T, conf config.Config, fixture, pluginName string) plugins.Plugin { _, err := repotest.InstallPlugin(fixture, conf.DataDir, pluginName) assert.Nil(t, err) diff --git a/main_test.go b/main_test.go index 5e2396be..2aa13499 100644 --- a/main_test.go +++ b/main_test.go @@ -99,9 +99,9 @@ func TestBatsTests(t *testing.T) { // runBatsFile(t, dir, "where_command.bats") //}) - //t.Run("which_command", func(t *testing.T) { - // runBatsFile(t, dir, "which_command.bats") - //}) + t.Run("which_command", func(t *testing.T) { + runBatsFile(t, dir, "which_command.bats") + }) } func runBatsFile(t *testing.T, dir, filename string) {