feat: download latest bundled BrowserOS server resources (#401)

* feat: download latest bundled BrowserOS server resources

* fix: guard artifact metadata root type
This commit is contained in:
Nikhil
2026-03-06 09:30:15 -08:00
committed by GitHub
parent 6ecf7ca3e4
commit 7759d6769f
4 changed files with 379 additions and 142 deletions

View File

@@ -88,80 +88,38 @@ copy_operations:
destination: "chrome/app/theme/default_200_percent/chromium"
type: "directory"
# BrowserOS Server Binary - Platform & Architecture specific
- name: "BrowserOS Server Binary - macOS ARM64"
source: "resources/binaries/browseros_server/browseros-server-darwin-arm64"
destination: "chrome/browser/browseros/server/resources/bin/browseros_server"
type: "file"
# BrowserOS Server resources - Platform & Architecture specific
- name: "BrowserOS Server Resources - macOS ARM64"
source: "resources/binaries/browseros_server/darwin-arm64/resources"
destination: "chrome/browser/browseros/server/resources"
type: "directory"
os: ["macos"]
arch: ["arm64"]
- name: "BrowserOS Server Binary - macOS x64"
source: "resources/binaries/browseros_server/browseros-server-darwin-x64"
destination: "chrome/browser/browseros/server/resources/bin/browseros_server"
type: "file"
- name: "BrowserOS Server Resources - macOS x64"
source: "resources/binaries/browseros_server/darwin-x64/resources"
destination: "chrome/browser/browseros/server/resources"
type: "directory"
os: ["macos"]
arch: ["x64"]
- name: "BrowserOS Server Binary - Linux ARM64"
source: "resources/binaries/browseros_server/browseros-server-linux-arm64"
destination: "chrome/browser/browseros/server/resources/bin/browseros_server"
type: "file"
- name: "BrowserOS Server Resources - Linux ARM64"
source: "resources/binaries/browseros_server/linux-arm64/resources"
destination: "chrome/browser/browseros/server/resources"
type: "directory"
os: ["linux"]
arch: ["arm64"]
- name: "BrowserOS Server Binary - Linux x64"
source: "resources/binaries/browseros_server/browseros-server-linux-x64"
destination: "chrome/browser/browseros/server/resources/bin/browseros_server"
type: "file"
- name: "BrowserOS Server Resources - Linux x64"
source: "resources/binaries/browseros_server/linux-x64/resources"
destination: "chrome/browser/browseros/server/resources"
type: "directory"
os: ["linux"]
arch: ["x64"]
- name: "BrowserOS Server Binary - Windows x64"
source: "resources/binaries/browseros_server/browseros-server-windows-x64.exe"
destination: "chrome/browser/browseros/server/resources/bin/browseros_server.exe"
type: "file"
- name: "BrowserOS Server Resources - Windows x64"
source: "resources/binaries/browseros_server/windows-x64/resources"
destination: "chrome/browser/browseros/server/resources"
type: "directory"
os: ["windows"]
arch: ["x64"]
# Bun Runtime - Platform & Architecture specific
- name: "Bun Runtime - macOS ARM64"
source: "resources/binaries/bun/bun-darwin-arm64"
destination: "chrome/browser/browseros/server/resources/bin/bun"
type: "file"
os: ["macos"]
arch: ["arm64"]
- name: "Bun Runtime - macOS x64"
source: "resources/binaries/bun/bun-darwin-x64"
destination: "chrome/browser/browseros/server/resources/bin/bun"
type: "file"
os: ["macos"]
arch: ["x64"]
- name: "Bun Runtime - Linux ARM64"
source: "resources/binaries/bun/bun-linux-arm64"
destination: "chrome/browser/browseros/server/resources/bin/bun"
type: "file"
os: ["linux"]
arch: ["arm64"]
- name: "Bun Runtime - Linux x64"
source: "resources/binaries/bun/bun-linux-x64"
destination: "chrome/browser/browseros/server/resources/bin/bun"
type: "file"
os: ["linux"]
arch: ["x64"]
- name: "Bun Runtime - Windows x64"
source: "resources/binaries/bun/bun-windows-x64.exe"
destination: "chrome/browser/browseros/server/resources/bin/bun.exe"
type: "file"
os: ["windows"]
arch: ["x64"]
# BrowserOS Server Bundle (platform-independent)
- name: "BrowserOS Server Bundle"
source: "resources/binaries/browseros_server/index.js"
destination: "chrome/browser/browseros/server/resources/index.js"
type: "file"

View File

@@ -6,6 +6,8 @@
# - Existing files are cleared and re-downloaded (ensures latest)
# - Build fails immediately if any download fails
# - For universal builds on macOS, both arm64 and x64 are downloaded automatically
# - `download_type: artifact_zip` downloads an archive, validates
# `artifact-metadata.json`, and extracts the declared files into `destination`
#
# Conditional Operations:
# - Use 'os' field to specify target operating systems
@@ -16,89 +18,53 @@
# - Supported values: debug, release
# - Use 'executable' field to mark files that need executable permissions
# - Set to true for binaries (permissions are not preserved during download)
# - Use 'download_type' field to switch from plain file download to archive extraction
# - Supported values: file, artifact_zip
#
# R2 Path Structure:
# binaries/browseros-server/{filename}
# artifacts/server/latest/browseros-server-resources-{target}.zip
#
# Example:
# - name: "My Binary - macOS ARM64"
# r2_key: "binaries/my-binary/my-binary-darwin-arm64"
# destination: "resources/binaries/my_binary/my-binary-darwin-arm64"
# - name: "My Artifact - macOS ARM64"
# r2_key: "artifacts/server/latest/browseros-server-resources-darwin-arm64.zip"
# destination: "resources/binaries/browseros_server/darwin-arm64"
# download_type: "artifact_zip"
# os: ["macos"]
# arch: ["arm64"]
download_operations:
# BrowserOS Server Binary - Platform & Architecture specific
- name: "BrowserOS Server - macOS ARM64"
r2_key: "binaries/browseros-server/browseros-server-darwin-arm64"
destination: "resources/binaries/browseros_server/browseros-server-darwin-arm64"
# BrowserOS Server resource bundles - Platform & Architecture specific
- name: "BrowserOS Server Resources - macOS ARM64"
r2_key: "artifacts/server/latest/browseros-server-resources-darwin-arm64.zip"
destination: "resources/binaries/browseros_server/darwin-arm64"
download_type: "artifact_zip"
os: ["macos"]
arch: ["arm64"]
executable: true
- name: "BrowserOS Server - macOS x64"
r2_key: "binaries/browseros-server/browseros-server-darwin-x64"
destination: "resources/binaries/browseros_server/browseros-server-darwin-x64"
- name: "BrowserOS Server Resources - macOS x64"
r2_key: "artifacts/server/latest/browseros-server-resources-darwin-x64.zip"
destination: "resources/binaries/browseros_server/darwin-x64"
download_type: "artifact_zip"
os: ["macos"]
arch: ["x64"]
executable: true
- name: "BrowserOS Server - Linux ARM64"
r2_key: "binaries/browseros-server/browseros-server-linux-arm64"
destination: "resources/binaries/browseros_server/browseros-server-linux-arm64"
- name: "BrowserOS Server Resources - Linux ARM64"
r2_key: "artifacts/server/latest/browseros-server-resources-linux-arm64.zip"
destination: "resources/binaries/browseros_server/linux-arm64"
download_type: "artifact_zip"
os: ["linux"]
arch: ["arm64"]
executable: true
- name: "BrowserOS Server - Linux x64"
r2_key: "binaries/browseros-server/browseros-server-linux-x64"
destination: "resources/binaries/browseros_server/browseros-server-linux-x64"
- name: "BrowserOS Server Resources - Linux x64"
r2_key: "artifacts/server/latest/browseros-server-resources-linux-x64.zip"
destination: "resources/binaries/browseros_server/linux-x64"
download_type: "artifact_zip"
os: ["linux"]
arch: ["x64"]
executable: true
- name: "BrowserOS Server - Windows x64"
r2_key: "binaries/browseros-server/browseros-server-windows-x64.exe"
destination: "resources/binaries/browseros_server/browseros-server-windows-x64.exe"
- name: "BrowserOS Server Resources - Windows x64"
r2_key: "artifacts/server/latest/browseros-server-resources-windows-x64.zip"
destination: "resources/binaries/browseros_server/windows-x64"
download_type: "artifact_zip"
os: ["windows"]
arch: ["x64"]
# Bun Runtime - Platform & Architecture specific
- name: "Bun Runtime - macOS ARM64"
r2_key: "binaries/bun/bun-darwin-arm64"
destination: "resources/binaries/bun/bun-darwin-arm64"
os: ["macos"]
arch: ["arm64"]
executable: true
- name: "Bun Runtime - macOS x64"
r2_key: "binaries/bun/bun-darwin-x64"
destination: "resources/binaries/bun/bun-darwin-x64"
os: ["macos"]
arch: ["x64"]
executable: true
- name: "Bun Runtime - Linux ARM64"
r2_key: "binaries/bun/bun-linux-arm64"
destination: "resources/binaries/bun/bun-linux-arm64"
os: ["linux"]
arch: ["arm64"]
executable: true
- name: "Bun Runtime - Linux x64"
r2_key: "binaries/bun/bun-linux-x64"
destination: "resources/binaries/bun/bun-linux-x64"
os: ["linux"]
arch: ["x64"]
executable: true
- name: "Bun Runtime - Windows x64"
r2_key: "binaries/bun/bun-windows-x64.exe"
destination: "resources/binaries/bun/bun-windows-x64.exe"
os: ["windows"]
arch: ["x64"]
# BrowserOS Server Bundle (platform-independent)
- name: "BrowserOS Server Bundle"
r2_key: "binaries/browseros-server/index.js"
destination: "resources/binaries/browseros_server/index.js"

View File

@@ -1,17 +1,20 @@
#!/usr/bin/env python3
"""Download module for fetching build resources from Cloudflare R2"""
import hashlib
import json
import shutil
import tempfile
import yaml
from pathlib import Path
from typing import List
import zipfile
from pathlib import Path, PurePosixPath
from typing import Any, List
from ...common.module import CommandModule, ValidationError
from ...common.context import Context
from ...common.utils import (
log_info,
log_error,
log_success,
log_warning,
get_platform,
)
@@ -21,6 +24,151 @@ from .r2 import (
download_file_from_r2,
)
ARTIFACT_ZIP_DOWNLOAD = "artifact_zip"
ARTIFACT_METADATA_NAME = "artifact-metadata.json"
COPY_CHUNK_SIZE = 1024 * 1024
def extract_artifact_zip(archive_path: Path, destination: Path) -> list[Path]:
"""Extract a BrowserOS resource artifact zip into a destination directory."""
with zipfile.ZipFile(archive_path, "r") as archive:
metadata_bytes = _read_artifact_metadata_bytes(archive)
metadata = _parse_artifact_metadata(metadata_bytes)
extracted_paths = _extract_artifact_files(
archive, metadata["files"], destination
)
destination.mkdir(parents=True, exist_ok=True)
(destination / ARTIFACT_METADATA_NAME).write_bytes(metadata_bytes)
return extracted_paths
def _read_artifact_metadata_bytes(archive: zipfile.ZipFile) -> bytes:
try:
return archive.read(ARTIFACT_METADATA_NAME)
except KeyError as exc:
raise RuntimeError(
f"Artifact archive is missing {ARTIFACT_METADATA_NAME}"
) from exc
def _parse_artifact_metadata(metadata_bytes: bytes) -> dict[str, Any]:
try:
metadata = json.loads(metadata_bytes.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("Artifact metadata is not valid JSON") from exc
if not isinstance(metadata, dict):
raise RuntimeError("Artifact metadata must be a JSON object")
files = metadata.get("files")
if not isinstance(files, list) or not files:
raise RuntimeError("Artifact metadata must contain a non-empty files list")
return metadata
def _extract_artifact_files(
archive: zipfile.ZipFile, files: list[dict[str, Any]], destination: Path
) -> list[Path]:
extracted_paths = []
for entry in files:
relative_path, expected_size, expected_sha256 = _parse_artifact_entry(entry)
archive_member = relative_path.as_posix()
dest_path = destination.joinpath(*relative_path.parts)
try:
source_file = archive.open(archive_member, "r")
except KeyError as exc:
raise RuntimeError(
f"Artifact archive is missing declared file: {archive_member}"
) from exc
dest_path.parent.mkdir(parents=True, exist_ok=True)
sha256 = hashlib.sha256()
total_size = 0
with source_file, open(dest_path, "wb") as output_file:
while chunk := source_file.read(COPY_CHUNK_SIZE):
output_file.write(chunk)
sha256.update(chunk)
total_size += len(chunk)
if total_size != expected_size:
raise RuntimeError(
f"Artifact file size mismatch for {archive_member}: "
f"expected {expected_size}, got {total_size}"
)
actual_sha256 = sha256.hexdigest()
if actual_sha256 != expected_sha256:
raise RuntimeError(
f"Artifact checksum mismatch for {archive_member}: "
f"expected {expected_sha256}, got {actual_sha256}"
)
if _should_mark_executable(relative_path):
dest_path.chmod(dest_path.stat().st_mode | 0o755)
extracted_paths.append(dest_path)
return extracted_paths
def _parse_artifact_entry(entry: Any) -> tuple[PurePosixPath, int, str]:
if not isinstance(entry, dict):
raise RuntimeError("Artifact metadata file entries must be objects")
relative_path = _normalize_artifact_path(entry.get("path"))
expected_sha256 = entry.get("sha256")
expected_size = entry.get("size")
if not isinstance(expected_sha256, str) or len(expected_sha256) != 64:
raise RuntimeError(
f"Artifact metadata has invalid sha256 for {relative_path.as_posix()}"
)
if not isinstance(expected_size, int) or expected_size < 0:
raise RuntimeError(
f"Artifact metadata has invalid size for {relative_path.as_posix()}"
)
return relative_path, expected_size, expected_sha256.lower()
def _normalize_artifact_path(raw_path: Any) -> PurePosixPath:
if not isinstance(raw_path, str) or not raw_path:
raise RuntimeError("Artifact metadata file entry is missing path")
relative_path = PurePosixPath(raw_path)
if relative_path.is_absolute() or ".." in relative_path.parts:
raise RuntimeError(f"Artifact metadata path is unsafe: {raw_path}")
if raw_path.endswith("/") or relative_path == PurePosixPath("."):
raise RuntimeError(f"Artifact metadata path is not a file: {raw_path}")
return relative_path
def _should_mark_executable(relative_path: PurePosixPath) -> bool:
parts = relative_path.parts
if get_platform() == "windows":
return False
return len(parts) >= 2 and parts[0] == "resources" and parts[1] == "bin"
def _clear_destination(dest_path: Path) -> None:
if not dest_path.exists():
return
if dest_path.is_dir():
shutil.rmtree(dest_path)
return
dest_path.unlink()
class DownloadResourcesModule(CommandModule):
"""Download resources from Cloudflare R2 before build
@@ -84,28 +232,48 @@ class DownloadResourcesModule(CommandModule):
for op in filtered_ops:
name = op.get("name", "Unnamed")
r2_key = op["r2_key"]
destination = op["destination"]
dest_path = context.root_dir / destination
log_info(f" {name}")
# Clear existing file (always re-download)
# Clear existing destination (always re-download)
if dest_path.exists():
dest_path.unlink()
_clear_destination(dest_path)
log_info(f" Cleared existing: {dest_path.name}")
# Download from R2
if not download_file_from_r2(client, r2_key, dest_path, bucket):
raise RuntimeError(f"Failed to download: {name}")
# Set executable permissions if specified
if op.get("executable", False):
dest_path.chmod(dest_path.stat().st_mode | 0o755)
log_info(f" Set executable permissions")
self._download_operation(client, bucket, op, dest_path)
log_success(f"Downloaded {len(filtered_ops)} resource(s) from R2")
def _download_operation(
self, client, bucket: str, operation: dict[str, Any], dest_path: Path
) -> None:
download_type = operation.get("download_type", "file")
r2_key = operation["r2_key"]
if download_type == ARTIFACT_ZIP_DOWNLOAD:
self._download_artifact_zip(client, bucket, r2_key, dest_path)
return
if not download_file_from_r2(client, r2_key, dest_path, bucket):
raise RuntimeError(f"Failed to download: {operation.get('name', r2_key)}")
if operation.get("executable", False):
dest_path.chmod(dest_path.stat().st_mode | 0o755)
log_info(" Set executable permissions")
def _download_artifact_zip(
self, client, bucket: str, r2_key: str, dest_path: Path
) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
archive_path = Path(temp_dir) / "artifact.zip"
if not download_file_from_r2(client, r2_key, archive_path, bucket):
raise RuntimeError(f"Failed to download artifact zip: {r2_key}")
extracted_paths = extract_artifact_zip(archive_path, dest_path)
log_info(f" Extracted {len(extracted_paths)} artifact file(s)")
def _filter_operations(
self,
operations: List[dict],

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Tests for BrowserOS resource artifact downloads."""
import hashlib
import json
import os
import stat
import tempfile
import unittest
import zipfile
from pathlib import Path
from build.modules.storage.download import (
ARTIFACT_METADATA_NAME,
extract_artifact_zip,
)
class ExtractArtifactZipTest(unittest.TestCase):
def test_extracts_declared_files_and_writes_metadata(self) -> None:
files = {
"resources/bin/browseros_server": b"server-binary",
"resources/bin/third_party/bun": b"bun-binary",
"resources/bin/third_party/rg": b"rg-binary",
}
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
archive_path = temp_path / "artifact.zip"
destination = temp_path / "output"
self._write_artifact_zip(archive_path, files)
extracted_paths = extract_artifact_zip(archive_path, destination)
self.assertEqual(len(extracted_paths), len(files))
metadata_path = destination / ARTIFACT_METADATA_NAME
self.assertTrue(metadata_path.exists())
for relative_path, content in files.items():
extracted_path = destination / relative_path
self.assertEqual(extracted_path.read_bytes(), content)
if os.name != "nt":
self.assertTrue(
os.stat(extracted_path).st_mode & stat.S_IXUSR,
f"{relative_path} should be executable",
)
def test_rejects_missing_declared_files(self) -> None:
files = {
"resources/bin/browseros_server": b"server-binary",
}
metadata_override = {
"version": "0.0.67",
"target": "darwin-arm64",
"generatedAt": "2026-03-06T16:19:09.676Z",
"files": [
{
"path": "resources/bin/browseros_server",
"sha256": hashlib.sha256(files["resources/bin/browseros_server"]).hexdigest(),
"size": len(files["resources/bin/browseros_server"]),
},
{
"path": "resources/bin/third_party/rg",
"sha256": hashlib.sha256(b"missing").hexdigest(),
"size": len(b"missing"),
},
],
}
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
archive_path = temp_path / "artifact.zip"
self._write_artifact_zip(archive_path, files, metadata_override)
with self.assertRaisesRegex(RuntimeError, "missing declared file"):
extract_artifact_zip(archive_path, temp_path / "output")
def test_rejects_checksum_mismatches(self) -> None:
files = {
"resources/bin/browseros_server": b"server-binary",
}
metadata_override = {
"version": "0.0.67",
"target": "darwin-arm64",
"generatedAt": "2026-03-06T16:19:09.676Z",
"files": [
{
"path": "resources/bin/browseros_server",
"sha256": hashlib.sha256(b"not-the-file").hexdigest(),
"size": len(files["resources/bin/browseros_server"]),
}
],
}
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
archive_path = temp_path / "artifact.zip"
self._write_artifact_zip(archive_path, files, metadata_override)
with self.assertRaisesRegex(RuntimeError, "checksum mismatch"):
extract_artifact_zip(archive_path, temp_path / "output")
def test_rejects_non_object_metadata_root(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
archive_path = temp_path / "artifact.zip"
with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr(ARTIFACT_METADATA_NAME, json.dumps(["not-a-dict"]))
with self.assertRaisesRegex(RuntimeError, "JSON object"):
extract_artifact_zip(archive_path, temp_path / "output")
def _write_artifact_zip(
self,
archive_path: Path,
files: dict[str, bytes],
metadata_override: dict | None = None,
) -> None:
metadata = metadata_override or self._build_metadata(files)
with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr(ARTIFACT_METADATA_NAME, json.dumps(metadata))
for relative_path, content in files.items():
archive.writestr(relative_path, content)
def _build_metadata(self, files: dict[str, bytes]) -> dict:
return {
"version": "0.0.67",
"target": "darwin-arm64",
"generatedAt": "2026-03-06T16:19:09.676Z",
"files": [
{
"path": relative_path,
"sha256": hashlib.sha256(content).hexdigest(),
"size": len(content),
}
for relative_path, content in files.items()
],
}
if __name__ == "__main__":
unittest.main()