mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2025-07-21 00:21:28 +03:00
601 lines
21 KiB
Python
Executable File
601 lines
21 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Main build orchestrator for Nxtscape Browser
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import click
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
# Import shared components
|
|
from context import BuildContext
|
|
from utils import load_config, log_info, log_warning, log_error, log_success, IS_MACOS, IS_WINDOWS
|
|
|
|
# Import modules
|
|
from modules.clean import clean
|
|
from modules.git import setup_git, setup_sparkle
|
|
from modules.patches import apply_patches
|
|
from modules.resources import copy_resources
|
|
from modules.chromium_replace import replace_chromium_files, add_file_to_replacements
|
|
from modules.string_replaces import apply_string_replacements
|
|
from modules.inject import inject_version
|
|
from modules.configure import configure
|
|
from modules.compile import build
|
|
from modules.gcs import upload_package_artifacts, upload_signed_artifacts
|
|
|
|
# Platform-specific imports
|
|
if IS_MACOS:
|
|
from modules.sign import sign, sign_universal, check_signing_environment
|
|
from modules.package import package, package_universal
|
|
from modules.postbuild import run_postbuild
|
|
elif IS_WINDOWS:
|
|
from modules.package_windows import package, package_universal, sign_binaries as sign
|
|
# Windows doesn't have universal signing
|
|
def sign_universal(contexts: list[BuildContext]) -> bool:
|
|
log_warning("Universal signing is not supported on Windows")
|
|
return True
|
|
def run_postbuild(ctx: BuildContext) -> None:
|
|
log_warning("Post-build tasks are not implemented for Windows yet")
|
|
else:
|
|
# Stub functions for other platforms (Linux, etc.)
|
|
def sign(ctx: BuildContext) -> bool:
|
|
log_warning("Signing is not implemented for this platform")
|
|
return True
|
|
def sign_universal(contexts: list[BuildContext]) -> bool:
|
|
log_warning("Universal signing is not implemented for this platform")
|
|
return True
|
|
def package(ctx: BuildContext) -> bool:
|
|
log_warning("Packaging is not implemented for this platform")
|
|
return True
|
|
def package_universal(contexts: list[BuildContext]) -> bool:
|
|
log_warning("Universal packaging is not implemented for this platform")
|
|
return True
|
|
def run_postbuild(ctx: BuildContext) -> None:
|
|
log_warning("Post-build tasks are not implemented for this platform")
|
|
|
|
from modules.slack import (
|
|
notify_build_started,
|
|
notify_build_step,
|
|
notify_build_success,
|
|
notify_build_failure,
|
|
notify_build_interrupted,
|
|
notify_gcs_upload,
|
|
)
|
|
|
|
|
|
def build_main(
|
|
config_file: Optional[Path] = None,
|
|
clean_flag: bool = False,
|
|
git_setup_flag: bool = False,
|
|
apply_patches_flag: bool = False,
|
|
sign_flag: bool = False,
|
|
package_flag: bool = False,
|
|
build_flag: bool = False,
|
|
arch: str = "", # Will use platform default if not specified
|
|
build_type: str = "debug",
|
|
chromium_src_dir: Optional[Path] = None,
|
|
slack_notifications: bool = False,
|
|
patch_interactive: bool = False,
|
|
patch_commit: bool = False,
|
|
upload_gcs: bool = True, # Default to uploading to GCS
|
|
):
|
|
"""Main build orchestration"""
|
|
log_info("🚀 Nxtscape Build System")
|
|
log_info("=" * 50)
|
|
|
|
# Check if sign flag is enabled and required environment variables are set
|
|
if sign_flag:
|
|
if not check_signing_environment():
|
|
sys.exit(1)
|
|
|
|
# Set Windows-specific environment variables
|
|
if IS_WINDOWS:
|
|
os.environ["DEPOT_TOOLS_WIN_TOOLCHAIN"] = "0"
|
|
log_info("🔧 Set DEPOT_TOOLS_WIN_TOOLCHAIN=0 for Windows build")
|
|
|
|
# Setup context
|
|
root_dir = Path(__file__).parent.parent
|
|
|
|
# Initialize chromium_src as None - will be set from CLI or config
|
|
chromium_src = None
|
|
|
|
# Load config if provided
|
|
config = None
|
|
gn_flags_file = None
|
|
architectures = [arch] if arch else [] # Empty list if no arch specified
|
|
universal = False
|
|
if config_file:
|
|
config = load_config(config_file)
|
|
log_info(f"📄 Loaded config from: {config_file}")
|
|
|
|
# Override parameters from config
|
|
if "build" in config:
|
|
build_type = config["build"].get("type", build_type)
|
|
arch = config["build"].get("architecture", arch)
|
|
# Check for multi-architecture builds
|
|
if "architectures" in config["build"]:
|
|
architectures = config["build"]["architectures"]
|
|
universal = config["build"].get("universal", False)
|
|
|
|
if "steps" in config:
|
|
clean_flag = config["steps"].get("clean", clean_flag)
|
|
git_setup_flag = config["steps"].get("git_setup", git_setup_flag)
|
|
apply_patches_flag = config["steps"].get(
|
|
"apply_patches", apply_patches_flag
|
|
)
|
|
build_flag = config["steps"].get("build", build_flag)
|
|
sign_flag = config["steps"].get("sign", sign_flag)
|
|
package_flag = config["steps"].get("package", package_flag)
|
|
|
|
# Override slack notifications from config if not explicitly set via CLI
|
|
if "notifications" in config:
|
|
slack_notifications = config["notifications"].get(
|
|
"slack", slack_notifications
|
|
)
|
|
|
|
if "gn_flags" in config and "file" in config["gn_flags"]:
|
|
gn_flags_file = Path(config["gn_flags"]["file"])
|
|
|
|
# Get chromium_src from config (only if not provided via CLI)
|
|
if (
|
|
not chromium_src_dir
|
|
and "paths" in config
|
|
and "chromium_src" in config["paths"]
|
|
):
|
|
config_chromium_src = Path(config["paths"]["chromium_src"])
|
|
chromium_src = config_chromium_src
|
|
log_info(f"📁 Using Chromium source from config: {chromium_src}")
|
|
|
|
# CLI takes precedence over config
|
|
if chromium_src_dir:
|
|
chromium_src = chromium_src_dir
|
|
log_info(f"📁 Using Chromium source from CLI: {chromium_src}")
|
|
|
|
# Enforce chromium_src requirement
|
|
if not chromium_src:
|
|
log_error("Chromium source directory is required!")
|
|
log_error(
|
|
"Provide it via --chromium-src CLI option or paths.chromium_src in config YAML"
|
|
)
|
|
log_error("Example: python build.py --chromium-src /path/to/chromium/src")
|
|
raise ValueError("chromium_src is required but not provided")
|
|
|
|
# Validate chromium_src path exists
|
|
if not chromium_src.exists():
|
|
log_error(f"Chromium source directory does not exist: {chromium_src}")
|
|
log_error("Please provide a valid chromium source path")
|
|
raise FileNotFoundError(f"Chromium source directory not found: {chromium_src}")
|
|
|
|
# If no architectures specified, use platform default
|
|
if not architectures:
|
|
from utils import get_platform_arch
|
|
architectures = [get_platform_arch()]
|
|
log_info(f"📍 Using platform default architecture: {architectures[0]}")
|
|
|
|
# Display build configuration
|
|
log_info(f"📍 Root: {root_dir}")
|
|
log_info(f"📍 Chromium source: {chromium_src}")
|
|
log_info(f"📍 Architectures: {architectures}")
|
|
log_info(f"📍 Universal build: {universal}")
|
|
log_info(f"📍 Build type: {build_type}")
|
|
|
|
# Start time for overall build
|
|
start_time = time.time()
|
|
|
|
# Notify build started (if enabled)
|
|
if slack_notifications:
|
|
notify_build_started(build_type, str(architectures))
|
|
|
|
# Run build steps
|
|
try:
|
|
built_contexts = []
|
|
all_gcs_uris = [] # Track all uploaded GCS URIs
|
|
|
|
# Build each architecture separately
|
|
for arch_name in architectures:
|
|
log_info(f"\n{'='*60}")
|
|
log_info(f"🏗️ Building for architecture: {arch_name}")
|
|
log_info(f"{'='*60}")
|
|
|
|
ctx = BuildContext(
|
|
root_dir=root_dir,
|
|
chromium_src=chromium_src,
|
|
architecture=arch_name,
|
|
build_type=build_type,
|
|
apply_patches=apply_patches_flag,
|
|
sign_package=sign_flag,
|
|
package=package_flag,
|
|
build=build_flag,
|
|
)
|
|
|
|
log_info(f"📍 Chromium: {ctx.chromium_version}")
|
|
log_info(f"📍 Nxtscape: {ctx.nxtscape_version}")
|
|
log_info(f"📍 Output directory: {ctx.out_dir}")
|
|
|
|
# Clean (only for first architecture to avoid conflicts)
|
|
if clean_flag and arch_name == architectures[0]:
|
|
clean(ctx)
|
|
if slack_notifications:
|
|
notify_build_step("Completed cleaning build artifacts")
|
|
|
|
# Git setup (only once for first architecture)
|
|
if git_setup_flag and arch_name == architectures[0]:
|
|
setup_git(ctx)
|
|
if slack_notifications:
|
|
notify_build_step("Completed Git setup and Chromium source")
|
|
|
|
# Apply patches (only once for first architecture)
|
|
if apply_patches_flag and arch_name == architectures[0]:
|
|
# Inject version into manifest files
|
|
inject_version(ctx)
|
|
|
|
# First do chromium file replacements
|
|
replace_chromium_files(ctx)
|
|
|
|
# Then apply string replacements
|
|
apply_string_replacements(ctx)
|
|
|
|
# Setup sparkle (macOS only)
|
|
if IS_MACOS:
|
|
setup_sparkle(ctx)
|
|
else:
|
|
log_info("Skipping Sparkle setup (macOS only)")
|
|
|
|
# Apply patches
|
|
apply_patches(ctx, interactive=patch_interactive, commit_each=patch_commit)
|
|
|
|
|
|
# Copy resources
|
|
copy_resources(ctx, commit_each=patch_commit)
|
|
|
|
|
|
if slack_notifications:
|
|
notify_build_step(
|
|
"Completed applying patches and copying resources"
|
|
)
|
|
|
|
# Build for this architecture
|
|
if build_flag:
|
|
if slack_notifications:
|
|
notify_build_step(f"Started building for {arch_name}")
|
|
configure(ctx, gn_flags_file)
|
|
build(ctx)
|
|
|
|
# Run post-build tasks
|
|
# run_postbuild(ctx)
|
|
|
|
if slack_notifications:
|
|
notify_build_step(f"Completed building for {arch_name}")
|
|
|
|
# Sign and package immediately after building each architecture
|
|
if sign_flag:
|
|
log_info(f"\n🔏 Signing {ctx.architecture} build...")
|
|
if slack_notifications:
|
|
notify_build_step(f"[{ctx.architecture}] Started signing")
|
|
sign(ctx)
|
|
if slack_notifications:
|
|
notify_build_step(f"[{ctx.architecture}] Completed signing")
|
|
|
|
if package_flag:
|
|
log_info(f"\n📦 Packaging {ctx.architecture} build...")
|
|
if slack_notifications:
|
|
package_type = "DMG" if IS_MACOS else "package"
|
|
notify_build_step(f"[{ctx.architecture}] Started {package_type} creation")
|
|
package(ctx)
|
|
if slack_notifications:
|
|
package_type = "DMG" if IS_MACOS else "package"
|
|
notify_build_step(f"[{ctx.architecture}] Completed {package_type} creation")
|
|
|
|
# Upload to GCS after packaging
|
|
gcs_uris = []
|
|
if upload_gcs:
|
|
success, gcs_uris = upload_package_artifacts(ctx)
|
|
if not success:
|
|
log_warning("Failed to upload package artifacts to GCS")
|
|
elif gcs_uris and slack_notifications:
|
|
notify_gcs_upload(ctx.architecture, gcs_uris)
|
|
all_gcs_uris.extend(gcs_uris)
|
|
|
|
built_contexts.append(ctx)
|
|
|
|
# Handle universal build if requested
|
|
if len(architectures) > 1 and universal:
|
|
# Universal build: merge, sign and package
|
|
log_info(f"\n{'='*60}")
|
|
log_info("🔄 Creating universal binary...")
|
|
log_info(f"{'='*60}")
|
|
|
|
# Import merge function
|
|
from modules.merge import merge_architectures
|
|
|
|
# Get paths for the built apps
|
|
arch1_app = built_contexts[0].get_app_path()
|
|
arch2_app = built_contexts[1].get_app_path()
|
|
|
|
# Clean up old universal output directory if it exists
|
|
universal_dir = built_contexts[0].chromium_src / "out/Default_universal"
|
|
if universal_dir.exists():
|
|
log_info("🧹 Cleaning up old universal output directory...")
|
|
from utils import safe_rmtree
|
|
safe_rmtree(universal_dir)
|
|
|
|
# Create fresh universal output path
|
|
universal_dir.mkdir(parents=True, exist_ok=True)
|
|
universal_app_path = universal_dir / built_contexts[0].NXTSCAPE_APP_NAME
|
|
|
|
# Find universalizer script
|
|
universalizer_script = root_dir / "build" / "universalizer_patched.py"
|
|
|
|
# Merge the architectures
|
|
if not merge_architectures(
|
|
arch1_app, arch2_app, universal_app_path, universalizer_script
|
|
):
|
|
raise RuntimeError(
|
|
"Failed to merge architectures into universal binary"
|
|
)
|
|
|
|
if slack_notifications:
|
|
notify_build_step(
|
|
"Completed merging architectures into universal binary"
|
|
)
|
|
|
|
if sign_flag:
|
|
if slack_notifications:
|
|
notify_build_step("[Universal] Started signing and notarization")
|
|
sign_universal(built_contexts)
|
|
if slack_notifications:
|
|
notify_build_step("[Universal] Completed signing and notarization")
|
|
|
|
if package_flag:
|
|
if slack_notifications:
|
|
package_type = "DMG" if IS_MACOS else "package"
|
|
notify_build_step(f"[Universal] Started {package_type} creation")
|
|
package_universal(built_contexts)
|
|
if slack_notifications:
|
|
package_type = "DMG" if IS_MACOS else "package"
|
|
notify_build_step(f"[Universal] Completed {package_type} creation")
|
|
|
|
# Upload universal package to GCS
|
|
universal_gcs_uris = []
|
|
if upload_gcs:
|
|
# Use the first context with universal architecture override
|
|
universal_ctx = built_contexts[0]
|
|
original_arch = universal_ctx.architecture
|
|
universal_ctx.architecture = "universal"
|
|
success, universal_gcs_uris = upload_package_artifacts(universal_ctx)
|
|
if not success:
|
|
log_warning("Failed to upload universal package artifacts to GCS")
|
|
elif universal_gcs_uris and slack_notifications:
|
|
notify_gcs_upload("universal", universal_gcs_uris)
|
|
all_gcs_uris.extend(universal_gcs_uris)
|
|
universal_ctx.architecture = original_arch
|
|
|
|
# Summary
|
|
elapsed = time.time() - start_time
|
|
mins = int(elapsed / 60)
|
|
secs = int(elapsed % 60)
|
|
|
|
log_info("\n" + "=" * 60)
|
|
log_success(
|
|
f"Build completed for {len(architectures)} architecture(s) in {mins}m {secs}s"
|
|
)
|
|
if universal and len(architectures) > 1:
|
|
log_success("Universal binary created successfully!")
|
|
log_info("=" * 60)
|
|
|
|
# Notify build success (if enabled)
|
|
if slack_notifications:
|
|
notify_build_success(mins, secs, gcs_uris=all_gcs_uris)
|
|
|
|
except KeyboardInterrupt:
|
|
log_warning("\nBuild interrupted")
|
|
if slack_notifications:
|
|
notify_build_interrupted()
|
|
sys.exit(130)
|
|
except Exception as e:
|
|
log_error(f"\nBuild failed: {e}")
|
|
if slack_notifications:
|
|
notify_build_failure(str(e))
|
|
sys.exit(1)
|
|
|
|
|
|
@click.command()
|
|
@click.option(
|
|
"--config",
|
|
"-c",
|
|
type=click.Path(exists=True, path_type=Path),
|
|
help="Load configuration from YAML file",
|
|
)
|
|
@click.option("--clean", "-C", is_flag=True, default=False, help="Clean before build")
|
|
@click.option("--git-setup", "-g", is_flag=True, default=False, help="Git setup")
|
|
@click.option(
|
|
"--apply-patches", "-p", is_flag=True, default=False, help="Apply patches"
|
|
)
|
|
@click.option(
|
|
"--sign", "-s", is_flag=True, default=False, help="Sign and notarize the app"
|
|
)
|
|
@click.option(
|
|
"--arch",
|
|
"-a",
|
|
type=click.Choice(["arm64", "x64"]),
|
|
default=None,
|
|
help="Architecture (defaults to platform-specific)",
|
|
)
|
|
@click.option(
|
|
"--build-type",
|
|
"-t",
|
|
type=click.Choice(["debug", "release"]),
|
|
default="debug",
|
|
help="Build type",
|
|
)
|
|
@click.option("--package", "-P", is_flag=True, default=False, help="Create DMG package")
|
|
@click.option("--build", "-b", is_flag=True, default=False, help="Build")
|
|
@click.option(
|
|
"--chromium-src",
|
|
"-S",
|
|
type=click.Path(exists=False, path_type=Path),
|
|
help="Path to Chromium source directory",
|
|
)
|
|
@click.option(
|
|
"--slack-notifications",
|
|
"-n",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Enable Slack notifications",
|
|
)
|
|
@click.option(
|
|
"--merge",
|
|
nargs=2,
|
|
type=click.Path(path_type=Path),
|
|
metavar="ARCH1_APP ARCH2_APP",
|
|
help="Merge two architecture builds: --merge path/to/arch1.app path/to/arch2.app",
|
|
)
|
|
@click.option(
|
|
"--add-replace",
|
|
type=click.Path(exists=True, path_type=Path),
|
|
help="Add a file to chromium_src replacement directory: --add-replace /path/to/chromium/src/file --chromium-src /path/to/chromium/src",
|
|
)
|
|
@click.option(
|
|
"--string-replace",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Apply string replacements to chromium files",
|
|
)
|
|
@click.option(
|
|
"--patch-interactive",
|
|
"-i",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Ask for confirmation before applying each patch",
|
|
)
|
|
@click.option(
|
|
"--patch-commit",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Create a git commit after applying each patch",
|
|
)
|
|
@click.option(
|
|
"--no-gcs-upload",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Skip uploading artifacts to Google Cloud Storage",
|
|
)
|
|
def main(
|
|
config,
|
|
clean,
|
|
git_setup,
|
|
apply_patches,
|
|
sign,
|
|
arch,
|
|
build_type,
|
|
package,
|
|
build,
|
|
chromium_src,
|
|
slack_notifications,
|
|
merge,
|
|
add_replace,
|
|
string_replace,
|
|
patch_interactive,
|
|
patch_commit,
|
|
no_gcs_upload,
|
|
):
|
|
"""Simple build system for Nxtscape Browser"""
|
|
|
|
# Validate chromium-src for commands that need it
|
|
if add_replace or merge or string_replace or (not config and chromium_src is None):
|
|
if not chromium_src:
|
|
if add_replace:
|
|
log_error("--add-replace requires --chromium-src to be specified")
|
|
log_error(
|
|
"Example: python build.py --add-replace /path/to/chromium/src/chrome/file.cc --chromium-src /path/to/chromium/src"
|
|
)
|
|
elif merge:
|
|
log_error("--merge requires --chromium-src to be specified")
|
|
log_error(
|
|
"Example: python build.py --merge app1.app app2.app --chromium-src /path/to/chromium/src"
|
|
)
|
|
elif string_replace:
|
|
log_error("--string-replace requires --chromium-src to be specified")
|
|
log_error(
|
|
"Example: python build.py --string-replace --chromium-src /path/to/chromium/src"
|
|
)
|
|
else:
|
|
log_error("--chromium-src is required when not using a config file")
|
|
log_error(
|
|
"Example: python build.py --chromium-src /path/to/chromium/src"
|
|
)
|
|
sys.exit(1)
|
|
|
|
# Validate chromium_src path exists
|
|
if not chromium_src.exists():
|
|
log_error(f"Chromium source directory does not exist: {chromium_src}")
|
|
sys.exit(1)
|
|
|
|
# Handle string-replace command
|
|
if string_replace:
|
|
# Get root directory
|
|
root_dir = Path(__file__).parent.parent
|
|
|
|
# Create a minimal context for string replacements
|
|
from context import BuildContext
|
|
|
|
ctx = BuildContext(
|
|
root_dir=root_dir,
|
|
chromium_src=chromium_src,
|
|
architecture="", # Use platform default
|
|
build_type="debug", # Not used for string replacements
|
|
)
|
|
|
|
# Apply string replacements
|
|
if apply_string_replacements(ctx):
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(1)
|
|
|
|
# Handle add-replace command
|
|
if add_replace:
|
|
# Get root directory
|
|
root_dir = Path(__file__).parent.parent
|
|
|
|
# Call the function from chromium_replace module
|
|
if add_file_to_replacements(add_replace, chromium_src, root_dir):
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(1)
|
|
|
|
# Handle merge command
|
|
if merge:
|
|
from modules.merge import handle_merge_command
|
|
|
|
arch1_path, arch2_path = merge
|
|
|
|
if handle_merge_command(arch1_path, arch2_path, chromium_src, sign, package):
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(1)
|
|
|
|
# Regular build workflow
|
|
build_main(
|
|
config_file=config,
|
|
clean_flag=clean,
|
|
git_setup_flag=git_setup,
|
|
apply_patches_flag=apply_patches,
|
|
sign_flag=sign,
|
|
package_flag=package,
|
|
build_flag=build,
|
|
arch=arch or "", # Pass empty string to use platform default
|
|
build_type=build_type,
|
|
chromium_src_dir=chromium_src,
|
|
slack_notifications=slack_notifications,
|
|
patch_interactive=patch_interactive,
|
|
patch_commit=patch_commit,
|
|
upload_gcs=not no_gcs_upload, # Invert the flag
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main.main(standalone_mode=False)
|