mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 15:46:22 +00:00
feat: add linux arm64 release support (#651)
* feat: support linux arm64 release artifacts * fix: address PR review comments for 0406-linux_arm64_support
This commit is contained in:
@@ -3,7 +3,10 @@
|
||||
# This config packages an already-built Linux application.
|
||||
# Use this when you have a pre-built app and only need to package it.
|
||||
#
|
||||
# Expects: out/Default/chrome (Linux binary)
|
||||
# Expects: out/Default_<arch>/browseros
|
||||
# Invoke with:
|
||||
# browseros build --config build/config/package.linux.yaml --arch x64
|
||||
# browseros build --config build/config/package.linux.yaml --arch arm64
|
||||
#
|
||||
# Environment Variables:
|
||||
# Use !env tag to reference environment variables:
|
||||
@@ -11,7 +14,6 @@
|
||||
|
||||
build:
|
||||
type: release
|
||||
architecture: x64 # Linux x64
|
||||
|
||||
gn_flags:
|
||||
file: build/config/gn/flags.linux.release.gn
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# BrowserOS Linux Release Build Configuration
|
||||
#
|
||||
# Run this config once per target architecture:
|
||||
# browseros build --config build/config/release.linux.yaml --arch x64
|
||||
# browseros build --config build/config/release.linux.yaml --arch arm64
|
||||
#
|
||||
# Environment Variables:
|
||||
# Use !env tag to reference environment variables:
|
||||
# Example: chromium_src: !env CHROMIUM_SRC
|
||||
|
||||
build:
|
||||
type: release
|
||||
architecture: x64 # Linux x64
|
||||
|
||||
gn_flags:
|
||||
file: build/config/gn/flags.linux.release.gn
|
||||
|
||||
51
packages/browseros/build/modules/package/linux.py
generated
51
packages/browseros/build/modules/package/linux.py
generated
@@ -21,6 +21,31 @@ from ...common.utils import (
|
||||
)
|
||||
from ...common.notify import get_notifier, COLOR_GREEN
|
||||
|
||||
LINUX_ARCHITECTURE_CONFIG = {
|
||||
"x64": {
|
||||
"appimage_tool": "appimagetool-x86_64.AppImage",
|
||||
"appimage_url": "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage",
|
||||
"appimage_arch": "x86_64",
|
||||
"deb_arch": "amd64",
|
||||
},
|
||||
"arm64": {
|
||||
"appimage_tool": "appimagetool-aarch64.AppImage",
|
||||
"appimage_url": "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage",
|
||||
"appimage_arch": "aarch64",
|
||||
"deb_arch": "arm64",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_linux_architecture_config(architecture: str) -> dict[str, str]:
|
||||
config = LINUX_ARCHITECTURE_CONFIG.get(architecture)
|
||||
if not config:
|
||||
supported = ", ".join(sorted(LINUX_ARCHITECTURE_CONFIG))
|
||||
raise ValueError(
|
||||
f"Unsupported Linux architecture: {architecture}. Supported: {supported}"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
class LinuxPackageModule(CommandModule):
|
||||
produces = ["appimage", "deb"]
|
||||
@@ -30,6 +55,10 @@ class LinuxPackageModule(CommandModule):
|
||||
def validate(self, ctx: Context) -> None:
|
||||
if not IS_LINUX():
|
||||
raise ValidationError("Linux packaging requires Linux")
|
||||
try:
|
||||
get_linux_architecture_config(ctx.architecture)
|
||||
except ValueError as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
|
||||
out_dir = join_paths(ctx.chromium_src, ctx.out_dir)
|
||||
chrome_binary = join_paths(out_dir, ctx.BROWSEROS_APP_NAME)
|
||||
@@ -73,7 +102,7 @@ class LinuxPackageModule(CommandModule):
|
||||
artifacts.append(deb_path.name)
|
||||
notifier.notify(
|
||||
"📦 Package Created",
|
||||
f"Linux packages created successfully",
|
||||
"Linux packages created successfully",
|
||||
{
|
||||
"Artifacts": ", ".join(artifacts),
|
||||
"Version": ctx.semantic_version,
|
||||
@@ -287,15 +316,16 @@ def download_appimagetool(ctx: Context) -> Optional[Path]:
|
||||
"""Download appimagetool if not available"""
|
||||
tool_dir = Path(join_paths(ctx.root_dir, "build", "tools"))
|
||||
tool_dir.mkdir(exist_ok=True)
|
||||
arch_config = get_linux_architecture_config(ctx.architecture)
|
||||
|
||||
tool_path = Path(join_paths(tool_dir, "appimagetool-x86_64.AppImage"))
|
||||
tool_path = Path(join_paths(tool_dir, arch_config["appimage_tool"]))
|
||||
|
||||
if tool_path.exists():
|
||||
log_info("✓ appimagetool already available")
|
||||
return tool_path
|
||||
|
||||
log_info("📥 Downloading appimagetool...")
|
||||
url = "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
url = arch_config["appimage_url"]
|
||||
|
||||
cmd = ["wget", "-O", str(tool_path), url]
|
||||
result = run_command(cmd, check=False)
|
||||
@@ -312,6 +342,7 @@ def download_appimagetool(ctx: Context) -> Optional[Path]:
|
||||
def create_appimage(ctx: Context, appdir: Path, output_path: Path) -> bool:
|
||||
"""Create AppImage from AppDir"""
|
||||
log_info("📦 Creating AppImage...")
|
||||
arch_config = get_linux_architecture_config(ctx.architecture)
|
||||
|
||||
# Download appimagetool if needed
|
||||
appimagetool = download_appimagetool(ctx)
|
||||
@@ -319,7 +350,7 @@ def create_appimage(ctx: Context, appdir: Path, output_path: Path) -> bool:
|
||||
return False
|
||||
|
||||
# Set architecture environment variable (required by appimagetool)
|
||||
arch = "x86_64" if ctx.architecture == "x64" else "aarch64"
|
||||
arch = arch_config["appimage_arch"]
|
||||
|
||||
# Create AppImage with ARCH env var set for this command only
|
||||
cmd = [
|
||||
@@ -384,7 +415,7 @@ def create_control_file(ctx: Context, debian_dir: Path) -> None:
|
||||
version = version.lstrip("v").replace(" ", "").replace("_", ".")
|
||||
|
||||
# Architecture mapping
|
||||
deb_arch = "amd64" if ctx.architecture == "x64" else "arm64"
|
||||
deb_arch = get_linux_architecture_config(ctx.architecture)["deb_arch"]
|
||||
|
||||
control_content = f"""Package: browseros
|
||||
Version: {version}
|
||||
@@ -653,7 +684,9 @@ def package_appimage(ctx: Context, package_dir: Path) -> Optional[Path]:
|
||||
"""
|
||||
log_info("🖼️ Building AppImage...")
|
||||
|
||||
appdir = Path(join_paths(package_dir, f"{ctx.BROWSEROS_APP_BASE_NAME}.AppDir"))
|
||||
appdir = Path(
|
||||
join_paths(package_dir, f"{ctx.BROWSEROS_APP_BASE_NAME}-{ctx.architecture}.AppDir")
|
||||
)
|
||||
if appdir.exists():
|
||||
safe_rmtree(appdir)
|
||||
|
||||
@@ -683,7 +716,9 @@ def package_deb(ctx: Context, package_dir: Path) -> Optional[Path]:
|
||||
"""
|
||||
log_info("📦 Building .deb package...")
|
||||
|
||||
debdir = Path(join_paths(package_dir, f"{ctx.BROWSEROS_APP_BASE_NAME}_deb"))
|
||||
debdir = Path(
|
||||
join_paths(package_dir, f"{ctx.BROWSEROS_APP_BASE_NAME}_{ctx.architecture}_deb")
|
||||
)
|
||||
if debdir.exists():
|
||||
safe_rmtree(debdir)
|
||||
|
||||
@@ -703,6 +738,8 @@ def package_deb(ctx: Context, package_dir: Path) -> Optional[Path]:
|
||||
return output_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def package_universal(contexts: List[Context]) -> bool:
|
||||
"""Linux doesn't support universal binaries"""
|
||||
log_warning("Universal binaries are not supported on Linux")
|
||||
|
||||
30
packages/browseros/build/modules/package/linux_test.py
generated
Normal file
30
packages/browseros/build/modules/package/linux_test.py
generated
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for Linux packaging architecture helpers."""
|
||||
|
||||
import unittest
|
||||
|
||||
from build.modules.package.linux import get_linux_architecture_config
|
||||
|
||||
|
||||
class LinuxArchitectureConfigTest(unittest.TestCase):
|
||||
def test_returns_x64_packaging_config(self) -> None:
|
||||
config = get_linux_architecture_config("x64")
|
||||
|
||||
self.assertEqual(config["appimage_tool"], "appimagetool-x86_64.AppImage")
|
||||
self.assertEqual(config["appimage_arch"], "x86_64")
|
||||
self.assertEqual(config["deb_arch"], "amd64")
|
||||
|
||||
def test_returns_arm64_packaging_config(self) -> None:
|
||||
config = get_linux_architecture_config("arm64")
|
||||
|
||||
self.assertEqual(config["appimage_tool"], "appimagetool-aarch64.AppImage")
|
||||
self.assertEqual(config["appimage_arch"], "aarch64")
|
||||
self.assertEqual(config["deb_arch"], "arm64")
|
||||
|
||||
def test_rejects_unsupported_architecture(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "Unsupported Linux architecture"):
|
||||
get_linux_architecture_config("universal")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -6,7 +6,6 @@ from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ...common.env import EnvConfig
|
||||
from ...common.utils import log_warning
|
||||
from ..storage import get_release_json, get_r2_client, BOTO3_AVAILABLE
|
||||
|
||||
PLATFORMS = ["macos", "win", "linux"]
|
||||
@@ -24,6 +23,8 @@ DOWNLOAD_PATH_MAPPING = {
|
||||
"linux": {
|
||||
"x64_appimage": "download/BrowserOS.AppImage",
|
||||
"x64_deb": "download/BrowserOS.deb",
|
||||
"arm64_appimage": "download/BrowserOS-arm64.AppImage",
|
||||
"arm64_deb": "download/BrowserOS-arm64.deb",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
57
packages/browseros/build/modules/storage/upload.py
generated
57
packages/browseros/build/modules/storage/upload.py
generated
@@ -4,7 +4,7 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple, cast
|
||||
|
||||
from ...common.module import CommandModule, ValidationError
|
||||
from ...common.context import Context
|
||||
@@ -21,6 +21,7 @@ from ...common.notify import get_notifier, COLOR_GREEN
|
||||
from .r2 import (
|
||||
BOTO3_AVAILABLE,
|
||||
get_r2_client,
|
||||
get_release_json,
|
||||
upload_file_to_r2,
|
||||
)
|
||||
|
||||
@@ -58,7 +59,10 @@ class UploadModule(CommandModule):
|
||||
log_info("\nUploading package artifacts to R2...")
|
||||
|
||||
extra_metadata = {}
|
||||
sparkle_signatures = ctx.artifacts.get("sparkle_signatures")
|
||||
sparkle_signatures = cast(
|
||||
Optional[dict[str, tuple[str, int]]],
|
||||
ctx.artifacts.get("sparkle_signatures"),
|
||||
)
|
||||
if sparkle_signatures:
|
||||
for filename, (sig, length) in sparkle_signatures.items():
|
||||
extra_metadata[filename] = {
|
||||
@@ -120,6 +124,36 @@ def generate_release_json(
|
||||
return release_data
|
||||
|
||||
|
||||
def merge_release_metadata(existing: Optional[Dict], new: Dict) -> Dict:
|
||||
if not existing:
|
||||
return new
|
||||
|
||||
merged = dict(existing)
|
||||
merged.update({key: value for key, value in new.items() if key != "artifacts"})
|
||||
|
||||
artifacts = dict(existing.get("artifacts", {}))
|
||||
artifacts.update(new.get("artifacts", {}))
|
||||
merged["artifacts"] = artifacts
|
||||
return merged
|
||||
|
||||
|
||||
def _get_linux_artifact_key(filename: str) -> Optional[str]:
|
||||
lower = filename.lower()
|
||||
|
||||
if ".appimage" in lower:
|
||||
if "arm64" in lower or "aarch64" in lower:
|
||||
return "arm64_appimage"
|
||||
if "x64" in lower or "x86_64" in lower:
|
||||
return "x64_appimage"
|
||||
elif ".deb" in lower:
|
||||
if "arm64" in lower or "aarch64" in lower:
|
||||
return "arm64_deb"
|
||||
if "amd64" in lower or "x64" in lower or "x86_64" in lower:
|
||||
return "x64_deb"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_artifact_key(filename: str, platform: str) -> str:
|
||||
"""Get artifact key name from filename
|
||||
|
||||
@@ -147,10 +181,10 @@ def _get_artifact_key(filename: str, platform: str) -> str:
|
||||
return "x64_zip"
|
||||
|
||||
elif platform == "linux":
|
||||
if ".appimage" in lower:
|
||||
return "x64_appimage"
|
||||
elif ".deb" in lower:
|
||||
return "x64_deb"
|
||||
artifact_key = _get_linux_artifact_key(filename)
|
||||
if artifact_key:
|
||||
return artifact_key
|
||||
log_warning(f"Unrecognized Linux artifact name: {filename}; using stem key")
|
||||
|
||||
return Path(filename).stem
|
||||
|
||||
@@ -181,7 +215,7 @@ def detect_artifacts(ctx: Context) -> List[Path]:
|
||||
|
||||
def upload_release_artifacts(
|
||||
ctx: Context,
|
||||
extra_metadata: Optional[Dict[str, Dict[str, any]]] = None,
|
||||
extra_metadata: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
) -> Tuple[bool, Optional[Dict]]:
|
||||
"""Upload release artifacts to R2 and generate release.json
|
||||
|
||||
@@ -240,6 +274,13 @@ def upload_release_artifacts(
|
||||
artifact_metadata.append(metadata)
|
||||
|
||||
release_data = generate_release_json(ctx, artifact_metadata, platform)
|
||||
if platform == "linux":
|
||||
# Linux x64 and arm64 release jobs must be sequenced. A parallel
|
||||
# fetch-merge-upload flow can still race and drop one architecture.
|
||||
existing_release_data = get_release_json(
|
||||
ctx.get_semantic_version(), platform, env
|
||||
)
|
||||
release_data = merge_release_metadata(existing_release_data, release_data)
|
||||
release_json_path = ctx.get_dist_dir() / "release.json"
|
||||
release_json_path.write_text(json.dumps(release_data, indent=2))
|
||||
|
||||
@@ -248,7 +289,7 @@ def upload_release_artifacts(
|
||||
return False, None
|
||||
|
||||
log_success(f"\nSuccessfully uploaded {len(artifacts)} artifact(s) to R2")
|
||||
log_info(f"\nRelease metadata:")
|
||||
log_info("\nRelease metadata:")
|
||||
log_info(f" Version: {release_data['version']}")
|
||||
if platform == "macos":
|
||||
log_info(f" Sparkle version: {release_data.get('sparkle_version', 'N/A')}")
|
||||
|
||||
85
packages/browseros/build/modules/storage/upload_test.py
generated
Normal file
85
packages/browseros/build/modules/storage/upload_test.py
generated
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for release artifact upload metadata helpers."""
|
||||
|
||||
import unittest
|
||||
|
||||
from build.modules.storage.upload import _get_artifact_key, merge_release_metadata
|
||||
|
||||
|
||||
class UploadMetadataTest(unittest.TestCase):
|
||||
def test_linux_x64_artifacts_use_x64_keys(self) -> None:
|
||||
self.assertEqual(
|
||||
_get_artifact_key("BrowserOS_v1.2.3_x64.AppImage", "linux"),
|
||||
"x64_appimage",
|
||||
)
|
||||
self.assertEqual(
|
||||
_get_artifact_key("BrowserOS_v1.2.3_amd64.deb", "linux"),
|
||||
"x64_deb",
|
||||
)
|
||||
|
||||
def test_linux_arm64_artifacts_use_arm64_keys(self) -> None:
|
||||
self.assertEqual(
|
||||
_get_artifact_key("BrowserOS_v1.2.3_arm64.AppImage", "linux"),
|
||||
"arm64_appimage",
|
||||
)
|
||||
self.assertEqual(
|
||||
_get_artifact_key("BrowserOS_v1.2.3_arm64.deb", "linux"),
|
||||
"arm64_deb",
|
||||
)
|
||||
self.assertEqual(
|
||||
_get_artifact_key("BrowserOS_v1.2.3_aarch64.deb", "linux"),
|
||||
"arm64_deb",
|
||||
)
|
||||
|
||||
def test_merge_release_metadata_preserves_existing_artifacts(self) -> None:
|
||||
existing = {
|
||||
"platform": "linux",
|
||||
"version": "1.2.3",
|
||||
"build_date": "old",
|
||||
"artifacts": {
|
||||
"x64_appimage": {"filename": "BrowserOS_v1.2.3_x64.AppImage"},
|
||||
"x64_deb": {"filename": "BrowserOS_v1.2.3_amd64.deb"},
|
||||
},
|
||||
}
|
||||
new = {
|
||||
"platform": "linux",
|
||||
"version": "1.2.3",
|
||||
"build_date": "new",
|
||||
"artifacts": {
|
||||
"arm64_appimage": {"filename": "BrowserOS_v1.2.3_arm64.AppImage"},
|
||||
"arm64_deb": {"filename": "BrowserOS_v1.2.3_arm64.deb"},
|
||||
},
|
||||
}
|
||||
|
||||
merged = merge_release_metadata(existing, new)
|
||||
|
||||
self.assertEqual(merged["build_date"], "new")
|
||||
self.assertEqual(
|
||||
sorted(merged["artifacts"]),
|
||||
["arm64_appimage", "arm64_deb", "x64_appimage", "x64_deb"],
|
||||
)
|
||||
|
||||
def test_merge_release_metadata_overwrites_matching_artifact_keys(self) -> None:
|
||||
existing = {
|
||||
"platform": "linux",
|
||||
"version": "1.2.3",
|
||||
"artifacts": {
|
||||
"x64_appimage": {"filename": "old.AppImage", "size": 1},
|
||||
},
|
||||
}
|
||||
new = {
|
||||
"platform": "linux",
|
||||
"version": "1.2.3",
|
||||
"artifacts": {
|
||||
"x64_appimage": {"filename": "new.AppImage", "size": 2},
|
||||
},
|
||||
}
|
||||
|
||||
merged = merge_release_metadata(existing, new)
|
||||
|
||||
self.assertEqual(merged["artifacts"]["x64_appimage"]["filename"], "new.AppImage")
|
||||
self.assertEqual(merged["artifacts"]["x64_appimage"]["size"], 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user