// Package sandbox wraps Blender (and similar) invocations in optional isolation. // // Backends: // - none: run on the host (current behavior) // - podman: rootless podman container; Blender tarball is bind-mounted (not baked into an image) // // GPU access is provided by passing host device nodes and common userspace lib roots // discovered at wrap time (NVIDIA / AMD ROCm / Intel DRM), not by shipping per-version // Blender container images. package sandbox import ( "fmt" "os" "os/exec" "strings" ) // Backend names accepted by New and CLI flags. const ( BackendNone = "none" BackendPodman = "podman" ) // Options configures sandbox construction. type Options struct { // Backend is none|podman (default none). Backend string // AllowNetwork keeps network in the sandbox (default false for podman). AllowNetwork bool // PodmanImage is used only for backend=podman. The image is a thin OS root; // Blender comes from a host bind of the versioned tarball tree. // Default: registry.fedoraproject.org/fedora-minimal:41 PodmanImage string } // Spec describes a Blender process to run under the sandbox. type Spec struct { // BlenderBinary is an absolute path to the blender executable. BlenderBinary string // Args are arguments after the binary (not including argv0). Args []string // WorkDir is the process working directory (job workspace); bind-mounted RW. WorkDir string // HomeDir is Blender HOME (usually WorkDir/home); bind-mounted RW. HomeDir string // Env is the full environment for the process (HOME, LD_LIBRARY_PATH, etc.). Env []string // GPU hints from host detection (used to select device/lib binds). HasAMD bool HasNVIDIA bool HasIntel bool // ForceCPU skips GPU device binds when true. ForceCPU bool // AllowNetwork overrides Options.AllowNetwork when non-nil... kept simple: use Options only. } // Wrapper turns a Spec into an *exec.Cmd ready to Start. type Wrapper interface { // Name returns the backend name. Name() string // Available reports whether required host tools exist. Available() error // Wrap builds the command. Callers own Start/Wait/pipes. Wrap(spec Spec) (*exec.Cmd, error) } // New returns a sandbox Wrapper for the given options. func New(opts Options) (Wrapper, error) { backend := strings.ToLower(strings.TrimSpace(opts.Backend)) if backend == "" { backend = BackendPodman } switch backend { case BackendNone: return &noneWrapper{}, nil case BackendPodman: img := opts.PodmanImage if img == "" { img = defaultPodmanImage } w := &podmanWrapper{allowNetwork: opts.AllowNetwork, image: img} return w, nil default: return nil, fmt.Errorf("unknown sandbox backend %q (want none or podman)", opts.Backend) } } // NormalizeBackend validates and returns a canonical backend name. func NormalizeBackend(s string) (string, error) { b := strings.ToLower(strings.TrimSpace(s)) if b == "" { return BackendPodman, nil } switch b { case BackendNone, BackendPodman: return b, nil default: return "", fmt.Errorf("unknown sandbox backend %q (want none or podman)", s) } } // LookPath is os/exec.LookPath, overridable in tests. var LookPath = exec.LookPath // pathExists reports whether path exists. func pathExists(p string) bool { _, err := os.Stat(p) return err == nil } // mustAbs returns an absolute path or the original on error. func mustAbs(p string) string { if p == "" { return p } abs, err := absPath(p) if err != nil { return p } return abs } // absPath is filepath.Abs, isolated for tests. var absPath = func(p string) (string, error) { return execAbs(p) }