Files
jiggablend/internal/runner/blender/binary.go
Justin Harms a3defe5cf6 Add tests for main package, manager, and various components
- Introduced unit tests for the main package to ensure compilation.
- Added tests for the manager, including validation of upload sessions and handling of Blender binary paths.
- Implemented tests for job token generation and validation, ensuring security and integrity.
- Created tests for configuration management and database schema to verify functionality.
- Added tests for logger and runner components to enhance overall test coverage and reliability.
2026-03-14 22:20:03 -05:00

163 lines
4.8 KiB
Go

// 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
}