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