Skip to content

Use the symlink-resolved location of Python in getpath on 3.14+ - #896

Merged
geofft merged 7 commits into
mainfrom
geofft/getpath
Dec 5, 2025
Merged

geofft merged 7 commits into
mainfrom
geofft/getpath

Conversation

@geofft

@geofft geofft commented Dec 1, 2025 •

Copy link
Copy Markdown
Collaborator

Mostly fixes #380, and also fixes #713 because we prefer the actual
binary path to argv[0].

@geofft geofft added platform:darwin Specific to the macOS platform platform:linux Specific to the Linux platform python:3.14 python:3.15 arch:x86_64 and removed python:3.15 labels Dec 1, 2025
@jjhelmus

jjhelmus commented Dec 2, 2025

Copy link
Copy Markdown
Contributor

Passing the path the the executable via the library variable in getpath.py has a smell as the variable is described as being the path to the dylib/DLL/so. When static linking there is no shared library so I see the logic in passing the path to the binary (as it is a DSO) itself.

@jjhelmus

jjhelmus commented Dec 2, 2025

Copy link
Copy Markdown
Contributor

With this patch the directory pointed to by home in the pyvenv.cfg is still searched if the library search fails (but it should not fail).

The change from python/cpython#106045 (comment) (below) would update this path to the directory of the base_executable when it exists. I'm somewhat partial to that solution in addition to this change.

@@ -411,6 +412,9 @@ def search_up(prefix, *landmarks, test=isfile):
                             if isfile(candidate):
                                 base_executable = candidate
                                 break
+                if base_executable and isfile(base_executable):
+                    # Update the executable directory to be based on the resolved base executable
+                    executable_dir = real_executable_dir = dirname(base_executable)
             # home key found; stop iterating over lines
             break

@jjhelmus

jjhelmus commented Dec 2, 2025

Copy link
Copy Markdown
Contributor

I've found following helpful check/reproduce/debug this issue:

./configure --prefix=/example
make -j
make DESTDIR=<some_real_path> install
cd <some_real_path>
./example/bin/python3.14 # can be used to create symlinks/venv/etc 

@jjhelmus jjhelmus closed this Dec 2, 2025
@zanieb

zanieb commented Dec 2, 2025

Copy link
Copy Markdown
Member

Did you mean to close this?

@jjhelmus jjhelmus reopened this Dec 2, 2025
@jjhelmus

jjhelmus commented Dec 2, 2025

Copy link
Copy Markdown
Contributor

Did you mean to close this?

Nope, oops

@geofft geofft changed the title Short-term getpath fix Use the symlink-resolved location of Python in getpath on 3.14+ Dec 5, 2025
@geofft
geofft marked this pull request as ready for review December 5, 2025 14:35
Comment thread cpython-unix/build-cpython.sh Resolved
@geofft

geofft commented Dec 5, 2025

Copy link
Copy Markdown
Collaborator Author

I think adding both this patch and the one in #903 is a good idea, though even with both patches I would consider it not quite a complete fix.

I've added some test cases based on #380 and #713. The patch in #903 by itself passes the tests for #380 but not the one in #713. (On the other hand the patch in #903 works on 3.11+.)

Getting this patch to work for 3.11-3.13 isn't hard, but I'm not sure if I will get to that today.

Mostly fixes #380, and also fixes #713 because we prefer the actual
binary path to argv[0].
geofft and others added 4 commits December 5, 2025 13:14
Patch CPython to allow venvs to be created from symlinks.
When inside a venv and base_executable is passed available. Use the
resolved path as the executable_dir rather than the one specified by
home in pyvenv.cfg
Comment on lines +303 to +305
# TODO: does not yet work on ARM64
# with self.subTest(msg="weird argv[0]"):
# assertPythonWorks(sys.executable, argv0="/dev/null")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a part of the skipif?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you can skipIf a single subTest. You can call self.skipTest("doesn't yet work on arm64"). I probably could have structured this one as a separate test case, though....

@geofft geofft added the ci:skip label Dec 5, 2025
@zanieb

zanieb commented Dec 5, 2025

Copy link
Copy Markdown
Member

Includes #903?

@jjhelmus

jjhelmus commented Dec 5, 2025 •

Copy link
Copy Markdown
Contributor

Includes #903?

Yes, this supersedes #903

@geofft

geofft commented Dec 5, 2025

Copy link
Copy Markdown
Collaborator Author

Yes, to fix the aarch64 failures.

@geofft
geofft merged commit 91c11ac into main Dec 5, 2025
7 of 19 checks passed
@geofft
geofft deleted the geofft/getpath branch December 5, 2025 19:37
topcoder1 added a commit to topcoder1/ci-workflows that referenced this pull request Oct 8, 2026
…#293)

## Problem

On macOS, `uv run pytest selftest/test_pinned_gate_tests_action.py`
fails 11 of 25 tests on every local run. That includes a clean
`origin/main` (2815beb). `Tests (Python)` passes on CI. Every failure is
the same error:

```
Error: Command '[.../runner_temp/pinned-gate-tests.XXXX/venv/bin/python', '-m', 'ensurepip', '--upgrade', '--default-pip']' returned non-zero exit status 1.
```

