Dependency acquisition during packaging left native wheels and packager inputs outside the pre-build cache save. Compose PM and existing providers into a preparation phase, then require builds to consume admitted inputs. Share native preparation with PM Bundle. Keep path-bound environments and signing outputs separate from reusable caches. Use read-only cache tokens for commit builds and preserve the one-command local build path. Verify pinned tools through PM, probe PTYs under the prepared Electron, and supply dmgbuild through a build-only PM package. Resolve bundled tool stores from their payload manifest so relocation preserves discovery. Validation: focused Python and JS tests, checkJs, Ruff, Windows checks, anti-slop, cache relocation, and network-denied Linux AppImage builds. Relocated runtime smoke passed with NixOS host libraries supplied. Native Windows/macOS signing and live GitHub cache behavior remain untested.
115 lines
5.1 KiB
Python
115 lines
5.1 KiB
Python
"""Prepare or consume the standalone PM payload, without desktop products."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
def prepare(source: Path, work: Path, cache: Path, out: Path, ref: str) -> Path:
|
|
from hermes_cli.runtime_state import _lock
|
|
from scripts.bundles.desktop_prepare import git, require_source
|
|
from scripts.bundles.desktop_toolchain import run_preparation
|
|
from scripts.bundles.native_prepared import prepared_path
|
|
from pm.lock import _write
|
|
|
|
for path in (work, cache, out):
|
|
if path.absolute() != path.resolve():
|
|
raise ValueError("native build roots must not be symlinked")
|
|
source, work, cache, out = (path.resolve() for path in (source, work, cache, out))
|
|
revision = git(source, "rev-parse", "--verify", f"{ref}^{{commit}}")
|
|
require_source(source, revision)
|
|
for destination in (work, cache, out):
|
|
if source.is_relative_to(destination):
|
|
raise ValueError("native build output must not contain the source checkout")
|
|
for left, right in ((work, cache), (out, cache), (work, out)):
|
|
if left.is_relative_to(right) or right.is_relative_to(left):
|
|
raise ValueError("native work, cache and output must be separate directories")
|
|
owner = work / ".native-preparation"
|
|
if work.exists():
|
|
if not owner.is_file() or owner.read_text(encoding="utf-8-sig") != str(source):
|
|
raise ValueError("native work directory is not owned by this checkout")
|
|
else:
|
|
work.mkdir(parents=True)
|
|
owner.write_text(str(source), encoding="utf-8")
|
|
with (work / ".lock").open("a+b") as lock:
|
|
if not _lock(lock.fileno(), wait=False):
|
|
raise ValueError("native preparation work directory is already in use")
|
|
result = prepared_path(out)
|
|
result.unlink(missing_ok=True)
|
|
request = work / "request.json"
|
|
_write(request, {"source": str(source), "work": str(work), "cache": str(cache),
|
|
"out": str(out), "ref": revision})
|
|
status = run_preparation(source, work, cache, request, worker=Path(__file__).resolve())
|
|
if status:
|
|
result.unlink(missing_ok=True)
|
|
raise RuntimeError(f"native preparation failed (exit {status})")
|
|
if not result.is_file():
|
|
raise RuntimeError("native preparation did not publish its result")
|
|
return result
|
|
|
|
|
|
def prepare_in_worker(request: dict) -> Path:
|
|
from scripts.bundles.desktop_prepare import require_source
|
|
from scripts.bundles.desktop_toolchain import prepare_tools
|
|
from scripts.bundles.native import prepare_native
|
|
|
|
source, work, cache, out = (Path(request[name]) for name in ("source", "work", "cache", "out"))
|
|
require_source(source, request["ref"])
|
|
_, _, env = prepare_tools(source, work, cache, os.environ)
|
|
return prepare_native(out=out, ref=request["ref"], source=source,
|
|
cache=Path(env["UV_CACHE_DIR"]), tools=cache / "tools", env=env)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--source", type=Path)
|
|
parser.add_argument("--work", type=Path)
|
|
parser.add_argument("--cache", type=Path)
|
|
parser.add_argument("--out", type=Path)
|
|
selection = parser.add_mutually_exclusive_group()
|
|
selection.add_argument("--ref")
|
|
selection.add_argument("--commit")
|
|
parser.add_argument("--prepare-only", action="store_true")
|
|
parser.add_argument("--prepared", type=Path)
|
|
parser.add_argument("--request", type=Path, help=argparse.SUPPRESS)
|
|
parser.add_argument("--worker", action="store_true", help=argparse.SUPPRESS)
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.worker:
|
|
if not args.request:
|
|
parser.error("worker requires --request")
|
|
prepare_in_worker(json.loads(args.request.read_text(encoding="utf-8-sig")))
|
|
return 0
|
|
if args.request:
|
|
parser.error("--request is worker-only")
|
|
if args.prepared:
|
|
if any((args.source, args.work, args.cache, args.out, args.ref, args.commit, args.prepare_only)):
|
|
parser.error("--prepared supplies the complete native build request")
|
|
prepared = args.prepared.absolute()
|
|
else:
|
|
if not all((args.source, args.work, args.cache, args.out)):
|
|
parser.error("preparation requires --source, --work, --cache and --out")
|
|
if args.commit:
|
|
from scripts.releases.commit_build import require_commit
|
|
require_commit(args.commit)
|
|
prepared = prepare(args.source, args.work, args.cache, args.out, args.commit or args.ref or "HEAD")
|
|
if args.prepare_only:
|
|
print(prepared)
|
|
return 0
|
|
from scripts.bundles.native import finish_native
|
|
return finish_native(prepared, {})
|
|
except (ValueError, OSError, RuntimeError, subprocess.CalledProcessError) as exc:
|
|
parser.exit(1, f"native build: {exc}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|