Merge pull request #17 from mcataford/refactor/modularization
refactor: modularization
This commit is contained in:
commit
b6d8cc534d
16 changed files with 379 additions and 344 deletions
|
@ -1,7 +1,9 @@
|
|||
package main
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
logger "v/logger"
|
||||
state "v/state"
|
||||
)
|
||||
|
||||
type Flags struct {
|
||||
|
@ -14,7 +16,7 @@ type Flags struct {
|
|||
// Command definition for CLI subcommands.
|
||||
type Command struct {
|
||||
Label string
|
||||
Handler func([]string, Flags, State) error
|
||||
Handler func([]string, Flags, state.State) error
|
||||
Usage string
|
||||
Description string
|
||||
}
|
||||
|
@ -34,7 +36,7 @@ type CLI struct {
|
|||
// This specifies a label that is used to route the user input to
|
||||
// the right command, a handler that is called when the label is used,
|
||||
// and usage/description details that get included in autogenerated help messaging.
|
||||
func (c CLI) AddCommand(label string, handler func([]string, Flags, State) error, usage string, description string) CLI {
|
||||
func (c CLI) AddCommand(label string, handler func([]string, Flags, state.State) error, usage string, description string) CLI {
|
||||
if c.Commands == nil {
|
||||
c.Commands = map[string]Command{}
|
||||
c.OrderedCommands = []string{}
|
||||
|
@ -48,7 +50,7 @@ func (c CLI) AddCommand(label string, handler func([]string, Flags, State) error
|
|||
|
||||
// Executes one of the registered commands if any match the provided
|
||||
// user arguments.
|
||||
func (c CLI) Run(args []string, currentState State) error {
|
||||
func (c CLI) Run(args []string, currentState state.State) error {
|
||||
if len(args) == 0 {
|
||||
c.Help()
|
||||
return nil
|
||||
|
@ -68,10 +70,10 @@ func (c CLI) Run(args []string, currentState State) error {
|
|||
// Prints autogenerated help documentation specifying command usage
|
||||
// and descriptions based on registered commands (see: AddCommand).
|
||||
func (c CLI) Help() {
|
||||
InfoLogger.Printf("v: A simple version manager. (v%s)\n---", Version)
|
||||
logger.InfoLogger.Printf("v: A simple version manager. (v%s)\n---", c.Metadata["Version"])
|
||||
for _, commandLabel := range c.OrderedCommands {
|
||||
command := c.Commands[commandLabel]
|
||||
InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description)
|
||||
logger.InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description)
|
||||
}
|
||||
}
|
||||
|
122
commands.go
122
commands.go
|
@ -2,7 +2,9 @@ package main
|
|||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
cli "v/cli"
|
||||
logger "v/logger"
|
||||
state "v/state"
|
||||
)
|
||||
|
||||
var DIRECTORIES = []string{
|
||||
|
@ -29,128 +31,20 @@ func writeShim(shimPath string) error {
|
|||
|
||||
// Sets up directories and files used to store downloaded archives,
|
||||
// installed runtimes and metadata.
|
||||
func Initialize(args []string, flags Flags, currentState State) error {
|
||||
func Initialize(args []string, flags cli.Flags, currentState state.State) error {
|
||||
if flags.AddPath {
|
||||
InfoLogger.Printf("export PATH=%s:$PATH\n", GetStatePath("shims"))
|
||||
logger.InfoLogger.Printf("export PATH=%s:$PATH\n", state.GetStatePath("shims"))
|
||||
return nil
|
||||
}
|
||||
|
||||
os.Mkdir(GetStatePath(), DEFAULT_PERMISSION)
|
||||
os.Mkdir(state.GetStatePath(), DEFAULT_PERMISSION)
|
||||
for _, dir := range DIRECTORIES {
|
||||
os.Mkdir(GetStatePath(dir), DEFAULT_PERMISSION)
|
||||
os.Mkdir(state.GetStatePath(dir), DEFAULT_PERMISSION)
|
||||
}
|
||||
|
||||
for _, shim := range SHIMS {
|
||||
writeShim(GetStatePath("shims", shim))
|
||||
writeShim(state.GetStatePath("shims", shim))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func UninstallPython(args []string, flags Flags, currentState State) error {
|
||||
runtimePath := GetStatePath("runtimes", "py-"+args[1])
|
||||
err := os.RemoveAll(runtimePath)
|
||||
return err
|
||||
}
|
||||
|
||||
func InstallPython(args []string, flags Flags, currentState State) error {
|
||||
version := args[1]
|
||||
|
||||
return InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
|
||||
}
|
||||
|
||||
func Use(args []string, flags Flags, currentState State) error {
|
||||
version := args[1]
|
||||
if err := ValidateVersion(version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
availableVersions := GetAvailableVersions()
|
||||
|
||||
found := false
|
||||
for _, v := range availableVersions {
|
||||
if v == version {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
InfoLogger.Println("Version not installed. Installing it first.")
|
||||
InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
|
||||
}
|
||||
|
||||
WriteState(version)
|
||||
InfoLogger.Printf("Now using Python %s\n", version)
|
||||
|
||||
return nil
|
||||
}
|
||||
func ListVersions(args []string, flags Flags, currentState State) error {
|
||||
installedVersions, err := ListInstalledVersions()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(installedVersions) == 0 {
|
||||
InfoLogger.Println("No versions installed!")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, d := range installedVersions {
|
||||
InfoLogger.Println(d)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Which prints out the system path to the executable being used by `python`.
|
||||
func Which(args []string, flags Flags, currentState State) error {
|
||||
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
|
||||
installedVersions, _ := ListInstalledVersions()
|
||||
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
|
||||
|
||||
var printedPath string
|
||||
|
||||
if selectedVersion.Source == "system" {
|
||||
_, sysPath := DetermineSystemPython()
|
||||
printedPath = sysPath + " (system)"
|
||||
} else if isInstalled {
|
||||
tag := VersionStringToStruct(selectedVersion.Version)
|
||||
printedPath = GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor())
|
||||
} else {
|
||||
InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
prefix := "Python path: "
|
||||
|
||||
if flags.RawOutput {
|
||||
prefix = ""
|
||||
} else {
|
||||
printedPath = Bold(printedPath)
|
||||
}
|
||||
|
||||
InfoLogger.Printf("%s%s\n", prefix, printedPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentVersion (called via `v version`) outputs the currently selected version
|
||||
// and what configures it. If the version is configured by a file, the file is returned
|
||||
// under "source", if the system Python is used, "system" is returned as a source.
|
||||
func CurrentVersion(args []string, flags Flags, currentState State) error {
|
||||
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
|
||||
installedVersions, _ := ListInstalledVersions()
|
||||
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
|
||||
|
||||
if !isInstalled {
|
||||
InfoLogger.Println(Bold(Yellow("WARNING: This version is not installed.")))
|
||||
}
|
||||
|
||||
if flags.RawOutput {
|
||||
InfoLogger.Println(selectedVersion.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
InfoLogger.Printf("Python version: %s\nSource: %s\n", Bold(selectedVersion.Version), Bold(selectedVersion.Source))
|
||||
return nil
|
||||
}
|
||||
|
|
131
commands_test.go
131
commands_test.go
|
@ -1,131 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
|
||||
os.Mkdir(GetStatePath("runtimes"), 0750)
|
||||
var out bytes.Buffer
|
||||
|
||||
InfoLogger.SetOutput(&out)
|
||||
defer InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
ListVersions([]string{}, Flags{}, State{})
|
||||
|
||||
captured := out.String()
|
||||
if captured != "No versions installed!\n" {
|
||||
t.Errorf("Unexpected message: %s", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionOutputsVersionsInstalled(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
|
||||
os.MkdirAll(GetStatePath("runtimes", "py-1.2.3"), 0750)
|
||||
var out bytes.Buffer
|
||||
|
||||
InfoLogger.SetOutput(&out)
|
||||
defer InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
ListVersions([]string{}, Flags{}, State{})
|
||||
|
||||
captured := out.String()
|
||||
if captured != "1.2.3\n" {
|
||||
t.Errorf("Unexpected message: %s", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionReturnsErrorOnFailure(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
InfoLogger.SetOutput(&out)
|
||||
defer InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
err := ListVersions([]string{}, Flags{}, State{})
|
||||
|
||||
captured := out.String()
|
||||
if captured != "" {
|
||||
t.Errorf("Captured unexpected message: %s", captured)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error returned, did not get one.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
InfoLogger.SetOutput(&out)
|
||||
defer InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
Which([]string{}, Flags{}, State{GlobalVersion: "1.2.3"})
|
||||
|
||||
captured := out.String()
|
||||
if captured != "The desired version (1.2.3) is not installed.\n" {
|
||||
t.Errorf("Unexpected message: %s", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
InfoLogger.SetOutput(&out)
|
||||
defer InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
os.MkdirAll(GetStatePath("runtimes", "py-1.2.3"), 0750)
|
||||
Which([]string{}, Flags{}, State{GlobalVersion: "1.2.3"})
|
||||
|
||||
captured := strings.TrimSpace(out.String())
|
||||
expected := GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
|
||||
if !strings.Contains(captured, expected) {
|
||||
t.Errorf("Unexpected message: %s, not %s", captured, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
InfoLogger.SetOutput(&out)
|
||||
defer InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
Which([]string{}, Flags{RawOutput: true}, State{})
|
||||
|
||||
captured := strings.TrimSpace(out.String())
|
||||
|
||||
if captured != "/bin/python (system)" {
|
||||
t.Errorf("%s != %s", captured, "/bin/python (system)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
InfoLogger.SetOutput(&out)
|
||||
defer InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
os.MkdirAll(GetStatePath("runtimes", "py-1.2.3"), 0750)
|
||||
Which([]string{}, Flags{RawOutput: true}, State{GlobalVersion: "1.2.3"})
|
||||
|
||||
captured := strings.TrimSpace(out.String())
|
||||
expected := GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
|
||||
if captured != expected {
|
||||
t.Errorf("Unexpected message: %s, not %s", captured, expected)
|
||||
}
|
||||
}
|
|
@ -1,26 +1,11 @@
|
|||
package main
|
||||
package exec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func VersionStringToStruct(version string) VersionTag {
|
||||
splitVersion := strings.Split(version, ".")
|
||||
|
||||
return VersionTag{Major: splitVersion[0], Minor: splitVersion[1], Patch: splitVersion[2]}
|
||||
}
|
||||
|
||||
func ValidateVersion(version string) error {
|
||||
if splitVersion := strings.Split(version, "."); len(splitVersion) != 3 {
|
||||
return errors.New("Invalid version string. Expected format 'a.b.c'.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunCommand is a thin wrapper around running command-line calls
|
||||
// programmatically. It abstracts common configuration like routing
|
||||
// output and handling the directory the calls are made from.
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log"
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package logger
|
||||
|
||||
const (
|
||||
RESET = "\033[0m"
|
119
python/commands.go
Normal file
119
python/commands.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package python
|
||||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
cli "v/cli"
|
||||
logger "v/logger"
|
||||
state "v/state"
|
||||
)
|
||||
|
||||
func UninstallPython(args []string, flags cli.Flags, currentState state.State) error {
|
||||
runtimePath := state.GetStatePath("runtimes", "py-"+args[1])
|
||||
err := os.RemoveAll(runtimePath)
|
||||
return err
|
||||
}
|
||||
|
||||
func InstallPython(args []string, flags cli.Flags, currentState state.State) error {
|
||||
version := args[1]
|
||||
|
||||
return InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
|
||||
}
|
||||
|
||||
func Use(args []string, flags cli.Flags, currentState state.State) error {
|
||||
version := args[1]
|
||||
if err := ValidateVersion(version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
availableVersions := state.GetAvailableVersions()
|
||||
|
||||
found := false
|
||||
for _, v := range availableVersions {
|
||||
if v == version {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
logger.InfoLogger.Println("Version not installed. Installing it first.")
|
||||
InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
|
||||
}
|
||||
|
||||
state.WriteState(version)
|
||||
logger.InfoLogger.Printf("Now using Python %s\n", version)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListVersions(args []string, flags cli.Flags, currentState state.State) error {
|
||||
installedVersions, err := ListInstalledVersions()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(installedVersions) == 0 {
|
||||
logger.InfoLogger.Println("No versions installed!")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, d := range installedVersions {
|
||||
logger.InfoLogger.Println(d)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Which prints out the system path to the executable being used by `python`.
|
||||
func Which(args []string, flags cli.Flags, currentState state.State) error {
|
||||
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
|
||||
installedVersions, _ := ListInstalledVersions()
|
||||
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
|
||||
|
||||
var printedPath string
|
||||
|
||||
if selectedVersion.Source == "system" {
|
||||
_, sysPath := DetermineSystemPython()
|
||||
printedPath = sysPath + " (system)"
|
||||
} else if isInstalled {
|
||||
tag := VersionStringToStruct(selectedVersion.Version)
|
||||
printedPath = state.GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor())
|
||||
} else {
|
||||
logger.InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
prefix := "Python path: "
|
||||
|
||||
if flags.RawOutput {
|
||||
prefix = ""
|
||||
} else {
|
||||
printedPath = logger.Bold(printedPath)
|
||||
}
|
||||
|
||||
logger.InfoLogger.Printf("%s%s\n", prefix, printedPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentVersion (called via `v version`) outputs the currently selected version
|
||||
// and what configures it. If the version is configured by a file, the file is returned
|
||||
// under "source", if the system Python is used, "system" is returned as a source.
|
||||
func CurrentVersion(args []string, flags cli.Flags, currentState state.State) error {
|
||||
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
|
||||
installedVersions, _ := ListInstalledVersions()
|
||||
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
|
||||
|
||||
if !isInstalled {
|
||||
logger.InfoLogger.Println(logger.Bold(logger.Yellow("WARNING: This version is not installed.")))
|
||||
}
|
||||
|
||||
if flags.RawOutput {
|
||||
logger.InfoLogger.Println(selectedVersion.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.InfoLogger.Printf("Python version: %s\nSource: %s\n", logger.Bold(selectedVersion.Version), logger.Bold(selectedVersion.Source))
|
||||
return nil
|
||||
}
|
135
python/commands_test.go
Normal file
135
python/commands_test.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package python
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
cli "v/cli"
|
||||
logger "v/logger"
|
||||
state "v/state"
|
||||
testutils "v/testutils"
|
||||
)
|
||||
|
||||
func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
os.Mkdir(state.GetStatePath("runtimes"), 0750)
|
||||
var out bytes.Buffer
|
||||
|
||||
logger.InfoLogger.SetOutput(&out)
|
||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
ListVersions([]string{}, cli.Flags{}, state.State{})
|
||||
|
||||
captured := out.String()
|
||||
if captured != "No versions installed!\n" {
|
||||
t.Errorf("Unexpected message: %s", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionOutputsVersionsInstalled(t *testing.T) {
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
|
||||
var out bytes.Buffer
|
||||
|
||||
logger.InfoLogger.SetOutput(&out)
|
||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
ListVersions([]string{}, cli.Flags{}, state.State{})
|
||||
|
||||
captured := out.String()
|
||||
if captured != "1.2.3\n" {
|
||||
t.Errorf("Unexpected message: %s", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionReturnsErrorOnFailure(t *testing.T) {
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
logger.InfoLogger.SetOutput(&out)
|
||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
err := ListVersions([]string{}, cli.Flags{}, state.State{})
|
||||
|
||||
captured := out.String()
|
||||
if captured != "" {
|
||||
t.Errorf("Captured unexpected message: %s", captured)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error returned, did not get one.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) {
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
logger.InfoLogger.SetOutput(&out)
|
||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
Which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"})
|
||||
|
||||
captured := out.String()
|
||||
if captured != "The desired version (1.2.3) is not installed.\n" {
|
||||
t.Errorf("Unexpected message: %s", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
logger.InfoLogger.SetOutput(&out)
|
||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
|
||||
Which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"})
|
||||
|
||||
captured := strings.TrimSpace(out.String())
|
||||
expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
|
||||
if !strings.Contains(captured, expected) {
|
||||
t.Errorf("Unexpected message: %s, not %s", captured, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) {
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
logger.InfoLogger.SetOutput(&out)
|
||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
Which([]string{}, cli.Flags{RawOutput: true}, state.State{})
|
||||
|
||||
captured := strings.TrimSpace(out.String())
|
||||
|
||||
if captured != "/bin/python (system)" {
|
||||
t.Errorf("%s != %s", captured, "/bin/python (system)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) {
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
logger.InfoLogger.SetOutput(&out)
|
||||
defer logger.InfoLogger.SetOutput(os.Stdout)
|
||||
|
||||
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
|
||||
Which([]string{}, cli.Flags{RawOutput: true}, state.State{GlobalVersion: "1.2.3"})
|
||||
|
||||
captured := strings.TrimSpace(out.String())
|
||||
expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
|
||||
if captured != expected {
|
||||
t.Errorf("Unexpected message: %s, not %s", captured, expected)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package python
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -9,6 +9,9 @@ import (
|
|||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
exec "v/exec"
|
||||
logger "v/logger"
|
||||
state "v/state"
|
||||
)
|
||||
|
||||
var pythonReleasesBaseURL = "https://www.python.org/ftp/python"
|
||||
|
@ -51,20 +54,20 @@ func InstallPythonDistribution(version string, noCache bool, verbose bool) error
|
|||
// Fetches the Python tarball for version <version> from python.org.
|
||||
func downloadSource(version string, skipCache bool) (PackageMetadata, error) {
|
||||
archiveName := "Python-" + version + ".tgz"
|
||||
archivePath := GetStatePath("cache", archiveName)
|
||||
archivePath := state.GetStatePath("cache", archiveName)
|
||||
sourceUrl, _ := url.JoinPath(pythonReleasesBaseURL, version, archiveName)
|
||||
|
||||
client := http.Client{}
|
||||
|
||||
InfoLogger.Println(Bold("Downloading source for Python " + version))
|
||||
InfoLogger.SetPrefix(" ")
|
||||
defer InfoLogger.SetPrefix("")
|
||||
logger.InfoLogger.Println(logger.Bold("Downloading source for Python " + version))
|
||||
logger.InfoLogger.SetPrefix(" ")
|
||||
defer logger.InfoLogger.SetPrefix("")
|
||||
|
||||
start := time.Now()
|
||||
_, err := os.Stat(archivePath)
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) || skipCache {
|
||||
InfoLogger.Println("Fetching from " + sourceUrl)
|
||||
logger.InfoLogger.Println("Fetching from " + sourceUrl)
|
||||
|
||||
resp, err := client.Get(sourceUrl)
|
||||
|
||||
|
@ -78,23 +81,23 @@ func downloadSource(version string, skipCache bool) (PackageMetadata, error) {
|
|||
|
||||
defer file.Close()
|
||||
} else {
|
||||
InfoLogger.Println("Found in cache: " + archivePath)
|
||||
logger.InfoLogger.Println("Found in cache: " + archivePath)
|
||||
}
|
||||
|
||||
InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
|
||||
logger.InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
|
||||
return PackageMetadata{ArchivePath: archivePath, Version: version}, nil
|
||||
}
|
||||
|
||||
func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, error) {
|
||||
InfoLogger.Println(Bold("Building from source"))
|
||||
InfoLogger.SetPrefix(" ")
|
||||
defer InfoLogger.SetPrefix("")
|
||||
logger.InfoLogger.Println(logger.Bold("Building from source"))
|
||||
logger.InfoLogger.SetPrefix(" ")
|
||||
defer logger.InfoLogger.SetPrefix("")
|
||||
|
||||
start := time.Now()
|
||||
|
||||
InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath)
|
||||
logger.InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath)
|
||||
|
||||
_, untarErr := RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, GetStatePath("cache"), !verbose)
|
||||
_, untarErr := exec.RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, state.GetStatePath("cache"), !verbose)
|
||||
|
||||
if untarErr != nil {
|
||||
return pkgMeta, untarErr
|
||||
|
@ -102,18 +105,18 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
|
|||
|
||||
unzippedRoot := strings.TrimSuffix(pkgMeta.ArchivePath, path.Ext(pkgMeta.ArchivePath))
|
||||
|
||||
InfoLogger.Println("Configuring installer")
|
||||
logger.InfoLogger.Println("Configuring installer")
|
||||
|
||||
targetDirectory := GetStatePath("runtimes", "py-"+pkgMeta.Version)
|
||||
targetDirectory := state.GetStatePath("runtimes", "py-"+pkgMeta.Version)
|
||||
|
||||
_, configureErr := RunCommand([]string{"./configure", "--prefix=" + targetDirectory, "--enable-optimizations"}, unzippedRoot, !verbose)
|
||||
_, configureErr := exec.RunCommand([]string{"./configure", "--prefix=" + targetDirectory, "--enable-optimizations"}, unzippedRoot, !verbose)
|
||||
|
||||
if configureErr != nil {
|
||||
return pkgMeta, configureErr
|
||||
}
|
||||
|
||||
InfoLogger.Println("Building")
|
||||
_, buildErr := RunCommand([]string{"make", "altinstall", "-j4"}, unzippedRoot, !verbose)
|
||||
logger.InfoLogger.Println("Building")
|
||||
_, buildErr := exec.RunCommand([]string{"make", "altinstall", "-j4"}, unzippedRoot, !verbose)
|
||||
|
||||
if buildErr != nil {
|
||||
return pkgMeta, buildErr
|
||||
|
@ -125,7 +128,7 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
|
|||
|
||||
pkgMeta.InstallPath = targetDirectory
|
||||
|
||||
InfoLogger.Printf("Installed Python %s at %s\n", pkgMeta.Version, pkgMeta.InstallPath)
|
||||
InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
|
||||
logger.InfoLogger.Printf("Installed Python %s at %s\n", pkgMeta.Version, pkgMeta.InstallPath)
|
||||
logger.InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
|
||||
return pkgMeta, nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package python
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
@ -8,33 +8,19 @@ import (
|
|||
"path"
|
||||
"slices"
|
||||
"testing"
|
||||
state "v/state"
|
||||
testutils "v/testutils"
|
||||
)
|
||||
|
||||
// SetupAndCleanupEnvironment sets up a test directory and
|
||||
// environment variables before the test and returns a cleanup
|
||||
// function that can be deferred to cleanup any changes to the
|
||||
// system.
|
||||
func SetupAndCleanupEnvironment(t *testing.T) func() {
|
||||
os.Setenv("V_ROOT", t.TempDir())
|
||||
|
||||
temporaryWd := t.TempDir()
|
||||
|
||||
os.Chdir(temporaryWd)
|
||||
|
||||
return func() {
|
||||
os.Unsetenv("V_ROOT")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
ioutil.WriteFile(GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777)
|
||||
ioutil.WriteFile(state.GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777)
|
||||
mockSystemPythonPath := t.TempDir()
|
||||
mockSystemPythonExecPath := path.Join(mockSystemPythonPath, "python")
|
||||
|
||||
oldPath := os.Getenv("PATH")
|
||||
os.Setenv("PATH", fmt.Sprintf("%s:/usr/bin", GetStatePath("shims")))
|
||||
os.Setenv("PATH", fmt.Sprintf("%s:/usr/bin", state.GetStatePath("shims")))
|
||||
defer os.Setenv("PATH", oldPath)
|
||||
sysVersion, sysPath := DetermineSystemPython()
|
||||
|
||||
|
@ -48,11 +34,11 @@ func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
// Writing a mock user-defined state.
|
||||
mockState := State{GlobalVersion: "1.0.0"}
|
||||
statePath := GetStatePath("state.json")
|
||||
mockState := state.State{GlobalVersion: "1.0.0"}
|
||||
statePath := state.GetStatePath("state.json")
|
||||
stateData, _ := json.Marshal(mockState)
|
||||
ioutil.WriteFile(statePath, stateData, 0750)
|
||||
|
||||
|
@ -60,7 +46,7 @@ func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T
|
|||
os.Chdir(temporaryWd)
|
||||
ioutil.WriteFile(path.Join(temporaryWd, ".python-version"), []byte("1.2.3"), 0750)
|
||||
|
||||
version, err := DetermineSelectedPythonVersion(ReadState())
|
||||
version, err := DetermineSelectedPythonVersion(state.ReadState())
|
||||
|
||||
if err != nil || version.Version != "1.2.3" {
|
||||
t.Errorf("Expected version to be %s, got %s instead.", "1.2.3", version.Version)
|
||||
|
@ -68,15 +54,15 @@ func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T
|
|||
}
|
||||
|
||||
func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
// Writing a mock user-defined state.
|
||||
mockState := State{GlobalVersion: "1.0.0"}
|
||||
statePath := GetStatePath("state.json")
|
||||
mockState := state.State{GlobalVersion: "1.0.0"}
|
||||
statePath := state.GetStatePath("state.json")
|
||||
stateData, _ := json.Marshal(mockState)
|
||||
ioutil.WriteFile(statePath, stateData, 0750)
|
||||
|
||||
version, err := DetermineSelectedPythonVersion(ReadState())
|
||||
version, err := DetermineSelectedPythonVersion(state.ReadState())
|
||||
|
||||
if err != nil || version.Version != mockState.GlobalVersion {
|
||||
t.Errorf("Expected version to be %s, got %s instead.", mockState.GlobalVersion, version)
|
||||
|
@ -84,9 +70,9 @@ func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
version, err := DetermineSelectedPythonVersion(ReadState())
|
||||
version, err := DetermineSelectedPythonVersion(state.ReadState())
|
||||
|
||||
if err != nil || version.Source != "system" {
|
||||
t.Errorf("Expected version to be 'SYSTEM', got %s instead.", version)
|
||||
|
@ -94,7 +80,7 @@ func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
temporaryWd := t.TempDir()
|
||||
os.Chdir(temporaryWd)
|
||||
|
@ -108,7 +94,7 @@ func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
temporaryWd := t.TempDir()
|
||||
|
||||
|
@ -125,7 +111,7 @@ func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
versionFound, found := SearchForPythonVersionFile()
|
||||
|
||||
|
@ -135,13 +121,13 @@ func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestListInstalledVersion(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
versions := []string{"1.2.3", "4.5.6", "7.8.9"}
|
||||
|
||||
os.Mkdir(GetStatePath("runtimes"), 0750)
|
||||
os.Mkdir(state.GetStatePath("runtimes"), 0750)
|
||||
for _, version := range versions {
|
||||
os.Mkdir(GetStatePath("runtimes", "py-"+version), 0750)
|
||||
os.Mkdir(state.GetStatePath("runtimes", "py-"+version), 0750)
|
||||
}
|
||||
|
||||
installedVersions, _ := ListInstalledVersions()
|
||||
|
@ -152,9 +138,9 @@ func TestListInstalledVersion(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
os.Mkdir(GetStatePath("runtimes"), 0750)
|
||||
os.Mkdir(state.GetStatePath("runtimes"), 0750)
|
||||
|
||||
installedVersions, _ := ListInstalledVersions()
|
||||
|
||||
|
@ -164,7 +150,7 @@ func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestListInstalledVersionNoRuntimesDir(t *testing.T) {
|
||||
defer SetupAndCleanupEnvironment(t)()
|
||||
defer testutils.SetupAndCleanupEnvironment(t)()
|
||||
|
||||
installedVersions, err := ListInstalledVersions()
|
||||
|
|
@ -1,19 +1,36 @@
|
|||
package main
|
||||
package python
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
exec "v/exec"
|
||||
state "v/state"
|
||||
)
|
||||
|
||||
func VersionStringToStruct(version string) VersionTag {
|
||||
splitVersion := strings.Split(version, ".")
|
||||
|
||||
return VersionTag{Major: splitVersion[0], Minor: splitVersion[1], Patch: splitVersion[2]}
|
||||
}
|
||||
|
||||
func ValidateVersion(version string) error {
|
||||
if splitVersion := strings.Split(version, "."); len(splitVersion) != 3 {
|
||||
return errors.New("Invalid version string. Expected format 'a.b.c'.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SelectedVersion struct {
|
||||
Version string
|
||||
Source string
|
||||
}
|
||||
|
||||
func ListInstalledVersions() ([]string, error) {
|
||||
runtimesDir := GetStatePath("runtimes")
|
||||
runtimesDir := state.GetStatePath("runtimes")
|
||||
entries, err := os.ReadDir(runtimesDir)
|
||||
|
||||
if err != nil {
|
||||
|
@ -65,7 +82,7 @@ func SearchForPythonVersionFile() (SelectedVersion, bool) {
|
|||
// file that would indicate which version is preferred. If none are found, the global
|
||||
// user-defined version (via `v use <version>`) is used. If there is none, the system
|
||||
// Python version is used.
|
||||
func DetermineSelectedPythonVersion(currentState State) (SelectedVersion, error) {
|
||||
func DetermineSelectedPythonVersion(currentState state.State) (SelectedVersion, error) {
|
||||
pythonFileVersion, pythonFileVersionFound := SearchForPythonVersionFile()
|
||||
|
||||
if pythonFileVersionFound {
|
||||
|
@ -73,7 +90,7 @@ func DetermineSelectedPythonVersion(currentState State) (SelectedVersion, error)
|
|||
}
|
||||
|
||||
if len(currentState.GlobalVersion) != 0 {
|
||||
return SelectedVersion{Version: currentState.GlobalVersion, Source: GetStatePath("state.json")}, nil
|
||||
return SelectedVersion{Version: currentState.GlobalVersion, Source: state.GetStatePath("state.json")}, nil
|
||||
}
|
||||
|
||||
systemVersion, _ := DetermineSystemPython()
|
||||
|
@ -83,7 +100,7 @@ func DetermineSelectedPythonVersion(currentState State) (SelectedVersion, error)
|
|||
// DetermineSystemPython returns the unshimmed Python version and path.
|
||||
// It assumes that /bin/python is where system Python lives.
|
||||
func DetermineSystemPython() (string, string) {
|
||||
versionOut, _ := RunCommand([]string{"/bin/python", "--version"}, GetStatePath(), true)
|
||||
versionOut, _ := exec.RunCommand([]string{"/bin/python", "--version"}, state.GetStatePath(), true)
|
||||
detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
|
||||
return strings.TrimSpace(detectedVersion), "/bin/python"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
go get golang.org/x/tools/cmd/cover
|
||||
go test -cover -v -coverprofile=cov.out
|
||||
go test ./... -cover -v -coverprofile=cov.out
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/json"
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/json"
|
22
testutils/setup.go
Normal file
22
testutils/setup.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// SetupAndCleanupEnvironment sets up a test directory and
|
||||
// environment variables before the test and returns a cleanup
|
||||
// function that can be deferred to cleanup any changes to the
|
||||
// system.
|
||||
func SetupAndCleanupEnvironment(t *testing.T) func() {
|
||||
os.Setenv("V_ROOT", t.TempDir())
|
||||
|
||||
temporaryWd := t.TempDir()
|
||||
|
||||
os.Chdir(temporaryWd)
|
||||
|
||||
return func() {
|
||||
os.Unsetenv("V_ROOT")
|
||||
}
|
||||
}
|
19
v.go
19
v.go
|
@ -2,6 +2,9 @@ package main
|
|||
|
||||
import (
|
||||
"os"
|
||||
cli "v/cli"
|
||||
python "v/python"
|
||||
state "v/state"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -13,26 +16,26 @@ const (
|
|||
// Main entrypoint.
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
currentState := ReadState()
|
||||
currentState := state.ReadState()
|
||||
|
||||
cli := CLI{
|
||||
cli := cli.CLI{
|
||||
Metadata: map[string]string{
|
||||
"Version": Version,
|
||||
},
|
||||
}
|
||||
|
||||
err := cli.AddCommand(
|
||||
"install", InstallPython, "v install <version>", "Downloads, builds and installs a new version of Python.",
|
||||
"install", python.InstallPython, "v install <version>", "Downloads, builds and installs a new version of Python.",
|
||||
).AddCommand(
|
||||
"uninstall", UninstallPython, "v uninstall <version>", "Uninstalls the given Python version.",
|
||||
"uninstall", python.UninstallPython, "v uninstall <version>", "Uninstalls the given Python version.",
|
||||
).AddCommand(
|
||||
"use", Use, "v use <version>", "Selects which Python version to use.",
|
||||
"use", python.Use, "v use <version>", "Selects which Python version to use.",
|
||||
).AddCommand(
|
||||
"ls", ListVersions, "v ls", "Lists the installed Python versions.",
|
||||
"ls", python.ListVersions, "v ls", "Lists the installed Python versions.",
|
||||
).AddCommand(
|
||||
"version", CurrentVersion, "v version", "Prints the current version and its source.",
|
||||
"version", python.CurrentVersion, "v version", "Prints the current version and its source.",
|
||||
).AddCommand(
|
||||
"which", Which, "v which", "Prints the path to the current Python version.",
|
||||
"which", python.Which, "v which", "Prints the path to the current Python version.",
|
||||
).AddCommand(
|
||||
"init", Initialize, "v init", "Initializes the v state.",
|
||||
).Run(args, currentState)
|
||||
|
|
Loading…
Reference in a new issue