mirror of
https://github.com/jimeh/evm.git
synced 2026-02-19 07:26:40 +00:00
refactor: extract core logic to a plain Go package
This commit is contained in:
276
manager/config.go
Normal file
276
manager/config.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sethvargo/go-envconfig"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var ErrConfig = fmt.Errorf("%w", Err)
|
||||
|
||||
type Mode string
|
||||
|
||||
const (
|
||||
User Mode = "user"
|
||||
System Mode = "system"
|
||||
)
|
||||
|
||||
type ConfigFile struct {
|
||||
Paths ConfigFilePaths `yaml:"paths" json:"paths"`
|
||||
}
|
||||
|
||||
type ConfigFilePaths struct {
|
||||
Shims string `yaml:"shims" json:"shims" env:"EVM_SHIMS,overwrite"`
|
||||
Sources string `yaml:"sources" json:"sources" env:"EVM_SOURCES,overwrite"`
|
||||
Versions string `yaml:"versions" json:"versions" env:"EVM_VERSIONS,overwrite"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Mode Mode `yaml:"mode" json:"mode"`
|
||||
Current CurrentConfig `yaml:"current" json:"current"`
|
||||
Paths PathsConfig `yaml:"paths" json:"paths"`
|
||||
}
|
||||
|
||||
type CurrentConfig struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
SetBy string `yaml:"set_by,omitempty" json:"set_by,omitempty"`
|
||||
}
|
||||
|
||||
type PathsConfig struct {
|
||||
Binary string `yaml:"binary" json:"binary"`
|
||||
Root string `yaml:"root" json:"root"`
|
||||
Shims string `yaml:"shims" json:"shims"`
|
||||
Sources string `yaml:"sources" json:"sources"`
|
||||
Versions string `yaml:"versions" json:"versions"`
|
||||
}
|
||||
|
||||
func NewConfig() (*Config, error) {
|
||||
mode := Mode(os.Getenv("EVM_MODE"))
|
||||
if mode != System {
|
||||
mode = User
|
||||
}
|
||||
|
||||
defaultRoot := filepath.Join(string(os.PathSeparator), "opt", "evm")
|
||||
if mode == User {
|
||||
defaultRoot = filepath.Join("$HOME", ".evm")
|
||||
}
|
||||
|
||||
if v := os.Getenv("EVM_ROOT"); v != "" {
|
||||
defaultRoot = v
|
||||
}
|
||||
|
||||
conf := &Config{
|
||||
Mode: mode,
|
||||
Paths: PathsConfig{
|
||||
Root: defaultRoot,
|
||||
Shims: "$EVM_ROOT/shims",
|
||||
Sources: "$EVM_ROOT/sources",
|
||||
Versions: "$EVM_ROOT/versions",
|
||||
},
|
||||
}
|
||||
|
||||
var err error
|
||||
conf.Paths.Root, err = conf.normalizePath(conf.Paths.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conf.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf.Paths.Shims, err = conf.normalizePath(conf.Paths.Shims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.Paths.Sources, err = conf.normalizePath(conf.Paths.Sources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.Paths.Versions, err = conf.normalizePath(conf.Paths.Versions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf.Paths.Binary, err = os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conf.PopulateCurrent()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
const currentFileName = "current"
|
||||
|
||||
func (conf *Config) PopulateCurrent() error {
|
||||
if v := os.Getenv("EVM_VERSION"); v != "" {
|
||||
conf.Current.Version = strings.TrimSpace(v)
|
||||
conf.Current.SetBy = "EVM_VERSION environment variable"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
currentFile := filepath.Join(conf.Paths.Root, currentFileName)
|
||||
b, err := os.ReadFile(currentFile)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if len(b) > 0 {
|
||||
conf.Current.Version = strings.TrimSpace(string(b))
|
||||
conf.Current.SetBy = currentFile
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var configFileNames = []string{
|
||||
"config.yaml",
|
||||
"config.yml",
|
||||
"config.json",
|
||||
"evm.yaml",
|
||||
"evm.yml",
|
||||
"evm.json",
|
||||
}
|
||||
|
||||
func (c *Config) load() error {
|
||||
var path string
|
||||
for _, name := range configFileNames {
|
||||
f := filepath.Join(c.Paths.Root, name)
|
||||
|
||||
_, err := os.Stat(f)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
path = f
|
||||
break
|
||||
}
|
||||
|
||||
cf := &ConfigFile{}
|
||||
if path != "" {
|
||||
var err error
|
||||
cf, err = c.loadConfigFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := envconfig.Process(context.Background(), cf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cf.Paths.Shims != "" {
|
||||
c.Paths.Shims = cf.Paths.Shims
|
||||
}
|
||||
if cf.Paths.Sources != "" {
|
||||
c.Paths.Sources = cf.Paths.Sources
|
||||
}
|
||||
if cf.Paths.Versions != "" {
|
||||
c.Paths.Versions = cf.Paths.Versions
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) loadConfigFile(path string) (*ConfigFile, error) {
|
||||
if path == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cf := &ConfigFile{}
|
||||
|
||||
buf := bytes.NewBuffer(content)
|
||||
switch filepath.Ext(path) {
|
||||
case ".yaml", ".yml":
|
||||
dec := yaml.NewDecoder(buf)
|
||||
dec.KnownFields(true)
|
||||
err = dec.Decode(cf)
|
||||
case ".json":
|
||||
dec := json.NewDecoder(buf)
|
||||
dec.DisallowUnknownFields()
|
||||
err = dec.Decode(cf)
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
`%w"%s" does not have a ".yaml", ".yml", `+
|
||||
`or ".json" file extension`,
|
||||
ErrConfig, path,
|
||||
)
|
||||
}
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cf, nil
|
||||
}
|
||||
|
||||
func (c *Config) normalizePath(path string) (string, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
|
||||
var homePrefix string
|
||||
switch {
|
||||
case strings.HasPrefix(path, "$HOME") ||
|
||||
strings.HasPrefix(path, "$home"):
|
||||
homePrefix = path[0:5]
|
||||
case strings.HasPrefix(path, "~"):
|
||||
homePrefix = path[0:1]
|
||||
}
|
||||
|
||||
if homePrefix != "" {
|
||||
if c.Mode == System {
|
||||
return "", fmt.Errorf(
|
||||
`%wEVM_MODE is set to "%s" which prohibits `+
|
||||
`using "$HOME" or "~" in EVM_ROOT`,
|
||||
ErrConfig, string(System),
|
||||
)
|
||||
}
|
||||
|
||||
var home string
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path = filepath.Join(
|
||||
home, strings.TrimPrefix(path, homePrefix))
|
||||
}
|
||||
|
||||
if c.Paths.Root == "" {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "$EVM_ROOT") {
|
||||
path = filepath.Join(c.Paths.Root, path[9:])
|
||||
} else if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(c.Paths.Root, path)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
286
manager/manager.go
Normal file
286
manager/manager.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
Err = errors.New("")
|
||||
ErrVersion = fmt.Errorf("%w", Err)
|
||||
ErrNoCurrentVersion = fmt.Errorf("%w", ErrVersion)
|
||||
ErrVersionNotFound = fmt.Errorf("%w", ErrVersion)
|
||||
ErrBinNotFound = fmt.Errorf("%w", ErrVersion)
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
Config *Config
|
||||
}
|
||||
|
||||
func New(config *Config) (*Manager, error) {
|
||||
if config == nil {
|
||||
var err error
|
||||
config, err = NewConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Manager{Config: config}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) CurrentVersion() string {
|
||||
return m.Config.Current.Version
|
||||
}
|
||||
|
||||
func (m *Manager) CurrentSetBy() string {
|
||||
return m.Config.Current.SetBy
|
||||
}
|
||||
|
||||
func (m *Manager) List(ctx context.Context) ([]*Version, error) {
|
||||
return newVersions(ctx, m.Config)
|
||||
}
|
||||
|
||||
func (m *Manager) Get(ctx context.Context, version string) (*Version, error) {
|
||||
return newVersion(ctx, m.Config, version)
|
||||
}
|
||||
|
||||
func (m *Manager) Use(ctx context.Context, version string) error {
|
||||
ver, err := m.Get(ctx, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentFile := filepath.Join(m.Config.Paths.Root, currentFileName)
|
||||
err = os.WriteFile(currentFile, []byte(ver.Version), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.rehashVersions(ctx, false, []*Version{ver})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) RehashAll(ctx context.Context) error {
|
||||
versions, err := m.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.rehashVersions(ctx, true, versions)
|
||||
}
|
||||
|
||||
func (m *Manager) RehashVersions(
|
||||
ctx context.Context,
|
||||
versions []string,
|
||||
) error {
|
||||
var vers []*Version
|
||||
for _, s := range versions {
|
||||
v, err := m.Get(ctx, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vers = append(vers, v)
|
||||
}
|
||||
|
||||
return m.rehashVersions(ctx, false, vers)
|
||||
}
|
||||
|
||||
func (m *Manager) rehashVersions(
|
||||
ctx context.Context,
|
||||
tidy bool,
|
||||
versions []*Version,
|
||||
) error {
|
||||
|
||||
programs := map[string]bool{}
|
||||
for _, ver := range versions {
|
||||
for _, bin := range ver.Binaries {
|
||||
base := filepath.Base(bin)
|
||||
programs[base] = true
|
||||
}
|
||||
}
|
||||
|
||||
shims, err := m.ListShims(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shimMap := map[string]bool{}
|
||||
for _, s := range shims {
|
||||
base := filepath.Base(s)
|
||||
shimMap[base] = true
|
||||
}
|
||||
|
||||
shim, err := m.shim()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.MkdirAll(m.Config.Paths.Shims, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name := range programs {
|
||||
shimFile := filepath.Join(m.Config.Paths.Shims, name)
|
||||
err = os.WriteFile(shimFile, shim, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var f fs.FileInfo
|
||||
f, err = os.Stat(shimFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.Mode().Perm() != 0o755 {
|
||||
err = os.Chmod(shimFile, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
delete(shimMap, name)
|
||||
}
|
||||
|
||||
if tidy {
|
||||
for name := range shimMap {
|
||||
shimFile := filepath.Join(m.Config.Paths.Shims, name)
|
||||
err := os.Remove(shimFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var shimTemplate = template.Must(template.New("other").Parse(
|
||||
`#!/usr/bin/env bash
|
||||
set -e
|
||||
[ -n "$EVM_DEBUG" ] && set -x
|
||||
|
||||
program="${0##*/}"
|
||||
export EVM_ROOT="{{.Paths.Root}}"
|
||||
exec "{{.Paths.Binary}}" exec "$program" "$@"
|
||||
`))
|
||||
|
||||
func (m *Manager) shim() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := shimTemplate.Execute(&buf, m.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (m *Manager) ListShims(ctx context.Context) ([]string, error) {
|
||||
entries, err := os.ReadDir(m.Config.Paths.Shims)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := []string{}
|
||||
|
||||
for _, entry := range entries {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
shimPath := filepath.Join(m.Config.Paths.Shims, entry.Name())
|
||||
f, err := os.Stat(shimPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f.Mode().IsRegular() {
|
||||
r = append(r, shimPath)
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Exec(
|
||||
ctx context.Context,
|
||||
program string,
|
||||
args []string,
|
||||
) error {
|
||||
current := m.CurrentVersion()
|
||||
if current == "" {
|
||||
return ErrNoCurrentVersion
|
||||
}
|
||||
|
||||
return m.ExecVersion(ctx, current, program, args)
|
||||
}
|
||||
|
||||
func (m *Manager) ExecVersion(
|
||||
ctx context.Context,
|
||||
version string,
|
||||
program string,
|
||||
args []string,
|
||||
) error {
|
||||
ver, err := m.Get(ctx, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bin, err := ver.FindBin(program)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
execArgs := append([]string{bin}, args...)
|
||||
execEnv := os.Environ()
|
||||
|
||||
// Prepend selected version's bin directory to PATH.
|
||||
for i := 0; i < len(execEnv); i++ {
|
||||
if strings.HasPrefix(execEnv[i], "PATH=") {
|
||||
execEnv[i] = "PATH=" + ver.BinDir + ":" + execEnv[i][5:]
|
||||
}
|
||||
}
|
||||
|
||||
return syscall.Exec(bin, execArgs, execEnv)
|
||||
}
|
||||
|
||||
func (m *Manager) FindBin(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
) ([]*Version, error) {
|
||||
versions, err := m.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var availableIn []*Version
|
||||
for _, ver := range versions {
|
||||
if _, err := ver.FindBin(name); err == nil {
|
||||
availableIn = append(availableIn, ver)
|
||||
}
|
||||
}
|
||||
|
||||
return availableIn, nil
|
||||
}
|
||||
114
manager/version.go
Normal file
114
manager/version.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
Current bool `yaml:"current" json:"current"`
|
||||
Path string `yaml:"path" json:"path"`
|
||||
BinDir string `yaml:"bin_dir" json:"bin_dir"`
|
||||
Binaries []string `yaml:"binaries" json:"binaries"`
|
||||
}
|
||||
|
||||
func (ver *Version) FindBin(name string) (string, error) {
|
||||
for _, b := range ver.Binaries {
|
||||
if filepath.Base(b) == name {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf(
|
||||
`%wExecutable "%s" not found in Emacs version %s`,
|
||||
ErrBinNotFound, name, ver.Version,
|
||||
)
|
||||
}
|
||||
|
||||
func newVersion(
|
||||
ctx context.Context,
|
||||
conf *Config,
|
||||
version string,
|
||||
) (*Version, error) {
|
||||
if version == "" {
|
||||
return nil, fmt.Errorf("%wversion cannot be empty", ErrVersion)
|
||||
}
|
||||
|
||||
path := filepath.Join(conf.Paths.Versions, version)
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"%wVersion %s is not available in %s",
|
||||
ErrVersionNotFound, version, conf.Paths.Versions,
|
||||
)
|
||||
}
|
||||
|
||||
ver := &Version{
|
||||
Version: version,
|
||||
Path: path,
|
||||
BinDir: filepath.Join(path, "bin"),
|
||||
Current: version == conf.Current.Version,
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(ver.BinDir)
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
binPath := filepath.Join(ver.BinDir, entry.Name())
|
||||
|
||||
f, err := os.Stat(binPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure f is Regular file and executable.
|
||||
if f.Mode().IsRegular() && f.Mode().Perm()&0111 == 0111 {
|
||||
ver.Binaries = append(ver.Binaries, binPath)
|
||||
}
|
||||
}
|
||||
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
func newVersions(ctx context.Context, conf *Config) ([]*Version, error) {
|
||||
results := []*Version{}
|
||||
|
||||
entries, err := os.ReadDir(conf.Paths.Versions)
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
ver, err := newVersion(ctx, conf, entry.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results = append(results, ver)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
Reference in New Issue
Block a user