package sandbox import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" ) const defaultPodmanImage = "registry.fedoraproject.org/fedora-minimal:41" type podmanWrapper struct { allowNetwork bool image string } func (w *podmanWrapper) Name() string { return BackendPodman } func (w *podmanWrapper) Available() error { if _, err := LookPath("podman"); err != nil { return fmt.Errorf("podman not found in PATH: %w", err) } return nil } func (w *podmanWrapper) Wrap(spec Spec) (*exec.Cmd, error) { if err := w.Available(); err != nil { return nil, err } bin := mustAbs(spec.BlenderBinary) work := mustAbs(spec.WorkDir) home := mustAbs(spec.HomeDir) if home == "" { home = filepath.Join(work, "home") } root := blenderRoot(bin) args := []string{ "run", "--rm", "--security-opt", "label=disable", // Keep user groups for /dev/dri access (render/video) "--group-add", "keep-groups", } if !w.allowNetwork { args = append(args, "--network=none") } // Map to same UID so bind mounts are writable uid := os.Getuid() gid := os.Getgid() args = append(args, "--user", fmt.Sprintf("%d:%d", uid, gid)) // Thin OS image + host Blender tree + job dir args = append(args, "-v", root+":"+root+":ro", "-v", work+":"+work+":rw", ) if home != work && !strings.HasPrefix(home, work+string(os.PathSeparator)) { _ = os.MkdirAll(home, 0755) args = append(args, "-v", home+":"+home+":rw") } // System libs for GPU ICDs / dynamic linker (Blender tarball is mostly self-contained) for _, p := range []string{"/usr", "/lib", "/lib64", "/etc"} { if pathExists(p) { args = append(args, "-v", p+":"+p+":ro") } } // GPU devices and extra lib roots for _, b := range ExistingBinds(GPUMounts(spec.HasAMD, spec.HasNVIDIA, spec.HasIntel, spec.ForceCPU)) { if !pathExists(b.Host) { continue } if b.Dev { // --device works for character devices; for /dev/dri use volume info, err := os.Stat(b.Host) if err != nil { continue } if info.IsDir() { args = append(args, "-v", b.Host+":"+b.Dest+":ro") } else { args = append(args, "--device", b.Host) } continue } mode := "ro" if !b.ReadOnly { mode = "rw" } args = append(args, "-v", b.Host+":"+b.Dest+":"+mode) } // Environment for _, e := range spec.Env { args = append(args, "-e", e) } if home != "" { args = append(args, "-e", "HOME="+home) } args = append(args, "--workdir", work) args = append(args, "--entrypoint", bin) args = append(args, w.image) // entrypoint is blender; remaining args are blender args args = append(args, spec.Args...) cmd := exec.Command("podman", args...) cmd.Dir = work return cmd, nil } // PodmanImageDefault returns the default thin runtime image name. func PodmanImageDefault() string { return defaultPodmanImage } // FormatUIDGID is a small helper for tests. func FormatUIDGID(uid, gid int) string { return strconv.Itoa(uid) + ":" + strconv.Itoa(gid) }