Files
patterm/internal/harness/env.go

210 lines
5.0 KiB
Go

package harness
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/hjbdev/patterm/internal/projectkey"
"github.com/hjbdev/patterm/internal/trust"
)
type Options struct {
PattermBin string
Scenario *Scenario
}
type testEnv struct {
Root string `json:"root"`
ConfigHome string `json:"xdg_config_home"`
DataHome string `json:"xdg_data_home"`
RuntimeDir string `json:"xdg_runtime_dir"`
BinDir string `json:"bin_dir"`
ProjectDir string `json:"project_dir"`
PattermBin string `json:"patterm_bin"`
Cols uint16 `json:"cols"`
Rows uint16 `json:"rows"`
}
func prepareEnv(opts Options) (*testEnv, []string, error) {
sc := opts.Scenario
root, err := os.MkdirTemp("", "patterm-harness-*")
if err != nil {
return nil, nil, err
}
env := &testEnv{
Root: root,
ConfigHome: filepath.Join(root, "config"),
DataHome: filepath.Join(root, "data"),
RuntimeDir: filepath.Join(root, "runtime"),
BinDir: filepath.Join(root, "bin"),
Cols: sc.Cols,
Rows: sc.Rows,
PattermBin: opts.PattermBin,
}
if env.ProjectDir = sc.ProjectDir; env.ProjectDir == "" {
env.ProjectDir = filepath.Join(root, "project")
}
for _, dir := range []string{env.ConfigHome, env.DataHome, env.RuntimeDir, env.BinDir, env.ProjectDir} {
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, nil, err
}
}
if env.PattermBin == "" {
env.PattermBin, err = defaultPattermBin()
if err != nil {
return nil, nil, err
}
}
if err := writeScripts(env.BinDir, sc.Scripts); err != nil {
return nil, nil, err
}
if err := writePresets(env.ConfigHome, sc.Presets); err != nil {
return nil, nil, err
}
if err := seedTrust(env, sc.Trust); err != nil {
return nil, nil, err
}
childEnv := append(os.Environ(),
"XDG_CONFIG_HOME="+env.ConfigHome,
"XDG_DATA_HOME="+env.DataHome,
"XDG_RUNTIME_DIR="+env.RuntimeDir,
"PATTERM_HARNESS=1",
"PATH="+env.BinDir+string(os.PathListSeparator)+os.Getenv("PATH"),
)
for k, v := range sc.Env {
childEnv = append(childEnv, k+"="+v)
}
return env, childEnv, nil
}
func writeScripts(bin string, scripts []ScenarioScript) error {
for _, script := range scripts {
if script.Name == "" {
return fmt.Errorf("script missing name")
}
if strings.Contains(script.Name, "/") {
return fmt.Errorf("script name %q must not contain /", script.Name)
}
path := filepath.Join(bin, script.Name)
if err := os.WriteFile(path, []byte(script.Body), 0o700); err != nil {
return err
}
}
return nil
}
func writePresets(configHome string, presets ScenarioPresets) error {
base := filepath.Join(configHome, "patterm", "presets")
if err := os.MkdirAll(filepath.Join(base, "agents"), 0o700); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(base, "processes"), 0o700); err != nil {
return err
}
for _, p := range presets.Agents {
if err := writePreset(filepath.Join(base, "agents", p.Name+".json"), p); err != nil {
return err
}
}
for _, p := range presets.Processes {
if err := writePreset(filepath.Join(base, "processes", p.Name+".json"), p); err != nil {
return err
}
}
return nil
}
func writePreset(path string, p ScenarioPreset) error {
if p.Name == "" {
return fmt.Errorf("preset missing name")
}
b, err := json.MarshalIndent(p, "", " ")
if err != nil {
return err
}
b = append(b, '\n')
return os.WriteFile(path, b, 0o600)
}
func seedTrust(env *testEnv, presets []string) error {
if len(presets) == 0 {
return nil
}
key, err := projectkey.Key(env.ProjectDir)
if err != nil {
return err
}
restore := setenv(map[string]string{"XDG_DATA_HOME": env.DataHome})
defer restore()
store, err := trust.Open(key)
if err != nil {
return err
}
for _, p := range presets {
if err := store.Grant(p); err != nil {
return err
}
}
return nil
}
func setenv(vals map[string]string) func() {
type old struct {
v string
ok bool
}
prev := map[string]old{}
for k, v := range vals {
ov, ok := os.LookupEnv(k)
prev[k] = old{v: ov, ok: ok}
_ = os.Setenv(k, v)
}
return func() {
for k, ov := range prev {
if ov.ok {
_ = os.Setenv(k, ov.v)
} else {
_ = os.Unsetenv(k)
}
}
}
}
func defaultPattermBin() (string, error) {
if p := os.Getenv("PATTERM_BIN"); p != "" {
return p, nil
}
return buildPattermBinary()
}
func buildPattermBinary() (string, error) {
root, err := repoRoot()
if err != nil {
return "", err
}
out := filepath.Join(os.TempDir(), "patterm-harness-bin", "patterm")
if err := os.MkdirAll(filepath.Dir(out), 0o700); err != nil {
return "", err
}
cmd := exec.Command("go", "build", "-o", out, "./cmd/patterm")
cmd.Dir = root
cmd.Env = os.Environ()
if b, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("build patterm: %w\n%s", err, string(b))
}
return out, nil
}
func repoRoot() (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("runtime.Caller failed")
}
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..")), nil
}