mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 15:46:22 +00:00
fix: OTA binary discovery for artifact-extracted structure (#411)
* fix: support artifact-extracted directory structure in OTA binary discovery The download_resources system now extracts server binaries into platform-specific subdirectories (e.g., darwin-arm64/resources/bin/), but the OTA module only looked for flat binary names. This adds find_server_binary() which checks both layouts, keeping backward compatibility with --binaries while supporting the new structure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: download server binaries from R2 instead of requiring --binaries Remove the --binaries flag from `ota server release`. The module now downloads artifact zips from artifacts/server/latest/ in R2, extracts them, then signs and packages as before. This eliminates the need to have mono build output locally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
15
packages/browseros/build/cli/ota.py
generated
15
packages/browseros/build/cli/ota.py
generated
@@ -58,14 +58,11 @@ def execute_module(ctx: Context, module) -> None:
|
||||
@server_app.command("release")
|
||||
def server_release(
|
||||
version: str = typer.Option(
|
||||
..., "--version", "-v", help="Version to release (e.g., 0.0.36)"
|
||||
..., "--version", "-v", help="Version to release (e.g., 0.0.69)"
|
||||
),
|
||||
channel: str = typer.Option(
|
||||
"alpha", "--channel", "-c", help="Release channel: alpha or prod"
|
||||
),
|
||||
binaries: Optional[Path] = typer.Option(
|
||||
None, "--binaries", "-b", help="Directory containing server binaries"
|
||||
),
|
||||
platform: Optional[str] = typer.Option(
|
||||
None, "--platform", "-p",
|
||||
help="Platform(s) to process, comma-separated (darwin_arm64, darwin_x64, linux_arm64, linux_x64, windows_x64)"
|
||||
@@ -73,17 +70,20 @@ def server_release(
|
||||
):
|
||||
"""Release BrowserOS Server OTA update
|
||||
|
||||
Downloads server binaries from R2 (artifacts/server/latest/),
|
||||
signs them, creates Sparkle update packages, and uploads to R2.
|
||||
|
||||
\b
|
||||
Full Release (all platforms):
|
||||
browseros ota server release --version 0.0.36 --channel alpha
|
||||
browseros ota server release --version 0.0.69 --channel alpha
|
||||
|
||||
\b
|
||||
Single Platform:
|
||||
browseros ota server release --version 0.0.36 --platform darwin_arm64
|
||||
browseros ota server release --version 0.0.69 --platform darwin_arm64
|
||||
|
||||
\b
|
||||
Multiple Platforms:
|
||||
browseros ota server release --version 0.0.36 --platform darwin_arm64,darwin_x64
|
||||
browseros ota server release --version 0.0.69 --platform darwin_arm64,darwin_x64
|
||||
"""
|
||||
log_info(f"🚀 BrowserOS Server OTA v{version}")
|
||||
log_info("=" * 70)
|
||||
@@ -93,7 +93,6 @@ def server_release(
|
||||
module = ServerOTAModule(
|
||||
version=version,
|
||||
channel=channel,
|
||||
binaries_dir=binaries,
|
||||
platform_filter=platform,
|
||||
)
|
||||
|
||||
|
||||
2
packages/browseros/build/modules/ota/__init__.py
generated
2
packages/browseros/build/modules/ota/__init__.py
generated
@@ -9,6 +9,7 @@ from .common import (
|
||||
SignedArtifact,
|
||||
SERVER_PLATFORMS,
|
||||
APPCAST_TEMPLATE,
|
||||
find_server_binary,
|
||||
)
|
||||
from .sign_binary import (
|
||||
sign_macos_binary,
|
||||
@@ -29,6 +30,7 @@ __all__ = [
|
||||
"parse_existing_appcast",
|
||||
"ExistingAppcast",
|
||||
"SignedArtifact",
|
||||
"find_server_binary",
|
||||
"sign_macos_binary",
|
||||
"notarize_macos_binary",
|
||||
"sign_windows_binary",
|
||||
|
||||
39
packages/browseros/build/modules/ota/common.py
generated
39
packages/browseros/build/modules/ota/common.py
generated
@@ -21,11 +21,11 @@ SPARKLE_NS = "http://www.andymatuschak.org/xml-namespaces/sparkle"
|
||||
ET.register_namespace("sparkle", SPARKLE_NS)
|
||||
|
||||
SERVER_PLATFORMS = [
|
||||
{"name": "darwin_arm64", "binary": "browseros-server-darwin-arm64", "os": "macos", "arch": "arm64"},
|
||||
{"name": "darwin_x64", "binary": "browseros-server-darwin-x64", "os": "macos", "arch": "x86_64"},
|
||||
{"name": "linux_arm64", "binary": "browseros-server-linux-arm64", "os": "linux", "arch": "arm64"},
|
||||
{"name": "linux_x64", "binary": "browseros-server-linux-x64", "os": "linux", "arch": "x86_64"},
|
||||
{"name": "windows_x64", "binary": "browseros-server-windows-x64.exe", "os": "windows", "arch": "x86_64"},
|
||||
{"name": "darwin_arm64", "binary": "browseros-server-darwin-arm64", "target": "darwin-arm64", "os": "macos", "arch": "arm64"},
|
||||
{"name": "darwin_x64", "binary": "browseros-server-darwin-x64", "target": "darwin-x64", "os": "macos", "arch": "x86_64"},
|
||||
{"name": "linux_arm64", "binary": "browseros-server-linux-arm64", "target": "linux-arm64", "os": "linux", "arch": "arm64"},
|
||||
{"name": "linux_x64", "binary": "browseros-server-linux-x64", "target": "linux-x64", "os": "linux", "arch": "x86_64"},
|
||||
{"name": "windows_x64", "binary": "browseros-server-windows-x64.exe", "target": "windows-x64", "os": "windows", "arch": "x86_64"},
|
||||
]
|
||||
|
||||
APPCAST_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
|
||||
@@ -76,6 +76,35 @@ class ExistingAppcast:
|
||||
artifacts: Dict[str, SignedArtifact]
|
||||
|
||||
|
||||
def find_server_binary(binaries_dir: Path, platform: dict) -> Optional[Path]:
|
||||
"""Find server binary in either flat or artifact-extracted directory structure.
|
||||
|
||||
Supports two layouts:
|
||||
Flat: {binaries_dir}/{binary_name} (e.g., browseros-server-darwin-arm64)
|
||||
Artifact: {binaries_dir}/{target}/resources/bin/browseros_server[.exe]
|
||||
|
||||
Args:
|
||||
binaries_dir: Root directory containing server binaries
|
||||
platform: Platform dict from SERVER_PLATFORMS
|
||||
|
||||
Returns:
|
||||
Path to binary if found, None otherwise
|
||||
"""
|
||||
# Flat structure (used with --binaries pointing to mono build output)
|
||||
flat_path = binaries_dir / platform["binary"]
|
||||
if flat_path.exists():
|
||||
return flat_path
|
||||
|
||||
# Artifact-extracted structure (used after download_resources)
|
||||
target = platform.get("target", platform["name"].replace("_", "-"))
|
||||
bin_name = "browseros_server.exe" if platform["os"] == "windows" else "browseros_server"
|
||||
artifact_path = binaries_dir / target / "resources" / "bin" / bin_name
|
||||
if artifact_path.exists():
|
||||
return artifact_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_existing_appcast(appcast_path: Path) -> Optional[ExistingAppcast]:
|
||||
"""Parse existing appcast XML file.
|
||||
|
||||
|
||||
89
packages/browseros/build/modules/ota/server.py
generated
89
packages/browseros/build/modules/ota/server.py
generated
@@ -25,6 +25,7 @@ from .common import (
|
||||
parse_existing_appcast,
|
||||
create_server_zip,
|
||||
get_appcast_path,
|
||||
find_server_binary,
|
||||
)
|
||||
from .sign_binary import (
|
||||
sign_macos_binary,
|
||||
@@ -32,18 +33,18 @@ from .sign_binary import (
|
||||
sign_windows_binary,
|
||||
get_entitlements_path,
|
||||
)
|
||||
from ..storage import get_r2_client, upload_file_to_r2
|
||||
from ..storage import get_r2_client, upload_file_to_r2, download_file_from_r2
|
||||
from ..storage.download import extract_artifact_zip
|
||||
|
||||
# R2 key pattern for server artifact zips
|
||||
ARTIFACT_R2_KEY = "artifacts/server/latest/browseros-server-resources-{target}.zip"
|
||||
|
||||
|
||||
class ServerOTAModule(CommandModule):
|
||||
"""OTA update module for BrowserOS Server binaries
|
||||
|
||||
This module handles the full OTA workflow:
|
||||
1. Sign individual binaries (codesign for macOS, CodeSignTool for Windows)
|
||||
2. Create zip packages with proper structure
|
||||
3. Sign zips with Sparkle Ed25519
|
||||
4. Upload to R2
|
||||
5. Generate and upload appcast XML
|
||||
Downloads server binaries from R2 (artifacts/server/latest/),
|
||||
signs them, creates Sparkle update zips, and uploads to R2.
|
||||
"""
|
||||
|
||||
produces = ["server_ota_artifacts", "server_appcast"]
|
||||
@@ -54,53 +55,28 @@ class ServerOTAModule(CommandModule):
|
||||
self,
|
||||
version: str = "",
|
||||
channel: str = "alpha",
|
||||
binaries_dir: Optional[Path] = None,
|
||||
platform_filter: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
version: Version string (e.g., "0.0.36")
|
||||
channel: Release channel ("alpha" or "prod")
|
||||
binaries_dir: Directory containing server binaries
|
||||
platform_filter: Platform(s) to process, comma-separated (e.g., "darwin_arm64,darwin_x64")
|
||||
"""
|
||||
self.version = version
|
||||
self.channel = channel
|
||||
self.binaries_dir = binaries_dir
|
||||
self.platform_filter = platform_filter
|
||||
self._download_dir: Optional[Path] = None
|
||||
|
||||
def validate(self, context: Context) -> None:
|
||||
ctx = context
|
||||
if not self.version:
|
||||
raise ValidationError("Version is required")
|
||||
|
||||
if self.channel not in ["alpha", "prod"]:
|
||||
raise ValidationError("Channel must be 'alpha' or 'prod'")
|
||||
|
||||
if self.binaries_dir:
|
||||
if not self.binaries_dir.exists():
|
||||
raise ValidationError(f"Binaries directory not found: {self.binaries_dir}")
|
||||
else:
|
||||
default_dir = ctx.root_dir / "resources" / "binaries" / "browseros_server"
|
||||
if not default_dir.exists():
|
||||
raise ValidationError(f"Default binaries directory not found: {default_dir}")
|
||||
self.binaries_dir = default_dir
|
||||
|
||||
platforms = self._get_platforms()
|
||||
for p in platforms:
|
||||
binary_name = p["binary"]
|
||||
binary_path = self.binaries_dir / binary_name
|
||||
if not binary_path.exists():
|
||||
raise ValidationError(f"Binary not found: {binary_path}")
|
||||
|
||||
if IS_MACOS():
|
||||
if not ctx.env.macos_certificate_name:
|
||||
if not context.env.macos_certificate_name:
|
||||
raise ValidationError("MACOS_CERTIFICATE_NAME required for signing")
|
||||
elif IS_WINDOWS():
|
||||
if not ctx.env.code_sign_tool_path:
|
||||
if not context.env.code_sign_tool_path:
|
||||
raise ValidationError("CODE_SIGN_TOOL_PATH required for signing")
|
||||
|
||||
if not ctx.env.has_r2_config():
|
||||
if not context.env.has_r2_config():
|
||||
raise ValidationError(
|
||||
"R2 configuration not set. Required env vars: "
|
||||
"R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY"
|
||||
@@ -113,11 +89,44 @@ class ServerOTAModule(CommandModule):
|
||||
return [p for p in SERVER_PLATFORMS if p["name"] in requested]
|
||||
return SERVER_PLATFORMS
|
||||
|
||||
def _download_artifacts(self, ctx: Context) -> Path:
|
||||
"""Download server artifact zips from R2 latest/ and extract them."""
|
||||
download_dir = Path(tempfile.mkdtemp(prefix="ota_artifacts_"))
|
||||
self._download_dir = download_dir
|
||||
|
||||
r2_client = get_r2_client(ctx.env)
|
||||
if not r2_client:
|
||||
raise RuntimeError("Failed to create R2 client")
|
||||
|
||||
bucket = ctx.env.r2_bucket
|
||||
platforms = self._get_platforms()
|
||||
|
||||
log_info("📥 Downloading server artifacts from R2...")
|
||||
|
||||
for platform in platforms:
|
||||
target = platform["target"]
|
||||
r2_key = ARTIFACT_R2_KEY.format(target=target)
|
||||
zip_path = download_dir / f"{target}.zip"
|
||||
extract_dir = download_dir / target
|
||||
|
||||
log_info(f" Downloading {target}...")
|
||||
if not download_file_from_r2(r2_client, r2_key, zip_path, bucket):
|
||||
raise RuntimeError(f"Failed to download artifact: {r2_key}")
|
||||
|
||||
extract_artifact_zip(zip_path, extract_dir)
|
||||
zip_path.unlink()
|
||||
|
||||
log_success(f"Downloaded {len(platforms)} artifact(s)")
|
||||
return download_dir
|
||||
|
||||
def execute(self, context: Context) -> None:
|
||||
ctx = context
|
||||
log_info(f"\n🚀 BrowserOS Server OTA v{self.version} ({self.channel})")
|
||||
log_info("=" * 70)
|
||||
|
||||
# Download artifacts from R2
|
||||
binaries_dir = self._download_artifacts(ctx)
|
||||
|
||||
platforms = self._get_platforms()
|
||||
temp_dir = Path(tempfile.mkdtemp())
|
||||
log_info(f"Temp directory: {temp_dir}")
|
||||
@@ -127,11 +136,13 @@ class ServerOTAModule(CommandModule):
|
||||
for platform in platforms:
|
||||
log_info(f"\n📦 Processing {platform['name']}...")
|
||||
|
||||
binary_name = platform["binary"]
|
||||
source_binary = self.binaries_dir / binary_name
|
||||
source_binary = find_server_binary(binaries_dir, platform)
|
||||
if not source_binary:
|
||||
log_warning(f"Binary not found for {platform['name']}, skipping")
|
||||
continue
|
||||
|
||||
# Copy binary to temp to preserve original
|
||||
temp_binary = temp_dir / binary_name
|
||||
temp_binary = temp_dir / platform["binary"]
|
||||
shutil.copy2(source_binary, temp_binary)
|
||||
|
||||
if not self._sign_binary(temp_binary, platform, ctx):
|
||||
|
||||
Reference in New Issue
Block a user