fix(cli): resolve the desktop-entry Exec independently of argv[0] - #80547
briandevans wants to merge 3 commits into
Conversation
`resolve_exec_command()` delegated to `relaunch.resolve_hermes_bin()`, which ranks `sys.argv[0]` first. That order is correct for its own job -- re-exec'ing *this* process, where argv[0] is runnable by construction -- and wrong as the source of a value baked into `~/.local/share/ applications/hermes.desktop`, which the desktop environment runs later from a process that shares nothing with this one. When the entry itself launches Hermes, argv[0] is the checkout's bare `hermes` script. That script runs under `/usr/bin/env python3` with no venv shim and imports `hermes_cli` from the caller's `sys.path`, which a cold menu launch does not provide. Persisting it strands the launcher, and the entry never heals because the broken launch dies at import before it can rewrite anything. The same root cause produces the second reported symptom. Because `Exec` alternates between the installed wrapper and the checkout script, the unchanged-contents guard in `install_desktop_entry()` never holds across alternating launches, so the entry is rewritten and `kbuildsycoca6` re-run on every other launch. Plasma drops the taskbar pin association and the pinned icon opens a second window instead of focusing the first. Discard a checkout-internal entry point and fall through to PATH, then to the interpreter. An installed wrapper reached through argv[0] is still preferred -- only the non-self-sufficient case is rejected. Also publish the entry through `utils.atomic_write_text` instead of `Path.write_text`. The entry is a durable user-facing artifact, and a truncate-then-write leaves a zero-length file if the write is interrupted, removing Hermes from the menu and killing the pin for good. `preserve_mode` keeps an existing 0o755 entry from transiting mkstemp's 0o600, and preserves the owner when a privileged process rewrites a user-owned file. Fixes NousResearch#80439
|
Additional real-world confirmation for the git/venv install path: We hit this exact cold-launch crash on Deepin 25 (XDG launcher click → For that layout your fall-through (drop checkout-internal entry → One note: we also append |
|
Thanks for the Deepin 25 repro — that's a useful independent confirmation, and the layout you describe is exactly the shape the fall-through targets. Your wrapper is the case that motivated the durability filter rather than a plain "prefer PATH" rule. On a cold menu launch Agreed on |
|
Follow-up to my earlier Deepin 25 confirmation: the installed-wrapper path is good, but I found a real cold-launch failure in the no-wrapper fallback on a uv-created venv.
I reproduced it from this PR's head by forcing the checkout-script argv0 and removing the wrapper from PATH. Executing the generated interpreter from a cold cwd then fails: $ cd /tmp
$ env -i HOME=/home/jay PATH=/usr/bin:/bin \
/home/jay/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/bin/python3.11 \
-c 'import hermes_cli'
ModuleNotFoundError: No module named 'hermes_cli'Preserving $ cd /tmp
$ env -i HOME=/home/jay PATH=/usr/bin:/bin \
/home/jay/.hermes/hermes-agent/venv/bin/python \
-c 'import hermes_cli; print(hermes_cli.__file__)'
/home/jay/.hermes/hermes-agent/hermes_cli/__init__.pySo the wrapper-present path still fixes the reported Deepin case, but Local verification on PR head: |
The interpreter fallback wrote `Path(sys.executable).resolve()`. That dereferences a venv's `bin/python` symlink, and CPython decides it is inside a venv by looking for `pyvenv.cfg` beside the path it was *invoked* through, not beside the symlink target. A uv-created venv points that symlink at `~/.local/share/uv/python/cpython-*/bin/python3.11`, outside the venv, so the persisted `Exec=` starts an interpreter with the venv's site-packages missing and cannot `import hermes_cli` at all — `ModuleNotFoundError` on every cold menu launch, and it never heals. Write `os.path.abspath(sys.executable)` instead: absolute, but lexical. This is also what every other production site that spawns this same module already does (`relaunch.py`, `uninstall.py`, `kanban_db.py` all pass a bare `sys.executable`); this module was the only one dereferencing it. Widen the durability filter while here. `_is_inside_checkout` rejects a non-durable entry point by location only, so it misses a hand-rolled `~/bin/hermes` or a console script from a non-venv editable install: both sit outside the checkout and both still resolve `python3` off `PATH` at exec time. A cold menu launch supplies the desktop session's `PATH`, not the shell's, so the interpreter they land on need not have Hermes on `sys.path`. Reject a candidate whose shebang program is `env` running a `python*` command, and fall through to the interpreter. The check matches the shebang program's basename exactly, so a hardcoded interpreter under a directory named `envs` is still kept.
|
You are right on both counts, and thank you for chasing the no-wrapper branch — that is the half of the fall-through your first repro never reached, so it went untested by construction. Fixed on the branch. Worth noting this module was the only production site in the repo dereferencing the interpreter. Your point about the test is also fair: I should be upfront that @darzi-admin's #82040 already carries the same While in there I widened the durability filter, because your original Deepin report exposes a gap the location check cannot cover. One site I deliberately left alone: the binary branch still writes On your exact three files ( |
monerostar
left a comment
There was a problem hiding this comment.
Ubuntu 26.04 on linux-5800x (kernel 7.0.0-28-generic). Compared main 3f832978d vs this tip 7468eb7b5 and sibling #80563.
Live matrix (mocked resolve_hermes_bin / sys.executable):
- host launcher
~/.local/bin/hermesis#!/usr/bin/env bashthat execs the venv python with absolute paths. main and this PR keep it as Exec. #80563 rejects any#!/usr/bin/envand falls back to-m, so it drops a working bash wrapper. - bare
#!/usr/bin/env python3script: main writes it into Exec (broken under a stripped desktop env). this PR discards it and lands on PATH hermes or-m. - venv
bin/pythonsymlink to a base interpreter outside the venv (same shape as this box:venv/bin/python->.hermes-runtime/.../python3.11): main and #80563 usePath.resolve()and persist the base path. this PR keeps the lexical venv path viaos.path.abspath, sopyvenv.cfgstill applies.
Also on this install, Path(venv/bin/python).resolve() != os.path.abspath(...) (runtime generation dir). that is exactly the durability hole.
pytest: tests/hermes_cli/test_linux_desktop_entry.py -> 34 passed.
Looks good. Prefer this over #80563 for the env-python-only shebang check, PATH retry, lexical interpreter, and atomic publish.
|
@monerostar Thanks for running the three-way matrix on Ubuntu 26.04. The venv-symlink case is awkward to reproduce deliberately, so a live install where Your Restating what I noted earlier today so it stays visible next to your comparison: @darzi-admin's #82040 carries the same |
|
Just hit this bug in the wild and independently applied a similar fix before finding this PR. Tested locally - 34/34 tests pass, Two things in this PR that made the difference over a simpler PATH-first approach (#80563 and my own first attempt):
The shebang parsing edge cases ( |
|
@gokhanyildirimlar Thanks for the independent repro and the 34/34 run — an unprompted second confirmation from someone who hit this in the wild is worth more than another round of my own testing, especially on the two points you singled out. Both of those are pinned by named tests on head 1. 2. Atomic write. Publishing goes through the shared The shebang cases are One thing worth restating next to your comparison, since you are weighing approaches: @darzi-admin's #82040 carries the same |
…bang Review case 3 (this PR's own bug): known-location probing ran before the primary was proven non-durable, so a valid external launcher as argv[0] (e.g. /opt/.../bin/hermes) plus a PATH miss plus a different ~/.local/bin/hermes silently switched installations. Reorder: an external primary is now returned immediately; probing runs only after the primary is proven checkout-internal AND PATH missed. Review case 1 (inherited from NousResearch#90492): _needs_interpreter() compared the shebang against Path(sys.executable).resolve().parent - on uv venvs the resolved parent is the base interpreter's dir, so a valid .venv/bin/python shebang was classified foreign. Compare the LEXICAL interpreter directory (abspath) instead. Review case 2 (inherited from NousResearch#90492): the no-wrapper fallback emitted the RESOLVED interpreter, which on uv venvs is the base python outside the venv tree - a dead entry (pyvenv.cfg no longer adjacent). Emit the lexical sys.executable (os.path.abspath), preserving venv semantics - the exact durability rule NousResearch#80547's review established. Tests updated: interpreter expectations now assert the lexical path; the venv-shebang fixture writes a realistic console-script shebang (lexical, as pip writes them) rather than a resolved one.
…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.
…bang Review case 3 (this PR's own bug): known-location probing ran before the primary was proven non-durable, so a valid external launcher as argv[0] (e.g. /opt/.../bin/hermes) plus a PATH miss plus a different ~/.local/bin/hermes silently switched installations. Reorder: an external primary is now returned immediately; probing runs only after the primary is proven checkout-internal AND PATH missed. Review case 1 (inherited from NousResearch#90492): _needs_interpreter() compared the shebang against Path(sys.executable).resolve().parent - on uv venvs the resolved parent is the base interpreter's dir, so a valid .venv/bin/python shebang was classified foreign. Compare the LEXICAL interpreter directory (abspath) instead. Review case 2 (inherited from NousResearch#90492): the no-wrapper fallback emitted the RESOLVED interpreter, which on uv venvs is the base python outside the venv tree - a dead entry (pyvenv.cfg no longer adjacent). Emit the lexical sys.executable (os.path.abspath), preserving venv semantics - the exact durability rule NousResearch#80547's review established. Tests updated: interpreter expectations now assert the lexical path; the venv-shebang fixture writes a realistic console-script shebang (lexical, as pip writes them) rather than a resolved one.
…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.
This is a sibling follow-up to #76456 (commit
eea6044)hermes desktop#76456 covered: it introducedhermes_cli/linux_desktop_entry.py, givinghermes desktopa real XDG launcher presence — absoluteExec, absoluteIcon, tool-gated menu-cache refresh, and an unchanged-contents guard so a launch does not churn the caches. All of that is right and stays.hermes desktop#76456 did not touch: which absolute path gets persisted. It reusesrelaunch.resolve_hermes_bin(), whose priority order was designed for re-exec, not for a value written to disk.Exec— by location, by shebang, and by keeping the interpreter path lexical — plus an atomic publish of the entry file. Additive only: no restructuring of the module, and all 14 of its original tests still pass (two fixture shebangs were made explicit; no assertion was changed).What does this PR do?
Fixes both symptoms in #80439. They are one root cause, not two bugs.
resolve_exec_command()delegated torelaunch.resolve_hermes_bin(), which documents its own priority as 1.sys.argv[0]if it resolves to a real executable, 2.shutil.which("hermes"), 3.None. That order is correct forresolve_hermes_bin's actual job — re-exec'ing this process, where argv[0] is runnable by construction — and wrong as the source of a value baked into~/.local/share/applications/hermes.desktop, which the desktop environment executes later from a process that shares nothing with this one.Symptom 1 — the launcher goes permanently dead. When the entry itself launches Hermes, argv[0] is the checkout's bare
hermesscript. That script is:— system
python3, no venv shim,hermes_cliimported from the caller'ssys.path. A terminal launch supplies that; a cold KDE/GNOME menu launch does not.install_desktop_entry()rewritesExecto that path, and every subsequent menu launch dies at import. The entry never heals, because the failing launch never reaches the code that would rewrite it.Symptom 2 — the KDE taskbar pin breaks. This falls out of symptom 1.
Execalternates between the installed wrapper (terminal launch) and the checkout script (menu launch), so the "skip the rewrite when contents are identical" guard never holds across alternating launches. The entry is rewritten andkbuildsycoca6 --noincrementalre-run every other launch; Plasma drops the pin's association with the entry and the pinned icon opens a second window instead of focusing the first.Fixing the
Execderivation makes the rendered contents converge to one stable value, which restores the idempotence guard and therefore fixes the pin. One change, both symptoms.Symptom 3 — the interpreter fallback lands outside its own venv. Reported against this branch by @magicJie on a uv-created venv, and the same root cause one level down: a per-invocation fact being replaced by a resolved one. The fallback wrote
str(Path(sys.executable).resolve()).sys.executableis<venv>/bin/python, a symlink into uv's standalone Python; CPython decides it is inside a venv by looking forpyvenv.cfgbeside the path it was invoked through, not beside the symlink target. Dereferencing it produces— outside the venv, so
site-packagesis gone and the persisted command fails withModuleNotFoundError: No module named 'hermes_cli'. Absolute was necessary but not sufficient; the path also has to stay inside the venv. Nowos.path.abspath(sys.executable). This module was the only production site in the repo dereferencing the interpreter —relaunch.build_relaunch_argv,uninstall.py's per-profile invocation andkanban_db.py's module-invocation helper all build this exact-m hermes_cli.mainargv from a baresys.executable, so the fix restores the module to the codebase's own convention.Also durable-but-not-checkout-internal: the
env pythonshebang._is_inside_checkoutrejects a candidate by where it lives. A hand-rolled~/bin/hermes, or a console script from a non-venv editable install, sits outside the checkout and was kept — yet it still resolvespython3offPATHwhen the launcher runs it, which is symptom 1 reached by a different route (a cold menu launch supplies the desktop session'sPATH, not the shell's). A candidate whose shebang program isenvinvoking apython*command is now rejected too. The check compares the shebang program's basename againstenvrather than substring-matching, so a real hardcoded interpreter under a directory namedenvsis still kept.Second, unrelated-to-argv0 but same durable artifact: the entry was published with a bare
entry_path.write_text(...), a truncate-then-write. An interruption between truncate and write leaves a zero-lengthhermes.desktop— Hermes disappears from the menu and the pin dies for good.utils.atomic_write_text's own docstring states it exists so "every destructive file rewrite in the codebase shares one implementation", so this call site was a deviation. It is now routed through the shared helper (same precedent as #79137 and #79746).preserve_mode=Truekeeps an already-0o755entry from transitingmkstemp's0o600, and preserves the owner when a privileged process rewrites a user-owned file; the unconditionalchmod(0o755)afterwards is retained because the module requires the entry to be executable regardless of the mode it previously had.Resolution precedence now persisted
resolve_hermes_bin()— unchanged, still argv[0] then PATH.project_root, or its shebang isenvdispatching apython*— discard it and takeshutil.which("hermes")instead; if that is also not durable, discard that too. (resolve_hermes_bin()already consults PATH last, so the retry only fires when the candidate came from argv[0].)os.path.abspath(sys.executable) -m hermes_cli.main desktop— absolute, but not symlink-resolved, so a venv interpreter stays in its venv.An installed wrapper reached through argv[0] (
~/.local/bin/hermes, a venv shim, a nix store path) is still preferred — only the candidates that cannot start Hermes from a cold session are rejected.tests/hermes_cli/test_relaunch.pyandresolve_hermes_binitself are deliberately untouched: their argv[0]-first order is correct for re-exec and is pinned by 12 tests. The defect is persistence, not the helper.Related Issue
Fixes #80439
Type of Change
Changes Made
hermes_cli/linux_desktop_entry.py_is_inside_checkout(path, project_root)— resolves both sides and reports whether an entry point lives inside the source tree. ReturnsFalseforNone/empty and swallows onlyValueError/OSErrorfromresolve()/relative_to()._is_env_python_wrapper(path)— reads at most 256 bytes, requires a#!, requires the program's basename to be exactlyenv, toleratesenv -S, and requires the command it runs to start withpython. An unreadable candidate returnsFalse: absence of evidence is not evidence of aPATHdependency, and this runs on the launch path, so it must not raise._is_durable_entry_point(path, project_root)— the two rejection clauses behind one name, soresolve_exec_commandreads as one question asked twice.resolve_exec_command()takes an optionalproject_rootand discards a non-durable candidate, falling through to PATH and then to the interpreter. Default staysNone, so calling it with no argument preserves the old behaviour exactly.os.path.abspath(sys.executable)rather thanstr(Path(sys.executable).resolve()).osis already imported at module scope.install_desktop_entry()passes itsproject_rootthrough, and publishes viautils.atomic_write_text(..., preserve_mode=True, create_mode=0o755). Theutilsimport is function-local, matching the module's existing lazy-import idiom (from hermes_cli.relaunch import ...) so the module stays import-light for the uninstaller and the Electron main process, as its docstring promises.tests/hermes_cli/test_linux_desktop_entry.py— 11 new tests (20 cases with parametrisation), placed beside the existingtest_exec_*and install clusters. Helpers_make_executableand_fake_whichadded next to the existing_make_project/_stub_tools;_make_executablenow takes the shebang so a test can say which kind of entry point it is standing up (INSTALLED_SHEBANGfor a console script,ENV_PYTHON_SHEBANGfor the checkout's launcher). No pre-existing assertion was changed.How to Test
Before → after, verified in both directions. Applying the new test file to clean
main(prod file untouched, run in a detached worktree) fails exactly the four regression tests, and passes on this branch:maintest_exec_prefers_path_wrapper_over_checkout_argv0Exec= checkout scriptExec= PATH wrappertest_exec_rejects_checkout_argv0_when_no_wrapper_on_pathExec= checkout scriptExec=<python> -m hermes_cli.main desktoptest_entry_is_stable_across_a_relaunch_through_itselftest_failed_publish_leaves_the_existing_entry_intactNone, prior entry byte-identical, no temp residuetest_exec_accepts_argv0_outside_the_checkouttest_install_publishes_atomically_and_leaves_no_temp.tmpresidue, mode0o755The last two pass on both sides by design — they are guards against over-correcting, not regression proofs.
Second round — symptom 3 and the shebang clause, verified against the previous head of this branch (
38e94f0) rather than cleanmain, since they regress code this PR itself introduced:38e94f0test_exec_keeps_the_lexical_venv_interpreter_pathExec= the base interpreter behind the venv symlinkExec= the venv's ownbin/python,pyvenv.cfgstill beside ittest_exec_rejects_env_python_wrapper_outside_the_checkoutExec= theenv pythonwrapperExec=<python> -m hermes_cli.main desktoptest_exec_keeps_an_interpreter_under_a_directory_named_envsenvis matched as a basename, not a substringtest_env_python_wrapper_detection(10 cases)env -S python3 -u,env node,#!/bin/sh, bareenv, a non-shebang ELF header, an empty fileEach clause was reverted in isolation to confirm it is the one doing the work: with
os.path.abspathreverted,test_exec_keeps_the_lexical_venv_interpreter_pathfails; with the shebang clause reverted andabspathkept, onlytest_exec_rejects_env_python_wrapper_outside_the_checkoutfails.Full run on this branch: 34 passed in
test_linux_desktop_entry.py(14 pre-existing + 20 new cases; no pre-existing test was modified). Adjacent suites also green:tests/hermes_cli/test_gui_command.py,tests/hermes_cli/test_gui_uninstall.py,tests/hermes_cli/test_relaunch.py— 34 passed, 68 combined.Manual, on the reporter's setup (Linux):
hermes desktopfrom a terminal via~/.local/bin/hermes→grep Exec ~/.local/share/applications/hermes.desktopshows the wrapper.grep Execagain → unchanged (before this PR it flipped to.../hermes-agent/hermes desktop).stat -c %Y ~/.local/share/applications/hermes.desktopbefore and after step 2 → unchanged, so nokbuildsycoca6run and the taskbar pin keeps focusing the existing window.Root-cause coverage (sibling-site sweep)
The concern is a per-invocation path baked into a durable, later-executed artifact.
grep -rn --include='*.py' 'resolve_hermes_bin'plus a sweep forExec=/.desktopwriters enumerates every site:linux_desktop_entry.resolve_exec_commandgit grep -n 'sys\.executable' -- '*.py' | grep -E 'resolve\(\)|realpath') the only production site in the repo that dereferenced the interpreter.linux_desktop_entry.render_desktop_entry(Exec=)linux_desktop_entry.install_desktop_entry(write_text)relaunch.resolve_hermes_bin/build_relaunch_argvsys.executable.linux_desktop_entry.resolve_exec_command, binary branch (str(Path(bin_path).resolve()))_is_inside_checkout, and for a wrapper the two choices trade off in opposite directions (lexical survives the target being replaced by an upgrade; resolved survives the symlink being removed). Neither can produce an import failure, so it reads as a maintainer's call rather than a bug.uninstall.pyper-profile invocation,kanban_db.pymodule-invocation helpersys.executable; they are the convention symptom 3 restores.gateway/run.py::_resolve_hermes_binshutil.whichfirst, and the result feeds aPopenwithin the same process lifetime, never a file.hermes_cli/kanban_db.pygateway.run._resolve_hermes_bin; same transient use.tools/environments/local.py::_resolve_hermes_bin_dirPATHprefix for a child environment; not argv[0]-derived, not persisted.ExecStart(hermes_cli/gateway.py)grep resolve_hermes_bin hermes_cli/gateway.py→ 0 hits.hermes_cli/gui_uninstall.pydesktop_entry_path()for removal only; noExecderivation, no write.resolve_exec_commandhas exactly one call site, so the signature change is fully covered.Deliberately not included
The issue also raises four items that are maintainer calls rather than bug fixes, so they are left out to keep this reviewable as one concern. Happy to follow up on any of them:
Execstabilised, the entry stops churning on its own, which is the part that actually broke the pin.--skip-buildin the generatedExec. Changes what a menu launch does, not whether it works.ELECTRON_OZONE_PLATFORM_HINT=x11autodetection. New runtime detection with its own compatibility surface.Path=key pointing at the checkout. Would let thesys.executable -m hermes_cli.mainfallback work from a bare, uninstalled checkout, but it is a different concern (working directory, not path durability) and a badPath=makes some launchers refuse the entry outright.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — ran the focused suite plus the adjacent CLI suites listed above, not the full treemonkeypatchonsys.platform, exactly as the existing tests in this file are, so it runs headlessly off-Linux). The manual KDE/Plasma steps above are not something I can verify first-hand.Documentation & Housekeeping
docs/, docstrings) — docstrings onresolve_exec_commandand the new helper explain the durability requirement and whyresolve_hermes_bin's order is right for its own callercli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/Ainstall_desktop_entryis already a no-op off Linux/BSD viais_supported();_is_inside_checkoutis purepathlib,_is_env_python_wrapperreads bytes and never executes the candidate (a Windows.exehas no#!and is simply kept), the one symlink-creating test isskipif os.name == "nt", and the atomic writer is the one the rest of the codebase already uses on every platformRelated / Positioning
Three open PRs touch
resolve_exec_command(). To be explicit about the overlap:os.path.abspath(sys.executable)conclusion for the interpreter fallback, with the samepyvenv.cfgreasoning, a few hours before symptom 3 was reported here — that change is not an original finding of this PR and I am not claiming it as one. fix(desktop): avoid updater-created Linux launchers #82040 also carries an unrelatedhermes_cli/main.pychange moving launcher registration past the--build-onlyreturn, which this PR does not touch. What this PR carries that fix(desktop): avoid updater-created Linux launchers #82040 does not: the location filter and the PATH-preference fall-through (so an installedbashwrapper is still preferred over dropping straight to the interpreter — the case in the Deepin 25 report above), and the atomic publish of the entry file.Path(sys.executable).resolve(), so it does not address symptom 3.resolve_hermes_bin()inhermes_cli/relaunch.py, upstream of this module and deliberately out of scope here.Happy to defer on any overlapping hunk, or to rebase onto whichever of these lands first.
Screenshots / Logs
Focused suite on this branch:
First-round tests against clean
main(production file unmodified):Second-round tests against the previous head of this branch (
38e94f0), each clause reverted on its own: