feat: basic use,install,list,help,init functionality (#1)
* feat: basic use,install,list,help,init functionality * docs: README notes * refactor: argument parser documentation + removing redundant comments * feat: add uninstall command * feat: command ordering * build: shortcut tooling to build, format * feat: improve messaging around install progress * refactor: rename argparse utils * docs: more README notes
This commit is contained in:
parent
55e652d484
commit
7a1912c02a
18 changed files with 579 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,6 +7,7 @@
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
v
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
33
README.md
33
README.md
|
@ -1,2 +1,35 @@
|
||||||
# v
|
# v
|
||||||
A version manager you might not want to use.
|
A version manager you might not want to use.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`v` is a simple version manager inspired from other tools like [asdf](https://github.com/asdf-vm/asdf), [pyenv](https://github.com/pyenv/pyenv), [n](https://github.com/tj/n) and [nvm](https://github.com/nvm-sh/nvm). At it's core, it's a reinvention of the wheel with some extras.
|
||||||
|
|
||||||
|
- First and foremost, while the first version is about Python version management, the plan is to expand to support a bunch more runtime (with an emphasis on simplifying adding more runtimes to manage);
|
||||||
|
- A lot of those tools are written as shellscript, which I find somewhat inscrutable. Go is a bit easier to read;
|
||||||
|
- ...? It's a reason to write some Go. :)
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
While the plan for the first release is to only support Python runtimes, expanding to others will be next so that `v` can just handle all/most version management needs.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Building your own and setting up
|
||||||
|
|
||||||
|
Pre-built binaries are not currently available. You can clone the repository and build your own via `. scripts/build`.
|
||||||
|
|
||||||
|
You should find a suitable place for the binary (`/usr/local/bin` is a good location) and if not already included, add its location to `$PATH`.
|
||||||
|
|
||||||
|
Finally, run `v init` to create directories to store artifacts and state (under `~/.v` unless override using the
|
||||||
|
`V_ROOT` environment variable) and add `~/.v/shims` to your `$PATH` as well.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
`v` will print a helpful list of available commands.
|
||||||
|
|
||||||
|
The most important things to know include `v install <version>` to install new versions and `v use <installed version>` to use a specific version of Python.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
The project isn't currently accepting contributions because it's not yet set up to do so. Stay tuned.
|
||||||
|
|
45
cmd/v.go
Normal file
45
cmd/v.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module v
|
||||||
|
|
||||||
|
go 1.21.1
|
78
internal/argparse/cli.go
Normal file
78
internal/argparse/cli.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package argparse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
stateManager "v/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command definition for CLI subcommands.
|
||||||
|
type Command struct {
|
||||||
|
Label string
|
||||||
|
Handler func([]string, Flags, stateManager.State) error
|
||||||
|
Usage string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents a CLI invocation.
|
||||||
|
// Must be initialized with commands via AddCommand before running
|
||||||
|
// with Run.
|
||||||
|
type CLI struct {
|
||||||
|
// Commands in enumeration order.
|
||||||
|
OrderedCommands []string
|
||||||
|
// Command metadata entries.
|
||||||
|
Commands map[string]Command
|
||||||
|
Metadata map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registers a command.
|
||||||
|
// 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, stateManager.State) error, usage string, description string) CLI {
|
||||||
|
if c.Commands == nil {
|
||||||
|
c.Commands = map[string]Command{}
|
||||||
|
c.OrderedCommands = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.OrderedCommands = append(c.OrderedCommands, label)
|
||||||
|
c.Commands[label] = Command{Label: label, Handler: handler, Usage: usage, Description: description}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executes one of the registered commands if any match the provided
|
||||||
|
// user arguments.
|
||||||
|
func (c CLI) Run(args []string, currentState stateManager.State) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
c.Help()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
command := args[0]
|
||||||
|
|
||||||
|
if command == "help" {
|
||||||
|
c.Help()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := CollectFlags(args)
|
||||||
|
return c.Commands[command].Handler(args, flags, currentState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints autogenerated help documentation specifying command usage
|
||||||
|
// and descriptions based on registered commands (see: AddCommand).
|
||||||
|
func (c CLI) Help() {
|
||||||
|
usageStrings := []string{}
|
||||||
|
|
||||||
|
for _, commandLabel := range c.OrderedCommands {
|
||||||
|
command := c.Commands[commandLabel]
|
||||||
|
usageStrings = append(usageStrings, fmt.Sprintf("\033[1m%-30s\033[0m%s", command.Usage, command.Description))
|
||||||
|
}
|
||||||
|
|
||||||
|
helpString := fmt.Sprintf(`v: A simple version manager. (v%s)
|
||||||
|
---
|
||||||
|
%s`, c.Metadata["Version"], strings.Join(usageStrings, "\n"))
|
||||||
|
|
||||||
|
fmt.Println(helpString)
|
||||||
|
}
|
25
internal/argparse/util.go
Normal file
25
internal/argparse/util.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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
|
||||||
|
}
|
61
internal/state/state.go
Normal file
61
internal/state/state.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Persistent state used by the CLI to track runtime information
|
||||||
|
// between calls.
|
||||||
|
type State struct {
|
||||||
|
GlobalVersion string `json:"globalVersion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStateDirectory() string {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
userDefinedRoot, found := os.LookupEnv("V_ROOT")
|
||||||
|
|
||||||
|
root := path.Join(home, ".v")
|
||||||
|
|
||||||
|
if found {
|
||||||
|
root = userDefinedRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPathFromStateDirectory(suffix string) string {
|
||||||
|
return path.Join(GetStateDirectory(), suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadState() State {
|
||||||
|
c, _ := ioutil.ReadFile(GetPathFromStateDirectory("state.json"))
|
||||||
|
|
||||||
|
state := State{}
|
||||||
|
|
||||||
|
json.Unmarshal(c, &state)
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteState(version string) {
|
||||||
|
state := State{GlobalVersion: version}
|
||||||
|
|
||||||
|
d, _ := json.Marshal(state)
|
||||||
|
ioutil.WriteFile(GetPathFromStateDirectory("state.json"), d, 0750)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAvailableVersions() []string {
|
||||||
|
entries, _ := os.ReadDir(GetPathFromStateDirectory("runtimes"))
|
||||||
|
|
||||||
|
versions := []string{}
|
||||||
|
|
||||||
|
for _, d := range entries {
|
||||||
|
versions = append(versions, strings.TrimPrefix(d.Name(), "py-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions
|
||||||
|
}
|
47
internal/subcommands/initialize.go
Normal file
47
internal/subcommands/initialize.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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
|
||||||
|
}
|
122
internal/subcommands/install.go
Normal file
122
internal/subcommands/install.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package subcommands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
argparse "v/internal/argparse"
|
||||||
|
stateManager "v/internal/state"
|
||||||
|
util "v/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pythonReleasesBaseURL = "https://www.python.org/ftp/python"
|
||||||
|
|
||||||
|
type PackageMetadata struct {
|
||||||
|
ArchivePath string
|
||||||
|
InstallPath string
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionTag struct {
|
||||||
|
Major string
|
||||||
|
Minor string
|
||||||
|
Patch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func InstallPython(args []string, flags argparse.Flags, currentState stateManager.State) error {
|
||||||
|
verbose := flags.Verbose
|
||||||
|
version := args[1]
|
||||||
|
|
||||||
|
if err := validateVersion(version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMetadata, dlerr := downloadSource(version, "")
|
||||||
|
|
||||||
|
if dlerr != nil {
|
||||||
|
return dlerr
|
||||||
|
}
|
||||||
|
_, err := buildFromSource(packageMetadata, verbose)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches the Python tarball for version <version> from python.org
|
||||||
|
// and stores it at <destination>.
|
||||||
|
func downloadSource(version string, destination string) (PackageMetadata, error) {
|
||||||
|
archiveName := fmt.Sprintf("Python-%s.tgz", version)
|
||||||
|
archivePath := stateManager.GetPathFromStateDirectory(path.Join("cache", archiveName))
|
||||||
|
sourceUrl := fmt.Sprintf("%s/%s/%s", pythonReleasesBaseURL, version, archiveName)
|
||||||
|
file, _ := os.Create(archivePath)
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
|
||||||
|
dlPrint := util.StartFmtGroup(fmt.Sprintf("Downloading source for Python %s", version))
|
||||||
|
|
||||||
|
dlPrint(fmt.Sprintf("Fetching from %s", sourceUrl))
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := client.Get(sourceUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return PackageMetadata{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
io.Copy(file, resp.Body)
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
dlPrint(fmt.Sprintf("✅ Done (%s)", time.Since(start)))
|
||||||
|
return PackageMetadata{ArchivePath: archivePath, Version: version}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, error) {
|
||||||
|
buildPrint := util.StartFmtGroup(fmt.Sprintf("Building from source"))
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
buildPrint(fmt.Sprintf("Unpacking source for %s", pkgMeta.ArchivePath))
|
||||||
|
|
||||||
|
_, untarErr := RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, stateManager.GetPathFromStateDirectory("cache"), !verbose)
|
||||||
|
|
||||||
|
if untarErr != nil {
|
||||||
|
return pkgMeta, untarErr
|
||||||
|
}
|
||||||
|
|
||||||
|
unzippedRoot := strings.TrimSuffix(pkgMeta.ArchivePath, path.Ext(pkgMeta.ArchivePath))
|
||||||
|
|
||||||
|
buildPrint("Configuring installer")
|
||||||
|
|
||||||
|
targetDirectory := stateManager.GetPathFromStateDirectory(path.Join("runtimes", fmt.Sprintf("py-%s", pkgMeta.Version)))
|
||||||
|
|
||||||
|
_, configureErr := RunCommand([]string{"./configure", fmt.Sprintf("--prefix=%s", targetDirectory), "--enable-optimizations"}, unzippedRoot, !verbose)
|
||||||
|
|
||||||
|
if configureErr != nil {
|
||||||
|
return pkgMeta, configureErr
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPrint("Building")
|
||||||
|
_, buildErr := RunCommand([]string{"make", "altinstall"}, unzippedRoot, !verbose)
|
||||||
|
|
||||||
|
if buildErr != nil {
|
||||||
|
return pkgMeta, buildErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if cleanupErr := os.RemoveAll(unzippedRoot); cleanupErr != nil {
|
||||||
|
return pkgMeta, cleanupErr
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgMeta.InstallPath = targetDirectory
|
||||||
|
|
||||||
|
buildPrint(fmt.Sprintf("Installed Python %s at %s", pkgMeta.Version, pkgMeta.InstallPath))
|
||||||
|
buildPrint(fmt.Sprintf("✅ Done (%s)", time.Since(start)))
|
||||||
|
return pkgMeta, nil
|
||||||
|
}
|
29
internal/subcommands/ls.go
Normal file
29
internal/subcommands/ls.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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
|
||||||
|
}
|
15
internal/subcommands/uninstall.go
Normal file
15
internal/subcommands/uninstall.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
|
}
|
34
internal/subcommands/use.go
Normal file
34
internal/subcommands/use.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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
|
||||||
|
}
|
33
internal/subcommands/util.go
Normal file
33
internal/subcommands/util.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package subcommands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunCommand(command []string, cwd string, quiet bool) (string, error) {
|
||||||
|
cmd := exec.Command(command[0], command[1:]...)
|
||||||
|
|
||||||
|
cmd.Dir = cwd
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
22
internal/subcommands/where.go
Normal file
22
internal/subcommands/where.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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
|
||||||
|
}
|
12
internal/subcommands/which.go
Normal file
12
internal/subcommands/which.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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
|
||||||
|
}
|
13
internal/util/fmtgroup.go
Normal file
13
internal/util/fmtgroup.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
3
scripts/build
Normal file
3
scripts/build
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
go build cmd/v.go
|
3
scripts/format
Normal file
3
scripts/format
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
go fmt ./...
|
Loading…
Reference in a new issue