Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions packages/browseros/bos_build/cli/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ..core.context import Context
from ..core.paths import get_package_root
from ..core.pipeline import validate_pipeline, show_available_modules
from ..core.planner import Switches, load_profile, plan, preflight
from ..core.planner import Switches, load_profile, plan_runs, preflight
from ..core.resolver import resolve_config, resolve_pipeline
from ..core.notify import (
notify_pipeline_end,
Expand Down Expand Up @@ -282,7 +282,14 @@ def main(
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(runs[0][1])}")
if len(runs) > 1:
# Runs may be heterogeneous (universal), so show each run's steps
for run_ctx, run_steps in runs:
log_info(
f"📍 Pipeline[{run_ctx.architecture}]: {' → '.join(run_steps)}"
)
else:
log_info(f"📍 Pipeline: {' → '.join(runs[0][1])}")
log_info("=" * 70)

os_name = "macOS" if IS_MACOS() else "Windows" if IS_WINDOWS() else "Linux"
Expand Down Expand Up @@ -345,9 +352,10 @@ def _resolve_preset_runs(
upload: Optional[bool],
chromium_src: Optional[Path],
) -> List[Tuple[Context, List[str]]]:
"""Resolve preset/profile + CLI overrides into per-arch (ctx, steps) runs.
"""Resolve preset/profile + CLI overrides into per-run (ctx, steps) pairs.

Precedence: CLI > profile > preset defaults.
Precedence: CLI > profile > preset defaults. Universal yields three
runs (see core/planner.plan_runs); everything else one run per arch.
"""
try:
switches = load_profile(_resolve_profile_path(profile)) if profile else Switches()
Expand All @@ -370,6 +378,11 @@ def _resolve_preset_runs(
overrides["upload"] = upload
switches = replace(switches, **overrides).resolved()

# Plan before resolving chromium_src so an invalid invocation
# (e.g. universal --no-sign) fails on the planning error, not on
# missing environment.
planned = plan_runs(switches)

# Shallow provisioning creates the checkout itself, so the src
# dir may not exist yet on a fresh runner.
src = _resolve_chromium_src(
Expand All @@ -383,14 +396,14 @@ def _resolve_preset_runs(
)

runs: List[Tuple[Context, List[str]]] = []
for run_arch in switches.architectures:
for run_arch, run_steps in planned:
ctx = Context(
chromium_src=src,
architecture=run_arch,
build_type=switches.build_type,
product=switches.product,
)
runs.append((ctx, plan(switches, run_arch)))
runs.append((ctx, run_steps))
return runs
except ValueError as e:
log_error(str(e))
Expand Down
17 changes: 5 additions & 12 deletions packages/browseros/bos_build/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ class Context:
SPARKLE_VERSION: str = "2.7.0"
WINSPARKLE_VERSION: str = "0.9.3"

# When set, get_app_path() returns this directly — UniversalBuildModule
# pins per-arch and universal app paths through it.
_fixed_app_path: Optional[Path] = None

artifact_registry: ArtifactRegistry = field(init=False)
env: EnvConfig = field(init=False)

Expand Down Expand Up @@ -209,15 +205,12 @@ def get_pkg_dmg_path(self) -> Path:
def get_app_path(self) -> Path:
"""Get built app path

Resolves strictly from this context's own out_dir (or _fixed_app_path
when set, as UniversalBuildModule does). Never probes other out dirs:
a stale product universal app must not hijack arch-specific
builds' sign/package stages. Universal flows resolve here too, since
architecture="universal" derives the product-specific universal out_dir.
Resolves strictly from this context's own out_dir. Never probes
other out dirs: a stale product universal app must not hijack
arch-specific builds' sign/package stages. Universal flows resolve
here too, since architecture="universal" derives the
product-specific universal out_dir.
"""
if self._fixed_app_path:
return self._fixed_app_path

# Debug builds may carry the dev-branded app name
if self.build_type == "debug" and IS_MACOS():
debug_app_name = f"{self.product.app_base_name} Dev.app"
Expand Down
11 changes: 0 additions & 11 deletions packages/browseros/bos_build/core/context_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,6 @@ def test_universal_architecture_resolves_universal_out_dir(self):
self.assertTrue(str(ctx.out_dir).endswith("Default_browseros_universal"))
self.assertEqual(ctx.get_app_path(), expected)

def test_fixed_app_path_short_circuits_resolution(self):
ctx = Context(
chromium_src=Path("/nonexistent-src"),
architecture="arm64",
build_type="release",
)
pinned = Path("/pinned") / ctx.BROWSEROS_APP_NAME
ctx._fixed_app_path = pinned

self.assertEqual(ctx.get_app_path(), pinned)

def test_browserclaw_context_derives_names_and_paths(self):
with (
mock.patch.object(context_mod, "IS_MACOS", return_value=True),
Expand Down
91 changes: 74 additions & 17 deletions packages/browseros/bos_build/core/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,85 @@ def plan(switches: Switches, arch: str, platform: Optional[str] = None) -> List[
sparkle_setup is a macOS build dependency even unsigned; WinSparkle
setup and the post-package sparkle_sign only exist on signed Windows
builds; unsigned Windows builds get mini_installer instead of
sign_windows; universal replaces everything after patches with
universal_build (which builds/signs/packages/uploads per arch
internally) and skips the resources copy.
sign_windows. Universal is not a flat pipeline — plan_runs() expands
it into three sequential runs.
"""
if arch == "universal":
raise ValueError("universal is planned as multiple runs; use plan_runs()")
platform = platform or get_platform()
switches = switches.resolved()

