Files
Nikhil 8de2bf984f feat: build linux x64 + arm64 in a single invocation (#652)
`release.linux.yaml` now declares `architecture: [x64, arm64]` and the
runner loops the entire pipeline once per architecture. depot_tools
fetches both Linux sysroots automatically — `git_setup` idempotently
ensures `target_cpus = ['x64', 'arm64']` is in `.gclient` before
`gclient sync`, so cross-compiling arm64 from an x64 host just works.

The resolver returns `List[Context]` (single-element for the common
single-arch case), and `build/cli/build.py` loops `execute_pipeline` over
the per-arch contexts. Modules stay 100% arch-agnostic — no new
orchestration module, no new YAML schema beyond the list form.

Also fix a cross-compile bug in `build/modules/package/linux.py`: the
appimagetool binary must match the BUILD machine's arch (it executes
locally), not the target arch. Split into a host-keyed
`LINUX_HOST_APPIMAGETOOL` lookup vs the existing target-keyed
`LINUX_ARCHITECTURE_CONFIG`. Target arch is still passed to appimagetool
via the `ARCH` env var.

- build/common/resolver.py: scalar OR list `architecture` -> List[Context]
- build/cli/build.py: loop pipeline per arch, log multi-arch headers
- build/config/release.linux.yaml: `architecture: [x64, arm64]`
- build/modules/setup/git.py: idempotent `target_cpus` edit on Linux
- build/modules/package/linux.py: host vs target appimagetool split
- build/modules/package/linux_test.py: cover the host/target split
2026-04-06 13:08:06 -07:00

159 lines
5.7 KiB
Python
Generated

#!/usr/bin/env python3
"""Git operations module for BrowserOS build system"""
import re
import subprocess
import tarfile
import urllib.request
from typing import List
from ...common.module import CommandModule, ValidationError
from ...common.context import Context
from ...common.utils import (
run_command,
log_info,
log_warning,
log_error,
log_success,
IS_LINUX,
IS_WINDOWS,
safe_rmtree,
)
class GitSetupModule(CommandModule):
produces = []
requires = []
description = "Checkout Chromium version and sync dependencies"
def validate(self, ctx: Context) -> None:
if not ctx.chromium_src.exists():
raise ValidationError(f"Chromium source not found: {ctx.chromium_src}")
if not ctx.chromium_version:
raise ValidationError("Chromium version not set")
def execute(self, ctx: Context) -> None:
log_info(f"\n🔀 Setting up Chromium {ctx.chromium_version}...")
log_info("📥 Fetching all tags from remote...")
run_command(["git", "fetch", "--tags", "--force"], cwd=ctx.chromium_src)
self._verify_tag_exists(ctx)
log_info(f"🔀 Checking out tag: {ctx.chromium_version}")
run_command(["git", "checkout", f"tags/{ctx.chromium_version}"], cwd=ctx.chromium_src)
# On Linux, depot_tools fetches per-arch sysroots automatically when
# `.gclient` declares `target_cpus`. Ensure both x64 and arm64 are
# listed before sync so cross-compilation just works on x64 hosts.
if IS_LINUX():
self._ensure_gclient_target_cpus(ctx, ["x64", "arm64"])
log_info("📥 Syncing dependencies (this may take a while)...")
if IS_WINDOWS():
run_command(["gclient.bat", "sync", "-D", "--no-history", "--shallow"], cwd=ctx.chromium_src)
else:
run_command(["gclient", "sync", "-D", "--no-history", "--shallow"], cwd=ctx.chromium_src)
log_success("Git setup complete")
def _ensure_gclient_target_cpus(self, ctx: Context, required: List[str]) -> None:
"""Idempotently add `target_cpus` to .gclient so depot_tools fetches
the matching Linux sysroots for cross-compilation.
depot_tools convention: .gclient lives one directory above
chromium_src (i.e. ../.gclient). It is a Python file with a list
of solution dicts followed by optional top-level assignments.
We append a `target_cpus = [...]` line if missing or merge in any
archs that aren't already present.
"""
gclient_path = ctx.chromium_src.parent / ".gclient"
if not gclient_path.exists():
log_warning(
f"⚠️ .gclient not found at {gclient_path}; "
f"skipping target_cpus bootstrap. "
f"Cross-arch builds may fail until you run `fetch chromium`."
)
return
content = gclient_path.read_text()
match = re.search(r"^\s*target_cpus\s*=\s*\[([^\]]*)\]", content, re.MULTILINE)
if match:
existing = re.findall(r"['\"]([^'\"]+)['\"]", match.group(1))
missing = [arch for arch in required if arch not in existing]
if not missing:
log_info(f"✓ .gclient target_cpus already includes {required}")
return
merged = sorted(set(existing) | set(required))
new_line = f"target_cpus = {merged!r}"
content = (
content[: match.start()] + new_line + content[match.end() :]
)
log_info(
f"📝 Updating .gclient target_cpus: {existing}{merged}"
)
else:
new_line = f"\ntarget_cpus = {required!r}\n"
content = content.rstrip() + "\n" + new_line
log_info(f"📝 Adding target_cpus = {required} to .gclient")
gclient_path.write_text(content)
def _verify_tag_exists(self, ctx: Context) -> None:
result = subprocess.run(
["git", "tag", "-l", ctx.chromium_version],
text=True,
capture_output=True,
cwd=ctx.chromium_src,
)
if not result.stdout or ctx.chromium_version not in result.stdout:
log_error(f"Tag {ctx.chromium_version} not found!")
log_info("Available tags (last 10):")
list_result = subprocess.run(
["git", "tag", "-l", "--sort=-version:refname"],
text=True,
capture_output=True,
cwd=ctx.chromium_src,
)
if list_result.stdout:
for tag in list_result.stdout.strip().split("\n")[:10]:
log_info(f" {tag}")
raise ValidationError(f"Git tag {ctx.chromium_version} not found")
class SparkleSetupModule(CommandModule):
produces = []
requires = []
description = "Download and setup Sparkle framework (macOS only)"
def validate(self, ctx: Context) -> None:
from ...common.utils import IS_MACOS
if not IS_MACOS():
raise ValidationError("Sparkle setup requires macOS")
def execute(self, ctx: Context) -> None:
log_info("\n✨ Setting up Sparkle framework...")
sparkle_dir = ctx.get_sparkle_dir()
if sparkle_dir.exists():
safe_rmtree(sparkle_dir)
sparkle_dir.mkdir(parents=True)
sparkle_url = ctx.get_sparkle_url()
sparkle_archive = sparkle_dir / "sparkle.tar.xz"
log_info(f"Downloading Sparkle from {sparkle_url}...")
urllib.request.urlretrieve(sparkle_url, sparkle_archive)
log_info("Extracting Sparkle...")
with tarfile.open(sparkle_archive, "r:xz") as tar:
tar.extractall(sparkle_dir)
sparkle_archive.unlink()
log_success("Sparkle setup complete")