Builder/spitfire/build.go
2025-04-13 19:21:04 +02:00

390 lines
12 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package spitfire
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"time"
)
// Array to store errors
var errors []string
var (
repoOperationsDone bool
repoMutex sync.Mutex
)
// SetGlobalEnv sets the MOZILLABUILD environment variable globally for the user (or system)
func SetGlobalEnv(variable, value string, scope string) error {
fmt.Printf("⚙️ Setting environment variable: %s=%s (scope: %s)\n", variable, value, scope)
if runtime.GOOS == "windows" {
var cmd *exec.Cmd
if scope == "user" {
cmd = exec.Command("setx", variable, value) // Set for current user
} else if scope == "system" {
cmd = exec.Command("setx", variable, value, "/M") // Set for system (requires admin privileges)
} else {
return fmt.Errorf("❌ unknown scope: %s", scope)
}
// Run the command
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("❌ failed to set environment variable %s=%s: %v\nOutput: %s", variable, value, err, string(out))
}
fmt.Printf("✅ Successfully set %s=%s\n", variable, value)
return nil
} else {
return fmt.Errorf("❌ global environment variable setting is not supported on non-Windows systems")
}
}
// Run an external command like scp or rsync
func runCommand(command string, args ...string) error {
fmt.Printf("Running command: %s %v\n", command, args)
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func runCommandInDir(dir string, name string, arg ...string) error {
cmd := exec.Command(name, arg...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// Function to resolve paths using absolute path
func ResolvePath(path string) (string, error) {
fmt.Printf("📍 Resolving path: %s\n", path)
// Convert Unix-style slashes to the platform's native slashes
path = filepath.FromSlash(path)
// Get the absolute path
absPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("❌ failed to resolve path: %s, error: %v", path, err)
}
fmt.Printf(" └─ Resolved to: %s\n", absPath)
return absPath, nil
}
// Function to download Mozilla source if not present
func DownloadSource(sourcePath, sourceRepo string) error {
fmt.Println("🔍 Checking Mozilla source repository...")
if _, err := os.Stat(sourcePath); os.IsNotExist(err) {
fmt.Println("📥 Mozilla source not found - cloning repository...")
if err := runCommand("hg", "clone", sourceRepo, sourcePath); err != nil {
return fmt.Errorf("❌ failed to clone Mozilla repository: %w", err)
}
fmt.Println("✅ Repository cloned successfully")
} else {
fmt.Println(" Mozilla source already exists")
}
return nil
}
// Function to discard uncommitted changes
func DiscardChanges(sourcePath string) error {
fmt.Println("🧹 Discarding uncommitted changes...")
if err := runCommand("hg", "revert", "--all", "--no-backup", "-R", sourcePath); err != nil {
return fmt.Errorf("❌ failed to revert changes: %w", err)
}
fmt.Println("✅ Changes discarded successfully")
return nil
}
// Function to clean build
func CleanBuild(sourcePath string, fullClean bool) error {
fmt.Println("🧼 Cleaning build...")
if err := runCommand("hg", "revert", "--all", "--no-backup", "-R", sourcePath); err != nil {
return fmt.Errorf("❌ failed to revert changes: %w", err)
}
if fullClean {
fmt.Println("🧹 Performing full clean (clobber)...")
machCmd := filepath.Join(sourcePath, "mach")
if runtime.GOOS == "windows" {
machCmd = filepath.Join(sourcePath, "mach.bat")
}
cmd := exec.Command(machCmd, "clobber")
cmd.Dir = sourcePath // Run in the source directory
if err := cmd.Run(); err != nil {
return fmt.Errorf("❌ failed to clean build: %w", err)
}
fmt.Println("✅ Build artifacts cleaned successfully")
}
return nil
}
// UpdateRepo updates an existing repository if not already done
func UpdateRepo(sourcePath string) error {
repoMutex.Lock()
defer repoMutex.Unlock()
if repoOperationsDone {
fmt.Println(" Repository operations already completed - skipping update")
return nil
}
const maxRetries = 3
var success bool
for attempt := 1; attempt <= maxRetries; attempt++ {
fmt.Printf("🔄 Update attempt %d/%d\n", attempt, maxRetries)
if err := runCommandInDir(sourcePath, "hg", "pull", "-u"); err != nil {
fmt.Println("❌ Update failed - removing corrupted repository")
os.RemoveAll(sourcePath)
if attempt < maxRetries {
time.Sleep(10 * time.Second)
}
continue
}
success = true
break
}
if success {
repoOperationsDone = true
return nil
}
return fmt.Errorf("failed to update repository after %d attempts", maxRetries)
}
// CloneRepo clones a fresh repository if not already done
func CloneRepo(sourcePath, url string) error {
repoMutex.Lock()
defer repoMutex.Unlock()
if repoOperationsDone {
fmt.Println(" Repository operations already completed - skipping clone")
return nil
}
const maxRetries = 3
var success bool
parentDir := filepath.Dir(sourcePath)
if err := os.MkdirAll(parentDir, 0755); err != nil {
return fmt.Errorf("failed to create parent directory: %w", err)
}
for attempt := 1; attempt <= maxRetries; attempt++ {
fmt.Printf("📥 Clone attempt %d/%d\n", attempt, maxRetries)
if err := runCommand("hg", "clone", "--stream", url, sourcePath); err != nil {
fmt.Println("❌ Clone failed - cleaning up")
os.RemoveAll(sourcePath)
if attempt < maxRetries {
time.Sleep(10 * time.Second)
}
continue
}
success = true
break
}
if success {
repoOperationsDone = true
return nil
}
return fmt.Errorf("failed to clone repository after %d attempts", maxRetries)
}
// Function to update patches
func UpdatePatches(patchesDir, patchesRepo, sourcePath string) {
fmt.Println("🔄 Updating patches...")
if _, err := os.Stat(patchesDir); err == nil {
fmt.Println(" Patches directory exists - updating...")
if err := os.Chdir(patchesDir); err != nil {
errors = append(errors, "❌ Failed to navigate to patches directory")
return
}
fmt.Println("🧹 Cleaning patches directory...")
if err := runCommand("git", "clean", "-xdf"); err != nil {
errors = append(errors, "❌ Failed to clean patches directory")
}
fmt.Println("💾 Stashing local changes...")
_ = runCommand("git", "stash", "push", "--include-untracked")
fmt.Println("📥 Fetching updates...")
if err := runCommand("git", "fetch"); err != nil {
errors = append(errors, "❌ Failed to fetch updates from patches repository")
}
fmt.Println("🔀 Rebasing changes...")
if runCommand("git", "show-ref", "--verify", "--quiet", "refs/heads/main") == nil {
if err := runCommand("git", "rebase", "origin/main"); err != nil {
errors = append(errors, "❌ Failed to rebase updates from main branch")
}
} else if runCommand("git", "show-ref", "--verify", "--quiet", "refs/heads/master") == nil {
if err := runCommand("git", "rebase", "origin/master"); err != nil {
errors = append(errors, "❌ Failed to rebase updates from master branch")
}
} else {
errors = append(errors, "❌ No valid branch (main or master) found in patches repository")
return
}
if runCommand("git", "stash", "list") == nil {
fmt.Println("↩️ Restoring stashed changes...")
_ = runCommand("git", "stash", "pop")
} else {
fmt.Println(" No stash entries found - skipping pop")
}
} else {
fmt.Println("📥 Cloning patches repository...")
if err := runCommand("git", "clone", patchesRepo, patchesDir); err != nil {
errors = append(errors, "❌ Failed to clone patches repository")
}
}
fmt.Println("📂 Copying files to source directory...")
if runtime.GOOS == "windows" || !isMsys2() {
fmt.Println("🖥️ Using robocopy for Windows...")
if err := runCommand("robocopy", patchesDir, sourcePath, "*", "/S", "/XF", ".git", "/XD", ".git"); err != nil {
errors = append(errors, "❌ Failed to copy files (Windows robocopy)")
}
} else {
fmt.Println("🐧 Using rsync for Unix...")
if err := runCommand("rsync", "-av", "--exclude=.git", patchesDir+"/", sourcePath+"/"); err != nil {
errors = append(errors, "❌ Failed to copy files (rsync)")
}
}
}
// Function to configure Spitfire
func Configure(sourcePath string) error {
fmt.Println("⚙️ Configuring Spitfire...")
if err := os.Chdir(sourcePath); err != nil {
return fmt.Errorf("❌ failed to navigate to source directory: %w", err)
}
machCmd := "./mach"
if runtime.GOOS == "windows" {
machCmd = ".\\mach"
}
fmt.Println("🔧 Running mach configure...")
return runCommand(machCmd, "configure")
}
// Function to build Spitfire
func Build(sourcePath string) error {
fmt.Println("🔨 Building Spitfire...")
if err := os.Chdir(sourcePath); err != nil {
return fmt.Errorf("❌ failed to navigate to source directory: %w", err)
}
machCmd := "./mach"
if runtime.GOOS == "windows" {
machCmd = ".\\mach"
}
fmt.Println("🏗️ Running mach build...")
return runCommand(machCmd, "build")
}
// Function to run the project after build
func RunProject(sourcePath string) error {
fmt.Println("🚀 Running the project...")
// Resolve the build directory
fmt.Println("🔍 Resolving build directory...")
buildDir, err := ResolveBuildDir(sourcePath)
if err != nil {
return fmt.Errorf("❌ error resolving build directory: %w", err)
}
fmt.Printf(" ├─ Build directory: %s\n", buildDir)
// List of possible binaries
binaries := []string{"firefox", "Firefox", "spitfire", "Spitfire"}
var binaryPath string
// Check for the existence of binaries
fmt.Println("🔎 Searching for executable binary...")
for _, binary := range binaries {
binaryPath = filepath.Join(buildDir, binary)
if _, err := os.Stat(binaryPath); err == nil {
fmt.Printf("✅ Found binary: %s\n", binaryPath)
break
}
binaryPath = ""
}
if binaryPath == "" {
return fmt.Errorf("❌ no suitable binary found to run the project")
}
// Create a unique profile directory for each run
profilePath := filepath.Join(buildDir, fmt.Sprintf("profile_%d", time.Now().UnixNano()))
fmt.Printf("📁 Creating temporary profile: %s\n", profilePath)
if err := os.Mkdir(profilePath, 0755); err != nil {
return fmt.Errorf("❌ failed to create profile directory: %w", err)
}
// Run the binary with the new profile
fmt.Printf("⚡ Launching: %s -no-remote -profile %s\n", binaryPath, profilePath)
cmd := exec.Command(binaryPath, "-no-remote", "-profile", profilePath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
fmt.Printf("✅ Running binary: %s with new profile at %s\n", binaryPath, profilePath)
runErr := cmd.Run()
// Clean up profile directory
fmt.Println("🧹 Cleaning up temporary profile...")
if removeErr := os.RemoveAll(profilePath); removeErr != nil {
fmt.Printf("⚠️ Warning: failed to delete profile directory: %v\n", removeErr)
} else {
fmt.Println("✅ Profile directory cleaned successfully")
}
if runErr != nil {
return fmt.Errorf("❌ failed to run the project: %w", runErr)
}
fmt.Println("🎉 Binary execution completed successfully")
return nil
}
// ResolveBuildDir detects the build directory dynamically
func ResolveBuildDir(sourcePath string) (string, error) {
fmt.Println("🔍 Detecting build directory...")
// The expected build directory pattern
globPattern := filepath.Join(sourcePath, "obj-*")
// Find matching directories
matches, err := filepath.Glob(globPattern)
if err != nil {
return "", fmt.Errorf("❌ error resolving build directory: %v", err)
}
if len(matches) == 0 {
return "", fmt.Errorf("❌ build directory not found under %s", sourcePath)
}
full := filepath.Join(matches[0], "dist", "bin")
// Return the first match (assumes one build directory exists)
return full, nil
}
// Function to print collected errors
func PrintErrors() {
if len(errors) > 0 {
fmt.Println("❌ The following errors occurred during execution:")
for _, err := range errors {
fmt.Printf(" - %s\n", err)
}
}
}