mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-19 11:31:03 +00:00
343 lines
12 KiB
Python
Generated
343 lines
12 KiB
Python
Generated
#!/usr/bin/env python3
|
|
"""
|
|
Universal Build Module - Build, sign, package, and upload universal binary for macOS
|
|
|
|
This module orchestrates building both architectures (arm64 + x64), signing each,
|
|
packaging each into DMGs, uploading each, then merging into a universal binary
|
|
and signing/packaging/uploading that as well.
|
|
|
|
Design:
|
|
For each arch (arm64, x64):
|
|
1. resources -> configure -> compile
|
|
2. sign -> package -> upload
|
|
|
|
Then:
|
|
3. Merge arm64 + x64 into universal
|
|
4. sign universal -> package -> upload
|
|
|
|
Output: 3 DMGs uploaded:
|
|
- BrowserOS_{version}_arm64_signed.dmg
|
|
- BrowserOS_{version}_x64_signed.dmg
|
|
- BrowserOS_{version}_universal_signed.dmg
|
|
|
|
Prerequisites (must run BEFORE this module):
|
|
- clean (optional)
|
|
- git_setup
|
|
- sparkle_setup (macOS)
|
|
- chromium_replace
|
|
- string_replaces
|
|
- patches
|
|
|
|
This module internally runs (for EACH architecture):
|
|
- resources (arch-specific binaries)
|
|
- configure (GN configuration)
|
|
- compile (ninja build)
|
|
- sign_macos (code signing + notarization)
|
|
- package_macos (DMG creation)
|
|
- upload (artifact upload)
|
|
|
|
Then merges and processes the universal binary.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from ...common.module import CommandModule, ValidationError
|
|
from ...common.context import Context
|
|
from ...common.utils import log_info, log_success, log_warning, IS_MACOS
|
|
|
|
# Architectures to build for universal binary
|
|
UNIVERSAL_ARCHITECTURES = ["arm64", "x64"]
|
|
|
|
|
|
class UniversalBuildModule(CommandModule):
|
|
"""Build, sign, package, and upload universal binary (arm64 + x64) for macOS
|
|
|
|
This module handles the complete multi-architecture build, sign, package,
|
|
and upload workflow. It internally creates separate contexts for arm64 and x64,
|
|
builds each, signs each, packages each into DMGs, uploads each, then merges
|
|
them into a universal binary and processes that as well.
|
|
|
|
The base context passed to this module can have any architecture value -
|
|
it will be ignored and arm64/x64 will be built explicitly.
|
|
|
|
Output artifacts:
|
|
- BrowserOS_{version}_arm64_signed.dmg
|
|
- BrowserOS_{version}_x64_signed.dmg
|
|
- BrowserOS_{version}_universal_signed.dmg
|
|
"""
|
|
|
|
produces = ["dmg_arm64", "dmg_x64", "dmg_universal"]
|
|
requires = []
|
|
description = (
|
|
"Build, sign, package, and upload universal binary (arm64 + x64) for macOS"
|
|
)
|
|
|
|
def validate(self, ctx: Context) -> None:
|
|
"""Validate universal build can run"""
|
|
if not IS_MACOS():
|
|
raise ValidationError("Universal builds only supported on macOS")
|
|
|
|
# Check universalizer script exists
|
|
universalizer = ctx.root_dir / "build/modules/package/universalizer_patched.py"
|
|
if not universalizer.exists():
|
|
raise ValidationError(f"Universalizer script not found: {universalizer}")
|
|
|
|
# Fail fast: check signing environment is configured
|
|
from ..sign.macos import check_signing_environment
|
|
|
|
if not check_signing_environment():
|
|
raise ValidationError(
|
|
"Signing environment not configured. "
|
|
"Required: MACOS_CERTIFICATE_NAME, notarization credentials"
|
|
)
|
|
|
|
def execute(self, ctx: Context) -> None:
|
|
"""Build arm64 + x64, sign/package/upload each, then merge and process universal"""
|
|
|
|
log_info("\n" + "=" * 70)
|
|
log_info("🔄 Universal Build Mode (Full Pipeline)")
|
|
log_info("Building arm64 + x64, signing, packaging, uploading each...")
|
|
log_info("Then merging into universal and processing that too.")
|
|
log_info("=" * 70)
|
|
|
|
# Import build modules
|
|
from ..resources.resources import ResourcesModule
|
|
from ..setup.configure import ConfigureModule
|
|
from .standard import CompileModule
|
|
|
|
# Import sign/package/upload modules
|
|
from ..sign.macos import MacOSSignModule
|
|
from ..package.macos import MacOSPackageModule
|
|
from ..storage import UploadModule
|
|
|
|
# Clean all build directories before starting
|
|
self._clean_build_directories(ctx)
|
|
|
|
built_apps = []
|
|
|
|
# Build + Sign + Package + Upload each architecture
|
|
for arch in UNIVERSAL_ARCHITECTURES:
|
|
log_info("\n" + "=" * 70)
|
|
log_info(f"🏗️ Processing architecture: {arch}")
|
|
log_info("=" * 70)
|
|
|
|
# Create architecture-specific context with fixed app path
|
|
arch_ctx = self._create_arch_context(ctx, arch)
|
|
|
|
log_info(f"📍 Chromium: {arch_ctx.chromium_version}")
|
|
log_info(f"📍 BrowserOS: {arch_ctx.browseros_build_offset}")
|
|
log_info(f"📍 Output directory: {arch_ctx.out_dir}")
|
|
|
|
# === BUILD PHASE ===
|
|
# Copy resources (arch-specific binaries like browseros_server, codex)
|
|
log_info(f"\n📦 Copying resources for {arch}...")
|
|
ResourcesModule().execute(arch_ctx)
|
|
|
|
# Configure build (GN gen)
|
|
log_info(f"\n🔧 Configuring {arch}...")
|
|
ConfigureModule().execute(arch_ctx)
|
|
|
|
# Compile (ninja)
|
|
log_info(f"\n🏗️ Compiling {arch}...")
|
|
CompileModule().execute(arch_ctx)
|
|
|
|
# Get app path for this architecture
|
|
app_path = arch_ctx.get_app_path()
|
|
|
|
if not app_path.exists():
|
|
raise RuntimeError(f"Build failed - app not found: {app_path}")
|
|
|
|
log_success(f"✅ {arch} build complete: {app_path}")
|
|
built_apps.append(app_path)
|
|
|
|
# === SIGN PHASE ===
|
|
log_info(f"\n🔏 Signing {arch} build...")
|
|
MacOSSignModule().execute(arch_ctx)
|
|
log_success(f"✅ {arch} signing complete")
|
|
|
|
# === PACKAGE PHASE ===
|
|
log_info(f"\n📦 Packaging {arch} build...")
|
|
MacOSPackageModule().execute(arch_ctx)
|
|
log_success(f"✅ {arch} packaging complete")
|
|
|
|
# === UPLOAD PHASE ===
|
|
log_info(f"\n☁️ Uploading {arch} artifacts...")
|
|
try:
|
|
UploadModule().execute(arch_ctx)
|
|
log_success(f"✅ {arch} upload complete")
|
|
except Exception as e:
|
|
log_warning(f"⚠️ {arch} upload failed (non-fatal): {e}")
|
|
|
|
# === MERGE INTO UNIVERSAL ===
|
|
log_info("\n" + "=" * 70)
|
|
log_info("🔄 Merging into universal binary...")
|
|
log_info("=" * 70)
|
|
|
|
self._merge_universal(ctx, built_apps[0], built_apps[1])
|
|
|
|
# Verify universal binary was created
|
|
universal_app = ctx.chromium_src / "out/Default_universal/BrowserOS.app"
|
|
if not universal_app.exists():
|
|
raise RuntimeError(f"Universal binary not found: {universal_app}")
|
|
|
|
log_success(f"✅ Universal binary created: {universal_app}")
|
|
|
|
# === SIGN + PACKAGE + UPLOAD UNIVERSAL ===
|
|
log_info("\n" + "=" * 70)
|
|
log_info("🔏 Processing universal binary...")
|
|
log_info("=" * 70)
|
|
|
|
universal_ctx = self._create_universal_context(ctx)
|
|
|
|
# Sign universal
|
|
log_info("\n🔏 Signing universal build...")
|
|
MacOSSignModule().execute(universal_ctx)
|
|
log_success("✅ Universal signing complete")
|
|
|
|
# Package universal
|
|
log_info("\n📦 Packaging universal build...")
|
|
MacOSPackageModule().execute(universal_ctx)
|
|
log_success("✅ Universal packaging complete")
|
|
|
|
# Upload universal
|
|
log_info("\n☁️ Uploading universal artifacts...")
|
|
try:
|
|
UploadModule().execute(universal_ctx)
|
|
log_success("✅ Universal upload complete")
|
|
except Exception as e:
|
|
log_warning(f"⚠️ Universal upload failed (non-fatal): {e}")
|
|
|
|
log_info("\n" + "=" * 70)
|
|
log_success("✅ Universal build pipeline complete!")
|
|
log_info("Artifacts created:")
|
|
log_info(
|
|
f" - arm64 DMG: {ctx.get_dist_dir() / ctx.get_artifact_name('dmg').replace('universal', 'arm64')}"
|
|
)
|
|
log_info(
|
|
f" - x64 DMG: {ctx.get_dist_dir() / ctx.get_artifact_name('dmg').replace('universal', 'x64')}"
|
|
)
|
|
log_info(
|
|
f" - universal DMG: {ctx.get_dist_dir() / universal_ctx.get_artifact_name('dmg')}"
|
|
)
|
|
log_info("=" * 70)
|
|
|
|
def _clean_build_directories(self, ctx: Context) -> None:
|
|
"""Clean architecture-specific and universal build directories
|
|
|
|
Args:
|
|
ctx: Base context
|
|
"""
|
|
from ...common.utils import safe_rmtree
|
|
|
|
log_info("\n🧹 Cleaning build directories...")
|
|
|
|
# Clean architecture-specific directories
|
|
for arch in UNIVERSAL_ARCHITECTURES:
|
|
arch_dir = ctx.chromium_src / f"out/Default_{arch}"
|
|
if arch_dir.exists():
|
|
log_info(f" Removing {arch_dir}")
|
|
safe_rmtree(arch_dir)
|
|
|
|
# Clean universal directory
|
|
universal_dir = ctx.chromium_src / "out/Default_universal"
|
|
if universal_dir.exists():
|
|
log_info(f" Removing {universal_dir}")
|
|
safe_rmtree(universal_dir)
|
|
|
|
log_success("✅ Build directories cleaned")
|
|
|
|
def _create_arch_context(self, base_ctx: Context, arch: str) -> Context:
|
|
"""Create a new context for a specific architecture
|
|
|
|
Args:
|
|
base_ctx: Base context with common settings
|
|
arch: Architecture to build (arm64 or x64)
|
|
|
|
Returns:
|
|
New Context object with architecture set and fixed app path
|
|
to prevent universal auto-detection
|
|
"""
|
|
ctx = Context(
|
|
root_dir=base_ctx.root_dir,
|
|
chromium_src=base_ctx.chromium_src,
|
|
architecture=arch,
|
|
build_type=base_ctx.build_type,
|
|
)
|
|
# Set fixed app path to prevent universal auto-detection in get_app_path()
|
|
# This is critical: after arm64 is built, get_app_path() would otherwise
|
|
# try to detect the universal dir for x64 context
|
|
ctx._fixed_app_path = (
|
|
ctx.chromium_src / f"out/Default_{arch}" / ctx.BROWSEROS_APP_NAME
|
|
)
|
|
return ctx
|
|
|
|
def _create_universal_context(self, base_ctx: Context) -> Context:
|
|
"""Create a new context for the universal binary
|
|
|
|
Args:
|
|
base_ctx: Base context with common settings
|
|
|
|
Returns:
|
|
New Context object configured for universal binary
|
|
"""
|
|
ctx = Context(
|
|
root_dir=base_ctx.root_dir,
|
|
chromium_src=base_ctx.chromium_src,
|
|
architecture="universal",
|
|
build_type=base_ctx.build_type,
|
|
)
|
|
# Set fixed app path to the universal binary
|
|
ctx._fixed_app_path = (
|
|
ctx.chromium_src / "out/Default_universal" / ctx.BROWSEROS_APP_NAME
|
|
)
|
|
# Override out_dir for universal
|
|
ctx.out_dir = "out/Default_universal"
|
|
return ctx
|
|
|
|
def _merge_universal(
|
|
self,
|
|
ctx: Context,
|
|
arm64_app: Path,
|
|
x64_app: Path,
|
|
) -> None:
|
|
"""Merge arm64 + x64 into universal binary
|
|
|
|
Args:
|
|
ctx: Base context
|
|
arm64_app: Path to arm64 .app bundle
|
|
x64_app: Path to x64 .app bundle
|
|
|
|
Raises:
|
|
RuntimeError: If merge fails
|
|
"""
|
|
# Use existing merge helper
|
|
from ..package.merge import merge_architectures
|
|
|
|
# Prepare output path
|
|
universal_dir = ctx.chromium_src / "out/Default_universal"
|
|
|
|
# Create universal directory (already cleaned in _clean_build_directories)
|
|
universal_dir.mkdir(parents=True, exist_ok=True)
|
|
universal_app = universal_dir / "BrowserOS.app"
|
|
|
|
# Find universalizer script
|
|
universalizer_script = (
|
|
ctx.root_dir / "build/modules/package/universalizer_patched.py"
|
|
)
|
|
|
|
log_info(f"📱 Input 1 (arm64): {arm64_app}")
|
|
log_info(f"📱 Input 2 (x64): {x64_app}")
|
|
log_info(f"🎯 Output (universal): {universal_app}")
|
|
log_info(f"🔧 Universalizer: {universalizer_script}")
|
|
|
|
# Merge the architectures
|
|
success = merge_architectures(
|
|
arch1_path=arm64_app,
|
|
arch2_path=x64_app,
|
|
output_path=universal_app,
|
|
universalizer_script=universalizer_script,
|
|
)
|
|
|
|
if not success:
|
|
raise RuntimeError("Failed to merge architectures into universal binary")
|