feat(golang-rewrite): create install command

* Create `versions.Install` function
* Create `versions.InstallVersion` function
* Create `versions.InstallAll` function
* Improve tests
* Create install command
This commit is contained in:
Trevor Brown 2024-08-17 16:27:18 -04:00
parent 63103a0b6a
commit 2de83c2ae1
3 changed files with 316 additions and 33 deletions

View File

@ -3,11 +3,14 @@ package cmd
import ( import (
"errors" "errors"
"fmt"
"log" "log"
"os" "os"
"strings"
"asdf/config" "asdf/config"
"asdf/internal/info" "asdf/internal/info"
"asdf/internal/versions"
"asdf/plugins" "asdf/plugins"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -50,6 +53,13 @@ func Execute(version string) {
return infoCommand(conf, version) return infoCommand(conf, version)
}, },
}, },
{
Name: "install",
Action: func(cCtx *cli.Context) error {
args := cCtx.Args()
return installCommand(logger, args.Get(0), args.Get(1))
},
},
{ {
Name: "plugin", Name: "plugin",
Action: func(_ *cli.Context) error { Action: func(_ *cli.Context) error {
@ -242,3 +252,64 @@ func formatUpdateResult(logger *log.Logger, pluginName, updatedToRef string, err
logger.Printf("updated %s to ref %s\n", pluginName, updatedToRef) logger.Printf("updated %s to ref %s\n", pluginName, updatedToRef)
} }
func installCommand(logger *log.Logger, toolName, version string) error {
conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
return err
}
dir, err := os.Getwd()
if err != nil {
return fmt.Errorf("unable to fetch current directory: %w", err)
}
if toolName == "" {
// Install all versions
errs := versions.InstallAll(conf, dir, os.Stdout, os.Stderr)
if len(errs) > 0 {
for _, err := range errs {
// write error stderr
os.Stderr.Write([]byte(err.Error()))
os.Stderr.Write([]byte("\n"))
}
return errs[0]
}
} else {
// Install specific version
plugin := plugins.New(conf, toolName)
if version == "" {
err = versions.Install(conf, plugin, dir, os.Stdout, os.Stderr)
if err != nil {
return err
}
} else {
parsedVersion, query := parseInstallVersion(version)
if parsedVersion == "latest" {
err = versions.InstallVersion(conf, plugin, version, query, os.Stdout, os.Stderr)
} else {
err = versions.InstallOneVersion(conf, plugin, version, os.Stdout, os.Stderr)
}
if err != nil {
logger.Printf("error installing version: %s", err)
}
}
}
return err
}
func parseInstallVersion(version string) (string, string) {
segments := strings.Split(version, ":")
if len(segments) > 1 && segments[0] == "latest" {
// Must be latest with filter
return "latest", segments[1]
}
return version, ""
}

View File

@ -13,6 +13,7 @@ import (
"asdf/config" "asdf/config"
"asdf/hook" "asdf/hook"
"asdf/internal/resolve"
"asdf/plugins" "asdf/plugins"
) )
@ -35,26 +36,69 @@ func (e UninstallableVersion) Error() string {
return fmt.Sprint(uninstallableVersionMsg) return fmt.Sprint(uninstallableVersionMsg)
} }
// TODO: Implement these functions // InstallAll installs all specified versions of every tool for the current
//func InstallAll() { // directory. Typically this will just be a single version, if not already
//} // installed, but it may be multiple versions if multiple versions for the tool
// are specified in the .tool-versions file.
func InstallAll(conf config.Config, dir string, stdOut io.Writer, stdErr io.Writer) (failures []error) {
plugins, err := plugins.List(conf, false, false)
if err != nil {
return []error{fmt.Errorf("unable to list plugins: %w", err)}
}
//func InstallOne() { // Ideally we should install these in the order they are specified in the
//} // closest .tool-versions file, but for now that is too complicated to
// implement.
for _, plugin := range plugins {
err := Install(conf, plugin, dir, stdOut, stdErr)
if err != nil {
failures = append(failures, err)
}
}
// InstallOneVersion installs a specific version of a specific tool return failures
func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string, _ bool, stdOut io.Writer, stdErr io.Writer) error { }
// Install installs all specified versions of a tool for the current directory.
// Typically this will just be a single version, if not already installed, but
// it may be multiple versions if multiple versions for the tool are specified
// in the .tool-versions file.
func Install(conf config.Config, plugin plugins.Plugin, dir string, stdOut io.Writer, stdErr io.Writer) error {
err := plugin.Exists() err := plugin.Exists()
if err != nil { if err != nil {
return err return err
} }
if version == systemVersion { versions, found, err := resolve.Version(conf, plugin, dir)
return UninstallableVersion{} if err != nil {
return err
}
if !found || len(versions.Versions) == 0 {
return errors.New("no version set")
}
for _, version := range versions.Versions {
err := InstallOneVersion(conf, plugin, version, stdOut, stdErr)
if err != nil {
return err
}
}
return nil
}
// InstallVersion installs a version of a specific tool, the version may be an
// exact version, or it may be `latest` or `latest` a regex query in order to
// select the latest version matching the provided pattern.
func InstallVersion(conf config.Config, plugin plugins.Plugin, version string, pattern string, stdOut io.Writer, stdErr io.Writer) error {
err := plugin.Exists()
if err != nil {
return err
} }
if version == latestVersion { if version == latestVersion {
versions, err := Latest(plugin, "") versions, err := Latest(plugin, pattern)
if err != nil { if err != nil {
return err return err
} }
@ -66,6 +110,20 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string
version = versions[0] version = versions[0]
} }
return InstallOneVersion(conf, plugin, version, stdOut, stdErr)
}
// InstallOneVersion installs a specific version of a specific tool
func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string, stdOut io.Writer, stdErr io.Writer) error {
err := plugin.Exists()
if err != nil {
return err
}
if version == systemVersion {
return UninstallableVersion{}
}
downloadDir := downloadPath(conf, plugin, version) downloadDir := downloadPath(conf, plugin, version)
installDir := installPath(conf, plugin, version) installDir := installPath(conf, plugin, version)
versionType, version := ParseString(version) versionType, version := ParseString(version)

View File

@ -1,6 +1,7 @@
package versions package versions
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -15,35 +16,166 @@ import (
const testPluginName = "lua" const testPluginName = "lua"
func TestInstallAll(t *testing.T) {
t.Run("installs multiple tools when multiple tool versions are specified", func(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
currentDir := t.TempDir()
secondPlugin := installPlugin(t, conf, "dummy_plugin", "another")
version := "1.0.0"
// write a version file
content := fmt.Sprintf("%s %s\n%s %s", plugin.Name, version, secondPlugin.Name, version)
writeVersionFile(t, currentDir, content)
err := InstallAll(conf, currentDir, &stdout, &stderr)
assert.Nil(t, err)
assertVersionInstalled(t, conf.DataDir, plugin.Name, version)
assertVersionInstalled(t, conf.DataDir, secondPlugin.Name, version)
})
t.Run("only installs tools with versions specified for current directory", func(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
currentDir := t.TempDir()
secondPlugin := installPlugin(t, conf, "dummy_plugin", "another")
version := "1.0.0"
// write a version file
content := fmt.Sprintf("%s %s\n", plugin.Name, version)
writeVersionFile(t, currentDir, content)
err := InstallAll(conf, currentDir, &stdout, &stderr)
assert.ErrorContains(t, err[0], "no version set")
assertVersionInstalled(t, conf.DataDir, plugin.Name, version)
assertNotInstalled(t, conf.DataDir, secondPlugin.Name, version)
})
t.Run("installs all tools even after one fails to install", func(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
currentDir := t.TempDir()
secondPlugin := installPlugin(t, conf, "dummy_plugin", "another")
version := "1.0.0"
// write a version file
content := fmt.Sprintf("%s %s\n%s %s", secondPlugin.Name, "non-existent-version", plugin.Name, version)
writeVersionFile(t, currentDir, content)
err := InstallAll(conf, currentDir, &stdout, &stderr)
assert.Empty(t, err)
assertNotInstalled(t, conf.DataDir, secondPlugin.Name, version)
assertVersionInstalled(t, conf.DataDir, plugin.Name, version)
})
}
func TestInstall(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
currentDir := t.TempDir()
t.Run("installs version of tool specified for current directory", func(t *testing.T) {
version := "1.0.0"
// write a version file
data := []byte(fmt.Sprintf("%s %s", plugin.Name, version))
err := os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666)
assert.Nil(t, err)
err = Install(conf, plugin, currentDir, &stdout, &stderr)
assert.Nil(t, err)
assertVersionInstalled(t, conf.DataDir, plugin.Name, version)
})
t.Run("returns error when plugin doesn't exist", func(t *testing.T) {
conf, _ := generateConfig(t)
stdout, stderr := buildOutputs()
err := Install(conf, plugins.New(conf, "non-existent"), currentDir, &stdout, &stderr)
assert.IsType(t, plugins.PluginMissing{}, err)
})
t.Run("returns error when no version set", func(t *testing.T) {
conf, _ := generateConfig(t)
stdout, stderr := buildOutputs()
currentDir := t.TempDir()
err := Install(conf, plugin, currentDir, &stdout, &stderr)
assert.EqualError(t, err, "no version set")
})
t.Run("if multiple versions are defined installs all of them", func(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
currentDir := t.TempDir()
versions := "1.0.0 2.0.0"
// write a version file
data := []byte(fmt.Sprintf("%s %s", plugin.Name, versions))
err := os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666)
assert.Nil(t, err)
err = Install(conf, plugin, currentDir, &stdout, &stderr)
assert.Nil(t, err)
assertVersionInstalled(t, conf.DataDir, plugin.Name, "1.0.0")
assertVersionInstalled(t, conf.DataDir, plugin.Name, "2.0.0")
})
}
func TestInstallVersion(t *testing.T) {
t.Setenv("ASDF_CONFIG_FILE", "testdata/asdfrc")
t.Run("returns error when plugin doesn't exist", func(t *testing.T) {
conf, _ := generateConfig(t)
stdout, stderr := buildOutputs()
err := InstallVersion(conf, plugins.New(conf, "non-existent"), "1.2.3", "", &stdout, &stderr)
assert.IsType(t, plugins.PluginMissing{}, err)
})
t.Run("installs latest version of tool when version is 'latest'", func(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
err := InstallVersion(conf, plugin, "latest", "", &stdout, &stderr)
assert.Nil(t, err)
assertVersionInstalled(t, conf.DataDir, plugin.Name, "2.0.0")
})
t.Run("installs specific version of tool", func(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
err := InstallVersion(conf, plugin, "latest", "^1.", &stdout, &stderr)
assert.Nil(t, err)
assertVersionInstalled(t, conf.DataDir, plugin.Name, "1.1.0")
})
}
func TestInstallOneVersion(t *testing.T) { func TestInstallOneVersion(t *testing.T) {
t.Setenv("ASDF_CONFIG_FILE", "testdata/asdfrc") t.Setenv("ASDF_CONFIG_FILE", "testdata/asdfrc")
t.Run("returns error when plugin doesn't exist", func(t *testing.T) { t.Run("returns error when plugin doesn't exist", func(t *testing.T) {
conf, _ := generateConfig(t) conf, _ := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugins.New(conf, "non-existent"), "1.2.3", false, &stdout, &stderr) err := InstallOneVersion(conf, plugins.New(conf, "non-existent"), "1.2.3", &stdout, &stderr)
assert.IsType(t, plugins.PluginMissing{}, err) assert.IsType(t, plugins.PluginMissing{}, err)
}) })
t.Run("returns error when plugin version is 'system'", func(t *testing.T) { t.Run("returns error when plugin version is 'system'", func(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, "system", false, &stdout, &stderr) err := InstallOneVersion(conf, plugin, "system", &stdout, &stderr)
assert.IsType(t, UninstallableVersion{}, err) assert.IsType(t, UninstallableVersion{}, err)
}) })
t.Run("installs latest version of tool when version is 'latest'", func(t *testing.T) {
conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, "latest", false, &stdout, &stderr)
assert.Nil(t, err)
})
t.Run("returns error when version doesn't exist", func(t *testing.T) { t.Run("returns error when version doesn't exist", func(t *testing.T) {
version := "other-dummy" version := "other-dummy"
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, version, false, &stdout, &stderr) err := InstallOneVersion(conf, plugin, version, &stdout, &stderr)
assert.Errorf(t, err, "failed to run install callback: exit status 1") assert.Errorf(t, err, "failed to run install callback: exit status 1")
want := "pre_asdf_download_lua other-dummy\npre_asdf_install_lua other-dummy\nDummy couldn't install version: other-dummy (on purpose)\n" want := "pre_asdf_download_lua other-dummy\npre_asdf_install_lua other-dummy\nDummy couldn't install version: other-dummy (on purpose)\n"
@ -55,19 +187,19 @@ func TestInstallOneVersion(t *testing.T) {
t.Run("returns error when version already installed", func(t *testing.T) { t.Run("returns error when version already installed", func(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr) err := InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err) assert.Nil(t, err)
assertInstalled(t, conf.DataDir, plugin.Name, "1.0.0") assertVersionInstalled(t, conf.DataDir, plugin.Name, "1.0.0")
// Install a second time // Install a second time
err = InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr) err = InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.NotNil(t, err) assert.NotNil(t, err)
}) })
t.Run("creates download directory", func(t *testing.T) { t.Run("creates download directory", func(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr) err := InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err) assert.Nil(t, err)
downloadPath := filepath.Join(conf.DataDir, "downloads", plugin.Name, "1.0.0") downloadPath := filepath.Join(conf.DataDir, "downloads", plugin.Name, "1.0.0")
@ -79,7 +211,7 @@ func TestInstallOneVersion(t *testing.T) {
t.Run("creates install directory", func(t *testing.T) { t.Run("creates install directory", func(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr) err := InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err) assert.Nil(t, err)
installPath := filepath.Join(conf.DataDir, "installs", plugin.Name, "1.0.0") installPath := filepath.Join(conf.DataDir, "installs", plugin.Name, "1.0.0")
@ -91,7 +223,7 @@ func TestInstallOneVersion(t *testing.T) {
t.Run("runs pre-download, pre-install and post-install hooks when installation successful", func(t *testing.T) { t.Run("runs pre-download, pre-install and post-install hooks when installation successful", func(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr) err := InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "", stderr.String()) assert.Equal(t, "", stderr.String())
want := "pre_asdf_download_lua 1.0.0\npre_asdf_install_lua 1.0.0\npost_asdf_install_lua 1.0.0\n" want := "pre_asdf_download_lua 1.0.0\npre_asdf_install_lua 1.0.0\npost_asdf_install_lua 1.0.0\n"
@ -101,7 +233,7 @@ func TestInstallOneVersion(t *testing.T) {
t.Run("installs successfully when plugin exists but version does not", func(t *testing.T) { t.Run("installs successfully when plugin exists but version does not", func(t *testing.T) {
conf, plugin := generateConfig(t) conf, plugin := generateConfig(t)
stdout, stderr := buildOutputs() stdout, stderr := buildOutputs()
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr) err := InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err) assert.Nil(t, err)
// Check download directory // Check download directory
@ -112,7 +244,7 @@ func TestInstallOneVersion(t *testing.T) {
assert.Empty(t, entries) assert.Empty(t, entries)
// Check install directory // Check install directory
assertInstalled(t, conf.DataDir, plugin.Name, "1.0.0") assertVersionInstalled(t, conf.DataDir, plugin.Name, "1.0.0")
}) })
t.Run("install successfully when plugin lacks download callback", func(t *testing.T) { t.Run("install successfully when plugin lacks download callback", func(t *testing.T) {
@ -123,7 +255,7 @@ func TestInstallOneVersion(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
plugin := plugins.New(conf, testPluginName) plugin := plugins.New(conf, testPluginName)
err = InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr) err = InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err) assert.Nil(t, err)
// no-download install script prints 'install' // no-download install script prints 'install'
@ -221,11 +353,19 @@ func buildOutputs() (strings.Builder, strings.Builder) {
return stdout, stderr return stdout, stderr
} }
func assertInstalled(t *testing.T, dataDir, pluginName, version string) { func assertVersionInstalled(t *testing.T, dataDir, pluginName, version string) {
t.Helper() t.Helper()
installPath := filepath.Join(dataDir, "installs", pluginName, version) installDir := filepath.Join(dataDir, "installs", pluginName, version)
entries, err := os.ReadDir(installPath) installedVersionFile := filepath.Join(installDir, "version")
bytes, err := os.ReadFile(installedVersionFile)
assert.Nil(t, err, "expected file from install to exist")
want := fmt.Sprintf("%s\n", version)
assert.Equal(t, want, string(bytes), "got wrong version")
entries, err := os.ReadDir(installDir)
assert.Nil(t, err) assert.Nil(t, err)
var fileNames []string var fileNames []string
@ -241,8 +381,10 @@ func assertNotInstalled(t *testing.T, dataDir, pluginName, version string) {
installPath := filepath.Join(dataDir, "installs", pluginName, version) installPath := filepath.Join(dataDir, "installs", pluginName, version)
entries, err := os.ReadDir(installPath) entries, err := os.ReadDir(installPath)
if err != nil && !os.IsNotExist(err) {
t.Errorf("failed to check directory %s due to error %s", installPath, err)
}
assert.Empty(t, entries) assert.Empty(t, entries)
assert.Nil(t, err)
} }
func generateConfig(t *testing.T) (config.Config, plugins.Plugin) { func generateConfig(t *testing.T) (config.Config, plugins.Plugin) {
@ -257,3 +399,15 @@ func generateConfig(t *testing.T) (config.Config, plugins.Plugin) {
return conf, plugins.New(conf, testPluginName) return conf, plugins.New(conf, testPluginName)
} }
func installPlugin(t *testing.T, conf config.Config, fixture, name string) plugins.Plugin {
_, err := repotest.InstallPlugin(fixture, conf.DataDir, name)
assert.Nil(t, err)
return plugins.New(conf, name)
}
func writeVersionFile(t *testing.T, dir, contents string) {
t.Helper()
err := os.WriteFile(filepath.Join(dir, ".tool-versions"), []byte(contents), 0o666)
assert.Nil(t, err)
}