mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 15:46:22 +00:00
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
This commit is contained in:
48
packages/browseros/build/cli/build.py
generated
48
packages/browseros/build/cli/build.py
generated
@@ -402,9 +402,11 @@ def main(
|
||||
"upload": upload,
|
||||
}
|
||||
|
||||
# Resolve build context (CONFIG mode or DIRECT mode)
|
||||
# Resolve build context (CONFIG mode or DIRECT mode).
|
||||
# Returns one Context per architecture — single-element for normal
|
||||
# builds, multi-element when YAML declares `architecture: [x64, arm64]`.
|
||||
try:
|
||||
ctx = resolve_config(cli_args, config_data)
|
||||
arch_ctxs = resolve_config(cli_args, config_data)
|
||||
except ValueError as e:
|
||||
log_error(str(e))
|
||||
raise typer.Exit(1)
|
||||
@@ -459,20 +461,40 @@ def main(
|
||||
os.environ["DEPOT_TOOLS_WIN_TOOLCHAIN"] = "0"
|
||||
log_info("Set DEPOT_TOOLS_WIN_TOOLCHAIN=0 for Windows build")
|
||||
|
||||
# Print build summary using the first context — versions and paths
|
||||
# are identical across per-arch contexts. Architecture is logged again
|
||||
# inside the loop below for multi-arch runs.
|
||||
summary_ctx = arch_ctxs[0]
|
||||
log_info(f"📍 Root: {root_dir}")
|
||||
log_info(f"📍 Chromium: {ctx.chromium_src}")
|
||||
log_info(f"📍 Architecture: {ctx.architecture}")
|
||||
log_info(f"📍 Build type: {ctx.build_type}")
|
||||
log_info(f"📍 Output: {ctx.out_dir}")
|
||||
log_info(f"📍 Semantic version: {ctx.semantic_version}")
|
||||
log_info(f"📍 Chromium version: {ctx.chromium_version}")
|
||||
log_info(f"📍 Build offset: {ctx.browseros_build_offset}")
|
||||
log_info(f"📍 Chromium: {summary_ctx.chromium_src}")
|
||||
if len(arch_ctxs) > 1:
|
||||
log_info(
|
||||
f"📍 Architectures: {[c.architecture for c in arch_ctxs]} (multi-arch loop)"
|
||||
)
|
||||
else:
|
||||
log_info(f"📍 Architecture: {summary_ctx.architecture}")
|
||||
log_info(f"📍 Build type: {summary_ctx.build_type}")
|
||||
log_info(f"📍 Semantic version: {summary_ctx.semantic_version}")
|
||||
log_info(f"📍 Chromium version: {summary_ctx.chromium_version}")
|
||||
log_info(f"📍 Build offset: {summary_ctx.browseros_build_offset}")
|
||||
log_info(f"📍 Pipeline: {' → '.join(pipeline)}")
|
||||
log_info("=" * 70)
|
||||
|
||||
# Set notification context for OS and architecture
|
||||
os_name = "macOS" if IS_MACOS() else "Windows" if IS_WINDOWS() else "Linux"
|
||||
set_build_context(os_name, ctx.architecture)
|
||||
|
||||
# Execute pipeline
|
||||
execute_pipeline(ctx, pipeline, AVAILABLE_MODULES, pipeline_name="build")
|
||||
# Execute the pipeline once per architecture. Modules see a normal
|
||||
# single-arch ctx; the runner is the only thing that knows about the
|
||||
# multi-arch loop.
|
||||
for i, arch_ctx in enumerate(arch_ctxs, start=1):
|
||||
if len(arch_ctxs) > 1:
|
||||
log_info("\n" + "#" * 70)
|
||||
log_info(
|
||||
f"# Architecture {i}/{len(arch_ctxs)}: {arch_ctx.architecture}"
|
||||
)
|
||||
log_info(f"# Output: {arch_ctx.out_dir}")
|
||||
log_info("#" * 70)
|
||||
|
||||
set_build_context(os_name, arch_ctx.architecture)
|
||||
execute_pipeline(
|
||||
arch_ctx, pipeline, AVAILABLE_MODULES, pipeline_name="build"
|
||||
)
|
||||
|
||||
91
packages/browseros/build/common/resolver.py
generated
91
packages/browseros/build/common/resolver.py
generated
@@ -26,11 +26,13 @@ from .context import Context
|
||||
from .env import EnvConfig
|
||||
from .utils import get_platform_arch, log_info
|
||||
|
||||
VALID_ARCHITECTURES = {"x64", "arm64", "universal"}
|
||||
|
||||
|
||||
def resolve_config(
|
||||
cli_args: Dict[str, Any],
|
||||
yaml_config: Optional[Dict[str, Any]] = None,
|
||||
) -> Context:
|
||||
) -> List[Context]:
|
||||
"""Resolve build configuration - single entry point.
|
||||
|
||||
Args:
|
||||
@@ -38,7 +40,9 @@ def resolve_config(
|
||||
yaml_config: Optional YAML configuration (triggers CONFIG mode)
|
||||
|
||||
Returns:
|
||||
Fully resolved Context object
|
||||
List of fully resolved Context objects. Single-element for the
|
||||
common single-arch case; multi-element when YAML declares
|
||||
`architecture: [x64, arm64]` (Linux multi-arch).
|
||||
|
||||
Raises:
|
||||
ValueError: If required fields missing or invalid
|
||||
@@ -59,7 +63,7 @@ def resolve_config(
|
||||
|
||||
def _resolve_config_mode(
|
||||
yaml_config: Dict[str, Any], cli_args: Dict[str, Any]
|
||||
) -> Context:
|
||||
) -> List[Context]:
|
||||
"""CONFIG MODE: YAML is base, CLI can override.
|
||||
|
||||
Args:
|
||||
@@ -67,7 +71,7 @@ def _resolve_config_mode(
|
||||
cli_args: CLI arguments (can override YAML values)
|
||||
|
||||
Returns:
|
||||
Context with values from YAML, optionally overridden by CLI
|
||||
List of Contexts. One per architecture when YAML provides a list.
|
||||
|
||||
Raises:
|
||||
ValueError: If required fields missing from both YAML and CLI
|
||||
@@ -94,41 +98,66 @@ def _resolve_config_mode(
|
||||
f"Expected directory with Chromium source code"
|
||||
)
|
||||
|
||||
# architecture: CLI override > YAML > platform default
|
||||
architecture = (
|
||||
cli_args.get("arch")
|
||||
or build_section.get("architecture")
|
||||
or build_section.get("arch")
|
||||
)
|
||||
arch_source = "cli" if cli_args.get("arch") else "yaml"
|
||||
if not architecture:
|
||||
architecture = get_platform_arch()
|
||||
# architecture: CLI override > YAML > platform default.
|
||||
# YAML may be a string OR a list (e.g. [x64, arm64]) — list form runs
|
||||
# the entire pipeline once per arch.
|
||||
cli_arch = cli_args.get("arch")
|
||||
yaml_arch = build_section.get("architecture") or build_section.get("arch")
|
||||
|
||||
if cli_arch:
|
||||
architectures = [cli_arch]
|
||||
arch_source = "cli"
|
||||
elif yaml_arch is not None:
|
||||
architectures = yaml_arch if isinstance(yaml_arch, list) else [yaml_arch]
|
||||
arch_source = "yaml"
|
||||
else:
|
||||
architectures = [get_platform_arch()]
|
||||
arch_source = "default"
|
||||
log_info(f"CONFIG MODE: Using platform default architecture: {architecture}")
|
||||
log_info(
|
||||
f"CONFIG MODE: Using platform default architecture: {architectures[0]}"
|
||||
)
|
||||
|
||||
for arch in architectures:
|
||||
if arch not in VALID_ARCHITECTURES:
|
||||
raise ValueError(
|
||||
f"CONFIG MODE: invalid architecture '{arch}'. "
|
||||
f"Valid: {sorted(VALID_ARCHITECTURES)}"
|
||||
)
|
||||
|
||||
# build_type: CLI override > YAML > debug
|
||||
build_type = cli_args.get("build_type") or build_section.get("type", "debug")
|
||||
build_type_source = "cli" if cli_args.get("build_type") else "yaml"
|
||||
|
||||
log_info(f"✓ CONFIG MODE: chromium_src={chromium_src} ({chromium_src_source})")
|
||||
log_info(f"✓ CONFIG MODE: architecture={architecture} ({arch_source})")
|
||||
if len(architectures) > 1:
|
||||
log_info(
|
||||
f"✓ CONFIG MODE: architectures={architectures} ({arch_source}, multi-arch loop)"
|
||||
)
|
||||
else:
|
||||
log_info(
|
||||
f"✓ CONFIG MODE: architecture={architectures[0]} ({arch_source})"
|
||||
)
|
||||
log_info(f"✓ CONFIG MODE: build_type={build_type} ({build_type_source})")
|
||||
|
||||
return Context(
|
||||
chromium_src=chromium_src,
|
||||
architecture=architecture,
|
||||
build_type=build_type,
|
||||
)
|
||||
return [
|
||||
Context(
|
||||
chromium_src=chromium_src,
|
||||
architecture=arch,
|
||||
build_type=build_type,
|
||||
)
|
||||
for arch in architectures
|
||||
]
|
||||
|
||||
|
||||
def _resolve_direct_mode(cli_args: Dict[str, Any]) -> Context:
|
||||
def _resolve_direct_mode(cli_args: Dict[str, Any]) -> List[Context]:
|
||||
"""DIRECT MODE: CLI > Env > Defaults.
|
||||
|
||||
Args:
|
||||
cli_args: CLI arguments (None if not provided by user)
|
||||
|
||||
Returns:
|
||||
Context with resolved values
|
||||
Single-element list with the resolved Context. DIRECT mode is
|
||||
always single-arch (CLI --arch is a scalar).
|
||||
|
||||
Raises:
|
||||
ValueError: If chromium_src not provided
|
||||
@@ -160,6 +189,12 @@ def _resolve_direct_mode(cli_args: Dict[str, Any]) -> Context:
|
||||
architecture = get_platform_arch()
|
||||
log_info(f"DIRECT MODE: Using platform default architecture: {architecture}")
|
||||
|
||||
if architecture not in VALID_ARCHITECTURES:
|
||||
raise ValueError(
|
||||
f"DIRECT MODE: invalid architecture '{architecture}'. "
|
||||
f"Valid: {sorted(VALID_ARCHITECTURES)}"
|
||||
)
|
||||
|
||||
# build_type: CLI > Default
|
||||
build_type = cli_args.get("build_type") or "debug"
|
||||
|
||||
@@ -167,11 +202,13 @@ def _resolve_direct_mode(cli_args: Dict[str, Any]) -> Context:
|
||||
log_info(f"✓ DIRECT MODE: architecture={architecture} (cli/env/default)")
|
||||
log_info(f"✓ DIRECT MODE: build_type={build_type} (cli/default)")
|
||||
|
||||
return Context(
|
||||
chromium_src=chromium_src,
|
||||
architecture=architecture,
|
||||
build_type=build_type,
|
||||
)
|
||||
return [
|
||||
Context(
|
||||
chromium_src=chromium_src,
|
||||
architecture=architecture,
|
||||
build_type=build_type,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def resolve_pipeline(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# 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
|
||||
# Builds both x64 and arm64 in a single invocation on a Linux x64 host.
|
||||
# The runner loops the entire pipeline once per architecture; depot_tools
|
||||
# fetches the matching sysroots automatically (see git_setup module).
|
||||
#
|
||||
# Run:
|
||||
# browseros build --config build/config/release.linux.yaml
|
||||
#
|
||||
# Environment Variables:
|
||||
# Use !env tag to reference environment variables:
|
||||
@@ -10,11 +13,12 @@
|
||||
|
||||
build:
|
||||
type: release
|
||||
architecture: [x64, arm64] # Builds both arches sequentially in one run
|
||||
|
||||
gn_flags:
|
||||
file: build/config/gn/flags.linux.release.gn
|
||||
|
||||
# Explicit module execution order
|
||||
# Explicit module execution order. Runs once per architecture above.
|
||||
modules:
|
||||
# Phase 1: Setup
|
||||
- clean
|
||||
|
||||
57
packages/browseros/build/modules/package/linux.py
generated
57
packages/browseros/build/modules/package/linux.py
generated
@@ -17,25 +17,41 @@ from ...common.utils import (
|
||||
run_command,
|
||||
safe_rmtree,
|
||||
join_paths,
|
||||
get_platform_arch,
|
||||
IS_LINUX,
|
||||
)
|
||||
from ...common.notify import get_notifier, COLOR_GREEN
|
||||
|
||||
# Target-arch packaging metadata. These describe the artifact we're
|
||||
# producing, not the build machine. `appimage_arch` is passed to
|
||||
# appimagetool via the ARCH env var; `deb_arch` is written into the
|
||||
# .deb control file.
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
# Host-arch tool selection. appimagetool is a normal binary that runs on
|
||||
# the build machine — when cross-compiling arm64 from an x64 host, we
|
||||
# still need the x86_64 tool to actually execute. Keyed on
|
||||
# get_platform_arch() (BUILD machine arch), NOT ctx.architecture.
|
||||
LINUX_HOST_APPIMAGETOOL = {
|
||||
"x64": (
|
||||
"appimagetool-x86_64.AppImage",
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage",
|
||||
),
|
||||
"arm64": (
|
||||
"appimagetool-aarch64.AppImage",
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_linux_architecture_config(architecture: str) -> dict[str, str]:
|
||||
config = LINUX_ARCHITECTURE_CONFIG.get(architecture)
|
||||
@@ -47,6 +63,19 @@ def get_linux_architecture_config(architecture: str) -> dict[str, str]:
|
||||
return config
|
||||
|
||||
|
||||
def get_host_appimagetool() -> tuple[str, str]:
|
||||
"""Return (filename, url) for the appimagetool binary that runs on
|
||||
the current build machine. Critical for cross-compile correctness."""
|
||||
host_arch = get_platform_arch()
|
||||
tool = LINUX_HOST_APPIMAGETOOL.get(host_arch)
|
||||
if not tool:
|
||||
supported = ", ".join(sorted(LINUX_HOST_APPIMAGETOOL))
|
||||
raise ValueError(
|
||||
f"No appimagetool binary for host arch '{host_arch}'. Supported: {supported}"
|
||||
)
|
||||
return tool
|
||||
|
||||
|
||||
class LinuxPackageModule(CommandModule):
|
||||
produces = ["appimage", "deb"]
|
||||
requires = []
|
||||
@@ -313,26 +342,30 @@ export CHROME_WRAPPER="${{THIS}}"
|
||||
|
||||
|
||||
def download_appimagetool(ctx: Context) -> Optional[Path]:
|
||||
"""Download appimagetool if not available"""
|
||||
"""Download the appimagetool binary that runs on the build machine.
|
||||
|
||||
Note: this is keyed on the HOST arch, not ctx.architecture. When
|
||||
cross-compiling arm64 packages from an x64 host, we still need the
|
||||
x86_64 appimagetool because the tool executes locally; the target
|
||||
arch is communicated via the ARCH env var in create_appimage().
|
||||
"""
|
||||
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, arch_config["appimage_tool"]))
|
||||
tool_filename, url = get_host_appimagetool()
|
||||
tool_path = Path(join_paths(tool_dir, tool_filename))
|
||||
|
||||
if tool_path.exists():
|
||||
log_info("✓ appimagetool already available")
|
||||
log_info(f"✓ appimagetool already available ({tool_filename})")
|
||||
return tool_path
|
||||
|
||||
log_info("📥 Downloading appimagetool...")
|
||||
url = arch_config["appimage_url"]
|
||||
|
||||
log_info(f"📥 Downloading {tool_filename}...")
|
||||
cmd = ["wget", "-O", str(tool_path), url]
|
||||
result = run_command(cmd, check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
tool_path.chmod(0o755)
|
||||
log_success("✓ Downloaded appimagetool")
|
||||
log_success(f"✓ Downloaded {tool_filename}")
|
||||
return tool_path
|
||||
else:
|
||||
log_error("Failed to download appimagetool")
|
||||
|
||||
@@ -2,22 +2,25 @@
|
||||
"""Tests for Linux packaging architecture helpers."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from build.modules.package.linux import get_linux_architecture_config
|
||||
from build.modules.package.linux import (
|
||||
LINUX_HOST_APPIMAGETOOL,
|
||||
get_host_appimagetool,
|
||||
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")
|
||||
|
||||
@@ -26,5 +29,35 @@ class LinuxArchitectureConfigTest(unittest.TestCase):
|
||||
get_linux_architecture_config("universal")
|
||||
|
||||
|
||||
class HostAppImageToolTest(unittest.TestCase):
|
||||
"""The appimagetool binary must match the BUILD machine's arch, not
|
||||
the target arch — otherwise cross-compiling arm64 packages from an x64
|
||||
host fails because the aarch64 tool can't execute on x64."""
|
||||
|
||||
def test_x64_host_picks_x86_64_tool(self) -> None:
|
||||
with patch(
|
||||
"build.modules.package.linux.get_platform_arch", return_value="x64"
|
||||
):
|
||||
filename, url = get_host_appimagetool()
|
||||
|
||||
self.assertEqual(filename, "appimagetool-x86_64.AppImage")
|
||||
self.assertIn("x86_64", url)
|
||||
|
||||
def test_arm64_host_picks_aarch64_tool(self) -> None:
|
||||
with patch(
|
||||
"build.modules.package.linux.get_platform_arch", return_value="arm64"
|
||||
):
|
||||
filename, url = get_host_appimagetool()
|
||||
|
||||
self.assertEqual(filename, "appimagetool-aarch64.AppImage")
|
||||
self.assertIn("aarch64", url)
|
||||
|
||||
def test_host_lookup_independent_of_target(self) -> None:
|
||||
# Both architectures must be present in the host lookup so cross
|
||||
# builds work in either direction.
|
||||
self.assertIn("x64", LINUX_HOST_APPIMAGETOOL)
|
||||
self.assertIn("arm64", LINUX_HOST_APPIMAGETOOL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
63
packages/browseros/build/modules/setup/git.py
generated
63
packages/browseros/build/modules/setup/git.py
generated
@@ -1,12 +1,24 @@
|
||||
#!/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_error, log_success, IS_WINDOWS, safe_rmtree
|
||||
from ...common.utils import (
|
||||
run_command,
|
||||
log_info,
|
||||
log_warning,
|
||||
log_error,
|
||||
log_success,
|
||||
IS_LINUX,
|
||||
IS_WINDOWS,
|
||||
safe_rmtree,
|
||||
)
|
||||
|
||||
|
||||
class GitSetupModule(CommandModule):
|
||||
@@ -32,6 +44,12 @@ class GitSetupModule(CommandModule):
|
||||
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)
|
||||
@@ -40,6 +58,49 @@ class GitSetupModule(CommandModule):
|
||||
|
||||
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],
|
||||
|
||||
Reference in New Issue
Block a user