if switches.preset == "debug":
return _plan_debug(switches, arch, platform)
return _plan_release(switches, arch, platform)
return _plan_debug(switches, platform)
return _plan_release(switches, platform)


def plan_runs(
switches: Switches, platform: Optional[str] = None
) -> List[Tuple[str, List[str]]]:
"""Per-run (arch, steps) plans — the shape cli/build.py executes.

Universal expands into three sequential runs on one prepped chromium
tree (arm64 build, x64 build, merge); every other architecture list
maps to one flat plan() per arch.
"""
platform = platform or get_platform()
switches = switches.resolved()
if "universal" in switches.architectures:
if len(switches.architectures) > 1:
raise ValueError("universal cannot be combined with other architectures")
return _plan_universal_runs(switches, platform)
return [(arch, plan(switches, arch, platform)) for arch in switches.architectures]


def _plan_universal_runs(
switches: Switches, platform: str
) -> List[Tuple[str, List[str]]]:
"""Universal = three runs sharing one chromium tree.

Run 1 preps the tree once (repeating clean/git_setup/patches would
reset it) and builds arm64; run 2 rebuilds the per-arch tail for x64
(resources stages arch-specific binaries, so it repeats); run 3
merges the pair into ctx(universal)'s app path and processes it like
any other build. Error precedence preserved from the flat planner:
preset, then platform, then sign.
"""
if switches.preset == "debug":
raise ValueError("universal architecture is not supported for debug builds")
if platform != "macos":
raise ValueError("universal architecture is only supported on macos")
if not switches.sign:
raise ValueError("universal builds are always signed; drop --no-sign")

prep: List[str] = []
prep.extend(_provision_steps(switches))
prep.append("sparkle_setup")
if switches.download:
prep.append("download_resources")
prep.extend(
[
"bundled_extensions",
"chromium_replace",
"string_replaces",
"series_patches",
"patches",
]
)

arch_tail = ["resources", "configure", "compile", "sign_macos", "package_macos"]
merge_run = ["merge_universal", "sign_macos", "package_macos"]
if switches.upload:
arch_tail.append("upload")
merge_run.append("upload")

return [
("arm64", prep + arch_tail),
("x64", list(arch_tail)),
("universal", merge_run),
]

def _plan_release(switches: Switches, arch: str, platform: str) -> List[str]:

def _plan_release(switches: Switches, platform: str) -> List[str]:
steps: List[str] = []
steps.extend(_provision_steps(switches))
if platform == "macos":
Expand All @@ -104,10 +170,9 @@ def _plan_release(switches: Switches, arch: str, platform: str) -> List[str]:

if switches.download:
steps.append("download_resources")
if arch != "universal":
steps.append("resources")
steps.extend(
[
"resources",
"bundled_extensions",
"chromium_replace",
"string_replaces",
Expand All @@ -116,12 +181,6 @@ def _plan_release(switches: Switches, arch: str, platform: str) -> List[str]:
]
)

if arch == "universal":
if platform != "macos":
raise ValueError("universal architecture is only supported on macos")
steps.append("universal_build")
return steps

steps.extend(["configure", "compile"])

if platform == "macos" and switches.sign:
Expand All @@ -138,9 +197,7 @@ def _plan_release(switches: Switches, arch: str, platform: str) -> List[str]:
return steps


def _plan_debug(switches: Switches, arch: str, platform: str) -> List[str]:
if arch == "universal":
raise ValueError("universal architecture is not supported for debug builds")
def _plan_debug(switches: Switches, platform: str) -> List[str]:
steps: List[str] = []
steps.extend(_provision_steps(switches))
if switches.download:
Expand Down
Loading
Loading