mirror of
https://github.com/asdf-vm/asdf.git
synced 2024-12-19 09:55:01 -07:00
Merge pull request #60 from asdf-vm/tb/shim-generation
feat(golang-rewrite): shim generation part 1
This commit is contained in:
commit
62c2ba18f4
2
go.mod
2
go.mod
@ -9,6 +9,7 @@ require (
|
||||
github.com/sethvargo/go-envconfig v1.0.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
golang.org/x/sys v0.15.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
)
|
||||
|
||||
@ -37,7 +38,6 @@ require (
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
167
internal/shims/shims.go
Normal file
167
internal/shims/shims.go
Normal file
@ -0,0 +1,167 @@
|
||||
// Package shims manages writing and parsing of asdf shim scripts.
|
||||
package shims
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"asdf/config"
|
||||
"asdf/internal/toolversions"
|
||||
"asdf/internal/versions"
|
||||
"asdf/plugins"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, executablePath := range executables {
|
||||
err := Write(conf, plugin, version, executablePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write generates a shim script and writes it to disk
|
||||
func Write(conf config.Config, plugin plugins.Plugin, version, executablePath string) error {
|
||||
err := ensureShimDirExists(conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shimName := filepath.Base(executablePath)
|
||||
shimPath := Path(conf, shimName)
|
||||
versions := []toolversions.ToolVersions{{Name: plugin.Name, Versions: []string{version}}}
|
||||
|
||||
if _, err := os.Stat(shimPath); err == nil {
|
||||
contents, err := os.ReadFile(shimPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldVersions := parse(string(contents))
|
||||
versions = toolversions.Unique(append(versions, oldVersions...))
|
||||
}
|
||||
|
||||
return os.WriteFile(shimPath, []byte(encode(shimName, versions)), 0o777)
|
||||
}
|
||||
|
||||
// Path returns the path for a shim script
|
||||
func Path(conf config.Config, shimName string) string {
|
||||
return filepath.Join(conf.DataDir, "shims", shimName)
|
||||
}
|
||||
|
||||
func ensureShimDirExists(conf config.Config) error {
|
||||
return os.MkdirAll(filepath.Join(conf.DataDir, "shims"), 0o777)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
dirs, err := ExecutableDirs(plugin)
|
||||
if err != nil {
|
||||
return executables, err
|
||||
}
|
||||
|
||||
installPath := versions.InstallPath(conf, plugin, version)
|
||||
paths := dirsToPaths(dirs, installPath)
|
||||
|
||||
for _, path := range paths {
|
||||
// Walk the directory and any sub directories
|
||||
err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If entry is dir or cannot be executed by the current user ignore it
|
||||
if info.IsDir() || unix.Access(path, unix.X_OK) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
executables = append(executables, path)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return executables, err
|
||||
}
|
||||
}
|
||||
return executables, err
|
||||
}
|
||||
|
||||
// ExecutableDirs returns a slice of directory names that tool executables are
|
||||
// contained in
|
||||
func ExecutableDirs(plugin plugins.Plugin) ([]string, error) {
|
||||
var stdOut strings.Builder
|
||||
var stdErr strings.Builder
|
||||
|
||||
err := plugin.RunCallback("list-bin-paths", []string{}, map[string]string{}, &stdOut, &stdErr)
|
||||
if err != nil {
|
||||
if _, ok := err.(plugins.NoCallbackError); ok {
|
||||
// assume all executables are located in /bin directory
|
||||
return []string{"bin"}, nil
|
||||
}
|
||||
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
// use output from list-bin-paths to determine locations for executables
|
||||
rawDirs := strings.Split(stdOut.String(), " ")
|
||||
var dirs []string
|
||||
|
||||
for _, dir := range rawDirs {
|
||||
dirs = append(dirs, strings.TrimSpace(dir))
|
||||
}
|
||||
|
||||
return dirs, nil
|
||||
}
|
||||
|
||||
func parse(contents string) (versions []toolversions.ToolVersions) {
|
||||
lines := strings.Split(contents, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "# asdf-plugin:") {
|
||||
segments := strings.Split(line, " ")
|
||||
// if doesn't have expected number of elements on line skip
|
||||
if len(segments) >= 4 {
|
||||
versions = append(versions, toolversions.ToolVersions{Name: segments[2], Versions: []string{segments[3]}})
|
||||
}
|
||||
}
|
||||
}
|
||||
return versions
|
||||
}
|
||||
|
||||
func encode(shimName string, toolVersions []toolversions.ToolVersions) string {
|
||||
var content string
|
||||
|
||||
content = "#!/usr/bin/env bash\n"
|
||||
|
||||
// Add all asdf-plugin comments
|
||||
for _, tool := range toolVersions {
|
||||
for _, version := range tool.Versions {
|
||||
content += fmt.Sprintf("# asdf-plugin: %s %s\n", tool.Name, version)
|
||||
}
|
||||
}
|
||||
|
||||
// Add call asdf exec to actually run real command
|
||||
content += fmt.Sprintf("exec asdf exec \"%s\" \"$@\"", shimName)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func dirsToPaths(dirs []string, root string) (paths []string) {
|
||||
for _, dir := range dirs {
|
||||
paths = append(paths, filepath.Join(root, dir))
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
179
internal/shims/shims_test.go
Normal file
179
internal/shims/shims_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
package shims
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"asdf/config"
|
||||
"asdf/internal/versions"
|
||||
"asdf/plugins"
|
||||
"asdf/repotest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const testPluginName = "lua"
|
||||
|
||||
func TestGenerateForVersion(t *testing.T) {
|
||||
version := "1.1.0"
|
||||
version2 := "2.0.0"
|
||||
conf, plugin := generateConfig(t)
|
||||
installVersion(t, conf, plugin, version)
|
||||
installVersion(t, conf, plugin, version2)
|
||||
executables, err := ToolExecutables(conf, plugin, 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))
|
||||
|
||||
// check for generated shims
|
||||
for _, executable := range executables {
|
||||
shimName := filepath.Base(executable)
|
||||
shimPath := Path(conf, shimName)
|
||||
assert.Nil(t, unix.Access(shimPath, unix.X_OK))
|
||||
}
|
||||
})
|
||||
|
||||
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))
|
||||
|
||||
// check for generated shims
|
||||
for _, executable := range executables {
|
||||
shimName := filepath.Base(executable)
|
||||
shimPath := Path(conf, shimName)
|
||||
assert.Nil(t, unix.Access(shimPath, unix.X_OK))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
version := "1.1.0"
|
||||
version2 := "2.0.0"
|
||||
conf, plugin := generateConfig(t)
|
||||
installVersion(t, conf, plugin, version)
|
||||
installVersion(t, conf, plugin, version2)
|
||||
executables, err := ToolExecutables(conf, plugin, version)
|
||||
executable := executables[0]
|
||||
assert.Nil(t, err)
|
||||
|
||||
t.Run("writes a new shim file when doesn't exist", func(t *testing.T) {
|
||||
executable := executables[0]
|
||||
err = Write(conf, plugin, version, executable)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// shim is executable
|
||||
shimName := filepath.Base(executable)
|
||||
shimPath := Path(conf, shimName)
|
||||
assert.Nil(t, unix.Access(shimPath, unix.X_OK))
|
||||
|
||||
// shim exists and has expected contents
|
||||
content, err := os.ReadFile(shimPath)
|
||||
assert.Nil(t, err)
|
||||
want := "#!/usr/bin/env bash\n# asdf-plugin: lua 1.1.0\nexec asdf exec \"dummy\" \"$@\""
|
||||
assert.Equal(t, want, string(content))
|
||||
os.Remove(shimPath)
|
||||
})
|
||||
|
||||
t.Run("updates an existing shim file when already present", func(t *testing.T) {
|
||||
// Write same shim for two versions
|
||||
assert.Nil(t, Write(conf, plugin, version, executable))
|
||||
assert.Nil(t, Write(conf, plugin, version2, executable))
|
||||
|
||||
// shim is still executable
|
||||
shimName := filepath.Base(executable)
|
||||
shimPath := Path(conf, shimName)
|
||||
assert.Nil(t, unix.Access(shimPath, unix.X_OK))
|
||||
|
||||
// has expected contents
|
||||
content, err := os.ReadFile(shimPath)
|
||||
assert.Nil(t, err)
|
||||
want := "#!/usr/bin/env bash\n# asdf-plugin: lua 2.0.0\n# asdf-plugin: lua 1.1.0\nexec asdf exec \"dummy\" \"$@\""
|
||||
assert.Equal(t, want, string(content))
|
||||
os.Remove(shimPath)
|
||||
})
|
||||
|
||||
t.Run("doesn't add the same version to a shim file twice", func(t *testing.T) {
|
||||
assert.Nil(t, Write(conf, plugin, version, executable))
|
||||
assert.Nil(t, Write(conf, plugin, version, executable))
|
||||
|
||||
// Shim doesn't contain any duplicate lines
|
||||
shimPath := Path(conf, filepath.Base(executable))
|
||||
content, err := os.ReadFile(shimPath)
|
||||
assert.Nil(t, err)
|
||||
want := "#!/usr/bin/env bash\n# asdf-plugin: lua 1.1.0\nexec asdf exec \"dummy\" \"$@\""
|
||||
assert.Equal(t, want, string(content))
|
||||
os.Remove(shimPath)
|
||||
})
|
||||
}
|
||||
|
||||
func TestToolExecutables(t *testing.T) {
|
||||
version := "1.1.0"
|
||||
conf, plugin := generateConfig(t)
|
||||
installVersion(t, conf, plugin, version)
|
||||
|
||||
t.Run("returns list of executables for plugin", func(t *testing.T) {
|
||||
executables, err := ToolExecutables(conf, plugin, 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", "other_bin"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecutableDirs(t *testing.T) {
|
||||
conf, plugin := generateConfig(t)
|
||||
installVersion(t, conf, plugin, "1.2.3")
|
||||
|
||||
t.Run("returns list only containing 'bin' when list-bin-paths callback missing", func(t *testing.T) {
|
||||
executables, err := ExecutableDirs(plugin)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, executables, []string{"bin"})
|
||||
})
|
||||
|
||||
t.Run("returns list of executable paths for tool version", func(t *testing.T) {
|
||||
data := []byte("echo 'foo bar'")
|
||||
err := os.WriteFile(filepath.Join(plugin.Dir, "bin", "list-bin-paths"), data, 0o777)
|
||||
assert.Nil(t, err)
|
||||
|
||||
executables, err := ExecutableDirs(plugin)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, executables, []string{"foo", "bar"})
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func buildOutputs() (strings.Builder, strings.Builder) {
|
||||
var stdout strings.Builder
|
||||
var stderr strings.Builder
|
||||
|
||||
return stdout, stderr
|
||||
}
|
||||
|
||||
func generateConfig(t *testing.T) (config.Config, plugins.Plugin) {
|
||||
t.Helper()
|
||||
testDataDir := t.TempDir()
|
||||
conf, err := config.LoadConfig()
|
||||
assert.Nil(t, err)
|
||||
conf.DataDir = testDataDir
|
||||
|
||||
_, err = repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName)
|
||||
assert.Nil(t, err)
|
||||
|
||||
return conf, plugins.New(conf, testPluginName)
|
||||
}
|
||||
|
||||
func installVersion(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) {
|
||||
t.Helper()
|
||||
stdout, stderr := buildOutputs()
|
||||
err := versions.InstallOneVersion(conf, plugin, version, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
}
|
@ -4,6 +4,7 @@ package toolversions
|
||||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -25,17 +26,6 @@ func FindToolVersions(filepath, toolName string) (versions []string, found bool,
|
||||
return versions, found, nil
|
||||
}
|
||||
|
||||
func findToolVersionsInContent(content, toolName string) (versions []string, found bool) {
|
||||
toolVersions := getAllToolsAndVersionsInContent(content)
|
||||
for _, tool := range toolVersions {
|
||||
if tool.Name == toolName {
|
||||
return tool.Versions, true
|
||||
}
|
||||
}
|
||||
|
||||
return versions, found
|
||||
}
|
||||
|
||||
// GetAllToolsAndVersions returns a list of all tools and associated versions
|
||||
// contained in a .tool-versions file
|
||||
func GetAllToolsAndVersions(filepath string) (toolVersions []ToolVersions, err error) {
|
||||
@ -48,14 +38,34 @@ func GetAllToolsAndVersions(filepath string) (toolVersions []ToolVersions, err e
|
||||
return toolVersions, nil
|
||||
}
|
||||
|
||||
func getAllToolsAndVersionsInContent(content string) (toolVersions []ToolVersions) {
|
||||
for _, line := range readLines(content) {
|
||||
tokens := parseLine(line)
|
||||
newTool := ToolVersions{Name: tokens[0], Versions: tokens[1:]}
|
||||
toolVersions = append(toolVersions, newTool)
|
||||
// Unique takes a slice of ToolVersions and returns a slice of unique tools and
|
||||
// versions.
|
||||
func Unique(versions []ToolVersions) (uniques []ToolVersions) {
|
||||
for _, version := range versions {
|
||||
var found bool
|
||||
|
||||
for index, unique := range uniques {
|
||||
if unique.Name == version.Name {
|
||||
// Duplicate name, check versions
|
||||
for _, versionNumber := range version.Versions {
|
||||
if !slices.Contains(unique.Versions, versionNumber) {
|
||||
unique.Versions = append(unique.Versions, versionNumber)
|
||||
}
|
||||
}
|
||||
|
||||
uniques[index] = unique
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// None with name found, add
|
||||
if !found {
|
||||
uniques = append(uniques, version)
|
||||
}
|
||||
}
|
||||
|
||||
return toolVersions
|
||||
return uniques
|
||||
}
|
||||
|
||||
// readLines reads all the lines in a given file
|
||||
@ -71,6 +81,27 @@ func readLines(content string) (lines []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func findToolVersionsInContent(content, toolName string) (versions []string, found bool) {
|
||||
toolVersions := getAllToolsAndVersionsInContent(content)
|
||||
for _, tool := range toolVersions {
|
||||
if tool.Name == toolName {
|
||||
return tool.Versions, true
|
||||
}
|
||||
}
|
||||
|
||||
return versions, found
|
||||
}
|
||||
|
||||
func getAllToolsAndVersionsInContent(content string) (toolVersions []ToolVersions) {
|
||||
for _, line := range readLines(content) {
|
||||
tokens := parseLine(line)
|
||||
newTool := ToolVersions{Name: tokens[0], Versions: tokens[1:]}
|
||||
toolVersions = append(toolVersions, newTool)
|
||||
}
|
||||
|
||||
return toolVersions
|
||||
}
|
||||
|
||||
func parseLine(line string) (tokens []string) {
|
||||
for _, token := range strings.Split(line, " ") {
|
||||
token = strings.TrimSpace(token)
|
||||
|
@ -51,6 +51,37 @@ func TestFindToolVersions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnique(t *testing.T) {
|
||||
t.Run("returns unique slice of tool versions when tool appears multiple times in slice", func(t *testing.T) {
|
||||
got := Unique([]ToolVersions{
|
||||
{Name: "foo", Versions: []string{"1"}},
|
||||
{Name: "foo", Versions: []string{"2"}},
|
||||
})
|
||||
|
||||
want := []ToolVersions{
|
||||
{Name: "foo", Versions: []string{"1", "2"}},
|
||||
}
|
||||
|
||||
assert.Equal(t, got, want)
|
||||
})
|
||||
|
||||
t.Run("returns unique slice of tool versions when given slice with multiple tools", func(t *testing.T) {
|
||||
got := Unique([]ToolVersions{
|
||||
{Name: "foo", Versions: []string{"1"}},
|
||||
{Name: "bar", Versions: []string{"2"}},
|
||||
{Name: "foo", Versions: []string{"2"}},
|
||||
{Name: "bar", Versions: []string{"2"}},
|
||||
})
|
||||
|
||||
want := []ToolVersions{
|
||||
{Name: "foo", Versions: []string{"1", "2"}},
|
||||
{Name: "bar", Versions: []string{"2"}},
|
||||
}
|
||||
|
||||
assert.Equal(t, got, want)
|
||||
})
|
||||
}
|
||||
|
||||
func TestfindToolVersionsInContent(t *testing.T) {
|
||||
t.Run("returns empty list with found false when empty content", func(t *testing.T) {
|
||||
versions, found := findToolVersionsInContent("", "ruby")
|
||||
|
@ -131,7 +131,7 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string
|
||||
}
|
||||
|
||||
downloadDir := downloadPath(conf, plugin, version)
|
||||
installDir := installPath(conf, plugin, version)
|
||||
installDir := InstallPath(conf, plugin, version)
|
||||
versionType, version := ParseString(version)
|
||||
|
||||
if Installed(conf, plugin, version) {
|
||||
@ -200,7 +200,7 @@ func asdfConcurrency(conf config.Config) string {
|
||||
|
||||
// Installed checks if a specific version of a tool is installed
|
||||
func Installed(conf config.Config, plugin plugins.Plugin, version string) bool {
|
||||
installDir := installPath(conf, plugin, version)
|
||||
installDir := InstallPath(conf, plugin, version)
|
||||
|
||||
// Check if version already installed
|
||||
_, err := os.Stat(installDir)
|
||||
@ -319,6 +319,7 @@ func downloadPath(conf config.Config, plugin plugins.Plugin, version string) str
|
||||
return filepath.Join(conf.DataDir, dataDirDownloads, plugin.Name, version)
|
||||
}
|
||||
|
||||
func installPath(conf config.Config, plugin plugins.Plugin, version string) string {
|
||||
// InstallPath returns the path to a tool installation
|
||||
func InstallPath(conf config.Config, plugin plugins.Plugin, version string) string {
|
||||
return filepath.Join(conf.DataDir, dataDirInstalls, plugin.Name, version)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user