Merge pull request #22 from mcataford/refactor/simplify-python-install
refactor(python): simplify python install
This commit is contained in:
commit
ecb808e16c
9 changed files with 55 additions and 49 deletions
|
@ -1,6 +1,7 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
logger "v/logger"
|
logger "v/logger"
|
||||||
|
@ -47,6 +48,12 @@ func (c *CLI) ListNamespaces() []string {
|
||||||
// Executes one of the registered commands if any match the provided
|
// Executes one of the registered commands if any match the provided
|
||||||
// user arguments.
|
// user arguments.
|
||||||
func (c CLI) Run(args []string, currentState state.State) error {
|
func (c CLI) Run(args []string, currentState state.State) error {
|
||||||
|
flags := collectFlags(args)
|
||||||
|
|
||||||
|
if flags.Verbose {
|
||||||
|
logger.DebugLogger.SetOutput(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
c.Help()
|
c.Help()
|
||||||
return nil
|
return nil
|
||||||
|
@ -59,8 +66,6 @@ func (c CLI) Run(args []string, currentState state.State) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := collectFlags(args)
|
|
||||||
|
|
||||||
namespace, isNamespace := c.Namespaces[action]
|
namespace, isNamespace := c.Namespaces[action]
|
||||||
|
|
||||||
if isNamespace {
|
if isNamespace {
|
||||||
|
|
17
exec/util.go
17
exec/util.go
|
@ -1,15 +1,16 @@
|
||||||
package exec
|
package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
logger "v/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunCommand is a thin wrapper around running command-line calls
|
// RunCommand is a thin wrapper around running command-line calls
|
||||||
// programmatically. It abstracts common configuration like routing
|
// programmatically. It abstracts common configuration like routing
|
||||||
// output and handling the directory the calls are made from.
|
// output and handling the directory the calls are made from.
|
||||||
func RunCommand(command []string, cwd string, quiet bool) (string, error) {
|
func RunCommand(command []string, cwd string) (string, error) {
|
||||||
cmd := exec.Command(command[0], command[1:]...)
|
cmd := exec.Command(command[0], command[1:]...)
|
||||||
|
|
||||||
cmd.Dir = cwd
|
cmd.Dir = cwd
|
||||||
|
@ -17,13 +18,11 @@ func RunCommand(command []string, cwd string, quiet bool) (string, error) {
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
var errOut strings.Builder
|
var errOut strings.Builder
|
||||||
|
|
||||||
if !quiet {
|
stdOutMultiWriter := io.MultiWriter(&out, logger.DebugLogger.Writer())
|
||||||
cmd.Stdout = os.Stdout
|
stdErrMultiWriter := io.MultiWriter(&errOut, logger.DebugLogger.Writer())
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
} else {
|
cmd.Stdout = stdOutMultiWriter
|
||||||
cmd.Stdout = &out
|
cmd.Stderr = stdErrMultiWriter
|
||||||
cmd.Stderr = &errOut
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return errOut.String(), err
|
return errOut.String(), err
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
InfoLogger = log.New(os.Stdout, "", 0)
|
InfoLogger = log.New(os.Stdout, "", 0)
|
||||||
|
DebugLogger = log.New(ioutil.Discard, "", 0)
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func uninstallPython(args []string, flags cli.Flags, currentState state.State) error {
|
func uninstallPython(args []string, flags cli.Flags, currentState state.State) error {
|
||||||
runtimePath := state.GetStatePath("runtimes", "py-"+args[1])
|
runtimePath := state.GetStatePath("runtimes", "python", args[1])
|
||||||
err := os.RemoveAll(runtimePath)
|
err := os.RemoveAll(runtimePath)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ func uninstallPython(args []string, flags cli.Flags, currentState state.State) e
|
||||||
func installPython(args []string, flags cli.Flags, currentState state.State) error {
|
func installPython(args []string, flags cli.Flags, currentState state.State) error {
|
||||||
version := args[1]
|
version := args[1]
|
||||||
|
|
||||||
return InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
|
return InstallPythonDistribution(version, flags.NoCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
func use(args []string, flags cli.Flags, currentState state.State) error {
|
func use(args []string, flags cli.Flags, currentState state.State) error {
|
||||||
|
@ -38,7 +38,7 @@ func use(args []string, flags cli.Flags, currentState state.State) error {
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
logger.InfoLogger.Println("Version not installed. Installing it first.")
|
logger.InfoLogger.Println("Version not installed. Installing it first.")
|
||||||
InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
|
InstallPythonDistribution(version, flags.NoCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.WriteState(version)
|
state.WriteState(version)
|
||||||
|
@ -79,7 +79,7 @@ func which(args []string, flags cli.Flags, currentState state.State) error {
|
||||||
printedPath = sysPath + " (system)"
|
printedPath = sysPath + " (system)"
|
||||||
} else if isInstalled {
|
} else if isInstalled {
|
||||||
tag := VersionStringToStruct(selectedVersion.Version)
|
tag := VersionStringToStruct(selectedVersion.Version)
|
||||||
printedPath = state.GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor())
|
printedPath = state.GetStatePath("runtimes", "python", selectedVersion.Version, "bin", "python"+tag.MajorMinor())
|
||||||
} else {
|
} else {
|
||||||
logger.InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version)
|
logger.InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
|
func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
|
||||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||||
|
|
||||||
os.Mkdir(state.GetStatePath("runtimes"), 0750)
|
os.MkdirAll(state.GetStatePath("runtimes", "python"), 0750)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
|
|
||||||
logger.InfoLogger.SetOutput(&out)
|
logger.InfoLogger.SetOutput(&out)
|
||||||
|
@ -31,7 +31,7 @@ func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
|
||||||
func TestListVersionOutputsVersionsInstalled(t *testing.T) {
|
func TestListVersionOutputsVersionsInstalled(t *testing.T) {
|
||||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||||
|
|
||||||
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
|
os.MkdirAll(state.GetStatePath("runtimes", "python", "1.2.3"), 0750)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
|
|
||||||
logger.InfoLogger.SetOutput(&out)
|
logger.InfoLogger.SetOutput(&out)
|
||||||
|
@ -89,11 +89,11 @@ func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
|
||||||
logger.InfoLogger.SetOutput(&out)
|
logger.InfoLogger.SetOutput(&out)
|
||||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||||
|
|
||||||
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
|
os.MkdirAll(state.GetStatePath("runtimes", "python", "1.2.3"), 0750)
|
||||||
which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"})
|
which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"})
|
||||||
|
|
||||||
captured := strings.TrimSpace(out.String())
|
captured := strings.TrimSpace(out.String())
|
||||||
expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
|
expected := state.GetStatePath("runtimes", "python", "1.2.3", "bin", "python1.2")
|
||||||
if !strings.Contains(captured, expected) {
|
if !strings.Contains(captured, expected) {
|
||||||
t.Errorf("Unexpected message: %s, not %s", captured, expected)
|
t.Errorf("Unexpected message: %s, not %s", captured, expected)
|
||||||
}
|
}
|
||||||
|
@ -124,11 +124,11 @@ func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) {
|
||||||
logger.InfoLogger.SetOutput(&out)
|
logger.InfoLogger.SetOutput(&out)
|
||||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||||
|
|
||||||
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
|
os.MkdirAll(state.GetStatePath("runtimes", "python", "1.2.3"), 0750)
|
||||||
which([]string{}, cli.Flags{RawOutput: true}, state.State{GlobalVersion: "1.2.3"})
|
which([]string{}, cli.Flags{RawOutput: true}, state.State{GlobalVersion: "1.2.3"})
|
||||||
|
|
||||||
captured := strings.TrimSpace(out.String())
|
captured := strings.TrimSpace(out.String())
|
||||||
expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
|
expected := state.GetStatePath("runtimes", "python", "1.2.3", "bin", "python1.2")
|
||||||
if captured != expected {
|
if captured != expected {
|
||||||
t.Errorf("Unexpected message: %s, not %s", captured, expected)
|
t.Errorf("Unexpected message: %s, not %s", captured, expected)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,14 @@ func (t VersionTag) MajorMinor() string {
|
||||||
return t.Major + "." + t.Minor
|
return t.Major + "." + t.Minor
|
||||||
}
|
}
|
||||||
|
|
||||||
func InstallPythonDistribution(version string, noCache bool, verbose bool) error {
|
// Installing new distribution happens in three stages:
|
||||||
|
// 1. Validating that the version number is of a valid format;
|
||||||
|
// 2. Downloading the source tarball;
|
||||||
|
// 3. Unzipping + building from source.
|
||||||
|
//
|
||||||
|
// The tarball is cached in the `cache` state directory and is reused
|
||||||
|
// if the same version is installed again later.
|
||||||
|
func InstallPythonDistribution(version string, noCache bool) error {
|
||||||
if err := ValidateVersion(version); err != nil {
|
if err := ValidateVersion(version); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -42,9 +49,8 @@ func InstallPythonDistribution(version string, noCache bool, verbose bool) error
|
||||||
if dlerr != nil {
|
if dlerr != nil {
|
||||||
return dlerr
|
return dlerr
|
||||||
}
|
}
|
||||||
_, err := buildFromSource(packageMetadata, verbose)
|
|
||||||
|
|
||||||
if err != nil {
|
if _, err := buildFromSource(packageMetadata); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,19 +63,16 @@ func downloadSource(version string, skipCache bool) (PackageMetadata, error) {
|
||||||
archivePath := state.GetStatePath("cache", archiveName)
|
archivePath := state.GetStatePath("cache", archiveName)
|
||||||
sourceUrl, _ := url.JoinPath(pythonReleasesBaseURL, version, archiveName)
|
sourceUrl, _ := url.JoinPath(pythonReleasesBaseURL, version, archiveName)
|
||||||
|
|
||||||
client := http.Client{}
|
|
||||||
|
|
||||||
logger.InfoLogger.Println(logger.Bold("Downloading source for Python " + version))
|
logger.InfoLogger.Println(logger.Bold("Downloading source for Python " + version))
|
||||||
logger.InfoLogger.SetPrefix(" ")
|
logger.InfoLogger.SetPrefix(" ")
|
||||||
defer logger.InfoLogger.SetPrefix("")
|
defer logger.InfoLogger.SetPrefix("")
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
_, err := os.Stat(archivePath)
|
|
||||||
|
|
||||||
if errors.Is(err, os.ErrNotExist) || skipCache {
|
if _, err := os.Stat(archivePath); errors.Is(err, os.ErrNotExist) || skipCache {
|
||||||
logger.InfoLogger.Println("Fetching from " + sourceUrl)
|
logger.InfoLogger.Println("Fetching from " + sourceUrl)
|
||||||
|
|
||||||
resp, err := client.Get(sourceUrl)
|
resp, err := http.Get(sourceUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PackageMetadata{}, err
|
return PackageMetadata{}, err
|
||||||
|
@ -88,7 +91,7 @@ func downloadSource(version string, skipCache bool) (PackageMetadata, error) {
|
||||||
return PackageMetadata{ArchivePath: archivePath, Version: version}, nil
|
return PackageMetadata{ArchivePath: archivePath, Version: version}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, error) {
|
func buildFromSource(pkgMeta PackageMetadata) (PackageMetadata, error) {
|
||||||
logger.InfoLogger.Println(logger.Bold("Building from source"))
|
logger.InfoLogger.Println(logger.Bold("Building from source"))
|
||||||
logger.InfoLogger.SetPrefix(" ")
|
logger.InfoLogger.SetPrefix(" ")
|
||||||
defer logger.InfoLogger.SetPrefix("")
|
defer logger.InfoLogger.SetPrefix("")
|
||||||
|
@ -97,9 +100,7 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
|
||||||
|
|
||||||
logger.InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath)
|
logger.InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath)
|
||||||
|
|
||||||
_, untarErr := exec.RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, state.GetStatePath("cache"), !verbose)
|
if _, untarErr := exec.RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, state.GetStatePath("cache")); untarErr != nil {
|
||||||
|
|
||||||
if untarErr != nil {
|
|
||||||
return pkgMeta, untarErr
|
return pkgMeta, untarErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,18 +108,19 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
|
||||||
|
|
||||||
logger.InfoLogger.Println("Configuring installer")
|
logger.InfoLogger.Println("Configuring installer")
|
||||||
|
|
||||||
targetDirectory := state.GetStatePath("runtimes", "py-"+pkgMeta.Version)
|
if _, err := os.Stat(state.GetStatePath("runtimes", "python")); os.IsNotExist(err) {
|
||||||
|
os.Mkdir(state.GetStatePath("runtimes", "python"), 0775)
|
||||||
|
}
|
||||||
|
|
||||||
_, configureErr := exec.RunCommand([]string{"./configure", "--prefix=" + targetDirectory, "--enable-optimizations"}, unzippedRoot, !verbose)
|
targetDirectory := state.GetStatePath("runtimes", "python", pkgMeta.Version)
|
||||||
|
|
||||||
if configureErr != nil {
|
if _, configureErr := exec.RunCommand([]string{"./configure", "--prefix=" + targetDirectory, "--enable-optimizations"}, unzippedRoot); configureErr != nil {
|
||||||
return pkgMeta, configureErr
|
return pkgMeta, configureErr
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoLogger.Println("Building")
|
logger.InfoLogger.Println("Building")
|
||||||
_, buildErr := exec.RunCommand([]string{"make", "altinstall", "-j4"}, unzippedRoot, !verbose)
|
|
||||||
|
|
||||||
if buildErr != nil {
|
if _, buildErr := exec.RunCommand([]string{"make", "altinstall", "-j4"}, unzippedRoot); buildErr != nil {
|
||||||
return pkgMeta, buildErr
|
return pkgMeta, buildErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +130,6 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
|
||||||
|
|
||||||
pkgMeta.InstallPath = targetDirectory
|
pkgMeta.InstallPath = targetDirectory
|
||||||
|
|
||||||
logger.InfoLogger.Printf("Installed Python %s at %s\n", pkgMeta.Version, pkgMeta.InstallPath)
|
logger.InfoLogger.Printf("✅ Installed Python %s at %s (%s)\n", pkgMeta.Version, pkgMeta.InstallPath, time.Since(start))
|
||||||
logger.InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
|
|
||||||
return pkgMeta, nil
|
return pkgMeta, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ type SelectedVersion struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListInstalledVersions() ([]string, error) {
|
func ListInstalledVersions() ([]string, error) {
|
||||||
runtimesDir := state.GetStatePath("runtimes")
|
runtimesDir := state.GetStatePath("runtimes", "python")
|
||||||
entries, err := os.ReadDir(runtimesDir)
|
entries, err := os.ReadDir(runtimesDir)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -40,7 +40,7 @@ func ListInstalledVersions() ([]string, error) {
|
||||||
installedVersions := []string{}
|
installedVersions := []string{}
|
||||||
|
|
||||||
for _, d := range entries {
|
for _, d := range entries {
|
||||||
installedVersions = append(installedVersions, strings.TrimPrefix(d.Name(), "py-"))
|
installedVersions = append(installedVersions, d.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
return installedVersions, nil
|
return installedVersions, nil
|
||||||
|
@ -100,7 +100,7 @@ func DetermineSelectedPythonVersion(currentState state.State) (SelectedVersion,
|
||||||
// DetermineSystemPython returns the unshimmed Python version and path.
|
// DetermineSystemPython returns the unshimmed Python version and path.
|
||||||
// It assumes that /bin/python is where system Python lives.
|
// It assumes that /bin/python is where system Python lives.
|
||||||
func DetermineSystemPython() (string, string) {
|
func DetermineSystemPython() (string, string) {
|
||||||
versionOut, _ := exec.RunCommand([]string{"/bin/python", "--version"}, state.GetStatePath(), true)
|
versionOut, _ := exec.RunCommand([]string{"/bin/python", "--version"}, state.GetStatePath())
|
||||||
detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
|
detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
|
||||||
return strings.TrimSpace(detectedVersion), "/bin/python"
|
return strings.TrimSpace(detectedVersion), "/bin/python"
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ func TestListInstalledVersion(t *testing.T) {
|
||||||
|
|
||||||
os.Mkdir(state.GetStatePath("runtimes"), 0750)
|
os.Mkdir(state.GetStatePath("runtimes"), 0750)
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
os.Mkdir(state.GetStatePath("runtimes", "py-"+version), 0750)
|
os.MkdirAll(state.GetStatePath("runtimes", "python", version), 0750)
|
||||||
}
|
}
|
||||||
|
|
||||||
installedVersions, _ := ListInstalledVersions()
|
installedVersions, _ := ListInstalledVersions()
|
||||||
|
@ -140,7 +140,7 @@ func TestListInstalledVersion(t *testing.T) {
|
||||||
func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
|
func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
|
||||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||||
|
|
||||||
os.Mkdir(state.GetStatePath("runtimes"), 0750)
|
os.MkdirAll(state.GetStatePath("runtimes", "python"), 0750)
|
||||||
|
|
||||||
installedVersions, _ := ListInstalledVersions()
|
installedVersions, _ := ListInstalledVersions()
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Persistent state used by the CLI to track runtime information
|
// Persistent state used by the CLI to track runtime information
|
||||||
|
@ -46,12 +45,12 @@ func WriteState(version string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAvailableVersions() []string {
|
func GetAvailableVersions() []string {
|
||||||
entries, _ := os.ReadDir(GetStatePath("runtimes"))
|
entries, _ := os.ReadDir(GetStatePath("runtimes", "python"))
|
||||||
|
|
||||||
versions := []string{}
|
versions := []string{}
|
||||||
|
|
||||||
for _, d := range entries {
|
for _, d := range entries {
|
||||||
versions = append(versions, strings.TrimPrefix(d.Name(), "py-"))
|
versions = append(versions, d.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
return versions
|
return versions
|
||||||
|
|
Loading…
Reference in a new issue