mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
395 lines
13 KiB
Python
395 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Windows packaging module for Nxtscape Browser
|
|
Based on ungoogled-chromium-windows packaging approach
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Optional, List
|
|
from context import BuildContext
|
|
from utils import (
|
|
run_command,
|
|
log_info,
|
|
log_error,
|
|
log_success,
|
|
log_warning,
|
|
join_paths,
|
|
IS_WINDOWS,
|
|
)
|
|
|
|
|
|
def package(ctx: BuildContext) -> bool:
|
|
"""Create Windows packages (installer and portable zip)"""
|
|
log_info("\n📦 Creating Windows packages...")
|
|
|
|
# First, ensure mini_installer is built
|
|
if not build_mini_installer(ctx):
|
|
log_error("Failed to build mini_installer")
|
|
return False
|
|
|
|
# Create both installer and portable zip
|
|
success = True
|
|
|
|
if create_installer(ctx):
|
|
log_success("Installer created successfully")
|
|
else:
|
|
log_error("Failed to create installer")
|
|
success = False
|
|
|
|
if create_portable_zip(ctx):
|
|
log_success("Portable ZIP created successfully")
|
|
else:
|
|
log_error("Failed to create portable ZIP")
|
|
success = False
|
|
|
|
return success
|
|
|
|
|
|
def build_mini_installer(ctx: BuildContext) -> bool:
|
|
"""Build the mini_installer target if it doesn't exist"""
|
|
log_info("\n🔨 Checking mini_installer build...")
|
|
|
|
# Get paths
|
|
build_output_dir = join_paths(ctx.chromium_src, ctx.out_dir)
|
|
mini_installer_path = build_output_dir / "mini_installer.exe"
|
|
|
|
if mini_installer_path.exists():
|
|
log_info("mini_installer.exe already exists")
|
|
return True
|
|
|
|
log_info("Building mini_installer target...")
|
|
|
|
# Build mini_installer using autoninja
|
|
try:
|
|
# Use autoninja.bat on Windows
|
|
autoninja_cmd = "autoninja.bat" if IS_WINDOWS else "autoninja"
|
|
|
|
# Build the mini_installer target
|
|
cmd = [
|
|
autoninja_cmd,
|
|
"-C",
|
|
ctx.out_dir, # Use relative path like in compile.py
|
|
"mini_installer",
|
|
]
|
|
|
|
# Change to chromium_src directory before running (like compile.py does)
|
|
import os
|
|
|
|
old_cwd = os.getcwd()
|
|
os.chdir(ctx.chromium_src)
|
|
|
|
try:
|
|
run_command(cmd)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
# Verify the file was created
|
|
if mini_installer_path.exists():
|
|
log_success("mini_installer built successfully")
|
|
return True
|
|
else:
|
|
log_error("mini_installer build completed but file not found")
|
|
return False
|
|
|
|
except Exception as e:
|
|
log_error(f"Failed to build mini_installer: {e}")
|
|
return False
|
|
|
|
|
|
def create_installer(ctx: BuildContext) -> bool:
|
|
"""Create Windows installer (mini_installer.exe)"""
|
|
log_info("\n🔧 Creating Windows installer...")
|
|
|
|
# Get paths
|
|
build_output_dir = join_paths(ctx.chromium_src, ctx.out_dir)
|
|
mini_installer_path = build_output_dir / "mini_installer.exe"
|
|
|
|
if not mini_installer_path.exists():
|
|
log_warning(f"mini_installer.exe not found at: {mini_installer_path}")
|
|
log_info(
|
|
"To build the installer, run: autoninja -C out\\Default_x64 mini_installer"
|
|
)
|
|
return False
|
|
|
|
# Create output directory
|
|
output_dir = ctx.get_dist_dir()
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Generate installer filename with version and architecture
|
|
installer_name = f"{ctx.get_app_base_name()}_{ctx.get_nxtscape_chromium_version()}_{ctx.architecture}_installer.exe"
|
|
installer_path = output_dir / installer_name
|
|
|
|
# Copy mini_installer to final location
|
|
try:
|
|
shutil.copy2(mini_installer_path, installer_path)
|
|
log_success(f"Installer created: {installer_name}")
|
|
return True
|
|
except Exception as e:
|
|
log_error(f"Failed to create installer: {e}")
|
|
return False
|
|
|
|
|
|
def create_portable_zip(ctx: BuildContext) -> bool:
|
|
"""Create ZIP of just the installer for easier distribution"""
|
|
log_info("\n📦 Creating installer ZIP package...")
|
|
|
|
# Get paths
|
|
build_output_dir = join_paths(ctx.chromium_src, ctx.out_dir)
|
|
mini_installer_path = build_output_dir / "mini_installer.exe"
|
|
|
|
if not mini_installer_path.exists():
|
|
log_warning(f"mini_installer.exe not found at: {mini_installer_path}")
|
|
log_info(
|
|
"To build the installer, run: autoninja -C out\\Default_x64 mini_installer"
|
|
)
|
|
return False
|
|
|
|
# Create output directory
|
|
output_dir = ctx.get_dist_dir()
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Generate ZIP filename with version and architecture
|
|
zip_name = f"{ctx.get_app_base_name()}_{ctx.get_nxtscape_chromium_version()}_{ctx.architecture}_installer.zip"
|
|
zip_path = output_dir / zip_name
|
|
|
|
# Create ZIP file containing just the installer
|
|
try:
|
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
# Add mini_installer.exe to the zip
|
|
installer_name = f"{ctx.get_app_base_name()}_{ctx.get_nxtscape_version()}_{ctx.architecture}_installer.exe"
|
|
zipf.write(mini_installer_path, installer_name)
|
|
|
|
# Get file size for logging
|
|
file_size = mini_installer_path.stat().st_size
|
|
log_info(f"Added installer to ZIP ({file_size // (1024*1024)} MB)")
|
|
|
|
log_success(f"Installer ZIP created: {zip_name}")
|
|
return True
|
|
except Exception as e:
|
|
log_error(f"Failed to create installer ZIP: {e}")
|
|
return False
|
|
|
|
|
|
def sign_binaries(ctx: BuildContext, certificate_name: Optional[str] = None) -> bool:
|
|
"""Sign Windows binaries using SSL.com CodeSignTool"""
|
|
log_info("\n🔏 Signing Windows binaries...")
|
|
|
|
# Get paths to sign
|
|
build_output_dir = join_paths(ctx.chromium_src, ctx.out_dir)
|
|
|
|
# List of binaries to sign
|
|
binaries_to_sign = [
|
|
build_output_dir / "chrome.exe",
|
|
build_output_dir / "mini_installer.exe",
|
|
]
|
|
|
|
# Check which binaries exist
|
|
existing_binaries = []
|
|
for binary in binaries_to_sign:
|
|
if binary.exists():
|
|
existing_binaries.append(binary)
|
|
log_info(f"Found binary to sign: {binary.name}")
|
|
else:
|
|
log_warning(f"Binary not found: {binary}")
|
|
|
|
if not existing_binaries:
|
|
log_error("No binaries found to sign")
|
|
return False
|
|
|
|
# Always use CodeSignTool for signing
|
|
return sign_with_codesigntool(existing_binaries)
|
|
|
|
|
|
def sign_with_codesigntool(binaries: List[Path]) -> bool:
|
|
"""Sign binaries using SSL.com CodeSignTool"""
|
|
log_info("Using SSL.com CodeSignTool for signing...")
|
|
|
|
# Get CodeSignTool directory from environment
|
|
codesigntool_dir = os.environ.get("CODE_SIGN_TOOL_PATH")
|
|
if not codesigntool_dir:
|
|
log_error("CODE_SIGN_TOOL_PATH not set in .env file")
|
|
log_error("Set CODE_SIGN_TOOL_PATH=C:/src/CodeSignTool-v1.3.2-windows")
|
|
return False
|
|
|
|
# Construct path to CodeSignTool.bat
|
|
codesigntool_path = Path(codesigntool_dir) / "CodeSignTool.bat"
|
|
if not codesigntool_path.exists():
|
|
log_error(f"CodeSignTool.bat not found at: {codesigntool_path}")
|
|
log_error(f"Make sure CODE_SIGN_TOOL_PATH points to the CodeSignTool directory")
|
|
return False
|
|
|
|
# Check for required environment variables
|
|
username = os.environ.get("ESIGNER_USERNAME")
|
|
password = os.environ.get("ESIGNER_PASSWORD")
|
|
totp_secret = os.environ.get("ESIGNER_TOTP_SECRET")
|
|
credential_id = os.environ.get("ESIGNER_CREDENTIAL_ID")
|
|
|
|
if not all([username, password, totp_secret]):
|
|
log_error("Missing required eSigner environment variables in .env:")
|
|
log_error(" ESIGNER_USERNAME=your-email")
|
|
log_error(" ESIGNER_PASSWORD=your-password")
|
|
log_error(" ESIGNER_TOTP_SECRET=your-totp-secret")
|
|
if not credential_id:
|
|
log_warning(" ESIGNER_CREDENTIAL_ID is recommended but optional")
|
|
return False
|
|
|
|
all_success = True
|
|
for binary in binaries:
|
|
try:
|
|
log_info(f"Signing {binary.name}...")
|
|
|
|
# Build command
|
|
# Create a temp output directory to avoid source/dest conflict
|
|
temp_output_dir = binary.parent / "signed_temp"
|
|
temp_output_dir.mkdir(exist_ok=True)
|
|
|
|
cmd = [
|
|
str(codesigntool_path),
|
|
"sign",
|
|
"-username",
|
|
username,
|
|
"-password",
|
|
f'"{password}"', # Always quote the password for shell
|
|
]
|
|
|
|
# Add credential_id BEFORE totp_secret (order matters!)
|
|
if credential_id:
|
|
cmd.extend(["-credential_id", credential_id])
|
|
|
|
cmd.extend(
|
|
[
|
|
"-totp_secret",
|
|
totp_secret,
|
|
"-input_file_path",
|
|
str(binary),
|
|
"-output_dir_path",
|
|
str(temp_output_dir),
|
|
"-override", # Add this back
|
|
]
|
|
)
|
|
|
|
# Note: Timestamp server is configured on SSL.com side automatically
|
|
|
|
# CodeSignTool needs to be run as a shell command for proper quote handling
|
|
cmd_str = " ".join(cmd)
|
|
log_info(f"Running: {cmd_str}")
|
|
|
|
import subprocess
|
|
|
|
result = subprocess.run(
|
|
cmd_str,
|
|
shell=True,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=str(codesigntool_path.parent),
|
|
)
|
|
|
|
# Print output for debugging
|
|
if result.stdout:
|
|
for line in result.stdout.split("\n"):
|
|
if line.strip():
|
|
log_info(line.strip())
|
|
if result.stderr:
|
|
for line in result.stderr.split("\n"):
|
|
if line.strip() and "WARNING" not in line:
|
|
log_error(line.strip())
|
|
|
|
# Check if signing actually succeeded by looking for error messages
|
|
# CodeSignTool returns 0 even on auth errors, so we need to check output
|
|
if result.stdout and "Error:" in result.stdout:
|
|
log_error(
|
|
f"✗ Failed to sign {binary.name} - Authentication or signing error"
|
|
)
|
|
all_success = False
|
|
continue
|
|
|
|
# Move the signed file back to original location
|
|
signed_file = temp_output_dir / binary.name
|
|
if signed_file.exists():
|
|
import shutil
|
|
|
|
shutil.move(str(signed_file), str(binary))
|
|
log_info(f"Moved signed {binary.name} to original location")
|
|
|
|
# Clean up temp directory
|
|
try:
|
|
temp_output_dir.rmdir()
|
|
except:
|
|
pass # Directory might not be empty
|
|
|
|
# Verify the file is actually signed (Windows only)
|
|
verify_cmd = [
|
|
"powershell",
|
|
"-Command",
|
|
f"(Get-AuthenticodeSignature '{binary}').Status",
|
|
]
|
|
try:
|
|
import subprocess
|
|
|
|
verify_result = subprocess.run(
|
|
verify_cmd, capture_output=True, text=True
|
|
)
|
|
if "Valid" in verify_result.stdout:
|
|
log_success(f"✓ {binary.name} signed and verified successfully")
|
|
else:
|
|
log_error(
|
|
f"✗ {binary.name} signing verification failed - Status: {verify_result.stdout.strip()}"
|
|
)
|
|
all_success = False
|
|
except:
|
|
log_warning(f"Could not verify signature for {binary.name}")
|
|
|
|
except Exception as e:
|
|
log_error(f"Failed to sign {binary.name}: {e}")
|
|
all_success = False
|
|
|
|
return all_success
|
|
|
|
|
|
def package_universal(contexts: List[BuildContext]) -> bool:
|
|
"""Windows doesn't support universal binaries like macOS"""
|
|
log_warning("Universal binaries are not supported on Windows")
|
|
log_info("Consider creating separate packages for each architecture")
|
|
return True
|
|
|
|
|
|
def get_target_cpu(build_output_dir: Path) -> str:
|
|
"""Get target CPU architecture from build configuration"""
|
|
args_gn_path = build_output_dir / "args.gn"
|
|
|
|
if not args_gn_path.exists():
|
|
return "x64" # Default
|
|
|
|
try:
|
|
args_gn_content = args_gn_path.read_text(encoding="utf-8")
|
|
for cpu in ("x64", "x86", "arm64"):
|
|
if f'target_cpu="{cpu}"' in args_gn_content:
|
|
return cpu
|
|
except Exception:
|
|
pass
|
|
|
|
return "x64" # Default
|
|
|
|
|
|
def create_files_cfg_package(ctx: BuildContext) -> bool:
|
|
"""Create package using Chromium's FILES.cfg approach (alternative method)"""
|
|
log_info("\n📦 Creating FILES.cfg-based package...")
|
|
|
|
build_output_dir = join_paths(ctx.chromium_src, ctx.out_dir)
|
|
files_cfg_path = (
|
|
ctx.chromium_src / "chrome" / "tools" / "build" / "win" / "FILES.cfg"
|
|
)
|
|
|
|
if not files_cfg_path.exists():
|
|
log_error(f"FILES.cfg not found at: {files_cfg_path}")
|
|
return False
|
|
|
|
# This would require implementing the filescfg module functionality
|
|
# from ungoogled-chromium, which is quite complex
|
|
log_warning("FILES.cfg packaging not yet implemented")
|
|
return False
|