git/orchard/main
tui/internal/plan/discover.go
110 lines3 KB last change raw · history
package plan

import (
	"os"
	"path/filepath"
	"runtime"
)

// marker identifies the Python side when walking the tree.
var marker = filepath.Join("updater", "cli.py")

// Discover fills in PYTHONPATH by finding the Python package, so `go run .`
// works from a checkout with no configuration.
//
// Order: an explicit path wins, then $UPDATER_HOME, then an upward search from
// the working directory, the executable, and this source file. The last one is
// what makes `go run` work: runtime.Caller records the path the module was
// compiled from, which is the checkout even though the binary itself is in a
// temporary directory.
func (b Backend) Discover(explicit string) Backend {
	if explicit != "" {
		return b.withPath(explicit)
	}
	for _, dir := range searchRoots() {
		if _, err := os.Stat(filepath.Join(dir, marker)); err == nil {
			return b.withPath(dir)
		}
	}
	return b // fall back to whatever the interpreter already has installed
}

func (b Backend) withPath(dir string) Backend {
	abs, err := filepath.Abs(dir)
	if err != nil {
		abs = dir
	}
	out := b
	out.Env = append(append([]string{}, b.Env...), "PYTHONPATH="+abs)
	return out
}

func searchRoots() []string {
	var roots []string
	add := func(dir string) {
		for _, r := range ancestors(dir) {
			roots = append(roots, r)
		}
	}

	if v := os.Getenv("UPDATER_HOME"); v != "" {
		roots = append(roots, v)
	}
	if wd, err := os.Getwd(); err == nil {
		add(wd)
	}
	if exe, err := os.Executable(); err == nil {
		add(filepath.Dir(exe))
	}
	if _, file, _, ok := runtime.Caller(0); ok {
		add(filepath.Dir(file))
	}

	seen := map[string]bool{}
	out := roots[:0]
	for _, r := range roots {
		if !seen[r] {
			seen[r] = true
			out = append(out, r)
		}
	}
	return out
}

// ancestors yields dir and then each parent, stopping after a few levels.
func ancestors(dir string) []string {
	var out []string
	cur, err := filepath.Abs(dir)
	if err != nil {
		return nil
	}
	for i := 0; i < 5; i++ {
		out = append(out, cur)
		parent := filepath.Dir(cur)
		if parent == cur {
			break
		}
		cur = parent
	}
	return out
}

// Hint is a human-readable explanation of how to fix a missing backend.
func (b Backend) Hint() string {
	dir := ""
	if len(b.Env) > 0 {
		for _, e := range b.Env {
			if len(e) > len("PYTHONPATH=") && e[:len("PYTHONPATH=")] == "PYTHONPATH=" {
				dir = e[len("PYTHONPATH="):]
			}
		}
	}
	if dir == "" {
		return "the Python package `updater` was not found.\n" +
			"Run from inside the checkout, or set UPDATER_HOME=/path/to/updater,\n" +
			"or pass --repo /path/to/updater."
	}
	return "looked for " + filepath.Join(dir, marker) + "\n" +
		"pass --repo /path/to/updater, or set UPDATER_HOME."
}