mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
Electron 42 dropped the postinstall that unpacked its binary into
node_modules/electron/dist. The binary now appears on the first
require('electron'), so parallel vitest workers race to unpack it
("Text file busy" in #128267's CI), and `hermes desktop --source` finds
no path.txt to launch.
electron-runtime.mjs runs Electron's own install.js once: after npm ci
for the desktop workspace, and in CI before the parallel checks. It first
checks the installed package against the desktop pin and the lockfile,
and does nothing when the runtime is already in place. On the current
40.10.2 pin, whose postinstall still unpacks the binary, it is a no-op.
Salvaged from #137648 (the runtime-preparation subset only).
119 lines
5.8 KiB
Python
119 lines
5.8 KiB
Python
"""Describe the desktop dependency snapshot using only the runner's stdlib.
|
|
|
|
Actions transports provider directories directly, including npm's existing
|
|
receipt. Restored bytes are candidates: preparation must still admit them.
|
|
This module never provisions tools or certifies a cache hit as prepared.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import re
|
|
import sys
|
|
|
|
if __package__ in (None, ""):
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
|
|
from scripts.ci.setup_toolchain import current_target, file_commands
|
|
|
|
|
|
CACHE_DOMAINS = ("tools", "python/runtime", "python/build", "npm/_cacache", "native", "packager")
|
|
_INPUT_FILES = (
|
|
"package-lock.json", "pyproject.toml", "uv.lock", "pm/lock.json", "pm/pyproject.toml", "pm/uv.lock",
|
|
"scripts/ci/desktop_build_cache.py", "scripts/ci/setup_toolchain.py",
|
|
"scripts/bundles/desktop_prepare.py", "scripts/bundles/desktop_toolchain.py",
|
|
"scripts/bundles/desktop_inputs.py", "scripts/bundles/native.py", "scripts/bundles/native_prepared.py",
|
|
"scripts/bundles/native_build.py",
|
|
"scripts/build/node-deps.mjs", "scripts/build/electron-runtime.mjs", "scripts/build/icon_environment.py",
|
|
"pm/native_build.py", "scripts/windows-build-deps.ps1",
|
|
"apps/desktop/scripts/stage-native-deps.mjs", "apps/desktop/scripts/prepare-packaging-tools.mjs",
|
|
"apps/desktop/scripts/build-command-screenshot-monitor.mjs",
|
|
"apps/desktop/scripts/build-hud-modifier-monitor.mjs",
|
|
"apps/desktop/electron/native/command-screenshot-monitor.m",
|
|
"apps/desktop/electron/native/hud-modifier-gesture.h",
|
|
"apps/desktop/electron/native/hud-modifier-gesture.cs",
|
|
"apps/desktop/electron/native/hud-modifier-monitor.m",
|
|
"apps/desktop/electron/native/hud-modifier-monitor-win.cs",
|
|
"apps/desktop/electron/native/hud-modifier-monitor-x11.c",
|
|
"apps/desktop/scripts/windows-bundle-tools.mjs", "apps/desktop/scripts/prepared-native-deps.mjs",
|
|
"apps/desktop/scripts/prepared-packaging.mjs", "apps/desktop/scripts/prepare-dmgbuild.mjs",
|
|
"apps/desktop/scripts/prepare_dmgbuild.py", "apps/desktop/scripts/probe-prepared-native.mjs",
|
|
)
|
|
|
|
|
|
def _input_digest(source: Path, lock: dict) -> str:
|
|
# A coarse lookup hint, not a second receipt or dependency resolver. Owner
|
|
# admission remains necessary even when this digest matches exactly.
|
|
files: set[str] = set(_INPUT_FILES)
|
|
files.update(str(path.relative_to(source)) for path in (source / "pm").glob("*.py"))
|
|
files.update(str(Path(path) / "package.json") for path in lock["packages"]
|
|
if "node_modules" not in path.split("/"))
|
|
digest = hashlib.sha256()
|
|
for name in sorted(files):
|
|
path = source / name
|
|
digest.update(name.encode() + b"\0")
|
|
digest.update(path.read_bytes() if path.is_file() else b"<missing>")
|
|
digest.update(b"\0")
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _cache_path(path: Path) -> str:
|
|
if any(char in str(path) for char in "*?[]!\r\n\0") or path.resolve() != path:
|
|
raise ValueError(f"unsafe cache path: {path}")
|
|
return str(path)
|
|
|
|
|
|
def describe_cache(source: Path, cache: Path, producer: str, *, work: Path | None = None) -> dict:
|
|
source, cache = source.resolve(), cache.resolve()
|
|
if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}", producer):
|
|
raise ValueError("producer must be a nonempty cache namespace")
|
|
if os.environ.get("HERMES_HOME") and cache.is_relative_to(Path(os.environ["HERMES_HOME"]).resolve()):
|
|
raise ValueError("cache must be separate from HERMES_HOME")
|
|
payload_only = producer == "payload-test"
|
|
lock = {"packages": {}} if payload_only else json.loads(
|
|
(source / "package-lock.json").read_text(encoding="utf-8-sig"))
|
|
# node_modules is never transported: npm ci deletes it before installing, so
|
|
# only npm's content cache (npm/_cacache) saves work on the next run.
|
|
domains = ("tools", "python/runtime", "native") if payload_only else CACHE_DOMAINS
|
|
paths = [_cache_path(cache / domain) for domain in domains]
|
|
private_roots = {"source": source, "work": work,
|
|
"HERMES_HOME": Path(os.environ["HERMES_HOME"]) if os.environ.get("HERMES_HOME") else None}
|
|
for name, private in private_roots.items():
|
|
if private is not None and any(private.resolve().is_relative_to(Path(path)) for path in paths):
|
|
raise ValueError(f"{name} must be outside every reusable cache path")
|
|
target = current_target()
|
|
host = hashlib.sha256(json.dumps([
|
|
platform.platform(), os.environ.get("ImageOS", ""), os.environ.get("ImageVersion", ""),
|
|
]).encode()).hexdigest()[:16]
|
|
prefix = f"desktop-inputs-v1-{producer}-{target}-{host}-"
|
|
input_prefix = f"{prefix}{_input_digest(source, lock)}-"
|
|
run = "-".join(os.environ.get(name, default) for name, default in (
|
|
("GITHUB_RUN_ID", "local"), ("GITHUB_RUN_ATTEMPT", "1"), ("GITHUB_JOB", "desktop"),
|
|
))
|
|
return {"target": target, "paths": paths, "restore_keys": [input_prefix, prefix],
|
|
"key": input_prefix + run}
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("phase", choices=["describe"])
|
|
parser.add_argument("--source", type=Path, required=True)
|
|
parser.add_argument("--cache", type=Path, required=True)
|
|
parser.add_argument("--work", type=Path)
|
|
parser.add_argument("--producer", required=True)
|
|
args = parser.parse_args()
|
|
description = describe_cache(args.source, args.cache, args.producer, work=args.work)
|
|
if os.environ.get("GITHUB_OUTPUT"):
|
|
file_commands("GITHUB_OUTPUT", {
|
|
"cache-key": description["key"], "cache-paths": json.dumps(description["paths"]),
|
|
"input-prefix": description["restore_keys"][0], "restore-prefix": description["restore_keys"][1],
|
|
})
|
|
print(json.dumps(description))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |