// Package blender handles Blender binary management and execution. package blender import ( "fmt" "log" "os" "os/exec" "path/filepath" "strings" "jiggablend/internal/runner/api" "jiggablend/internal/runner/workspace" ) // Manager handles Blender binary downloads and management. type Manager struct { manager *api.ManagerClient workspaceDir string } // NewManager creates a new Blender manager. func NewManager(managerClient *api.ManagerClient, workspaceDir string) *Manager { return &Manager{ manager: managerClient, workspaceDir: workspaceDir, } } // GetBinaryPath returns the path to the Blender binary for a specific version. // Downloads from manager and extracts if not already present. func (m *Manager) GetBinaryPath(version string) (string, error) { blenderDir := filepath.Join(m.workspaceDir, "blender-versions") if err := os.MkdirAll(blenderDir, 0755); err != nil { return "", fmt.Errorf("failed to create blender directory: %w", err) } // Check if already installed - look for version folder first versionDir := filepath.Join(blenderDir, version) binaryPath := filepath.Join(versionDir, "blender") // Check if version folder exists and contains the binary if versionInfo, err := os.Stat(versionDir); err == nil && versionInfo.IsDir() { // Version folder exists, check if binary is present if binaryInfo, err := os.Stat(binaryPath); err == nil { // Verify it's actually a file (not a directory) if !binaryInfo.IsDir() { absBinaryPath, err := ResolveBinaryPath(binaryPath) if err != nil { return "", err } log.Printf("Found existing Blender %s installation at %s", version, absBinaryPath) return absBinaryPath, nil } } // Version folder exists but binary is missing - might be incomplete installation log.Printf("Version folder %s exists but binary not found, will re-download", versionDir) } // Download from manager log.Printf("Downloading Blender %s from manager", version) reader, err := m.manager.DownloadBlender(version) if err != nil { return "", err } defer reader.Close() // Manager serves pre-decompressed .tar files - extract directly log.Printf("Extracting Blender %s...", version) if err := workspace.ExtractTarStripPrefix(reader, versionDir); err != nil { return "", fmt.Errorf("failed to extract blender: %w", err) } // Verify binary exists if _, err := os.Stat(binaryPath); err != nil { return "", fmt.Errorf("blender binary not found after extraction") } absBinaryPath, err := ResolveBinaryPath(binaryPath) if err != nil { return "", err } log.Printf("Blender %s installed at %s", version, absBinaryPath) return absBinaryPath, nil } // GetBinaryForJob returns the Blender binary path for a job. // Uses the version from metadata or falls back to system blender. func (m *Manager) GetBinaryForJob(version string) (string, error) { if version == "" { return ResolveBinaryPath("blender") } return m.GetBinaryPath(version) } // ResolveBinaryPath resolves a Blender executable to an absolute path. func ResolveBinaryPath(blenderBinary string) (string, error) { if blenderBinary == "" { return "", fmt.Errorf("blender binary path is empty") } if strings.Contains(blenderBinary, string(filepath.Separator)) { absPath, err := filepath.Abs(blenderBinary) if err != nil { return "", fmt.Errorf("failed to resolve blender binary path %q: %w", blenderBinary, err) } return absPath, nil } resolvedPath, err := exec.LookPath(blenderBinary) if err != nil { return "", fmt.Errorf("failed to locate blender binary %q in PATH: %w", blenderBinary, err) } absPath, err := filepath.Abs(resolvedPath) if err != nil { return "", fmt.Errorf("failed to resolve blender binary path %q: %w", resolvedPath, err) } return absPath, nil } // TarballEnv returns a copy of baseEnv with LD_LIBRARY_PATH set so that a // tarball Blender installation can find its bundled libs (e.g. lib/python3.x). // If blenderBinary is the system "blender" or has no path component, baseEnv is // returned unchanged. func TarballEnv(blenderBinary string, baseEnv []string) []string { if blenderBinary == "" || blenderBinary == "blender" { return baseEnv } if !strings.Contains(blenderBinary, string(os.PathSeparator)) { return baseEnv } blenderDir := filepath.Dir(blenderBinary) libDir := filepath.Join(blenderDir, "lib") ldLib := libDir for _, e := range baseEnv { if strings.HasPrefix(e, "LD_LIBRARY_PATH=") { existing := strings.TrimPrefix(e, "LD_LIBRARY_PATH=") if existing != "" { ldLib = libDir + ":" + existing } break } } out := make([]string, 0, len(baseEnv)+1) done := false for _, e := range baseEnv { if strings.HasPrefix(e, "LD_LIBRARY_PATH=") { out = append(out, "LD_LIBRARY_PATH="+ldLib) done = true continue } out = append(out, e) } if !done { out = append(out, "LD_LIBRARY_PATH="+ldLib) } return out }