Files
BrowserOS/build/modules/package.py
2025-07-01 22:44:43 -04:00

278 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""
DMG creation and packaging module for BrowserOS
"""
import sys
import shutil
from pathlib import Path
from typing import Optional, List
from context import BuildContext
from utils import run_command, log_info, log_error, log_success
def package(ctx: BuildContext) -> bool:
"""Create DMG package (only if not done by signing)"""
if ctx.sign_package:
# Already handled by signing process
return True
log_info("\n📀 Creating DMG package...")
app_path = ctx.get_app_path()
dmg_dir = ctx.root_dir / "dmg"
dmg_name = ctx.get_dmg_name()
dmg_path = dmg_dir / dmg_name
# Use Chromium's pkg-dmg tool
pkg_dmg_path = ctx.get_pkg_dmg_path()
if create_dmg(app_path, dmg_path, "BrowserOS", pkg_dmg_path):
log_success(f"Created {dmg_name}")
return True
else:
log_error("Failed to create DMG")
raise RuntimeError("Failed to create DMG")
def create_dmg(app_path: Path, dmg_path: Path, volume_name: str = "BrowserOS",
pkg_dmg_path: Optional[Path] = None) -> bool:
"""Create a DMG package from an app bundle"""
log_info(f"\n📀 Creating DMG package: {dmg_path.name}")
# Verify app exists
if not app_path.exists():
log_error(f"App not found at: {app_path}")
return False
# Create DMG directory if needed
dmg_path.parent.mkdir(parents=True, exist_ok=True)
# Remove existing DMG if present
if dmg_path.exists():
log_info(f" Removing existing DMG: {dmg_path.name}")
dmg_path.unlink()
# Build command
cmd = []
if pkg_dmg_path and pkg_dmg_path.exists():
# Use Chromium's pkg-dmg tool if available
cmd = [str(pkg_dmg_path)]
else:
# Fallback to system pkg-dmg if available
pkg_dmg_system = shutil.which('pkg-dmg')
if pkg_dmg_system:
cmd = [pkg_dmg_system]
else:
log_error("No pkg-dmg tool found")
return False
cmd.extend([
"--sourcefile",
"--source", str(app_path),
"--target", str(dmg_path),
"--volname", volume_name,
"--symlink", "/Applications:/Applications",
"--format", "UDBZ"
])
# Add verbosity for Chromium's pkg-dmg
if pkg_dmg_path:
cmd.extend(["--verbosity", "2"])
try:
run_command(cmd)
log_success(f"DMG created: {dmg_path}")
return True
except Exception as e:
log_error(f"Failed to create DMG: {e}")
return False
def sign_dmg(dmg_path: Path, certificate_name: str) -> bool:
"""Sign a DMG file"""
log_info(f"\n🔏 Signing DMG: {dmg_path.name}")
if not dmg_path.exists():
log_error(f"DMG not found at: {dmg_path}")
return False
try:
run_command([
"codesign", "--sign", certificate_name,
"--force", "--timestamp", str(dmg_path)
])
# Verify signature
log_info("🔍 Verifying DMG signature...")
run_command(["codesign", "-vvv", str(dmg_path)])
log_success("DMG signed successfully")
return True
except Exception as e:
log_error(f"Failed to sign DMG: {e}")
return False
def notarize_dmg(dmg_path: Path, keychain_profile: str = "notarytool-profile") -> bool:
"""Notarize a DMG file"""
log_info(f"\n📤 Notarizing DMG: {dmg_path.name}")
if not dmg_path.exists():
log_error(f"DMG not found at: {dmg_path}")
return False
try:
# Submit for notarization
log_info("📤 Submitting DMG for notarization (this may take a while)...")
result = run_command(
["xcrun", "notarytool", "submit", str(dmg_path),
"--keychain-profile", keychain_profile, "--wait"],
check=False
)
log_info(result.stdout)
if result.stderr:
log_error(result.stderr)
if result.returncode != 0:
log_error("DMG notarization submission failed")
return False
# Check if accepted
if "status: Accepted" not in result.stdout:
log_error("DMG notarization failed - status was not 'Accepted'")
# Try to extract submission ID for debugging
for line in result.stdout.split('\n'):
if 'id:' in line:
submission_id = line.split('id:')[1].strip().split()[0]
log_info(f"Get detailed logs with: xcrun notarytool log {submission_id} --keychain-profile \"{keychain_profile}\"")
break
return False
log_success("DMG notarization successful - status: Accepted")
# Staple the ticket
log_info("📎 Stapling notarization ticket to DMG...")
result = run_command(
["xcrun", "stapler", "staple", str(dmg_path)],
check=False
)
if result.returncode != 0:
log_error("Failed to staple notarization ticket to DMG")
return False
log_success("DMG notarization ticket stapled successfully")
# Verify stapling
log_info("🔍 Verifying DMG stapling...")
result = run_command(
["xcrun", "stapler", "validate", str(dmg_path)],
check=False
)
if result.returncode != 0:
log_error("DMG stapling verification failed")
return False
log_success("DMG stapling verification successful")
# Final security assessment
log_info("🔍 Performing final security assessment...")
result = run_command(
["spctl", "-a", "-vvv", "-t", "open",
"--context", "context:primary-signature", str(dmg_path)],
check=False
)
if result.returncode != 0:
log_error("Final security assessment failed")
return False
log_success("Final security assessment passed")
return True
except Exception as e:
log_error(f"Unexpected error during DMG notarization: {e}")
return False
def create_signed_notarized_dmg(app_path: Path, dmg_path: Path,
certificate_name: str,
volume_name: str = "BrowserOS",
pkg_dmg_path: Optional[Path] = None,
keychain_profile: str = "notarytool-profile") -> bool:
"""Create, sign, and notarize a DMG in one go"""
log_info("="*70)
log_info("📦 Creating signed and notarized DMG package")
log_info("="*70)
# Create DMG
if not create_dmg(app_path, dmg_path, volume_name, pkg_dmg_path):
return False
# Sign DMG
if not sign_dmg(dmg_path, certificate_name):
return False
# Notarize DMG
if not notarize_dmg(dmg_path, keychain_profile):
return False
log_info("="*70)
log_success(f"DMG package ready: {dmg_path}")
log_info("="*70)
return True
def package_universal(contexts: List[BuildContext]) -> bool:
"""Create DMG package for universal binary"""
log_info("=" * 70)
log_info("📦 Creating universal DMG package...")
log_info("=" * 70)
if len(contexts) < 2:
log_error("Universal packaging requires at least 2 architectures")
return False
# Use the universal app path
universal_dir = contexts[0].chromium_src / "out/Default_universal"
universal_app_path = universal_dir / contexts[0].NXTSCAPE_APP_NAME
if not universal_app_path.exists():
log_error(f"Universal app not found: {universal_app_path}")
return False
# Create DMG in root dmg directory
dmg_dir = contexts[0].root_dir / "dmg"
dmg_dir.mkdir(parents=True, exist_ok=True)
# Create a temporary universal context for DMG naming
universal_ctx = BuildContext(
root_dir=contexts[0].root_dir,
chromium_src=contexts[0].chromium_src,
architecture="universal",
build_type=contexts[0].build_type,
apply_patches=False,
sign_package=contexts[0].sign_package,
package=False,
build=False,
)
# Use context's DMG naming
dmg_name = universal_ctx.get_dmg_name()
dmg_path = dmg_dir / dmg_name
# Get pkg-dmg tool
pkg_dmg_path = contexts[0].get_pkg_dmg_path()
# Create the universal DMG
if create_dmg(universal_app_path, dmg_path, "BrowserOS", pkg_dmg_path):
log_success(f"Universal DMG created: {dmg_name}")
return True
else:
log_error("Failed to create universal DMG")
return False