From 45a297a8e526094e8fce6e2c5c0fd89b381d1765 Mon Sep 17 00:00:00 2001 From: ewy Date: Tue, 14 Apr 2026 16:37:17 +0200 Subject: i have to commit at some point! --- runner/base.go | 82 +++++++++++++++++++++ runner/python/filetarget.go | 51 +++++++++++++ runner/python/indexer.go | 44 +++++++++++ runner/python/projtarget.go | 50 +++++++++++++ runner/python/runner.go | 86 ++++++++++++++++++++++ runner/shell/hydrated.go | 17 +++++ runner/shell/shell.go | 176 ++++++++++++++++++++++++++++++++++++++++++++ runner/shell/shell_test.go | 22 ++++++ runner/shell/target.go | 35 +++++++++ 9 files changed, 563 insertions(+) create mode 100644 runner/base.go create mode 100644 runner/python/filetarget.go create mode 100644 runner/python/indexer.go create mode 100644 runner/python/projtarget.go create mode 100644 runner/python/runner.go create mode 100644 runner/shell/hydrated.go create mode 100644 runner/shell/shell.go create mode 100644 runner/shell/shell_test.go create mode 100644 runner/shell/target.go (limited to 'runner') diff --git a/runner/base.go b/runner/base.go new file mode 100644 index 0000000..7a6ce81 --- /dev/null +++ b/runner/base.go @@ -0,0 +1,82 @@ +package runner + +import ( + "os/exec" + "pik/identity" + "pik/model" +) + +type BaseTarget struct { + identity.Identity + MyTags model.Tags +} + +func (t *BaseTarget) Tags() model.Tags { + return t.MyTags +} + +func (t *BaseTarget) Matches(input string) bool { + return t.Identity.Is(input) +} + +func (t *BaseTarget) ShortestId() string { + return t.Reduced +} + +func Hydrated[T model.Target](in T) BaseHydration[T] { + return BaseHydration[T]{ + BaseTarget: in, + } +} + +type BaseHydration[T model.Target] struct { + BaseTarget T +} + +func (b BaseHydration[T]) Matches(input string) bool { + return b.BaseTarget.Matches(input) +} + +func (b BaseHydration[T]) Create(s *model.Source) *exec.Cmd { + return b.BaseTarget.Create(s) +} + +func (b BaseHydration[T]) Sub() []string { + return b.BaseTarget.Sub() +} + +func (b BaseHydration[T]) Label() string { + return b.BaseTarget.Label() +} + +func (b BaseHydration[T]) Hydrate(src *model.Source) (model.HydratedTarget, error) { + return b, nil +} + +func (b BaseHydration[T]) Visible() bool { + return b.BaseTarget.Visible() +} + +func (b BaseHydration[T]) Tags() model.Tags { + return b.BaseTarget.Tags() +} + +func (b BaseHydration[T]) ShortestId() string { + return b.BaseTarget.ShortestId() +} + +func (b BaseHydration[T]) Icon() string { + return " " +} + +func (b BaseHydration[T]) Description() string { + return "//TODO" +} + +func (b BaseHydration[T]) Target() model.Target { + return b.BaseTarget +} + +func (b BaseTarget) Visible() bool { + return b.Tags().Visible() +} diff --git a/runner/python/filetarget.go b/runner/python/filetarget.go new file mode 100644 index 0000000..6793f4b --- /dev/null +++ b/runner/python/filetarget.go @@ -0,0 +1,51 @@ +package python + +import ( + "os/exec" + "path/filepath" + "pik/model" + "pik/runner" +) + +type FileTarget struct { + runner.BaseTarget + File string +} + +type HydratedFileTarget struct { + runner.BaseHydration[*FileTarget] +} + +func (h *HydratedFileTarget) Icon() string { + return "\uE606" +} + +func (p *FileTarget) Create(s *model.Source) *exec.Cmd { + var cmd []string + if Python.Uv != "" { + cmd = []string{Python.Uv, "run", "--", p.File} + } else if venv := Python.VenvFor(s); venv != "" { + cmd = []string{filepath.Join(s.Path, venv, "bin", "python3"), p.File} + } else { + sysPath, err := exec.LookPath("python3") + if err != nil { + return nil + } + cmd = []string{sysPath, p.File} + } + return exec.Command(cmd[0], cmd[1:]...) +} + +func (p *FileTarget) Sub() []string { + return nil +} + +func (p *FileTarget) Label() string { + return p.Full +} + +func (p *FileTarget) Hydrate(src *model.Source) (model.HydratedTarget, error) { + return &HydratedFileTarget{ + BaseHydration: runner.Hydrated(p), + }, nil +} diff --git a/runner/python/indexer.go b/runner/python/indexer.go new file mode 100644 index 0000000..d0dd4e3 --- /dev/null +++ b/runner/python/indexer.go @@ -0,0 +1,44 @@ +package python + +import ( + "github.com/pelletier/go-toml/v2" + "io/fs" + "os" + "path/filepath" + "pik/model" +) + +type Pyproj struct { + Project struct { + Scripts map[string]string + } +} + +func (p python) Index(path string, f fs.FS, runners []model.Runner) ([]model.Target, error) { + for _, pt := range VenvPaths { + if stat, err := fs.Stat(f, filepath.Join(pt)); err == nil { + if stat.IsDir() { + p.Venvs[path] = filepath.Join(path, pt) + } + } + } + content, err := fs.ReadFile(f, "pyproject.toml") + if os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, err + } + + pp := &Pyproj{} + + err = toml.Unmarshal(content, pp) + if err != nil { + return nil, err + } + + var targets = make([]model.Target, 0, len(pp.Project.Scripts)) + for n, s := range pp.Project.Scripts { + targets = append(targets, Python.CreateProjTarget(n, s)) + } + return targets, nil +} diff --git a/runner/python/projtarget.go b/runner/python/projtarget.go new file mode 100644 index 0000000..1604948 --- /dev/null +++ b/runner/python/projtarget.go @@ -0,0 +1,50 @@ +package python + +import ( + "os/exec" + "path/filepath" + "pik/model" + "pik/runner" +) + +type ProjTarget struct { + runner.BaseTarget + Cmd string +} + +type HydratedProjTarget struct { + runner.BaseHydration[*ProjTarget] +} + +func (h *HydratedProjTarget) Icon() string { + return "\uE606" +} + +func (h *HydratedProjTarget) Description() string { + //TODO implement me + return "//TODO" +} + +func (p *ProjTarget) Create(s *model.Source) *exec.Cmd { + var cmd []string + if Python.Uv != "" { + cmd = []string{Python.Uv, "run", "--", p.Cmd} + } else if venv := Python.VenvFor(s); venv != "" { + cmd = []string{filepath.Join(s.Path, venv, "bin", "python"), p.Cmd} + } + return exec.Command(cmd[0], cmd[1:]...) +} + +func (p *ProjTarget) Sub() []string { + return nil +} + +func (p *ProjTarget) Label() string { + return p.Cmd +} + +func (p *ProjTarget) Hydrate(src *model.Source) (model.HydratedTarget, error) { + return &HydratedProjTarget{ + BaseHydration: runner.Hydrated(p), + }, nil +} diff --git a/runner/python/runner.go b/runner/python/runner.go new file mode 100644 index 0000000..b06d8ab --- /dev/null +++ b/runner/python/runner.go @@ -0,0 +1,86 @@ +package python + +import ( + "errors" + "io/fs" + "os/exec" + "path/filepath" + "pik/identity" + "pik/model" + "pik/runner" +) + +type python struct { + Venvs map[string]string + Uv string + System string +} + +func (p python) Init() error { + uv, err := exec.LookPath("uv") + if err != nil && !errors.Is(err, exec.ErrNotFound) { + return err + } + p.Uv = uv + sys, err := exec.LookPath("python3") + if err == nil { + p.System = sys + } + return err +} + +func (p python) Hydrate(target model.Target) (model.HydratedTarget, error) { + //TODO implement me + panic("implement me") +} + +func (p python) Wants(fs fs.FS, file string, entry fs.DirEntry) (bool, error) { + return !entry.IsDir() && filepath.Ext(entry.Name()) == ".py", nil +} + +func (p python) VenvFor(src *model.Source) string { + venvPath := p.Venvs[src.Path] + if venvPath != "" { + return venvPath + } + return "" +} + +func (p python) PyFor(src *model.Source) []string { + if p.Uv != "" { + return []string{p.Uv, "run", "--"} + } + if venv := p.VenvFor(src); venv != "" { + return []string{filepath.Join(src.Path, venv, "bin", "python")} + } + return nil +} + +func (p python) CreateProjTarget(name string, cmd string) model.Target { + return &ProjTarget{ + BaseTarget: runner.BaseTarget{ + Identity: identity.New(name), + }, + Cmd: cmd, + } +} + +func (p python) CreateTarget(fs fs.FS, source string, file string, entry fs.DirEntry) (model.Target, error) { + _, filename := filepath.Split(file) + return &FileTarget{ + BaseTarget: runner.BaseTarget{ + Identity: identity.New(filename), + MyTags: model.TagsFromFilename(filename), + }, + File: file, + }, nil +} + +var VenvPaths = []string{ + ".venv", + "venv", +} + +var Python = &python{ + Venvs: map[string]string{}, +} diff --git a/runner/shell/hydrated.go b/runner/shell/hydrated.go new file mode 100644 index 0000000..5a86d5a --- /dev/null +++ b/runner/shell/hydrated.go @@ -0,0 +1,17 @@ +package shell + +import ( + "pik/runner" +) + +type HydratedShellTarget struct { + runner.BaseHydration[*ShellTarget] +} + +func (h *HydratedShellTarget) Icon() string { + return "\uF489" +} + +func (h *HydratedShellTarget) Description() string { + return "//TODO" +} diff --git a/runner/shell/shell.go b/runner/shell/shell.go new file mode 100644 index 0000000..5333279 --- /dev/null +++ b/runner/shell/shell.go @@ -0,0 +1,176 @@ +package shell + +import ( + "bufio" + "errors" + "io/fs" + "os/exec" + "path/filepath" + "pik/identity" + "pik/indexers/pikdex" + "pik/model" + "pik/runner" + "pik/spool" + "slices" + "strings" +) + +//TODO: Clean up shell selection? Maybe default to bash? + +var NoContentError = errors.New("not enough content in target") +var NoShellError = errors.New("could not find any shell interpreters") + +var ExtShellMap = map[string]string{ + ".sh": "bash", + ".ps1": "powershell", +} + +var Shells = []string{"bash", "bash.exe", "zsh", "fish", "powershell", "powershell.exe", "cmd.exe"} + +var Runner = &shell{ + Locations: map[string]string{}, +} + +type shell struct { + Locations map[string]string +} + +var WrongTargetError = errors.New("wrong target type") + +func (s *shell) Hydrate(target model.Target) (model.HydratedTarget, error) { + cast, ok := target.(*ShellTarget) + if !ok { + return nil, WrongTargetError + } + hyd := &HydratedShellTarget{BaseHydration: runner.Hydrated(cast)} + return hyd, nil +} + +func (s *shell) Wants(f fs.FS, file string, entry fs.DirEntry) (bool, error) { + if entry != nil && entry.IsDir() { + return false, nil + } + + fd, err := f.Open(file) + if err != nil { + return false, err + } + scanner := bufio.NewScanner(fd) + scanner.Split(bufio.ScanRunes) + if !scanner.Scan() { + return false, nil + } + txt := scanner.Text() + if txt == "#" { // + return true, nil + } + for k, _ := range ExtShellMap { + if strings.HasSuffix(file, k) { + return true, nil + } + } + return false, nil +} + +func (s *shell) Find(shell string) (string, error) { + if s.Locations[shell] != "" { + return s.Locations[shell], nil + } + + if p, err := exec.LookPath(shell); err == nil { + s.Locations[shell] = p + return shell, nil + } else { + return "", err + } +} + +func (s *shell) CreateTarget(fs fs.FS, src string, file string, _ fs.DirEntry) (model.Target, error) { + shell, err := s.ShellFor(fs, file) + if err != nil { + return nil, err + } + _, filename := filepath.Split(file) + var sub []string + split := strings.Split(file, "/") + for _, p := range split { + if slices.Contains(pikdex.Roots, p) { + continue + } + if filename == p { + continue + } + sub = append(sub, p) + } + return &ShellTarget{ + BaseTarget: runner.BaseTarget{ + Identity: identity.New(filename), + MyTags: model.TagsFromFilename(filename), + }, + Shell: shell, + Script: file, + SubValue: sub, + }, nil +} + +func (s *shell) ShellFor(fs fs.FS, file string) (string, error) { + + var shell, shebang string + + // low-hanging fruit - indicative filename + if byFile := s.ShellByFilename(file); byFile != "" { + return byFile, nil + } + + fd, err := fs.Open(file) + if err != nil { + return "", err + } + scanner := bufio.NewScanner(fd) + scanner.Split(bufio.ScanLines) + if !scanner.Scan() { + return "", NoContentError + } + txt := scanner.Text() + if strings.HasPrefix(txt, "#!") { + // shebang found + for _, potentialShell := range Shells { + if strings.Contains(txt, potentialShell) { + shebang = shell + if loc, err := s.Find(potentialShell); err == nil { + shell = loc + } else { + _, _ = spool.Warn("script has %s but could not find %s (%s)\n", shebang, potentialShell) + } + } + } + } + + if shebang == "" { + // if no shebang, just send the first one we find + for _, s := range Shells { + if p, err := exec.LookPath(s); err != nil { + shell = p + } + } + } + + if shell == "" { + return "", NoShellError + } + + return shell, nil + +} + +func (s *shell) ShellByFilename(file string) string { + ext := filepath.Ext(file) + if ExtShellMap[ext] != "" { + sh, err := s.Find(ExtShellMap[ext]) + if err == nil { + return sh + } + } + + return "" +} diff --git a/runner/shell/shell_test.go b/runner/shell/shell_test.go new file mode 100644 index 0000000..b28bd6c --- /dev/null +++ b/runner/shell/shell_test.go @@ -0,0 +1,22 @@ +package shell + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestShell_ShellByFilename(t *testing.T) { + s := &shell{ + Locations: map[string]string{"powershell": "/pws", "bash": "/bash"}, + } + inputs := map[string]string{ + "script.ps1": "/pws", + "script.sh": "/bash", + "asdf": "", + } + + for k, v := range inputs { + sh := s.ShellByFilename(k) + assert.Equal(t, sh, v) + } +} diff --git a/runner/shell/target.go b/runner/shell/target.go new file mode 100644 index 0000000..e823d41 --- /dev/null +++ b/runner/shell/target.go @@ -0,0 +1,35 @@ +package shell + +import ( + "os/exec" + "path/filepath" + "pik/model" + "pik/runner" +) + +type ShellTarget struct { + runner.BaseTarget + Shell string + Script string + SubValue []string +} + +func (s *ShellTarget) String() string { + return s.Label() +} + +func (s *ShellTarget) Hydrate(_ *model.Source) (model.HydratedTarget, error) { + return Runner.Hydrate(s) +} + +func (s *ShellTarget) Sub() []string { + return s.SubValue +} + +func (s *ShellTarget) Label() string { + return s.Identity.Full +} + +func (s *ShellTarget) Create(src *model.Source) *exec.Cmd { + return exec.Command(s.Shell, filepath.Join(src.Path, s.Script)) +} -- cgit v1.3