mirror of
https://github.com/asdf-vm/asdf.git
synced 2024-12-24 20:35:03 -07:00
5d5d04fbb7
* Replace direct `fmt.Println()` usage in a test with `t.Log()` * Rename `cmd` to `cli` * Move asdf command from module root * Fix some linter warnings, thus enabling some tests that were being skipped * Fix `Makefile` * Rename module to `github.com/asdf-vm/asdf` * Fix `TestGetAllToolsAndVersionsInContent/returns_empty_list_with_found_true_and_no_error_when_empty_content` * Rewrite `Unique()` to be a bit more straightforwards * Get workflow checks passing again toolversions.Unique is ever so slightly faster, technically. ``` goos: linux goarch: amd64 pkg: github.com/asdf-vm/asdf/internal/toolversions cpu: AMD Ryzen 9 3900X 12-Core Processor │ /tmp/old.txt │ /tmp/new.txt │ │ sec/op │ sec/op vs base │ Unique-24 346.5n ± 1% 342.4n ± 1% -1.17% (p=0.027 n=10) │ /tmp/old.txt │ /tmp/new.txt │ │ B/op │ B/op vs base │ Unique-24 160.0 ± 0% 160.0 ± 0% ~ (p=1.000 n=10) ¹ ¹ all samples are equal │ /tmp/old.txt │ /tmp/new.txt │ │ allocs/op │ allocs/op vs base │ Unique-24 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ ¹ all samples are equal ```
438 lines
13 KiB
Go
438 lines
13 KiB
Go
// Package shims manages writing and parsing of asdf shim scripts.
|
|
package shims
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/asdf-vm/asdf/internal/config"
|
|
"github.com/asdf-vm/asdf/internal/hook"
|
|
"github.com/asdf-vm/asdf/internal/installs"
|
|
"github.com/asdf-vm/asdf/internal/paths"
|
|
"github.com/asdf-vm/asdf/internal/plugins"
|
|
"github.com/asdf-vm/asdf/internal/resolve"
|
|
"github.com/asdf-vm/asdf/internal/toolversions"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const shimDirName = "shims"
|
|
|
|
// UnknownCommandError is an error returned when a shim is not found
|
|
type UnknownCommandError struct {
|
|
shim string
|
|
}
|
|
|
|
func (e UnknownCommandError) Error() string {
|
|
return fmt.Sprintf("unknown command: %s", e.shim)
|
|
}
|
|
|
|
// NoVersionSetError is returned when shim is found but no version matches
|
|
type NoVersionSetError struct {
|
|
shim string
|
|
}
|
|
|
|
func (e NoVersionSetError) Error() string {
|
|
return fmt.Sprintf("no versions set for %s", e.shim)
|
|
}
|
|
|
|
// NoExecutableForPluginError is returned when a compatible version is found
|
|
// but no executable matching the name is located.
|
|
type NoExecutableForPluginError struct {
|
|
shim string
|
|
tools []string
|
|
versions []string
|
|
}
|
|
|
|
func (e NoExecutableForPluginError) Error() string {
|
|
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
|
|
// to the executable that the shim resolves to.
|
|
func FindExecutable(conf config.Config, shimName, currentDirectory string) (string, plugins.Plugin, string, bool, error) {
|
|
shimPath := Path(conf, shimName)
|
|
|
|
if _, err := os.Stat(shimPath); err != nil {
|
|
return "", plugins.Plugin{}, "", false, UnknownCommandError{shim: shimName}
|
|
}
|
|
|
|
toolVersions, err := GetToolsAndVersionsFromShimFile(shimPath)
|
|
if err != nil {
|
|
return "", plugins.Plugin{}, "", false, err
|
|
}
|
|
|
|
existingPluginToolVersions := make(map[plugins.Plugin]resolve.ToolVersions)
|
|
|
|
// loop over tools and check if the plugin for them still exists
|
|
for _, shimToolVersion := range toolVersions {
|
|
plugin := plugins.New(conf, shimToolVersion.Name)
|
|
if plugin.Exists() == nil {
|
|
|
|
versions, found, err := resolve.Version(conf, plugin, currentDirectory)
|
|
if err != nil {
|
|
return "", plugins.Plugin{}, "", false, nil
|
|
}
|
|
|
|
if found {
|
|
tempVersions := toolversions.Intersect(shimToolVersion.Versions, versions.Versions)
|
|
if slices.Contains(versions.Versions, "system") {
|
|
tempVersions = append(tempVersions, "system")
|
|
}
|
|
|
|
parsedVersions := toolversions.ParseSlice(versions.Versions)
|
|
for _, parsedVersion := range parsedVersions {
|
|
if parsedVersion.Type == "path" {
|
|
tempVersions = append(tempVersions, toolversions.Format(parsedVersion))
|
|
}
|
|
}
|
|
|
|
versions.Versions = tempVersions
|
|
existingPluginToolVersions[plugin] = versions
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(existingPluginToolVersions) == 0 {
|
|
return "", plugins.Plugin{}, "", false, NoVersionSetError{shim: shimName}
|
|
}
|
|
|
|
for plugin, toolVersions := range existingPluginToolVersions {
|
|
for _, version := range toolVersions.Versions {
|
|
parsedVersion := toolversions.Parse(version)
|
|
if parsedVersion.Type == "system" {
|
|
if executablePath, found := SystemExecutableOnPath(conf, shimName); found {
|
|
return executablePath, plugin, version, true, nil
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
if parsedVersion.Type == "path" {
|
|
executablePath, err := GetExecutablePath(conf, plugin, shimName, parsedVersion)
|
|
if err == nil {
|
|
return executablePath, plugin, version, true, nil
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
executablePath, err := GetExecutablePath(conf, plugin, shimName, parsedVersion)
|
|
if err == nil {
|
|
return executablePath, plugin, version, true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
tools := []string{}
|
|
versions := []string{}
|
|
for plugin := range existingPluginToolVersions {
|
|
tools = append(tools, plugin.Name)
|
|
versions = append(versions, existingPluginToolVersions[plugin].Versions...)
|
|
}
|
|
|
|
return "", plugins.Plugin{}, "", false, NoExecutableForPluginError{shim: shimName, tools: tools, versions: versions}
|
|
}
|
|
|
|
// SystemExecutableOnPath returns the path to the system executable if found,
|
|
// removes asdf shim directory from search
|
|
func SystemExecutableOnPath(conf config.Config, executableName string) (string, bool) {
|
|
currentPath := os.Getenv("PATH")
|
|
executablePath, err := ExecutableOnPath(paths.RemoveFromPath(currentPath, Directory(conf)), executableName)
|
|
return executablePath, err == nil
|
|
}
|
|
|
|
// ExecutableOnPath returns the path to an executable if one is found on the
|
|
// provided paths. `path` must be in the same format as the `PATH` environment
|
|
// variable.
|
|
func ExecutableOnPath(path, command string) (string, error) {
|
|
currentPath := os.Getenv("PATH")
|
|
defer os.Setenv("PATH", currentPath)
|
|
os.Setenv("PATH", path)
|
|
return exec.LookPath(command)
|
|
}
|
|
|
|
// GetExecutablePath returns the path of the executable
|
|
func GetExecutablePath(conf config.Config, plugin plugins.Plugin, shimName string, version toolversions.Version) (string, error) {
|
|
executables, err := ToolExecutables(conf, plugin, version)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
executable := ""
|
|
|
|
for _, executablePath := range executables {
|
|
if filepath.Base(executablePath) == shimName {
|
|
executable = executablePath
|
|
}
|
|
}
|
|
|
|
path, err := getCustomExecutablePath(conf, plugin, shimName, version, executable)
|
|
if err == nil {
|
|
return path, err
|
|
}
|
|
|
|
if executable != "" {
|
|
return executable, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("executable not found")
|
|
}
|
|
|
|
// GetToolsAndVersionsFromShimFile takes a file path and parses out the tools
|
|
// and versions present in it and returns them as a slice containing info in
|
|
// ToolVersions structs.
|
|
func GetToolsAndVersionsFromShimFile(shimPath string) (versions []toolversions.ToolVersions, err error) {
|
|
contents, err := os.ReadFile(shimPath)
|
|
if err != nil {
|
|
return versions, err
|
|
}
|
|
|
|
versions = parse(string(contents))
|
|
versions = toolversions.Unique(versions)
|
|
|
|
return versions, err
|
|
}
|
|
|
|
func getCustomExecutablePath(conf config.Config, plugin plugins.Plugin, shimName string, version toolversions.Version, executablePath string) (string, error) {
|
|
var stdOut strings.Builder
|
|
var stdErr strings.Builder
|
|
|
|
installPath := installs.InstallPath(conf, plugin, version)
|
|
env := map[string]string{"ASDF_INSTALL_TYPE": "version"}
|
|
|
|
relativePath, err := filepath.Rel(installPath, executablePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
err = plugin.RunCallback("exec-path", []string{installPath, shimName, relativePath}, env, &stdOut, &stdErr)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(installPath, strings.TrimSpace(stdOut.String())), err
|
|
}
|
|
|
|
// RemoveAll removes all shim scripts
|
|
func RemoveAll(conf config.Config) error {
|
|
shimDir := filepath.Join(conf.DataDir, shimDirName)
|
|
entries, err := os.ReadDir(shimDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
os.RemoveAll(path.Join(shimDir, entry.Name()))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateAll generates shims for all executables of every version of every
|
|
// plugin.
|
|
func GenerateAll(conf config.Config, stdOut io.Writer, stdErr io.Writer) error {
|
|
plugins, err := plugins.List(conf, false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, plugin := range plugins {
|
|
err := GenerateForPluginVersions(conf, plugin, stdOut, stdErr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateForPluginVersions generates all shims for all installed versions of
|
|
// a tool.
|
|
func GenerateForPluginVersions(conf config.Config, plugin plugins.Plugin, stdOut io.Writer, stdErr io.Writer) error {
|
|
installedVersions, err := installs.Installed(conf, plugin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, version := range installedVersions {
|
|
parsedVersion := toolversions.Parse(version)
|
|
GenerateForVersion(conf, plugin, parsedVersion, 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 toolversions.Version, stdOut io.Writer, stdErr io.Writer) error {
|
|
err := hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_reshim_%s", plugin.Name), []string{toolversions.Format(version)}, stdOut, stdErr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_reshim_%s", plugin.Name), []string{toolversions.Format(version)}, stdOut, stdErr)
|
|
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 toolversions.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{toolversions.Format(version)}}}
|
|
|
|
if _, err := os.Stat(shimPath); err == nil {
|
|
oldVersions, err := GetToolsAndVersionsFromShimFile(shimPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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, shimDirName, shimName)
|
|
}
|
|
|
|
// Directory returns the path to the shims directory for the current
|
|
// configuration.
|
|
func Directory(conf config.Config) string {
|
|
return filepath.Join(conf.DataDir, shimDirName)
|
|
}
|
|
|
|
func ensureShimDirExists(conf config.Config) error {
|
|
return os.MkdirAll(filepath.Join(conf.DataDir, shimDirName), 0o777)
|
|
}
|
|
|
|
// ToolExecutables returns a slice of executables for a given tool version
|
|
func ToolExecutables(conf config.Config, plugin plugins.Plugin, version toolversions.Version) (executables []string, err error) {
|
|
paths, err := ExecutablePaths(conf, plugin, version)
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
|
|
for _, path := range paths {
|
|
entries, err := os.ReadDir(path)
|
|
if _, ok := err.(*os.PathError); err != nil && !ok {
|
|
return executables, err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
// 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 {
|
|
continue
|
|
}
|
|
|
|
executables = append(executables, filePath)
|
|
}
|
|
}
|
|
return executables, err
|
|
}
|
|
|
|
// ExecutablePaths returns a slice of absolute directory paths that tool
|
|
// executables are contained in.
|
|
func ExecutablePaths(conf config.Config, plugin plugins.Plugin, version toolversions.Version) ([]string, error) {
|
|
dirs, err := ExecutableDirs(plugin)
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
|
|
installPath := installs.InstallPath(conf, plugin, version)
|
|
return dirsToPaths(dirs, installPath), nil
|
|
}
|
|
|
|
// 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
|
|
}
|