fix: keep venv interpreter path in desktop entry Exec - #94051
GitTradWang wants to merge 2 commits into
Conversation
resolve_exec_command() used Path.resolve() on sys.executable, which follows the venv's bin/python symlink (uv and pip both symlink into a shared base-interpreter tree). The generated .desktop Exec then prefixed the bare base interpreter, which never activates the venv (pyvenv.cfg is discovered from the unresolved argv[0]) and crashes on the first third-party import — ModuleNotFoundError: yaml, silent since Terminal=false. Every 'hermes desktop' or 'hermes update' rewrote the entry, so launching from the launcher icon broke after every upgrade. - add _resolve_exec_path(): keep the venv path when the executable sits inside a venv (pyvenv.cfg found above), resolve otherwise - use it for both sys.executable and the resolved hermes bin - updater drive form (argv[0] == the interpreter itself, e.g. 'python -m hermes_cli.main desktop --build-only') now emits '<venv-python> -m hermes_cli.main desktop' instead of treating 'desktop' as a script path - update affected tests and add regressions for the uv-style symlink and the interpreter-drive form
- _is_python_interpreter() now matches only real interpreter names (python, python3, python3.11, python2.7) and rejects lookalikes such as python3-config - the env-shebang and venv-shebang tests build a simulated uv-style venv and assert concrete paths instead of mirroring the implementation via _resolve_exec_path() - add a direct unit test for _is_python_interpreter()
Overall: correct and well-explained fix — venv detection really does key off unresolved Suggestions:
Minor: consider asserting in |
…preter match Three hardening pieces that no other open PR in this space carries together, consolidating the good ideas from the sibling PRs with credit: - _running_interpreter(): keep sys.executable LEXICAL only when it is venv-semantic (pyvenv.cfg at or above it in the tree); otherwise resolve(). Blanket abspath (this PR's previous form, NousResearch#92516/NousResearch#94115/ NousResearch#94544) preserves venv semantics but loses durability when the executable is a re-pointable symlink OUTSIDE any venv; blanket resolve() (NousResearch#90492) loses venv semantics. Detection picks the right one per path. Idea lineage credited in the docstring. - Atomic entry write: install_desktop_entry now goes through utils.atomic_write_text (temp+fsync+rename) instead of a plain write_text. An interrupted plain write leaves a zero-byte entry that permanently breaks the taskbar pin. This piece was in NousResearch#80547, which closed unmerged with it unlanded - ported here. - _is_interpreter(): strict regex basename match (python[23]?(\d+)?(\.\d+)?) with the bin/Scripts parent guard - rejects python3-config, pythonw and other lookalikes the startswith() form accepted (regex approach independently proposed in NousResearch#94051). Verified live: venv context (pyvenv.cfg present) keeps the lexical path; non-venv context resolves; three-context convergence intact (A==B, C falls back to runnable -m under real wrapper-absence); atomic write produces non-empty entries with 0755 on create; suites 35 passed/6 skipped; ruff clean.
…preter match Three hardening pieces that no other open PR in this space carries together, consolidating the good ideas from the sibling PRs with credit: - _running_interpreter(): keep sys.executable LEXICAL only when it is venv-semantic (pyvenv.cfg at or above it in the tree); otherwise resolve(). Blanket abspath (this PR's previous form, NousResearch#92516/NousResearch#94115/ NousResearch#94544) preserves venv semantics but loses durability when the executable is a re-pointable symlink OUTSIDE any venv; blanket resolve() (NousResearch#90492) loses venv semantics. Detection picks the right one per path. Idea lineage credited in the docstring. - Atomic entry write: install_desktop_entry now goes through utils.atomic_write_text (temp+fsync+rename) instead of a plain write_text. An interrupted plain write leaves a zero-byte entry that permanently breaks the taskbar pin. This piece was in NousResearch#80547, which closed unmerged with it unlanded - ported here. - _is_interpreter(): strict regex basename match (python[23]?(\d+)?(\.\d+)?) with the bin/Scripts parent guard - rejects python3-config, pythonw and other lookalikes the startswith() form accepted (regex approach independently proposed in NousResearch#94051). Verified live: venv context (pyvenv.cfg present) keeps the lexical path; non-venv context resolves; three-context convergence intact (A==B, C falls back to runnable -m under real wrapper-absence); atomic write produces non-empty entries with 0755 on create; suites 35 passed/6 skipped; ruff clean.
actavisbulgaria-ai
left a comment
There was a problem hiding this comment.
Verified end-to-end on a real uv-managed install (Fedora 44 / KDE Plasma, Wayland)
Ran this PR's code on the exact setup the bug targets — ~/.hermes/hermes-agent/venv/bin/python is a symlink into ~/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/ (the real uv venv condition):
1. Unit tests — pytest tests/hermes_cli/test_linux_desktop_entry.py in a fresh env (pytest + PyYAML only): 18 passed, 2 skipped.
2. Generated Exec= on this machine via resolve_exec_command() with sys.argv[0] = the venv launcher:
Exec=/home/kiril/.hermes/hermes-agent/venv/bin/hermes desktop
Clean form, no interpreter prefix — _needs_interpreter() correctly returns False because _resolve_exec_path keeps the venv shim, so exe_dir matches the #!/…/venv/bin/python3 shebang. The pre-fix code on this same machine generated the broken:
Exec=…/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/bin/python3.11 …/hermes desktop
→ ModuleNotFoundError: No module named 'hermes_cli' on launch (invisible, Terminal=false).
_resolve_exec_path walking all parents for pyvenv.cfg (rather than just parent.parent) is the right call — it survives deeper shim layouts. This covers the whole bug class: both argv sites and the _needs_interpreter sibling path that earlier PRs missed.
For maintainers consolidating the pile: #92122 and #92278 also touch all three sites and generate the same correct Exec= on this machine — but this PR additionally carries passing tests and the most robust pyvenv.cfg walk.
Confirmed on a second environment (Ubuntu 26.04.1 LTS, KDE Plasma 6 / X11)Exact same repro as the issue — and the fix direction is verified end-to-end here. Environment
Observed
Verification of the fix
This PR looks correct and covers the updater path too ( |
What does this PR do?
Fixes the Linux desktop-entry
Execline being rewritten with a bare (non-venv) Python interpreter, which made launching Hermes Desktop from the launcher icon crash silently after every upgrade on uv-created venvs.Root cause:
resolve_exec_command()inhermes_cli/linux_desktop_entry.pycalledPath(sys.executable).resolve().uv venv(and pip) makebin/pythona symlink into a shared base-interpreter tree, and.resolve()follows that link — so the generatedExecprefixed the bare base interpreter (e.g.~/.local/share/uv/python/.../bin/python3.11). That interpreter never activates the venv (CPython discoverspyvenv.cfgfrom the unresolved argv[0]), so it crashes on the first third-party import (ModuleNotFoundError: yaml), with nothing on screen becauseTerminal=false.Every
hermes desktoprun re-installs the desktop entry (main.py→_register_linux_desktop_entry()), andhermes updaterebuilds it throughpython -m hermes_cli.main desktop --build-only. So after each upgrade the icon entry was broken again, whilehermes desktopfrom a shell kept working — confusing: "works in terminal, flashes and dies from the icon".Fix:
_resolve_exec_path(): when the executable sits inside a venv (apyvenv.cfgexists above it), keep the unresolved venv path; resolve only otherwise. Use it for bothsys.executableand the resolved hermes binary, so theExecprefix stays on the venv interpreter that actually loads site-packages._is_python_interpreter(): when the resolved bin is the interpreter itself (the updater drivespython -m hermes_cli.main desktop --build-only, soargv[0]is the interpreter), emit<venv-python> -m hermes_cli.main desktopinstead of<venv-python> desktop(which would treatdesktopas a script path).Related Issue
Fixes #94058
Type of Change
Changes Made
hermes_cli/linux_desktop_entry.py:_resolve_exec_path()— venv-symlink-preserving path resolution_is_python_interpreter()— detect interpreter-drive formresolve_exec_command()and_needs_interpreter()tests/hermes_cli/test_linux_desktop_entry.py:test_exec_keeps_venv_symlink_in_interpreter_prefix(uv-style symlink regression)test_exec_uses_module_form_when_bin_is_python_interpreter(updater drive form)_is_python_interpreter()to real interpreter names; tests assert concrete venv paths instead of mirroring the implementationHow to Test
uv venv): runhermes desktoponce, then inspect~/.local/share/applications/hermes.desktop— before the fix,Execstarts with the resolved base interpreter (~/.local/share/uv/python/.../bin/python3.11); after, it starts with the venv python (.../venv/bin/python).hermes update→ entry survives with the venv path intact.pytest tests/hermes_cli/test_linux_desktop_entry.py -q→ 17 passed, 2 skipped.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A