From 9f19f9916626de6dc27dc248b53b877249b9fdff Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:46:01 +0530 Subject: [PATCH] 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. --- hermes_cli/main_desktop.py | 24 +++++++++++++++++++ hermes_cli/source_build.py | 48 +++++++++++++++++++++----------------- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/hermes_cli/main_desktop.py b/hermes_cli/main_desktop.py index 6b59fa966a..88b4eeacb4 100644 --- a/hermes_cli/main_desktop.py +++ b/hermes_cli/main_desktop.py @@ -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") diff --git a/hermes_cli/source_build.py b/hermes_cli/source_build.py index 726250366a..4af113a369 100644 --- a/hermes_cli/source_build.py +++ b/hermes_cli/source_build.py @@ -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.