Skip to content

Commit

Permalink
feat(golang-rewrite): implement asdf env command
Browse files Browse the repository at this point in the history
* Create `CallbackPath` method on `Plugin` struct
* Correct behavior of `asdf shimversions` command
* Update `shims.FindExecutable` function to return plugin
* Create `repotest.WritePluginCallback` function
* Create `execenv` package for invoking `exec-env` plugin callback
* Make `MapToSlice` a public function
* Return resolved version from `shims.FindExecutable` function
* Create `shims.ExecutablePaths` function
* Enable `shim_env_command.bats` tests
* Implement `asdf env` command
  • Loading branch information
Stratus3D committed Dec 18, 2024
1 parent 0e43521 commit 26a3815
Show file tree
Hide file tree
Showing 11 changed files with 390 additions and 59 deletions.
151 changes: 138 additions & 13 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import (
"io"
"log"
"os"
osexec "os/exec"
"path/filepath"
"slices"
"strings"
"text/tabwriter"

"asdf/internal/config"
"asdf/internal/exec"
"asdf/internal/execenv"
"asdf/internal/execute"
"asdf/internal/help"
"asdf/internal/info"
"asdf/internal/installs"
"asdf/internal/paths"
"asdf/internal/plugins"
"asdf/internal/resolve"
"asdf/internal/shims"
Expand Down Expand Up @@ -59,6 +63,15 @@ func Execute(version string) {
return currentCommand(logger, tool)
},
},
{
Name: "env",
Action: func(cCtx *cli.Context) error {
shimmedCommand := cCtx.Args().Get(0)
args := cCtx.Args().Slice()

return envCommand(logger, shimmedCommand, args)
},
},
{
Name: "exec",
Action: func(cCtx *cli.Context) error {
Expand Down Expand Up @@ -335,6 +348,83 @@ func formatVersions(versions []string) string {
}
}

func envCommand(logger *log.Logger, shimmedCommand string, args []string) error {
command := "env"

if shimmedCommand == "" {
logger.Printf("usage: asdf env <command>")
return fmt.Errorf("usage: asdf env <command>")
}

if len(args) >= 2 {
command = args[1]
}

realArgs := []string{}
if len(args) > 2 {
realArgs = args[2:]
}

conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
return err
}

_, plugin, version, err := getExecutable(logger, conf, shimmedCommand)
if err != nil {
return err
}

parsedVersion := toolversions.Parse(version)
execPaths, err := shims.ExecutablePaths(conf, plugin, parsedVersion)
if err != nil {
return err
}
callbackEnv := map[string]string{
"ASDF_INSTALL_TYPE": parsedVersion.Type,
"ASDF_INSTALL_VERSION": parsedVersion.Value,
"ASDF_INSTALL_PATH": installs.InstallPath(conf, plugin, parsedVersion),
"PATH": setPath(conf, execPaths),
}

var env map[string]string
var fname string

if parsedVersion.Type == "system" {
env = execute.SliceToMap(os.Environ())
newPath := paths.RemoveFromPath(env["PATH"], shims.Directory(conf))
env["PATH"] = newPath
var found bool
fname, found = shims.FindSystemExecutable(conf, command)
if !found {
fmt.Println("not found")
return err
}
} else {
env, err = execenv.Generate(plugin, callbackEnv)
if _, ok := err.(plugins.NoCallbackError); !ok && err != nil {
return err
}

fname, err = osexec.LookPath(command)
if err != nil {
return err
}
}

err = exec.Exec(fname, realArgs, execute.MapToSlice(env))
if err != nil {
fmt.Printf("err %#+v\n", err.Error())
}
return err
}

func setPath(conf config.Config, pathes []string) string {
currentPath := os.Getenv("PATH")
return strings.Join(pathes, ":") + ":" + paths.RemoveFromPath(currentPath, shims.Directory(conf))
}

