electron-builder resolves every piece of packaging artwork from the workspace
— the app icon, the icon.ico extraResource, the MSIX logo set before-build
stages into build/appx, and the exe identity stamp — so a flavored canary or
commit build copies its rendered icon set over apps/desktop/assets before
packaging.
Those files stopped being gitignored in 4b7229d612, which turned a write that
was invisible into 15 dirty paths. The source custody check then refuses the
checkout twice: inside the same build, at the check that used to sit after the
copy, and on every later build from the first check on a now permanently dirty
tree.
Hold the render at the workspace path for the packaging window only and
restore the admitted files afterwards, whether packaging finished or failed.
The final custody check now asserts the build handed the checkout back.
276 lines
14 KiB
Python
276 lines
14 KiB
Python
"""Prepared desktop inputs bind a source and reject stale files before building."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
def _project(tmp_path: Path) -> tuple[Path, str]:
|
|
from pm.store import current_target
|
|
source = tmp_path / "source with spaces"
|
|
source.mkdir()
|
|
(source / "pyproject.toml").write_text('[project]\nname="fixture"\nversion="1.2.3"\n', encoding="utf-8")
|
|
(source / "package.json").write_text('{"workspaces": ["apps/desktop"]}', encoding="utf-8")
|
|
(source / "package-lock.json").write_text('{"packages": {}}', encoding="utf-8")
|
|
(source / ".gitignore").write_text(".build/\n", encoding="utf-8")
|
|
(source / "pm").mkdir()
|
|
(source / "pm/lock.json").write_text(json.dumps({"schema": 1, "packages": {
|
|
name: {"version": "1.0.0", "artifacts": {current_target(): {"url": "https://example.test/tool", "sha256": "a" * 64}}}
|
|
for name in ("python", "node", "npm")
|
|
}}), encoding="utf-8")
|
|
subprocess.run(["git", "init", str(source)], check=True, capture_output=True)
|
|
subprocess.run(["git", "add", "."], cwd=source, check=True)
|
|
subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "fixture"], cwd=source, check=True, capture_output=True)
|
|
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip()
|
|
return source, commit
|
|
|
|
|
|
def test_stable_build_accepts_the_admitted_commit_before_the_final_tag_exists(tmp_path, monkeypatch):
|
|
from scripts.bundles.desktop_inputs import identity_environment
|
|
from scripts.bundles.desktop_prepare import BuildRequest
|
|
|
|
source, commit = _project(tmp_path)
|
|
subprocess.run([
|
|
"git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
|
|
"-c", "tag.gpgSign=false", "tag", "-a", "rc.1-v1.2.4", "-m", "claim",
|
|
], cwd=source, check=True, env={**__import__("os").environ,
|
|
"GIT_COMMITTER_DATE": "2026-08-29T01:02:03Z"})
|
|
monkeypatch.setenv("RELEASE_CLAIM_TAG", "rc.1-v1.2.4")
|
|
claim_object = subprocess.check_output(
|
|
["git", "rev-parse", "rc.1-v1.2.4"], cwd=source, text=True,
|
|
).strip()
|
|
monkeypatch.setenv("RELEASE_CLAIM_OBJECT", claim_object)
|
|
request = BuildRequest.create(
|
|
source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
|
|
work=tmp_path / "work", cache=tmp_path / "cache", bundle_env={},
|
|
)
|
|
|
|
assert request.commit == commit
|
|
assert request.version == "1.2.4"
|
|
# The payload identity stays plain; the attempt ref lives only in the claim env.
|
|
assert request.tag == "v1.2.4"
|
|
assert request.release_epoch == 1787965323
|
|
env = identity_environment(request, "bundled", {"HERMES_RELEASE_EPOCH": "1"})
|
|
assert env["HERMES_RELEASE_EPOCH"] == "1787965323"
|
|
|
|
monkeypatch.setenv("RELEASE_CLAIM_OBJECT", "f" * 40)
|
|
with pytest.raises(ValueError, match="exact claim tag object"):
|
|
BuildRequest.create(
|
|
source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
|
|
work=tmp_path / "wrong-work", cache=tmp_path / "wrong-cache", bundle_env={},
|
|
)
|
|
|
|
|
|
def test_native_version_capture_uses_admitted_build_environment(tmp_path, monkeypatch):
|
|
from scripts.bundles.desktop import capture
|
|
|
|
monkeypatch.delenv("HERMES_RELEASE_EPOCH", raising=False)
|
|
env = {**os.environ, "HERMES_RELEASE_EPOCH": "1787965323"}
|
|
script = "import os; print(os.environ['HERMES_RELEASE_EPOCH'])"
|
|
assert capture([sys.executable, "-c", script], tmp_path, env) == env["HERMES_RELEASE_EPOCH"]
|
|
|
|
|
|
def test_stable_build_rejects_a_claim_tag_for_another_version(tmp_path, monkeypatch):
|
|
from scripts.bundles.desktop_prepare import BuildRequest
|
|
|
|
source, commit = _project(tmp_path)
|
|
subprocess.run([
|
|
"git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
|
|
"-c", "tag.gpgSign=false", "tag", "-a", "rc.2-v1.2.5", "-m", "claim",
|
|
], cwd=source, check=True, env={**__import__("os").environ,
|
|
"GIT_COMMITTER_DATE": "2026-08-29T01:02:03Z"})
|
|
monkeypatch.setenv("RELEASE_CLAIM_TAG", "rc.2-v1.2.5")
|
|
monkeypatch.setenv("RELEASE_CLAIM_OBJECT", subprocess.check_output(
|
|
["git", "rev-parse", "rc.2-v1.2.5"], cwd=source, text=True).strip())
|
|
with pytest.raises(ValueError, match="exact claim tag"):
|
|
BuildRequest.create(
|
|
source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
|
|
work=tmp_path / "work", cache=tmp_path / "cache", bundle_env={},
|
|
)
|
|
|
|
|
|
def test_prepared_input_roundtrip_rejects_mutation_and_foreign_source(tmp_path):
|
|
from scripts.bundles.desktop_prepare import BuildRequest, PreparedDesktop
|
|
|
|
source, commit = _project(tmp_path)
|
|
work = tmp_path / "work"
|
|
cache = tmp_path / "cache"
|
|
request = BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
|
|
from pm.lock import Facts
|
|
from pm.store import tree_digest
|
|
from pm.registry import get_package
|
|
store = cache / "tools"
|
|
binaries = {}
|
|
for name in ("python", "node", "npm"):
|
|
package = get_package(name)
|
|
entry = store / package.store_entry("1.0.0", request.target)
|
|
binary = package.binary(entry, request.target)
|
|
assert binary is not None
|
|
binary.parent.mkdir(parents=True)
|
|
binary.write_bytes(b"published tool fixture")
|
|
binaries[name] = binary
|
|
Facts(store / "facts.json").record(name, "1.0.0", entry.name, package.env(entry, request.target), store,
|
|
target=request.target, artifacts=["a" * 64], digest=tree_digest(entry))
|
|
work.mkdir()
|
|
executable, node = binaries["python"], binaries["node"]
|
|
icons = work / "icon-environment" / "bin" / "python"
|
|
icons.parent.mkdir(parents=True)
|
|
icons.write_bytes(b"prepared icon interpreter")
|
|
icon_library = work / "icon-environment" / "library"
|
|
icon_library.write_bytes(b"prepared library")
|
|
native = work / "native"
|
|
native.mkdir()
|
|
(native / "binding.node").write_bytes(b"prepared native input")
|
|
packager = work / "packager.json"
|
|
packager.write_text("{}", encoding="utf-8")
|
|
prepared = PreparedDesktop.record(request, python=executable, node=node, icon_python=icons,
|
|
native=native, packager=packager, payload=None, native_toolchain="fixture-toolchain")
|
|
path = work / "prepared.json"
|
|
prepared.write(path)
|
|
restored = PreparedDesktop.load(path)
|
|
restored.validate()
|
|
assert restored.request.commit == commit
|
|
assert restored.request.version == "1.2.3"
|
|
assert "environment" not in json.loads(path.read_text())
|
|
icon_library.unlink()
|
|
with pytest.raises(ValueError, match="changed|stale|missing"):
|
|
restored.validate()
|
|
icon_library.write_bytes(b"prepared library")
|
|
(native / "binding.node").write_bytes(b"changed")
|
|
with pytest.raises(ValueError, match="changed|stale"):
|
|
restored.validate()
|
|
(native / "binding.node").write_bytes(b"prepared native input")
|
|
(source / "pyproject.toml").write_text('[project]\nname="fixture"\nversion="9.9.9"\n', encoding="utf-8")
|
|
with pytest.raises(ValueError, match="source|checkout"):
|
|
restored.validate()
|
|
|
|
|
|
def test_prepared_paths_cannot_escape_their_owned_roots(tmp_path):
|
|
from scripts.bundles.desktop_prepare import BuildRequest, PreparedDesktop
|
|
|
|
source, commit = _project(tmp_path)
|
|
work, cache = tmp_path / "work", tmp_path / "cache"
|
|
request = BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
|
|
external = tmp_path / "external"
|
|
external.write_bytes(b"not preparation-owned")
|
|
with pytest.raises(ValueError, match="outside|owned"):
|
|
PreparedDesktop.record(request, python=external, node=external, icon_python=external,
|
|
native=external, packager=external, payload=None, native_toolchain="fixture-toolchain")
|
|
|
|
|
|
def test_prepared_cli_rejects_missing_result_without_provisioning(tmp_path):
|
|
source = Path(__file__).resolve().parents[2]
|
|
absent = tmp_path / "absent" / "prepared.json"
|
|
result = subprocess.run([sys.executable, str(source / "scripts/bundles/desktop.py"), "--prepared", str(absent)],
|
|
cwd=tmp_path, capture_output=True, text=True, timeout=30)
|
|
assert result.returncode != 0
|
|
assert "preparation" in result.stderr.lower()
|
|
assert "unrecognized arguments" not in result.stderr
|
|
assert not absent.parent.exists()
|
|
|
|
|
|
def test_source_admission_rejects_dirty_checkout_and_invalid_store_before_writes(tmp_path):
|
|
from scripts.bundles.desktop_prepare import BuildRequest
|
|
|
|
source, commit = _project(tmp_path)
|
|
work = tmp_path / "work"
|
|
cache = tmp_path / "cache"
|
|
for tag, selected in [(None, commit), ("v1.2.0+canary.20260911T120000Z", None)]:
|
|
with pytest.raises(ValueError, match="Store.*stable"):
|
|
BuildRequest.create(source, tag=tag, commit=selected, variant="store", work=work, cache=cache, bundle_env={})
|
|
(source / "package.json").write_text("{}", encoding="utf-8")
|
|
(source / "pyproject.toml").write_text('staged fixture contents', encoding="utf-8")
|
|
subprocess.run(["git", "add", "pyproject.toml"], cwd=source, check=True)
|
|
(source / "generated output").mkdir()
|
|
(source / "generated output" / "receipt.json").write_text('untracked fixture contents', encoding="utf-8")
|
|
with pytest.raises(ValueError, match="source|checkout") as rejected:
|
|
BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
|
|
status = subprocess.check_output(
|
|
["git", "status", "--porcelain", "--untracked-files=all"], cwd=source, text=True,
|
|
).rstrip("\r\n")
|
|
assert str(rejected.value).endswith("\n" + status)
|
|
assert 'generated output/receipt.json' in str(rejected.value)
|
|
assert 'fixture contents' not in str(rejected.value)
|
|
assert not work.exists()
|
|
assert not cache.exists()
|
|
|
|
|
|
def test_preparation_never_reuses_an_unowned_work_directory(tmp_path):
|
|
from scripts.bundles.desktop_prepare import BuildRequest, prepare
|
|
|
|
source, commit = _project(tmp_path)
|
|
work = tmp_path / "someone-elses-files"
|
|
work.mkdir()
|
|
precious = work / "request.json"
|
|
precious.write_text("do not replace", encoding="utf-8")
|
|
request = BuildRequest.create(source, tag=None, commit=commit, variant="light",
|
|
work=work, cache=tmp_path / "cache", bundle_env={})
|
|
with pytest.raises(ValueError, match="owned"):
|
|
prepare(request)
|
|
assert precious.read_text() == "do not replace"
|
|
|
|
|
|
def test_checkout_lock_excludes_a_second_build_process(tmp_path):
|
|
from scripts.bundles.desktop_inputs import build_lock
|
|
|
|
source, _ = _project(tmp_path)
|
|
project = Path(__file__).resolve().parents[2]
|
|
probe = (
|
|
"import sys; from pathlib import Path; sys.path.insert(0,sys.argv[1]); "
|
|
"from scripts.bundles.desktop_inputs import build_lock; "
|
|
"lock=build_lock(Path(sys.argv[2])); lock.__enter__(); print('acquired'); lock.__exit__(None,None,None)"
|
|
)
|
|
command = [sys.executable, "-I", "-c", probe, str(project), str(source)]
|
|
with build_lock(source):
|
|
blocked = subprocess.run(command, capture_output=True, text=True, timeout=30)
|
|
assert blocked.returncode != 0 and "another desktop" in blocked.stderr
|
|
released = subprocess.run(command, capture_output=True, text=True, timeout=30)
|
|
assert released.returncode == 0, released.stderr
|
|
assert released.stdout.strip() == "acquired"
|
|
|
|
|
|
def test_flavored_icon_staging_hands_the_admitted_checkout_back(tmp_path):
|
|
from scripts.bundles.desktop import flavored_assets
|
|
from scripts.bundles.desktop_prepare import require_source
|
|
|
|
source, commit = _project(tmp_path)
|
|
assets = source / "apps/desktop/assets"
|
|
(assets / "appx").mkdir(parents=True)
|
|
(assets / "icon.png").write_bytes(b"admitted icon")
|
|
(assets / "appx/Square150x150Logo.png").write_bytes(b"admitted tile")
|
|
subprocess.run(["git", "add", "."], cwd=source, check=True)
|
|
subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
|
|
"commit", "-m", "icons"], cwd=source, check=True, capture_output=True)
|
|
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip()
|
|
|
|
rendered = tmp_path / "products/icons"
|
|
(rendered / "appx").mkdir(parents=True)
|
|
(rendered / "icon.png").write_bytes(b"canary icon")
|
|
(rendered / "icon-mac.png").write_bytes(b"canary dock icon")
|
|
(rendered / "appx/Square150x150Logo.png").write_bytes(b"canary tile")
|
|
|
|
# Packaging reads artwork from the workspace path, so the render is live
|
|
# there — and the custody check rightly refuses the checkout in that window.
|
|
with flavored_assets(rendered, assets):
|
|
assert (assets / "icon.png").read_bytes() == b"canary icon"
|
|
assert (assets / "icon-mac.png").read_bytes() == b"canary dock icon"
|
|
with pytest.raises(ValueError, match="clean source checkout"):
|
|
require_source(source, commit)
|
|
|
|
# A finished or failed package leaves the admitted files back in place, with
|
|
# render-only additions removed; a dirty tree breaks the next build.
|
|
assert (assets / "icon.png").read_bytes() == b"admitted icon"
|
|
assert (assets / "appx/Square150x150Logo.png").read_bytes() == b"admitted tile"
|
|
assert not (assets / "icon-mac.png").exists()
|
|
require_source(source, commit)
|
|
|
|
with pytest.raises(RuntimeError, match="packaging failed"):
|
|
with flavored_assets(rendered, assets):
|
|
raise RuntimeError("packaging failed")
|
|
require_source(source, commit)
|