Files
BrowserOS/packages/browseros/build/modules/merge.py

315 lines
9.4 KiB
Python

#!/usr/bin/env python3
"""
Universal binary merge module for Nxtscape Browser
Provides functions to merge two architecture builds into a universal binary
"""
import os
import sys
import shutil
from pathlib import Path
from typing import List
from context import BuildContext
from utils import run_command, log_info, log_error, log_success, log_warning
def merge_architectures(
arch1_path: Path,
arch2_path: Path,
output_path: Path,
universalizer_script: Path = None,
) -> bool:
"""
Merge two architecture builds into a universal binary
Args:
arch1_path: Path to first architecture .app bundle
arch2_path: Path to second architecture .app bundle
output_path: Path where universal .app bundle should be created
universalizer_script: Path to universalizer script (optional)
Returns:
True if successful, False otherwise
"""
log_info("🔄 Merging architecture builds into universal binary...")
# Validate input paths
if not arch1_path.exists():
log_error(f"Architecture 1 app not found: {arch1_path}")
return False
if not arch2_path.exists():
log_error(f"Architecture 2 app not found: {arch2_path}")
return False
log_info(f"📱 Input 1: {arch1_path}")
log_info(f"📱 Input 2: {arch2_path}")
log_info(f"🎯 Output: {output_path}")
# Find universalizer script
if universalizer_script is None:
# Try to find it relative to this module
current_dir = Path(__file__).parent.parent
universalizer_script = current_dir / "universalizer_patched.py"
if not universalizer_script.exists():
log_error(f"Universalizer script not found: {universalizer_script}")
return False
# Create output directory if needed
output_path.parent.mkdir(parents=True, exist_ok=True)
# Remove existing output if present
if output_path.exists():
log_info(f"Removing existing output: {output_path}")
shutil.rmtree(output_path)
try:
# Run universalizer
cmd = [
sys.executable,
str(universalizer_script),
str(arch1_path),
str(arch2_path),
str(output_path),
]
log_info(f"Running universalizer...")
log_info(f"Command: {' '.join(cmd)}")
run_command(cmd)
if output_path.exists():
log_success(f"Universal binary created: {output_path}")
return True
else:
log_error("Universal binary creation failed - output not found")
return False
except Exception as e:
log_error(f"Failed to create universal binary: {e}")
return False
def create_minimal_context(
app_path: Path, chromium_src: Path, root_dir: Path, architecture: str = "universal"
) -> BuildContext:
"""Create a minimal BuildContext for signing/packaging operations"""
out_dir_path = app_path.parent # out/Default_universal
log_info(f"Creating context from app path: {app_path}")
log_info(f" Out dir: {out_dir_path}")
log_info(f" Chromium src: {chromium_src}")
log_info(f" Root dir: {root_dir}")
ctx = BuildContext(
root_dir=root_dir,
chromium_src=chromium_src,
architecture=architecture,
build_type="release", # Assume release for universal builds
apply_patches=False,
sign_package=True,
package=True,
build=False,
)
# Override out_dir to match the actual location
ctx.out_dir = out_dir_path.name
# Override get_app_path to return the actual app path for merge operations
def get_app_path_override():
return app_path
ctx.get_app_path = get_app_path_override
log_info(f"Context created with out_dir: {ctx.out_dir}")
log_info(f"App path: {ctx.get_app_path()}")
log_info(f"PKG-DMG path: {ctx.get_pkg_dmg_path()}")
return ctx
def merge_sign_package(
arch1_path: Path,
arch2_path: Path,
output_path: Path,
chromium_src: Path,
root_dir: Path,
sign: bool = True,
package: bool = True,
universalizer_script: Path = None,
) -> bool:
"""
Complete workflow: merge, sign, and package universal binary
Args:
arch1_path: Path to first architecture .app bundle
arch2_path: Path to second architecture .app bundle
output_path: Path where universal .app bundle should be created
chromium_src: Path to chromium source directory
root_dir: Path to project root directory
sign: Whether to sign the universal binary
package: Whether to create DMG package
universalizer_script: Path to universalizer script (optional)
Returns:
True if successful, False otherwise
"""
log_info("=" * 70)
log_info("🚀 Starting merge, sign, and package workflow...")
log_info("=" * 70)
# Step 1: Merge architectures
if not merge_architectures(
arch1_path, arch2_path, output_path, universalizer_script
):
return False
# Step 2: Sign (if requested)
if sign:
log_info("\n" + "=" * 70)
log_info("🔏 Signing universal binary...")
log_info("=" * 70)
try:
from modules.sign import sign_app
ctx = create_minimal_context(output_path, chromium_src, root_dir)
if not sign_app(ctx, create_dmg=False):
log_error("Failed to sign universal binary")
return False
log_success("Universal binary signed successfully!")
except ImportError as e:
log_error(f"Could not import signing module: {e}")
return False
except Exception as e:
log_error(f"Signing failed: {e}")
return False
# Step 3: Package (if requested)
if package:
log_info("\n" + "=" * 70)
log_info("📦 Creating DMG package...")
log_info("=" * 70)
try:
from modules.package import create_dmg
ctx = create_minimal_context(output_path, chromium_src, root_dir)
# Create DMG in parent directory
dmg_dir = ctx.root_dir / "dmg"
dmg_dir.mkdir(parents=True, exist_ok=True)
dmg_name = ctx.get_dmg_name()
dmg_path = dmg_dir / dmg_name
pkg_dmg_path = ctx.get_pkg_dmg_path()
# pkg-dmg should now be available since we enforce chromium-src path
if not pkg_dmg_path.exists():
log_error(f"Chromium pkg-dmg not found at: {pkg_dmg_path}")
log_error("Make sure you provided the correct --chromium-src path")
return False
if create_dmg(output_path, dmg_path, "BrowserOS", pkg_dmg_path):
log_success(f"DMG created: {dmg_name}")
else:
log_error("Failed to create DMG")
return False
except ImportError as e:
log_error(f"Could not import packaging module: {e}")
return False
except Exception as e:
log_error(f"Packaging failed: {e}")
return False
log_info("\n" + "=" * 70)
log_success("Merge, sign, and package workflow completed successfully!")
log_info("=" * 70)
return True
def handle_merge_command(
arch1_path: Path,
arch2_path: Path,
chromium_src: Path,
sign: bool = False,
package: bool = False,
) -> bool:
"""
Handle the merge command from CLI
Args:
arch1_path: Path to first architecture .app bundle
arch2_path: Path to second architecture .app bundle
chromium_src: Path to chromium source directory
sign: Whether to sign the universal binary
package: Whether to create DMG package
Returns:
True if successful, False otherwise
"""
log_info("🔄 Running merge command...")
log_info(f" Arch 1: {arch1_path}")
log_info(f" Arch 2: {arch2_path}")
log_info(f" Sign: {sign}")
log_info(f" Package: {package}")
log_info(f"📁 Using Chromium source: {chromium_src}")
# Validate input paths exist
if not arch1_path.exists():
log_error(f"Architecture 1 app not found: {arch1_path}")
return False
if not arch2_path.exists():
log_error(f"Architecture 2 app not found: {arch2_path}")
return False
# Get root_dir from where this module is located
root_dir = Path(__file__).parent.parent.parent
log_info(f"📂 Using root directory: {root_dir}")
# Auto-generate output path in chromium source
# Get the app name from BuildContext
from context import BuildContext
temp_ctx = BuildContext(
root_dir=root_dir,
chromium_src=chromium_src,
architecture="universal",
build_type="release",
)
output_path = (
chromium_src / "out" / "Default_universal" / temp_ctx.NXTSCAPE_APP_NAME
)
log_info(f" Output: {output_path} (auto-generated)")
try:
success = merge_sign_package(
arch1_path=arch1_path,
arch2_path=arch2_path,
output_path=output_path,
chromium_src=chromium_src,
root_dir=root_dir,
sign=sign,
package=package,
)
if success:
log_success("Merge command completed successfully!")
else:
log_error("Merge command failed!")
return success
except Exception as e:
log_error(f"Merge command failed with exception: {e}")
import traceback
traceback.print_exc()
return False