func execCommand(logger *log.Logger, command string, args []string) error {
if command == "" {
logger.Printf("usage: asdf exec <command>")
Expand All @@ -347,16 +437,51 @@ func execCommand(logger *log.Logger, command string, args []string) error {
return err
}

currentDir, err := os.Getwd()
executable, plugin, version, err := getExecutable(logger, conf, command)
fmt.Printf("version %#+v\n", version)
fmt.Println("here")
if err != nil {
return err
}

if len(args) > 1 {
args = args[1:]
} else {
args = []string{}
}

parsedVersion := toolversions.Parse(version)
fmt.Printf("parsedVersion %#+v\n", parsedVersion)
paths, err := shims.ExecutablePaths(conf, plugin, parsedVersion)
if err != nil {
logger.Printf("unable to get current directory: %s", err)
return err
}
callbackEnv := map[string]string{
"ASDF_INSTALL_TYPE": parsedVersion.Type,
"ASDF_INSTALL_VERSION": parsedVersion.Value,
"ASDF_INSTALL_PATH": installs.InstallPath(conf, plugin, parsedVersion),
"PATH": setPath(conf, paths),
}

env, _ := execenv.Generate(plugin, callbackEnv)
return exec.Exec(executable, args, execute.MapToSlice(env))
}

executable, found, err := shims.FindExecutable(conf, command, currentDir)
func getExecutable(logger *log.Logger, conf config.Config, command string) (executable string, plugin plugins.Plugin, version string, err error) {
currentDir, err := os.Getwd()
if err != nil {
logger.Printf("unable to get current directory: %s", err)
return "", plugins.Plugin{}, "", err
}

executable, plugin, version, found, err := shims.FindExecutable(conf, command, currentDir)
if err != nil {

if _, ok := err.(shims.NoExecutableForPluginError); ok {
logger.Printf("No executable %s found for current version. Please select a different version or install %s manually for the current version", command, command)
os.Exit(1)
return "", plugin, version, err
}
shimPath := shims.Path(conf, command)
toolVersions, _ := shims.GetToolsAndVersionsFromShimFile(shimPath)

Expand All @@ -383,21 +508,16 @@ func execCommand(logger *log.Logger, command string, args []string) error {
}

os.Exit(126)
return err
return executable, plugins.Plugin{}, "", err
}

if !found {
logger.Print("executable not found")
os.Exit(126)
return fmt.Errorf("executable not found")
}
if len(args) > 1 {
args = args[1:]
} else {
args = []string{}
return executable, plugins.Plugin{}, "", fmt.Errorf("executable not found")
}

return exec.Exec(executable, args, os.Environ())
return executable, plugin, version, nil
}

func anyInstalled(conf config.Config, toolVersions []toolversions.ToolVersions) bool {
Expand Down Expand Up @@ -842,6 +962,11 @@ func reshimCommand(logger *log.Logger, tool, version string) (err error) {
}

func shimVersionsCommand(logger *log.Logger, shimName string) error {
if shimName == "" {
logger.Printf("usage: asdf shimversions <command>")
return fmt.Errorf("usage: asdf shimversions <command>")
}

conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
Expand All @@ -852,7 +977,7 @@ func shimVersionsCommand(logger *log.Logger, shimName string) error {
toolVersions, err := shims.GetToolsAndVersionsFromShimFile(shimPath)
for _, toolVersion := range toolVersions {
for _, version := range toolVersion.Versions {
fmt.Printf("%s %s", toolVersion.Name, version)
fmt.Printf("%s %s\n", toolVersion.Name, version)
}
}
return err
Expand All @@ -877,7 +1002,7 @@ func whichCommand(logger *log.Logger, command string) error {
return errors.New("must provide command")
}

path, _, err := shims.FindExecutable(conf, command, currentDir)
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")
Expand Down
1 change: 1 addition & 0 deletions docs/guide/upgrading-from-v0-14-to-v0-15.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ versions are supported. The affected commands:
* `asdf plugin-list-all` -> `asdf plugin list all`
* `asdf plugin-update` -> `asdf plugin update`
* `asdf plugin-remove` -> `asdf plugin remove`
* `asdf shim-versions` -> `asdf shimversions`

### `asdf global` and `asdf local` commands have been replaced by the `asdf set` command

Expand Down
49 changes: 49 additions & 0 deletions internal/execenv/execenv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Package execenv contains logic for generating execution environing using a plugin's
// exec-env callback script if available.
package execenv

import (
"fmt"
"strings"

"asdf/internal/execute"
"asdf/internal/plugins"
)

const execEnvCallbackName = "exec-env"

// Generate runs exec-env callback if available and captures the environment
// variables it sets. It then parses them and returns them as a map.
func Generate(plugin plugins.Plugin, callbackEnv map[string]string) (env map[string]string, err error) {
execEnvPath, err := plugin.CallbackPath(execEnvCallbackName)
if err != nil {
return callbackEnv, err
}

var stdout strings.Builder

// This is done to support the legacy behavior. exec-env is the only asdf
// callback that works by exporting environment variables. Because of this,
// executing the callback isn't enough. We actually need to source it (.) so
// the environment variables get set, and then run `env` so they get printed
// to STDOUT.
expression := execute.NewExpression(fmt.Sprintf(". \"%s\"; env", execEnvPath), []string{})
expression.Env = callbackEnv
expression.Stdout = &stdout
err = expression.Run()

return envMap(stdout.String()), err
}

func envMap(env string) map[string]string {
slice := map[string]string{}

for _, envVar := range strings.Split(env, "\n") {
varValue := strings.Split(envVar, "=")
if len(varValue) == 2 {
slice[varValue[0]] = varValue[1]
}
}

return slice
}
43 changes: 43 additions & 0 deletions internal/execenv/execenv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package execenv

import (
"testing"

"asdf/internal/config"
"asdf/internal/plugins"
"asdf/repotest"

"github.com/stretchr/testify/assert"
)

const (
testPluginName = "lua"
testPluginName2 = "ruby"
)

func TestGenerate(t *testing.T) {
testDataDir := t.TempDir()

t.Run("returns map of environment variables", func(t *testing.T) {
conf := config.Config{DataDir: testDataDir}
_, err := repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName)
assert.Nil(t, err)
plugin := plugins.New(conf, testPluginName)
assert.Nil(t, repotest.WritePluginCallback(plugin.Dir, "exec-env", "#!/usr/bin/env bash\nexport BAZ=bar"))
env, err := Generate(plugin, map[string]string{"ASDF_INSTALL_VERSION": "test"})
assert.Nil(t, err)
assert.Equal(t, "bar", env["BAZ"])
assert.Equal(t, "test", env["ASDF_INSTALL_VERSION"])
})

t.Run("returns error when plugin lacks exec-env callback", func(t *testing.T) {
conf := config.Config{DataDir: testDataDir}
_, err := repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName2)
assert.Nil(t, err)
plugin := plugins.New(conf, testPluginName2)
env, err := Generate(plugin, map[string]string{})
assert.Equal(t, err.(plugins.NoCallbackError).Error(), "Plugin named ruby does not have a callback named exec-env")
_, found := env["FOO"]
assert.False(t, found)
})
}
33 changes: 24 additions & 9 deletions internal/execute/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (c Command) Run() error {

cmd := exec.Command("bash", "-c", command)

cmd.Env = mapToSlice(c.Env)
cmd.Env = MapToSlice(c.Env)
cmd.Stdin = c.Stdin

// Capture stdout and stderr
Expand All @@ -57,18 +57,33 @@ func (c Command) Run() error {
return cmd.Run()
}

// MapToSlice converts an env map to env slice suitable for syscall.Exec
func MapToSlice(env map[string]string) (slice []string) {
for key, value := range env {
slice = append(slice, fmt.Sprintf("%s=%s", key, value))
}

return slice
}

// SliceToMap converts an env map to env slice suitable for syscall.Exec
func SliceToMap(env []string) map[string]string {
envMap := map[string]string{}

for _, envVar := range env {
varValue := strings.Split(envVar, "=")
if len(varValue) == 2 {
envMap[varValue[0]] = varValue[1]
}
}

return envMap
}

func formatArgString(args []string) string {
var newArgs []string
for _, str := range args {
newArgs = append(newArgs, fmt.Sprintf("\"%s\"", str))
}
return strings.Join(newArgs, " ")
}

func mapToSlice(env map[string]string) (slice []string) {
for key, value := range env {
slice = append(slice, fmt.Sprintf("%s=%s", key, value))
}

return slice
}
Loading

0 comments on commit 26a3815

Please sign in to comment.