fix(update): reuse the packaged desktop app when HEAD has not moved
build_update_products rebuilt, repackaged and reinstalled the desktop app on every completion, including 'Already up to date', retried tails and takeover re-entries, and could stop a running app to free the output. Reuse it when the commit the packaged app ships (its baked install-stamp.json) is HEAD and its freshness receipt is current. Freshness alone is not enough: the baked stamp is an input only the build rewrites, so after a pull that touched no other desktop input the receipt reads current while the app names the previous commit.
This commit is contained in:
@@ -125,6 +125,30 @@ def _desktop_build_needed(desktop_dir: Path, project_root: Path, *, source_mode:
|
||||
return dist_dir is None or not source_product_current(project_root, "desktop", dist_dir)
|
||||
|
||||
|
||||
def _packaged_desktop_current_for_head(desktop_dir: Path, project_root: Path) -> bool:
|
||||
"""True when the packaged app was built from HEAD and none of its inputs changed since.
|
||||
|
||||
Freshness alone cannot answer this for an update: the baked ``install-stamp.json`` is a
|
||||
desktop input that only the build itself rewrites, so after a pull that touched no other
|
||||
desktop input the receipt still reads current while the app names the previous commit.
|
||||
The commit the packaged app actually ships is the missing half; anything unreadable
|
||||
means "build".
|
||||
"""
|
||||
dist_dir = _renderer_bundle_dir(desktop_dir, source_mode=False)
|
||||
if dist_dir is None:
|
||||
return False
|
||||
try:
|
||||
stamp = json.loads((dist_dir.parent.parent / "install-stamp.json").read_text(encoding="utf-8-sig"))
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
from hermes_cli.version_info import _run_git
|
||||
|
||||
commit = stamp.get("commit") if isinstance(stamp, dict) else None
|
||||
if not isinstance(commit, str) or commit != _run_git(project_root, "rev-parse", "HEAD"):
|
||||
return False
|
||||
return not _desktop_build_needed(desktop_dir, project_root, source_mode=False)
|
||||
|
||||
|
||||
def _desktop_packaged_executable(desktop_dir: Path) -> Optional[Path]:
|
||||
"""Return the current platform's unpacked Electron app executable."""
|
||||
return _desktop_packaged_executable_in(desktop_dir / "release")
|
||||
|
||||
@@ -115,9 +115,10 @@ def build_update_products(project_root: Path, *, desktop: bool) -> None:
|
||||
publish_stage("Updating Node dependencies")
|
||||
prepare_source_dependencies(project_root, workspaces, env=env, explicit=True)
|
||||
# An update that changed no TUI/web input reuses the receipted output, as the
|
||||
# launch path already does; recompiling it produces the same bytes. Desktop has
|
||||
# no such skip: its baked install stamp carries the commit, so every update
|
||||
# that moves HEAD changes a desktop input anyway.
|
||||
# launch path already does; recompiling it produces the same bytes. Desktop
|
||||
# additionally needs the packaged app to name HEAD (its baked stamp carries the
|
||||
# commit), so it is reused only when HEAD did not move: "Already up to date",
|
||||
# a retried tail, a takeover re-entry.
|
||||
if "ui-tui" in frontends:
|
||||
if source_product_current(project_root, "tui", project_root / "ui-tui/dist"):
|
||||
print(" ✓ TUI is up to date")
|
||||
@@ -131,28 +132,33 @@ def build_update_products(project_root: Path, *, desktop: bool) -> None:
|
||||
publish_stage("Building the web UI")
|
||||
build_source_web(project_root, env=env)
|
||||
if desktop:
|
||||
from hermes_cli.main_desktop import _refresh_installed_desktop_apps, build_prepared_desktop
|
||||
from hermes_cli.main_desktop import (
|
||||
_packaged_desktop_current_for_head, _refresh_installed_desktop_apps, build_prepared_desktop)
|
||||
|
||||
publish_stage("Building the desktop app")
|
||||
# The desktop build mutates checkout-scoped node_modules and
|
||||
# apps/desktop/release; serialize it against a concurrent manual
|
||||
# `hermes desktop` (#93940). The update path waits rather than exits:
|
||||
# the in-flight build it queues behind produces the same fresh tree
|
||||
# this update needs.
|
||||
from hermes_cli.desktop_build_lock import DesktopBuildLock
|
||||
desktop_dir = project_root / "apps/desktop"
|
||||
if _packaged_desktop_current_for_head(desktop_dir, project_root):
|
||||
print(" ✓ Desktop app is up to date")
|
||||
else:
|
||||
publish_stage("Building the desktop app")
|
||||
# The desktop build mutates checkout-scoped node_modules and
|
||||
# apps/desktop/release; serialize it against a concurrent manual
|
||||
# `hermes desktop` (#93940). The update path waits rather than exits:
|
||||
# the in-flight build it queues behind produces the same fresh tree
|
||||
# this update needs.
|
||||
from hermes_cli.desktop_build_lock import DesktopBuildLock
|
||||
|
||||
build_lock = DesktopBuildLock(project_root)
|
||||
build_lock.acquire(wait=True)
|
||||
try:
|
||||
build_prepared_desktop(
|
||||
project_root / "apps/desktop", source_mode=False,
|
||||
npm=shutil.which("npm", path=env["PATH"]), env=env, icons=project_root,
|
||||
)
|
||||
finally:
|
||||
build_lock.release()
|
||||
build_lock = DesktopBuildLock(project_root)
|
||||
build_lock.acquire(wait=True)
|
||||
try:
|
||||
build_prepared_desktop(
|
||||
desktop_dir, source_mode=False,
|
||||
npm=shutil.which("npm", path=env["PATH"]), env=env, icons=project_root,
|
||||
)
|
||||
finally:
|
||||
build_lock.release()
|
||||
# A current release/ can still sit beside a stale installed copy (an earlier
|
||||
# update rebuilt but never installed); healing must not wait for the next build.
|
||||
_refresh_installed_desktop_apps(project_root / "apps/desktop")
|
||||
_refresh_installed_desktop_apps(desktop_dir)
|
||||
# A configured memory provider that no longer ships in core is installed from the
|
||||
# catalog for every profile home sharing this venv (config, data and tool names
|
||||
# unchanged). The update must finish even if the migration blows up.
|
||||
|
||||
Reference in New Issue
Block a user