Merge pull request #22 from mcataford/refactor/simplify-python-install

refactor(python): simplify python install
This commit is contained in:
Marc 2024-01-28 23:35:37 -05:00 committed by GitHub
commit ecb808e16c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 55 additions and 49 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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)
) )

View file

@ -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

View file

@ -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)
} }

View file

@ -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
} }

View file

@ -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"
} }

View file

@ -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()

View file

@ -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