Files
BrowserOS/build/modules/package.py
Nikhil Sonti 361c4cfb31 windows build support
- Added platform detection constants: `IS_WINDOWS`, `IS_MACOS`, `IS_LINUX`
- Created platform utilities:
  - `get_platform()` - Returns "windows", "macos", or "linux"
  - `get_platform_arch()` - Returns default architecture (x64 for Windows, arm64/x64 for macOS)
  - `get_executable_extension()` - Returns ".exe" on Windows, empty otherwise
  - `get_app_extension()` - Returns ".exe" on Windows, ".app" on macOS
  - `normalize_path()` - Handles platform-specific path separators
  - `join_paths()` - Platform-aware path joining

- Automatic platform detection and configuration
- Platform-specific app names:
  - Windows: `chrome.exe` and `BrowserOS.exe`
  - macOS: `Chromium.app` and `BrowserOS.app`
- Platform-specific output directories:
  - Windows: `out\Default_x64`
  - macOS: `out/Default_arm64` or `out/Default_x64`
- All path methods now use `join_paths()` for consistent path handling
- GN flags file path now includes platform: `flags.{platform}.{build_type}.gn`

- Created `flags.windows.debug.gn` - Windows debug build configuration
- Created `flags.windows.release.gn` - Windows release build configuration
- Configured for Windows-specific settings:
  - Uses system Visual Studio and Windows SDK
  - Target CPU: x64
  - Windows-specific optimizations (LLD linker, thin LTO)

- Uses `join_paths()` for all path operations
- Works with Windows paths correctly

- Uses `join_paths()` for VERSION file path
- Works with Windows executable names

- Replaced Unix `curl` command with Python's `urllib.request`
- Replaced Unix `tar` command with Python's `tarfile` module
- Now fully cross-platform

- Switched from `patch` command to `git apply` for better cross-platform support
- Uses `git apply` with `--3way` fallback for better conflict resolution
- No longer needs to find patch.exe on Windows
- Works seamlessly on all platforms where Git is installed

- Added platform-specific imports
- macOS-only modules (sign, package, postbuild) are conditionally imported
- Stub functions provided for non-macOS platforms
- Sparkle setup is skipped on non-macOS platforms

- `sign.py` - Added note that this is macOS-specific (Windows would use signtool.exe)
- `package.py` - Added note that this is macOS-specific (Windows would use MSI/NSIS)
- `postbuild.py` - macOS-specific (modifies Info.plist files)

```powershell
python build\build.py --chromium-src C:\src\chromium --build

python build\build.py --chromium-src C:\src\chromium --configure --build

python build\build.py --chromium-src C:\src\chromium --build --build-type release
```

```bash
python build/build.py --chromium-src /mnt/c/src/chromium --apply-patches

python build\build.py --chromium-src C:\src\chromium --build
```

1. **Python 3.x** - With pip for dependencies
2. **depot_tools** - Added to PATH (includes gn, gclient, autoninja)
3. **Visual Studio** - For Windows SDK and compiler
4. **Git** - Required for version control and applying patches
5. **Chromium source** - Properly synced with depot_tools

 Clean
 Git operations (clone, fetch, checkout)
 Patches (with Git for Windows)
 Configure (gn gen)
 Build (autoninja)
 String replacements
 Resource copying
 Chromium file replacements

 Sign - Requires Windows signtool.exe implementation
 Package - Requires Windows MSI/NSIS packaging
 Post-build - Requires Windows manifest handling
 Universal builds - macOS-specific feature

1. Create `modules/win/sign.py` using signtool.exe
2. Create `modules/win/package.py` for MSI/exe packaging
3. Create `modules/win/postbuild.py` for manifest handling
4. Update `build.py` to dynamically import platform modules
2025-07-03 14:40:05 -07:00

279 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""
DMG creation and packaging module for BrowserOS
NOTE: This module is macOS-specific. Windows packaging would require different tools (MSI/NSIS)
"""
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