mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
198
packages/browseros/build/modules/storage/download.py
generated
198
packages/browseros/build/modules/storage/download.py
generated
@@ -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],
|
||||
|
||||
145
packages/browseros/build/modules/storage/download_test.py
generated
Normal file
145
packages/browseros/build/modules/storage/download_test.py
generated
Normal 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()
|
||||
Reference in New Issue
Block a user