// Package blender handles Blender binary management and execution. package blender import ( "fmt" "log" "os" "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() { log.Printf("Found existing Blender %s installation at %s", version, binaryPath) return binaryPath, 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") } log.Printf("Blender %s installed at %s", version, binaryPath) return binaryPath, 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 "blender", nil // System blender } return m.GetBinaryPath(version) } // 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 }