mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 15:46:22 +00:00
`release.linux.yaml` now declares `architecture: [x64, arm64]` and the runner loops the entire pipeline once per architecture. depot_tools fetches both Linux sysroots automatically — `git_setup` idempotently ensures `target_cpus = ['x64', 'arm64']` is in `.gclient` before `gclient sync`, so cross-compiling arm64 from an x64 host just works. The resolver returns `List[Context]` (single-element for the common single-arch case), and `build/cli/build.py` loops `execute_pipeline` over the per-arch contexts. Modules stay 100% arch-agnostic — no new orchestration module, no new YAML schema beyond the list form. Also fix a cross-compile bug in `build/modules/package/linux.py`: the appimagetool binary must match the BUILD machine's arch (it executes locally), not the target arch. Split into a host-keyed `LINUX_HOST_APPIMAGETOOL` lookup vs the existing target-keyed `LINUX_ARCHITECTURE_CONFIG`. Target arch is still passed to appimagetool via the `ARCH` env var. - build/common/resolver.py: scalar OR list `architecture` -> List[Context] - build/cli/build.py: loop pipeline per arch, log multi-arch headers - build/config/release.linux.yaml: `architecture: [x64, arm64]` - build/modules/setup/git.py: idempotent `target_cpus` edit on Linux - build/modules/package/linux.py: host vs target appimagetool split - build/modules/package/linux_test.py: cover the host/target split
501 lines
16 KiB
Python
Generated
501 lines
16 KiB
Python
Generated
#!/usr/bin/env python3
|
|
"""Build CLI - Modular build system for BrowserOS"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import typer
|
|
|
|
# Import common modules
|
|
from ..common.context import Context
|
|
from ..common.config import load_config, validate_required_envs
|
|
from ..common.pipeline import validate_pipeline, show_available_modules
|
|
from ..common.resolver import resolve_config, resolve_pipeline
|
|
from ..common.notify import (
|
|
notify_pipeline_start,
|
|
notify_pipeline_end,
|
|
notify_pipeline_error,
|
|
notify_module_start,
|
|
notify_module_completion,
|
|
set_build_context,
|
|
)
|
|
from ..common.module import ValidationError
|
|
from ..common.utils import (
|
|
log_error,
|
|
log_info,
|
|
log_success,
|
|
log_warning,
|
|
IS_MACOS,
|
|
IS_WINDOWS,
|
|
IS_LINUX,
|
|
)
|
|
|
|
# Import all module classes
|
|
from ..modules.setup.clean import CleanModule
|
|
from ..modules.setup.git import GitSetupModule, SparkleSetupModule
|
|
from ..modules.setup.configure import ConfigureModule
|
|
from ..modules.compile import CompileModule, UniversalBuildModule
|
|
from ..modules.patches.patches import PatchesModule
|
|
from ..modules.patches.series_patches import SeriesPatchesModule
|
|
from ..modules.resources.chromium_replace import ChromiumReplaceModule
|
|
from ..modules.resources.string_replaces import StringReplacesModule
|
|
from ..modules.resources.resources import ResourcesModule
|
|
from ..modules.extensions import BundledExtensionsModule
|
|
from ..modules.storage import UploadModule, DownloadResourcesModule
|
|
|
|
# Platform-specific modules (imported unconditionally - validation handles platform checks)
|
|
from ..modules.sign.macos import MacOSSignModule
|
|
from ..modules.sign.windows import WindowsSignModule
|
|
from ..modules.sign.linux import LinuxSignModule
|
|
from ..modules.sign.sparkle import SparkleSignModule
|
|
from ..modules.package.macos import MacOSPackageModule
|
|
from ..modules.package.windows import WindowsPackageModule
|
|
from ..modules.package.linux import LinuxPackageModule
|
|
|
|
AVAILABLE_MODULES = {
|
|
# Setup & Environment
|
|
"clean": CleanModule,
|
|
"git_setup": GitSetupModule,
|
|
"sparkle_setup": SparkleSetupModule,
|
|
"configure": ConfigureModule,
|
|
# Patches & Resources
|
|
"patches": PatchesModule,
|
|
"series_patches": SeriesPatchesModule,
|
|
"chromium_replace": ChromiumReplaceModule,
|
|
"string_replaces": StringReplacesModule,
|
|
"download_resources": DownloadResourcesModule, # Download binaries from R2
|
|
"resources": ResourcesModule,
|
|
"bundled_extensions": BundledExtensionsModule,
|
|
# Build
|
|
"compile": CompileModule,
|
|
"universal_build": UniversalBuildModule, # macOS universal binary (arm64 + x64)
|
|
# Sign (platform-specific, validated at runtime)
|
|
"sign_macos": MacOSSignModule,
|
|
"sign_windows": WindowsSignModule,
|
|
"sign_linux": LinuxSignModule,
|
|
"sparkle_sign": SparkleSignModule, # macOS Sparkle signing for auto-update
|
|
# Package (platform-specific, validated at runtime)
|
|
"package_macos": MacOSPackageModule,
|
|
"package_windows": WindowsPackageModule,
|
|
"package_linux": LinuxPackageModule,
|
|
# Storage (upload/download)
|
|
"upload": UploadModule,
|
|
}
|
|
|
|
|
|
def _get_sign_module():
|
|
"""Get platform-specific sign module name"""
|
|
if IS_MACOS():
|
|
return "sign_macos"
|
|
elif IS_WINDOWS():
|
|
return "sign_windows"
|
|
elif IS_LINUX():
|
|
return "sign_linux"
|
|
else:
|
|
log_error("Unsupported platform for packaging")
|
|
sys.exit(1)
|
|
|
|
|
|
def _get_package_module():
|
|
"""Get platform-specific package module name"""
|
|
if IS_MACOS():
|
|
return "package_macos"
|
|
elif IS_WINDOWS():
|
|
return "package_windows"
|
|
elif IS_LINUX():
|
|
return "package_linux"
|
|
else:
|
|
log_error("Unsupported platform for packaging")
|
|
sys.exit(1)
|
|
|
|
|
|
# Fixed execution order - flags enable/disable phases, order is always the same
|
|
EXECUTION_ORDER = [
|
|
# Phase 1: Setup & Clean
|
|
("setup", ["clean", "git_setup", "sparkle_setup"]),
|
|
# Phase 2: Patches & Resources
|
|
(
|
|
"prep",
|
|
[
|
|
"download_resources",
|
|
"resources",
|
|
"bundled_extensions",
|
|
"chromium_replace",
|
|
"string_replaces",
|
|
"patches",
|
|
"configure",
|
|
],
|
|
),
|
|
# Phase 3: Build
|
|
("build", ["compile"]),
|
|
# Phase 4: Code Signing (platform-aware)
|
|
("sign", [_get_sign_module()]),
|
|
# Phase 5: Packaging (platform-aware)
|
|
("package", [_get_package_module()]),
|
|
# Phase 6: Upload
|
|
("upload", ["upload"]),
|
|
]
|
|
|
|
# Modules that trigger Slack notifications (to reduce verbosity)
|
|
NOTIFY_MODULES = [
|
|
"compile",
|
|
"sign_macos",
|
|
"sign_windows",
|
|
"sign_linux",
|
|
"package_macos",
|
|
"package_windows",
|
|
"package_linux",
|
|
"upload",
|
|
]
|
|
|
|
|
|
def execute_pipeline(
|
|
ctx: Context,
|
|
pipeline: list[str],
|
|
available_modules: dict,
|
|
pipeline_name: str = "build",
|
|
) -> None:
|
|
"""Execute a build pipeline by running modules sequentially.
|
|
|
|
Args:
|
|
ctx: Build context with paths and configuration
|
|
pipeline: List of module names to execute in order
|
|
available_modules: Dictionary mapping module names to module classes
|
|
pipeline_name: Name of pipeline for notifications (default: "build")
|
|
|
|
Raises:
|
|
typer.Exit: On module validation failure, execution failure, or interrupt
|
|
|
|
Design:
|
|
- Executes modules sequentially in pipeline order
|
|
- Validates each module before execution (fail fast)
|
|
- Tracks timing for each module and total pipeline
|
|
- Sends notifications at key lifecycle events
|
|
- Handles interrupts (Ctrl+C) gracefully with cleanup
|
|
"""
|
|
start_time = time.time()
|
|
notify_pipeline_start(pipeline_name, pipeline)
|
|
|
|
try:
|
|
for module_name in pipeline:
|
|
log_info(f"\n{'='*70}")
|
|
log_info(f"🔧 Running module: {module_name}")
|
|
log_info(f"{'='*70}")
|
|
|
|
# Instantiate module
|
|
module_class = available_modules[module_name]
|
|
module = module_class()
|
|
|
|
# Notify module start and track timing (only for key modules)
|
|
if module_name in NOTIFY_MODULES:
|
|
notify_module_start(module_name)
|
|
module_start = time.time()
|
|
|
|
# Validate right before executing (fail fast)
|
|
try:
|
|
module.validate(ctx)
|
|
except ValidationError as e:
|
|
log_error(f"Validation failed for {module_name}: {e}")
|
|
notify_pipeline_error(
|
|
pipeline_name, f"{module_name} validation failed: {e}"
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
# Execute module
|
|
try:
|
|
module.execute(ctx)
|
|
module_duration = time.time() - module_start
|
|
if module_name in NOTIFY_MODULES:
|
|
notify_module_completion(module_name, module_duration)
|
|
log_success(f"Module {module_name} completed in {module_duration:.1f}s")
|
|
except Exception as e:
|
|
log_error(f"Module {module_name} failed: {e}")
|
|
notify_pipeline_error(pipeline_name, f"{module_name} failed: {e}")
|
|
raise typer.Exit(1)
|
|
|
|
# Pipeline completed successfully
|
|
duration = time.time() - start_time
|
|
mins = int(duration / 60)
|
|
secs = int(duration % 60)
|
|
|
|
log_info("\n" + "=" * 70)
|
|
log_success(f"✅ Pipeline completed successfully in {mins}m {secs}s")
|
|
log_info("=" * 70)
|
|
|
|
notify_pipeline_end(pipeline_name, duration)
|
|
|
|
except KeyboardInterrupt:
|
|
log_error("\n❌ Pipeline interrupted")
|
|
notify_pipeline_error(pipeline_name, "Interrupted by user")
|
|
raise typer.Exit(130)
|
|
except typer.Exit:
|
|
# Re-raise typer.Exit (from validation/execution failures)
|
|
raise
|
|
except Exception as e:
|
|
log_error(f"\n❌ Pipeline failed: {e}")
|
|
notify_pipeline_error(pipeline_name, str(e))
|
|
raise typer.Exit(1)
|
|
|
|
|
|
def main(
|
|
config: Optional[Path] = typer.Option(
|
|
None,
|
|
"--config",
|
|
"-c",
|
|
help="Load configuration from YAML file",
|
|
exists=True,
|
|
),
|
|
modules: Optional[str] = typer.Option(
|
|
None,
|
|
"--modules",
|
|
"-m",
|
|
help="Comma-separated list of modules to run",
|
|
),
|
|
list_modules: bool = typer.Option(
|
|
False,
|
|
"--list",
|
|
"-l",
|
|
help="List all available modules and exit",
|
|
),
|
|
# Pipeline phase flags (auto-ordered execution)
|
|
setup: bool = typer.Option(
|
|
False,
|
|
"--setup",
|
|
help="Run setup phase (clean, git_setup, sparkle_setup)",
|
|
),
|
|
prep: bool = typer.Option(
|
|
False,
|
|
"--prep",
|
|
help="Run prep phase (resources, chromium_replace, string_replaces, patches, configure)",
|
|
),
|
|
build: bool = typer.Option(
|
|
False,
|
|
"--build",
|
|
help="Run build phase (compile)",
|
|
),
|
|
sign: bool = typer.Option(
|
|
False,
|
|
"--sign",
|
|
help="Run sign phase (platform-specific: sign_macos/windows/linux)",
|
|
),
|
|
package: bool = typer.Option(
|
|
False,
|
|
"--package",
|
|
help="Run package phase (platform-specific: package_macos/windows/linux)",
|
|
),
|
|
upload: bool = typer.Option(
|
|
False,
|
|
"--upload",
|
|
help="Run upload phase (upload artifacts)",
|
|
),
|
|
# Global options that override config
|
|
arch: Optional[str] = typer.Option(
|
|
None,
|
|
"--arch",
|
|
"-a",
|
|
help="Architecture (arm64, x64, universal)",
|
|
),
|
|
build_type: Optional[str] = typer.Option(
|
|
None,
|
|
"--build-type",
|
|
"-t",
|
|
help="Build type (debug or release)",
|
|
),
|
|
chromium_src: Optional[Path] = typer.Option(
|
|
None,
|
|
"--chromium-src",
|
|
"-S",
|
|
help="Path to Chromium source directory",
|
|
),
|
|
):
|
|
"""BrowserOS Build System - Modular pipeline executor
|
|
|
|
Build BrowserOS using phase flags (auto-ordered), explicit modules, or configs.
|
|
|
|
\b
|
|
Phase Flags (Recommended - Auto-Ordered):
|
|
browseros build --setup --build --sign --package
|
|
browseros build --build --sign # Skip setup
|
|
browseros build --package --sign # Flags work in any order!
|
|
|
|
\b
|
|
Explicit Modules (Power Users):
|
|
browseros build --modules clean,compile,sign_macos
|
|
|
|
\b
|
|
Config Files (CI/CD):
|
|
browseros build --config release.yaml --arch arm64
|
|
|
|
\b
|
|
List Available:
|
|
browseros build --list # Show all modules and phases
|
|
|
|
Note: Phase flags always execute in correct order regardless of how you write them.
|
|
--sign and --package auto-select platform (macos/windows/linux)
|
|
"""
|
|
|
|
# Handle --list flag
|
|
if list_modules:
|
|
show_available_modules(AVAILABLE_MODULES)
|
|
return
|
|
|
|
# Check for mutually exclusive options
|
|
has_config = config is not None
|
|
has_modules = modules is not None
|
|
has_flags = any([setup, prep, build, sign, package, upload])
|
|
|
|
options_provided = sum([has_config, has_modules, has_flags])
|
|
|
|
if options_provided == 0:
|
|
typer.echo(
|
|
"Error: Specify --config, --modules, or phase flags (--setup, --build, etc.)\n"
|
|
)
|
|
typer.echo("Use --help for usage information")
|
|
typer.echo("Use --list to see available modules")
|
|
raise typer.Exit(1)
|
|
|
|
if options_provided > 1:
|
|
log_error("Specify only ONE of: --config, --modules, or phase flags")
|
|
log_error("Examples:")
|
|
log_error(" browseros build --setup --build --sign")
|
|
log_error(" browseros build --modules clean,compile")
|
|
log_error(" browseros build --config release.yaml")
|
|
raise typer.Exit(1)
|
|
|
|
# CONFIG MODE validation: YAML controls everything, CLI build flags not allowed
|
|
if has_config:
|
|
conflicting_flags = []
|
|
if arch is not None:
|
|
conflicting_flags.append("--arch")
|
|
if build_type is not None:
|
|
conflicting_flags.append("--build-type")
|
|
|
|
if conflicting_flags:
|
|
log_error(
|
|
f"CONFIG MODE: Cannot use {', '.join(conflicting_flags)} with --config"
|
|
)
|
|
log_error("When using --config, ALL build parameters come from YAML")
|
|
log_error("Remove the conflicting flags or don't use --config")
|
|
raise typer.Exit(1)
|
|
|
|
log_info("🚀 BrowserOS Build System")
|
|
log_info("=" * 70)
|
|
|
|
# Load YAML config if provided
|
|
config_data = load_config(config) if config else None
|
|
|
|
# Build CLI arguments dictionary for resolver
|
|
root_dir = Path(__file__).parent.parent.parent
|
|
cli_args = {
|
|
"chromium_src": chromium_src,
|
|
"arch": arch,
|
|
"build_type": build_type,
|
|
"modules": modules,
|
|
"setup": setup,
|
|
"prep": prep,
|
|
"build": build,
|
|
"sign": sign,
|
|
"package": package,
|
|
"upload": upload,
|
|
}
|
|
|
|
# Resolve build context (CONFIG mode or DIRECT mode).
|
|
# Returns one Context per architecture — single-element for normal
|
|
# builds, multi-element when YAML declares `architecture: [x64, arm64]`.
|
|
try:
|
|
arch_ctxs = resolve_config(cli_args, config_data)
|
|
except ValueError as e:
|
|
log_error(str(e))
|
|
raise typer.Exit(1)
|
|
|
|
# Resolve pipeline (CONFIG mode or DIRECT mode)
|
|
try:
|
|
pipeline = resolve_pipeline(
|
|
cli_args,
|
|
config_data,
|
|
execution_order=EXECUTION_ORDER,
|
|
)
|
|
except ValueError as e:
|
|
log_error(str(e))
|
|
raise typer.Exit(1)
|
|
|
|
# Show execution plan for flag-based mode
|
|
if has_flags:
|
|
log_info("\n📋 Execution Plan (auto-ordered):")
|
|
log_info("-" * 70)
|
|
phase_names = []
|
|
if setup:
|
|
phase_names.append("setup")
|
|
if prep:
|
|
phase_names.append("prep")
|
|
log_warning("⚠️ --prep does NOT apply series_patches. Run 'browseros build -m series_patches' separately if needed.")
|
|
if build:
|
|
phase_names.append("build")
|
|
if sign:
|
|
phase_names.append(f"sign (→ {_get_sign_module()})")
|
|
if package:
|
|
phase_names.append(f"package (→ {_get_package_module()})")
|
|
if upload:
|
|
phase_names.append("upload")
|
|
|
|
for phase_name in phase_names:
|
|
log_info(f" ✓ {phase_name}")
|
|
|
|
log_info(f"\n Pipeline: {' → '.join(pipeline)}")
|
|
log_info("-" * 70)
|
|
|
|
# Validate required environment variables (YAML-specific)
|
|
if config_data:
|
|
required_envs = config_data.get("required_envs", [])
|
|
if required_envs:
|
|
validate_required_envs(required_envs)
|
|
|
|
# Validate pipeline modules exist
|
|
validate_pipeline(pipeline, AVAILABLE_MODULES)
|
|
|
|
# Set Windows-specific environment
|
|
if IS_WINDOWS():
|
|
os.environ["DEPOT_TOOLS_WIN_TOOLCHAIN"] = "0"
|
|
log_info("Set DEPOT_TOOLS_WIN_TOOLCHAIN=0 for Windows build")
|
|
|
|
# Print build summary using the first context — versions and paths
|
|
# are identical across per-arch contexts. Architecture is logged again
|
|
# inside the loop below for multi-arch runs.
|
|
summary_ctx = arch_ctxs[0]
|
|
log_info(f"📍 Root: {root_dir}")
|
|
log_info(f"📍 Chromium: {summary_ctx.chromium_src}")
|
|
if len(arch_ctxs) > 1:
|
|
log_info(
|
|
f"📍 Architectures: {[c.architecture for c in arch_ctxs]} (multi-arch loop)"
|
|
)
|
|
else:
|
|
log_info(f"📍 Architecture: {summary_ctx.architecture}")
|
|
log_info(f"📍 Build type: {summary_ctx.build_type}")
|
|
log_info(f"📍 Semantic version: {summary_ctx.semantic_version}")
|
|
log_info(f"📍 Chromium version: {summary_ctx.chromium_version}")
|
|
log_info(f"📍 Build offset: {summary_ctx.browseros_build_offset}")
|
|
log_info(f"📍 Pipeline: {' → '.join(pipeline)}")
|
|
log_info("=" * 70)
|
|
|
|
os_name = "macOS" if IS_MACOS() else "Windows" if IS_WINDOWS() else "Linux"
|
|
|
|
# Execute the pipeline once per architecture. Modules see a normal
|
|
# single-arch ctx; the runner is the only thing that knows about the
|
|
# multi-arch loop.
|
|
for i, arch_ctx in enumerate(arch_ctxs, start=1):
|
|
if len(arch_ctxs) > 1:
|
|
log_info("\n" + "#" * 70)
|
|
log_info(
|
|
f"# Architecture {i}/{len(arch_ctxs)}: {arch_ctx.architecture}"
|
|
)
|
|
log_info(f"# Output: {arch_ctx.out_dir}")
|
|
log_info("#" * 70)
|
|
|
|
set_build_context(os_name, arch_ctx.architecture)
|
|
execute_pipeline(
|
|
arch_ctx, pipeline, AVAILABLE_MODULES, pipeline_name="build"
|
|
)
|