diff --git a/apps/desktop/BUILDING.md b/apps/desktop/BUILDING.md index 6e02dc3a69..0acccb4ea3 100644 --- a/apps/desktop/BUILDING.md +++ b/apps/desktop/BUILDING.md @@ -140,6 +140,11 @@ Timestamp services, Azure signing, Apple notarization and publication remain online operations. Validate unsigned packaging with dependency networking denied on each target, then verify signed installers and launchers on their native hosts. +macOS packaging retains the caller's login `HOME` for keychain import and signing. +An explicit keychain path does not make Security.framework work under a scratch +home. Dependency preparation and product compilation still use the isolated home. +Hermes state and explicit dependency-cache paths remain build-owned during packaging. + ## Commit-only builds To preview a build for a pushed revision, run: diff --git a/scripts/bundles/desktop.py b/scripts/bundles/desktop.py index fde83d94c9..81eec9b136 100644 --- a/scripts/bundles/desktop.py +++ b/scripts/bundles/desktop.py @@ -59,7 +59,7 @@ def build_prepared(path: Path, builder_args: list[str], variant: str | None = No def _build_prepared(prepared, builder_args: list[str], variant: str | None) -> None: prepared.validate() - from scripts.bundles.desktop_inputs import build_environment, select_variant + from scripts.bundles.desktop_inputs import build_environment, packaging_environment, select_variant from scripts.bundles.native import finish_native request = prepared.request @@ -109,7 +109,8 @@ def _build_prepared(prepared, builder_args: list[str], variant: str | None) -> N if metadata["file"]: version_args = [f'-c.extraMetadata.shortVersion={metadata["file"]}', f'-c.extraMetadata.shortVersionWindows={metadata["file"]}'] require_source(repo, request.commit) - run([node, "scripts/run-electron-builder.mjs", *package_args, *version_args, *builder_args], cwd=desktop, env=env) + run([node, "scripts/run-electron-builder.mjs", *package_args, *version_args, *builder_args], cwd=desktop, + env=packaging_environment(env, os.environ, request.target)) def main() -> None: diff --git a/scripts/bundles/desktop_inputs.py b/scripts/bundles/desktop_inputs.py index 2e6a649213..10d090ae4d 100644 --- a/scripts/bundles/desktop_inputs.py +++ b/scripts/bundles/desktop_inputs.py @@ -54,6 +54,16 @@ def build_environment(prepared: PreparedDesktop, variant: str, inherited: Mappin return env +def packaging_environment(build: Mapping[str, str], inherited: Mapping[str, str], + target: str) -> dict[str, str]: + env = dict(build) + if target.startswith("darwin-"): + # Security.framework needs the login HOME for both key import and signing, + # even with an explicit keychain. Keep dependency preparation isolated. + env["HOME"] = inherited.get("HERMES_REAL_HOME") or inherited.get("HOME") or str(Path.home()) + return env + + def select_variant(prepared: PreparedDesktop, variant: str | None) -> str: from scripts.termux.deb_version import channel_for_tag diff --git a/tests/scripts/test_desktop_toolchain.py b/tests/scripts/test_desktop_toolchain.py index e8b682ee61..749677fe49 100644 --- a/tests/scripts/test_desktop_toolchain.py +++ b/tests/scripts/test_desktop_toolchain.py @@ -150,6 +150,25 @@ def test_bootstrap_real_pm_resolves_only_build_owned_state(tmp_path): assert all(path.is_relative_to(work) for path in (partials, uv_default, state)) +@pytest.mark.parametrize("target", ["darwin-arm64", "darwin-x64", "linux-x64", "win32-arm64"]) +def test_packaging_preserves_keychain_home_without_retargeting_build_state(tmp_path, target): + from scripts.bundles.desktop_inputs import packaging_environment + from scripts.bundles.desktop_toolchain import bootstrap_environment + + source, work, cache = (tmp_path / name for name in ("source", "work", "cache")) + login_home = str(tmp_path / "login") + inherited = {"HOME": login_home, "CSC_KEYCHAIN": "explicit.keychain"} + isolated = bootstrap_environment(source, work, cache, inherited) + before = isolated.copy() + for caller in (inherited, {**inherited, "HOME": str(work / "launcher-home"), + "HERMES_REAL_HOME": login_home}, {}): + packaged = packaging_environment(isolated, caller, target) + expected_home = (caller.get("HERMES_REAL_HOME") or caller.get("HOME") or str(Path.home()) + if target.startswith("darwin-") else isolated["HOME"]) + assert packaged == {**isolated, "HOME": expected_home} + assert isolated == before + + @pytest.mark.platforms("linux", "macos", "windows") def test_prepare_tools_uses_pm_native_pins_and_separate_cache(tmp_path, monkeypatch): import pm