`_run_action()` made `shim/python` a **symlink** to `sys.executable` and
put `shim/` first on PATH. That makes the action's bare `python -I -m
venv "$venv"` run the test's interpreter.

When CPython starts through a symlink (outside a macOS framework build),
it keeps the symlink's own path as `sys._base_executable`. So `venv`
writes `home = <tmp>/shim` into the new venv's `pyvenv.cfg`. That
directory has no stdlib. The new venv's python looks for its stdlib
starting from `home`, finds none there, and falls back:

| Interpreter | Fallback | Result |
|---|---|---|
| CI's `setup-python`; Docker `python:3.13` (`/usr/local`) |
compile-time prefix, which is where it is really installed | works by
accident |
| uv python-build-standalone with
[astral-sh/python-build-standalone#896](astral-sh/python-build-standalone#896)
(merged 2025-12-05; seen in 3.13.12 and 3.13.16) | patched getpath
re-derives the directory from the symlink-resolved interpreter | works |
| uv python-build-standalone without it (the 3.13.9 build uv cached
here, built 2025-10-28) | compile-time prefix `/install` | `Could not
find platform independent libraries <prefix>`, then `No module named
'encodings'`, so ensurepip exits 1 |

So the bug is not specific to macOS. uv's cpython 3.13.9 fails the same
11 tests on Linux too.

## Fix

- **The shim is now an exec wrapper.** `_write_python_shim()` writes
`shim/python` as `#!/bin/sh` + `exec <realpath of sys.executable> "$@"`,
with the path `shlex.quote`d. The interpreter starts from its real path,
so `venv` records the real bin directory as `home`. No fallback is
involved.
- **The action still gets the same interpreter.** The old symlinked
python also ran as the base interpreter, not inside the uv `.venv`: its
`pyvenv.cfg` lookup happened next to the shim. Only the path venv sees
has changed.
- **`_run_action()` now checks every venv the action built.**
- It checks that `home` is not the shim's directory, and that the python
in `home` is this interpreter.
- It compares interpreters, not directories. A macOS framework build
(Homebrew) records `/opt/homebrew/opt/python@3.13/bin` as `home`
whatever path it starts through, and that is correct there.
- CI's interpreter hides the bug, so through a symlink shim the cases'
own assertions stay green on CI. This check is what fails there, whether
the symlink comes back in the helper or at `_run_action`'s own call.
- It reads each workdir's `venv/pyvenv.cfg` with no existence check, so
if the action's layout changes the check fails loudly instead of being
skipped.

The action itself is unchanged, and so are all existing assertions. The
suite still has 25 tests.

## Verification

All runs below are of the whole file. A mutant is the fixed file with
the shim reverted to a symlink, in the helper or at `_run_action`'s
call.

| Interpreter | `origin/main` | Fixed | Helper back to symlink | Call
site back to symlink |
|---|---|---|---|---|
| macOS (Darwin 25.5, arm64), uv cpython 3.13.9 | 11 failed, 14 passed |
**25 passed** | 15 failed, 10 passed | 15 failed, 10 passed |
| macOS, Homebrew 3.13.12 (framework build) | — | **25 passed** | 25
passed (bug can't occur) | — |
| Linux (Docker `python:3.13`, arm64), `/usr/local` 3.13.16, standing in
for `setup-python` | 25 passed (bug hidden) | **25 passed** | 15 failed,
10 passed | 15 failed, 10 passed |
| Linux (Docker, arm64), uv cpython 3.13.9 | 11 failed, 14 passed (same
ensurepip error) | **25 passed** | — | — |

- **What the mutants fail on:** every mutant failure is the new check:
`venv home is the python shim's dir`. The 10 cases that still pass are
input validation and structural checks; none of them builds a venv.
- **Exec-bit mutant (macOS):** a shim left non-executable fails loudly
(`FileNotFoundError` on the missing `pyvenv.cfg`).
- **ruff 0.15.10 (CI's pin):** `ruff format --check` and `ruff check`
are clean.
- **Docker runs:** each copies the worktree from a read-only mount, then
runs `uv sync --frozen --all-extras` and `uv run --no-sync pytest`, like
`Tests (Python)` does.
- **Independent review:** a code-reviewer lane returned COMMENT with
three findings, all addressed in this version.
- The first guard compared directories and failed falsely on Homebrew.
It now compares interpreters.
- The guard didn't cover `_run_action`'s own call. The check now runs in
`_run_action`.
- Two docstring claims weren't true for framework builds. They are now
qualified.
- **Codex pre-review:** skipped; this is a selftest-only diff, below the
threshold.

**Not changed here:** on `origin/main`, four must-fail cases passed on
macOS only because the venv build failed:
`test_ungated_recursive_delete_fails` and three rows of
`test_count_cases_fail`. They now fail at the new check instead. They
still assert only `rc != 0`, so a failure after a good venv build would
still satisfy them (pip unable to install from the wheelhouse, for
example). Tightening them is a separate change.

Auto-merge rationale: none. This is a manual merge, as for every
topcoder1/ci-workflows PR (no automerge lane is wired on this repo). The
diff is selftest-only (`safe_test`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:skip platform:darwin Specific to the macOS platform platform:linux Specific to the Linux platform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os.execl fails to execute Python Virtualenvs created from links are broken

3 participants