refactor: simplify project structure (#2)

* refactor: remove file structure cruft

* refactor: flatten file structure entirely

* refactor: combine utils
This commit is contained in:
Marc 2023-11-04 21:28:11 -04:00 committed by GitHub
parent 7a1912c02a
commit ab711a28b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 207 additions and 263 deletions

View file

@ -1,15 +1,18 @@
package argparse package main
import ( import (
"fmt" "fmt"
"strings" "strings"
stateManager "v/internal/state"
) )
type Flags struct {
Verbose bool
}
// Command definition for CLI subcommands. // Command definition for CLI subcommands.
type Command struct { type Command struct {
Label string Label string
Handler func([]string, Flags, stateManager.State) error Handler func([]string, Flags, State) error
Usage string Usage string
Description string Description string
} }
@ -29,7 +32,7 @@ type CLI struct {
// This specifies a label that is used to route the user input to // 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, // the right command, a handler that is called when the label is used,
// and usage/description details that get included in autogenerated help messaging. // and usage/description details that get included in autogenerated help messaging.
func (c CLI) AddCommand(label string, handler func([]string, Flags, stateManager.State) error, usage string, description string) CLI { func (c CLI) AddCommand(label string, handler func([]string, Flags, State) error, usage string, description string) CLI {
if c.Commands == nil { if c.Commands == nil {
c.Commands = map[string]Command{} c.Commands = map[string]Command{}
c.OrderedCommands = []string{} c.OrderedCommands = []string{}
@ -43,7 +46,7 @@ func (c CLI) AddCommand(label string, handler func([]string, Flags, stateManager
// 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 stateManager.State) error { func (c CLI) Run(args []string, currentState State) error {
if len(args) == 0 { if len(args) == 0 {
c.Help() c.Help()
return nil return nil
@ -56,7 +59,7 @@ func (c CLI) Run(args []string, currentState stateManager.State) error {
return nil return nil
} }
flags := CollectFlags(args) flags := collectFlags(args)
return c.Commands[command].Handler(args, flags, currentState) return c.Commands[command].Handler(args, flags, currentState)
} }
@ -76,3 +79,21 @@ func (c CLI) Help() {
fmt.Println(helpString) fmt.Println(helpString)
} }
// Traverses input arguments and extracts flags of
// the form --<flag-label>.
func collectFlags(args []string) Flags {
collected := Flags{}
for _, arg := range args {
if !strings.HasPrefix(arg, "--") {
continue
}
if arg == "--verbose" {
collected.Verbose = true
}
}
return collected
}

View file

@ -1,45 +0,0 @@
package main
import (
"os"
argparse "v/internal/argparse"
stateManager "v/internal/state"
subcommands "v/internal/subcommands"
)
const (
Version = "0.0.1"
Author = "mcataford <hello@karnov.club>"
)
// Main entrypoint.
func main() {
args := os.Args[1:]
currentState := stateManager.ReadState()
cli := argparse.CLI{
Metadata: map[string]string{
"Version": Version,
},
}
err := cli.AddCommand(
"install", subcommands.InstallPython, "v install <version>", "Downloads, builds and installs a new version of Python.",
).AddCommand(
"uninstall", subcommands.UninstallPython, "v uninstall <version>", "Uninstalls the given Python version.",
).AddCommand(
"use", subcommands.Use, "v use <version>", "Selects which Python version to use.",
).AddCommand(
"ls", subcommands.ListVersions, "v ls", "Lists the installed Python versions.",
).AddCommand(
"where", subcommands.Where, "v where", "Prints the path to the current Python version.",
).AddCommand(
"which", subcommands.Which, "v which", "Prints the current Python version.",
).AddCommand(
"init", subcommands.Initialize, "v init", "Initializes the v state.",
).Run(args, currentState)
if err != nil {
panic(err)
}
}

111
commands.go Normal file
View file

@ -0,0 +1,111 @@
package main
import (
"errors"
"fmt"
"os"
"path"
"strings"
)
var DIRECTORIES = []string{
"cache",
"runtimes",
"shims",
}
var SHIMS = []string{
"python",
"python3",
}
const DEFAULT_PERMISSION = 0775
func writeShim(shimPath string) error {
shimContent := []byte("#!/bin/bash\n$(vm where) $@")
if err := os.WriteFile(shimPath, shimContent, DEFAULT_PERMISSION); err != nil {
return err
}
return nil
}
// Sets up directories and files used to store downloaded archives,
// installed runtimes and metadata.
func Initialize(args []string, flags Flags, currentState State) error {
stateDirectory := GetStateDirectory()
os.Mkdir(stateDirectory, DEFAULT_PERMISSION)
for _, dir := range DIRECTORIES {
os.Mkdir(GetPathFromStateDirectory(dir), DEFAULT_PERMISSION)
}
for _, shim := range SHIMS {
writeShim(GetPathFromStateDirectory(path.Join("shims", shim)))
}
return nil
}
func UninstallPython(args []string, flags Flags, currentState State) error {
runtimePath := GetPathFromStateDirectory(path.Join("runtimes", fmt.Sprintf("py-%s", args[1])))
err := os.RemoveAll(runtimePath)
return err
}
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 {
return errors.New("Version not installed.")
}
WriteState(version)
fmt.Printf("Now using Python %s\n", version)
return nil
}
func ListVersions(args []string, flags Flags, currentState State) error {
runtimesDir := GetPathFromStateDirectory("runtimes")
entries, err := os.ReadDir(runtimesDir)
if err != nil {
return err
}
if len(entries) == 0 {
fmt.Println("No versions installed!")
return nil
}
for _, d := range entries {
fmt.Println(strings.TrimPrefix(d.Name(), "py-"))
}
return nil
}
func Where(args []string, flags Flags, currentState State) error {
version := currentState.GlobalVersion
tag := VersionStringToStruct(version)
withoutPatch := fmt.Sprintf("%s.%s", tag.Major, tag.Minor)
fmt.Printf("%s/runtimes/py-%s/bin/python%s\n", GetStateDirectory(), currentState.GlobalVersion, withoutPatch)
return nil
}
func Which(args []string, flags Flags, currentState State) error {
fmt.Println(currentState.GlobalVersion)
return nil
}

View file

@ -1,4 +1,4 @@
package subcommands package main
import ( import (
"fmt" "fmt"
@ -8,9 +8,6 @@ import (
"path" "path"
"strings" "strings"
"time" "time"
argparse "v/internal/argparse"
stateManager "v/internal/state"
util "v/internal/util"
) )
var pythonReleasesBaseURL = "https://www.python.org/ftp/python" var pythonReleasesBaseURL = "https://www.python.org/ftp/python"
@ -27,11 +24,11 @@ type VersionTag struct {
Patch string Patch string
} }
func InstallPython(args []string, flags argparse.Flags, currentState stateManager.State) error { func InstallPython(args []string, flags Flags, currentState State) error {
verbose := flags.Verbose verbose := flags.Verbose
version := args[1] version := args[1]
if err := validateVersion(version); err != nil { if err := ValidateVersion(version); err != nil {
return err return err
} }
@ -53,13 +50,13 @@ func InstallPython(args []string, flags argparse.Flags, currentState stateManage
// and stores it at <destination>. // and stores it at <destination>.
func downloadSource(version string, destination string) (PackageMetadata, error) { func downloadSource(version string, destination string) (PackageMetadata, error) {
archiveName := fmt.Sprintf("Python-%s.tgz", version) archiveName := fmt.Sprintf("Python-%s.tgz", version)
archivePath := stateManager.GetPathFromStateDirectory(path.Join("cache", archiveName)) archivePath := GetPathFromStateDirectory(path.Join("cache", archiveName))
sourceUrl := fmt.Sprintf("%s/%s/%s", pythonReleasesBaseURL, version, archiveName) sourceUrl := fmt.Sprintf("%s/%s/%s", pythonReleasesBaseURL, version, archiveName)
file, _ := os.Create(archivePath) file, _ := os.Create(archivePath)
client := http.Client{} client := http.Client{}
dlPrint := util.StartFmtGroup(fmt.Sprintf("Downloading source for Python %s", version)) dlPrint := StartFmtGroup(fmt.Sprintf("Downloading source for Python %s", version))
dlPrint(fmt.Sprintf("Fetching from %s", sourceUrl)) dlPrint(fmt.Sprintf("Fetching from %s", sourceUrl))
start := time.Now() start := time.Now()
@ -80,12 +77,12 @@ func downloadSource(version string, destination string) (PackageMetadata, error)
} }
func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, error) { func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, error) {
buildPrint := util.StartFmtGroup(fmt.Sprintf("Building from source")) buildPrint := StartFmtGroup(fmt.Sprintf("Building from source"))
start := time.Now() start := time.Now()
buildPrint(fmt.Sprintf("Unpacking source for %s", pkgMeta.ArchivePath)) buildPrint(fmt.Sprintf("Unpacking source for %s", pkgMeta.ArchivePath))
_, untarErr := RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, stateManager.GetPathFromStateDirectory("cache"), !verbose) _, untarErr := RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, GetPathFromStateDirectory("cache"), !verbose)
if untarErr != nil { if untarErr != nil {
return pkgMeta, untarErr return pkgMeta, untarErr
@ -95,7 +92,7 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
buildPrint("Configuring installer") buildPrint("Configuring installer")
targetDirectory := stateManager.GetPathFromStateDirectory(path.Join("runtimes", fmt.Sprintf("py-%s", pkgMeta.Version))) targetDirectory := GetPathFromStateDirectory(path.Join("runtimes", fmt.Sprintf("py-%s", pkgMeta.Version)))
_, configureErr := RunCommand([]string{"./configure", fmt.Sprintf("--prefix=%s", targetDirectory), "--enable-optimizations"}, unzippedRoot, !verbose) _, configureErr := RunCommand([]string{"./configure", fmt.Sprintf("--prefix=%s", targetDirectory), "--enable-optimizations"}, unzippedRoot, !verbose)

View file

@ -1,25 +0,0 @@
package argparse
import (
"strings"
)
type Flags struct {
Verbose bool
}
func CollectFlags(args []string) Flags {
collected := Flags{}
for _, arg := range args {
if !strings.HasPrefix(arg, "--") {
continue
}
if arg == "--verbose" {
collected.Verbose = true
}
}
return collected
}

View file

@ -1,47 +0,0 @@
package subcommands
import (
"os"
"path"
argparse "v/internal/argparse"
stateManager "v/internal/state"
)
var DIRECTORIES = []string{
"cache",
"runtimes",
"shims",
}
var SHIMS = []string{
"python",
"python3",
}
const DEFAULT_PERMISSION = 0775
func writeShim(shimPath string) error {
shimContent := []byte("#!/bin/bash\n$(vm where) $@")
if err := os.WriteFile(shimPath, shimContent, DEFAULT_PERMISSION); err != nil {
return err
}
return nil
}
// Sets up directories and files used to store downloaded archives,
// installed runtimes and metadata.
func Initialize(args []string, flags argparse.Flags, currentState stateManager.State) error {
stateDirectory := stateManager.GetStateDirectory()
os.Mkdir(stateDirectory, DEFAULT_PERMISSION)
for _, dir := range DIRECTORIES {
os.Mkdir(stateManager.GetPathFromStateDirectory(dir), DEFAULT_PERMISSION)
}
for _, shim := range SHIMS {
writeShim(stateManager.GetPathFromStateDirectory(path.Join("shims", shim)))
}
return nil
}

View file

@ -1,29 +0,0 @@
package subcommands
import (
"fmt"
"os"
"strings"
argparse "v/internal/argparse"
stateManager "v/internal/state"
)
func ListVersions(args []string, flags argparse.Flags, currentState stateManager.State) error {
runtimesDir := stateManager.GetPathFromStateDirectory("runtimes")
entries, err := os.ReadDir(runtimesDir)
if err != nil {
return err
}
if len(entries) == 0 {
fmt.Println("No versions installed!")
return nil
}
for _, d := range entries {
fmt.Println(strings.TrimPrefix(d.Name(), "py-"))
}
return nil
}

View file

@ -1,15 +0,0 @@
package subcommands
import (
"fmt"
"os"
"path"
argparse "v/internal/argparse"
stateManager "v/internal/state"
)
func UninstallPython(args []string, flags argparse.Flags, currentState stateManager.State) error {
runtimePath := stateManager.GetPathFromStateDirectory(path.Join("runtimes", fmt.Sprintf("py-%s", args[1])))
err := os.RemoveAll(runtimePath)
return err
}

View file

@ -1,34 +0,0 @@
package subcommands
import (
"errors"
"fmt"
argparse "v/internal/argparse"
stateManager "v/internal/state"
)
func Use(args []string, flags argparse.Flags, currentState stateManager.State) error {
version := args[1]
if err := validateVersion(version); err != nil {
return err
}
availableVersions := stateManager.GetAvailableVersions()
found := false
for _, v := range availableVersions {
if v == version {
found = true
break
}
}
if !found {
return errors.New("Version not installed.")
}
stateManager.WriteState(version)
fmt.Printf("Now using Python %s\n", version)
return nil
}

View file

@ -1,22 +0,0 @@
package subcommands
import (
"fmt"
"strings"
argparse "v/internal/argparse"
stateManager "v/internal/state"
)
func versionStringToStruct(version string) VersionTag {
splitVersion := strings.Split(version, ".")
return VersionTag{Major: splitVersion[0], Minor: splitVersion[1], Patch: splitVersion[2]}
}
func Where(args []string, flags argparse.Flags, currentState stateManager.State) error {
version := currentState.GlobalVersion
tag := versionStringToStruct(version)
withoutPatch := fmt.Sprintf("%s.%s", tag.Major, tag.Minor)
fmt.Printf("%s/runtimes/py-%s/bin/python%s\n", stateManager.GetStateDirectory(), currentState.GlobalVersion, withoutPatch)
return nil
}

View file

@ -1,12 +0,0 @@
package subcommands
import (
"fmt"
argparse "v/internal/argparse"
stateManager "v/internal/state"
)
func Which(args []string, flags argparse.Flags, currentState stateManager.State) error {
fmt.Println(currentState.GlobalVersion)
return nil
}

View file

@ -1,13 +0,0 @@
package util
import (
"fmt"
)
func StartFmtGroup(label string) func(string) {
fmt.Printf("\033[1m%s\033[0m\n", label)
return func(message string) {
fmt.Printf(" %s\n", message)
}
}

View file

@ -1,3 +1,3 @@
#!/usr/bin/bash #!/usr/bin/bash
go build cmd/v.go go build .

View file

@ -1,4 +1,4 @@
package state package main
import ( import (
"encoding/json" "encoding/json"

View file

@ -1,13 +1,28 @@
package subcommands package main
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
) )
func validateVersion(version string) error { func StartFmtGroup(label string) func(string) {
fmt.Printf("\033[1m%s\033[0m\n", label)
return func(message string) {
fmt.Printf(" %s\n", message)
}
}
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 { if splitVersion := strings.Split(version, "."); len(splitVersion) != 3 {
return errors.New("Invalid version string. Expected format 'a.b.c'.") return errors.New("Invalid version string. Expected format 'a.b.c'.")
} }

42
v.go Normal file
View file

@ -0,0 +1,42 @@
package main
import (
"os"
)
const (
Version = "0.0.1"
Author = "mcataford <hello@karnov.club>"
)
// Main entrypoint.
func main() {
args := os.Args[1:]
currentState := ReadState()
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.",
).AddCommand(
"uninstall", UninstallPython, "v uninstall <version>", "Uninstalls the given Python version.",
).AddCommand(
"use", Use, "v use <version>", "Selects which Python version to use.",
).AddCommand(
"ls", ListVersions, "v ls", "Lists the installed Python versions.",
).AddCommand(
"where", Where, "v where", "Prints the path to the current Python version.",
).AddCommand(
"which", Which, "v which", "Prints the current Python version.",
).AddCommand(
"init", Initialize, "v init", "Initializes the v state.",
).Run(args, currentState)
if err != nil {
panic(err)
}
}