Files
BrowserOS/packages/browseros/build/common/resolver.py
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

358 lines
11 KiB
Python
Generated

#!/usr/bin/env python3
"""
Configuration resolver - single source of truth for all config resolution
Two mutually exclusive modes:
1. CONFIG mode (--config FILE): YAML controls everything
2. DIRECT mode (no --config): CLI args > Env > Defaults
Precedence (CONFIG mode):
- YAML (authoritative)
- Env vars (only for secrets/credentials via EnvConfig)
- Error if required fields missing
Precedence (DIRECT mode):
- CLI args (explicit, Typer defaults must be None)
- Environment variables (CHROMIUM_SRC, ARCH)
- Hardcoded defaults
This centralizes ALL configuration resolution in one place.
"""
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple
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,
) -> List[Context]:
"""Resolve build configuration - single entry point.
Args:
cli_args: Dictionary of CLI arguments (all values should be None if not provided)
yaml_config: Optional YAML configuration (triggers CONFIG mode)
Returns:
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
Modes:
- CONFIG mode (yaml_config provided): YAML is authoritative
- DIRECT mode (no yaml_config): CLI > Env > Defaults
Note:
root_dir is always computed from package location via get_package_root(),
never from config or cwd.
"""
if yaml_config:
return _resolve_config_mode(yaml_config, cli_args)
else:
return _resolve_direct_mode(cli_args)
def _resolve_config_mode(
yaml_config: Dict[str, Any], cli_args: Dict[str, Any]
) -> List[Context]:
"""CONFIG MODE: YAML is base, CLI can override.
Args:
yaml_config: YAML configuration dictionary
cli_args: CLI arguments (can override YAML values)
Returns:
List of Contexts. One per architecture when YAML provides a list.
Raises:
ValueError: If required fields missing from both YAML and CLI
"""
build_section = yaml_config.get("build", {})
# chromium_src: CLI override > YAML > error
chromium_src_str = cli_args.get("chromium_src") or build_section.get("chromium_src")
if not chromium_src_str:
raise ValueError(
"CONFIG MODE: chromium_src required in YAML!\n"
"Add to your config:\n"
" build:\n"
" chromium_src: /path/to/chromium"
)
chromium_src = Path(chromium_src_str)
chromium_src_source = "cli" if cli_args.get("chromium_src") else "yaml"
# Validate chromium_src exists
if not chromium_src.exists():
raise ValueError(
f"CONFIG MODE: chromium_src does not exist: {chromium_src}\n"
f"Expected directory with Chromium source code"
)
# 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: {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})")
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=arch,
build_type=build_type,
)
for arch in architectures
]
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:
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
"""
env = EnvConfig()
# chromium_src: CLI > Env > Error
chromium_src = cli_args.get("chromium_src") or env.chromium_src
if not chromium_src:
raise ValueError(
"DIRECT MODE: chromium_src required!\n"
"Provide via one of:\n"
" --chromium-src PATH\n"
" CHROMIUM_SRC environment variable"
)
chromium_src = Path(chromium_src)
# Validate chromium_src exists
if not chromium_src.exists():
raise ValueError(
f"DIRECT MODE: chromium_src does not exist: {chromium_src}\n"
f"Expected directory with Chromium source code"
)
# architecture: CLI > Env > Platform default
architecture = cli_args.get("arch") or env.arch
if not architecture:
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"
log_info(f"✓ DIRECT MODE: chromium_src={chromium_src} (cli/env)")
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,
)
]
def resolve_pipeline(
cli_args: Dict[str, Any],
yaml_config: Optional[Dict[str, Any]] = None,
execution_order: Optional[List[Tuple[str, List[str]]]] = None,
) -> List[str]:
"""Resolve build pipeline - single entry point.
Args:
cli_args: CLI arguments dictionary
yaml_config: Optional YAML configuration (triggers CONFIG mode)
execution_order: Required for DIRECT mode with phase flags
Returns:
List of module names in execution order
Raises:
ValueError: If no pipeline specified or conflicting modes
Modes:
- CONFIG mode: Returns yaml_config["modules"]
- DIRECT mode: --modules or phase flags
"""
if yaml_config:
return _resolve_pipeline_config_mode(yaml_config)
else:
return _resolve_pipeline_direct_mode(cli_args, execution_order)
def _resolve_pipeline_config_mode(yaml_config: Dict[str, Any]) -> List[str]:
"""CONFIG MODE: Pipeline from YAML modules list.
Args:
yaml_config: YAML configuration dictionary
Returns:
Module list from YAML
Raises:
ValueError: If modules not specified in YAML
"""
modules = yaml_config.get("modules")
if not modules:
raise ValueError(
"CONFIG MODE: modules required in YAML!\n"
"Add to your config:\n"
" modules: [clean, configure, compile, sign_macos]"
)
log_info(f"✓ CONFIG MODE: pipeline={modules} (yaml)")
return modules
def _resolve_pipeline_direct_mode(
cli_args: Dict[str, Any],
execution_order: Optional[List[Tuple[str, List[str]]]],
) -> List[str]:
"""DIRECT MODE: Pipeline from --modules or phase flags.
Args:
cli_args: CLI arguments dictionary
execution_order: Phase execution order (required for flag mode)
Returns:
Module list in execution order
Raises:
ValueError: If no pipeline specified or both modes used
"""
has_modules = cli_args.get("modules") is not None
has_flags = _has_phase_flags(cli_args)
if not has_modules and not has_flags:
raise ValueError(
"DIRECT MODE: No pipeline specified!\n"
"Use one of:\n"
" --modules clean,compile,...\n"
" --setup --build --sign (phase flags)"
)
if has_modules and has_flags:
raise ValueError(
"DIRECT MODE: Cannot use both --modules and phase flags!\n"
"Choose one approach."
)
if has_modules:
modules_str = cli_args["modules"]
pipeline = [m.strip() for m in modules_str.split(",")]
log_info(f"✓ DIRECT MODE: pipeline={pipeline} (--modules)")
return pipeline
if has_flags:
if execution_order is None:
raise ValueError(
"DIRECT MODE: execution_order required for phase flag resolution"
)
pipeline = _build_pipeline_from_flags(cli_args, execution_order)
log_info(f"✓ DIRECT MODE: pipeline={pipeline} (phase flags)")
return pipeline
raise ValueError("DIRECT MODE: Internal error - no pipeline resolution matched")
def _has_phase_flags(cli_args: Dict[str, Any]) -> bool:
"""Check if any phase flags are set.
Args:
cli_args: CLI arguments dictionary
Returns:
True if any phase flag is True
"""
phase_flags = ["setup", "prep", "build", "sign", "package", "upload"]
return any(cli_args.get(flag, False) for flag in phase_flags)
def _build_pipeline_from_flags(
cli_args: Dict[str, Any],
execution_order: List[Tuple[str, List[str]]],
) -> List[str]:
"""Build pipeline from phase flags with fixed execution order.
Args:
cli_args: CLI arguments with phase flag keys
execution_order: List of (phase_name, modules) defining order
Returns:
Module list in predetermined order
"""
enabled_phases = {
"setup": cli_args.get("setup", False),
"prep": cli_args.get("prep", False),
"build": cli_args.get("build", False),
"sign": cli_args.get("sign", False),
"package": cli_args.get("package", False),
"upload": cli_args.get("upload", False),
}
pipeline = []
for phase_name, phase_modules in execution_order:
if enabled_phases.get(phase_name, False):
pipeline.extend(phase_modules)
return pipeline