diff --git a/CHANGELOG.md b/CHANGELOG.md index b96765d4d8..3e68eef802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu - Feature: five new language extractors — **COBOL** (`.cbl`/`.cob`/`.cobol`/`.cpy`; programs, paragraphs, `PERFORM`/`CALL`/`COPY`, pure-regex, no new dependency) (#3713, thanks @Abdul535), **VB.NET** (`.vb`; case-insensitive types/methods, `Inherits`/`Implements`/`Handles`) (#3717, thanks @Abdul535), **R** (`.r`/`.R`; assignment-form function defs, `library`/`source`, S4/R6 classes) (#3715, thanks @Abdul535), **Solidity** (`.sol`; contracts/interfaces/libraries, `is` inheritance, imports, modifiers) (#3716, thanks @Abdul535), and **Erlang** (`.erl`/`.hrl`/`.escript`; modules, functions by arity, behaviours, local + remote `foo:bar()` calls) (#3714, thanks @Abdul535). The R and Erlang grammars ship via the `r`/`erlang` extras (they have no standalone PyPI wheel); Solidity and VB.NET have their own extras. - Feature: Go interface method requirements now resolve to the interface, and their parameter/return types emit `references` edges (#3672, #3737, thanks @rajatnagda45, @oleksii-tumanov). - Feature: a Rust `self.method()` call resolves across files for simple generic impls (`impl Foo`) (#3653, thanks @oleksii-tumanov). -- Fix: `graph.json` is now deterministic across runs — `update`/`extract`/`cluster-only`/`label` pin `PYTHONHASHSEED` via a one-time re-exec, so hash-seed-sensitive community detection (Leiden/Louvain) produces identical output run-to-run and matches hook-triggered rebuilds (#3743, #3641, thanks @ayushcodes10). +- Fix: `graph.json` is now deterministic across runs — `update`/`extract`/`cluster-only`/`label` pin `PYTHONHASHSEED` via a one-time re-exec, so hash-seed-sensitive community detection (Leiden/Louvain) produces identical output run-to-run and matches hook-triggered rebuilds. The re-exec now runs as `python -m graphify` instead of replaying `argv[0]` as a script path, since a uv/pip/pipx console-script launcher on Windows is a native `.exe` with no `.py` content — every affected command previously failed outright with "can't open file" there (#3743, #3641, #3779, thanks @ayushcodes10). - Fix: a same-relation edge collision now keeps the higher-confidence edge (EXTRACTED over INFERRED) instead of resolving by arrival order (#3711, thanks @shobhitagnihotri69). - Fix: a spec-conformant method-node duplicate (dropped class segment / leading dot) is now deduplicated onto its canonical AST node, gated on a method-shaped label and a single unambiguous candidate (#3719, #3705, thanks @shobhitagnihotri69). - Fix: intra-module Go `imports_from` edges now repoint onto the imported package's real file nodes instead of dangling at a `go_pkg_` sink; external/stdlib imports stay external (#3748, thanks @carterko23). diff --git a/graphify/__main__.py b/graphify/__main__.py index d4f266ce4b..a2c5416de2 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -522,7 +522,17 @@ def _pin_hash_seed_if_needed() -> None: if "PYTHONHASHSEED" in os.environ or "PYTEST_CURRENT_TEST" in os.environ: return try: - os.execvpe(sys.executable, [sys.executable, *sys.argv], {**os.environ, "PYTHONHASHSEED": "0"}) + # Re-exec as a module (-m graphify) rather than replaying sys.argv[0] + # as a script path: that works for a POSIX console-script wrapper or + # a `python -m graphify` invocation, but a uv/pip/pipx console-script + # launcher on Windows is a native .exe with no .py content, so + # `python.exe ` fails outright with "can't open + # file" -- every command this function touches (#3779). + os.execvpe( + sys.executable, + [sys.executable, "-m", "graphify", *sys.argv[1:]], + {**os.environ, "PYTHONHASHSEED": "0"}, + ) except OSError: pass diff --git a/tests/test_pin_hash_seed.py b/tests/test_pin_hash_seed.py index d20eeb9ac0..2aad777b5b 100644 --- a/tests/test_pin_hash_seed.py +++ b/tests/test_pin_hash_seed.py @@ -56,10 +56,23 @@ def test_reexecs_for_hash_sensitive_commands_when_unset(): for cmd in ("update", "extract", "cluster-only", "label"): outcome = _run_probe(["graphify", cmd, "."]) assert outcome["called"], f"{cmd} must re-exec with PYTHONHASHSEED pinned" - assert outcome["argv"] == [sys.executable, "graphify", cmd, "."] + assert outcome["argv"] == [sys.executable, "-m", "graphify", cmd, "."] assert outcome["env_hashseed"] == "0" +def test_reexec_does_not_depend_on_argv0_being_a_runnable_script(): + """#3779: a uv/pip/pipx console-script launcher on Windows is a native + .exe with no .py content, so `python.exe ` fails + outright with "can't open file" the moment argv[0] is replayed as a + script path. Re-execing via `-m graphify` never touches argv[0] at + all, so a launcher stub that isn't even a real file must not matter.""" + outcome = _run_probe(["/some/launcher/stub/with/no/py/content", "update", "."]) + assert outcome["called"] + assert outcome["argv"] == [sys.executable, "-m", "graphify", "update", "."], ( + "the launcher stub path must never appear in the re-exec argv" + ) + + def test_does_not_reexec_when_already_set(): outcome = _run_probe(["graphify", "update", "."], extra_env={"PYTHONHASHSEED": "1"}) assert not outcome["called"], "an explicit PYTHONHASHSEED must never be overridden"