Skip to content

Commit 07c2b34

Browse files
committed
Auto-discover libpython via python3 sysconfig
The Python-in-Lean bridge previously only tried (a) libpython already loaded into the host process, (b) LEANPY_LIBPYTHON, and (c) common sonames via the dyld search path. On a typical pyenv / uv / framework install, none of those find anything, so opening Main.lean in VS Code and #eval'ing a function that calls into the bridge would fail with 'could not load libpython'. Add step (3.5): popen("python3 -c '<sysconfig snippet>'") and dlopen the printed path. Falls through to the soname list if python3 isn't on PATH or returns nothing useful. This means the typed-numpy example now works in #eval inside VS Code with no env var fiddling, as long as python3 on PATH has numpy importable.
1 parent 5fb57a5 commit 07c2b34

3 files changed

Lines changed: 54 additions & 20 deletions

File tree

LeanPy/native/python_bridge.c

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,21 +263,57 @@ static const char *const PY_CANDIDATES[] = {
263263
NULL
264264
};
265265

266+
/* Ask `python3` (or `python`) on PATH where its own libpython lives.
267+
* This handles pyenv / uv-managed / framework Pythons whose libdir
268+
* isn't on the dyld search path. The call is one-shot and only runs
269+
* during the first `LeanPy.Python.init`. */
270+
static int try_python_subprocess(const char *python_exe) {
271+
char cmd[512];
272+
int n = snprintf(cmd, sizeof(cmd),
273+
"%s -c 'import os, sysconfig; "
274+
"libdir = sysconfig.get_config_var(\"LIBDIR\"); "
275+
"soname = sysconfig.get_config_var(\"INSTSONAME\") "
276+
"or sysconfig.get_config_var(\"LDLIBRARY\"); "
277+
"print(os.path.join(libdir, soname) "
278+
"if libdir and soname else \"\")' 2>/dev/null",
279+
python_exe);
280+
if (n <= 0 || (size_t)n >= sizeof(cmd)) return 0;
281+
FILE *fp = popen(cmd, "r");
282+
if (!fp) return 0;
283+
char buf[4096];
284+
char *line = fgets(buf, sizeof(buf), fp);
285+
pclose(fp);
286+
if (!line) return 0;
287+
size_t len = strlen(line);
288+
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) line[--len] = '\0';
289+
if (len == 0) return 0;
290+
py_handle = dlopen(line, RTLD_NOW | RTLD_GLOBAL);
291+
return py_handle != NULL;
292+
}
293+
266294
static int try_load_python(void) {
267-
/* First, prefer libpython already loaded into the process (this is
268-
* the typical case when LeanPy runs inside a Python host — using a
269-
* different libpython would cause two independent CPython VMs and
270-
* immediate crashes). */
295+
/* 1. Prefer libpython already loaded into the process (this is
296+
* the typical case when LeanPy runs inside a Python host —
297+
* using a different libpython would cause two independent
298+
* CPython VMs and immediate crashes). */
271299
py_handle = dlopen(NULL, RTLD_LAZY);
272300
if (py_handle && dlsym(py_handle, "Py_Initialize")) return 1;
273301
py_handle = NULL;
274302

275-
/* Honour LEANPY_LIBPYTHON if set. */
303+
/* 2. Honour LEANPY_LIBPYTHON if set. */
276304
const char *override = getenv("LEANPY_LIBPYTHON");
277305
if (override && *override) {
278306
py_handle = dlopen(override, RTLD_NOW | RTLD_GLOBAL);
279307
if (py_handle) return 1;
280308
}
309+
310+
/* 3. Ask python3 / python on PATH for its libpython. Catches
311+
* pyenv, uv, and framework installs whose lib dir isn't in
312+
* the dyld search path. */
313+
if (try_python_subprocess("python3")) return 1;
314+
if (try_python_subprocess("python")) return 1;
315+
316+
/* 4. Fall back to common sonames (relies on dyld search path). */
281317
for (int i = 0; PY_CANDIDATES[i]; i++) {
282318
py_handle = dlopen(PY_CANDIDATES[i], RTLD_NOW | RTLD_GLOBAL);
283319
if (py_handle) return 1;

examples/03_numpy_typed/README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ Storage and arithmetic are forwarded to numpy via `LeanPy.Python` —
2727
the phantom types are erased at runtime.
2828

2929
There is no Python driver. `lake build` produces a binary at
30-
`lean/.lake/build/bin/demo`; the `run` wrapper at the example root
31-
just sets `LEANPY_LIBPYTHON` to a `libpython3.X` and runs the binary
32-
with `uv run` so numpy from the local venv is on `sys.path`.
30+
`lean/.lake/build/bin/demo`. The bridge auto-discovers `libpython` by
31+
asking `python3` on PATH for its sysconfig, so the binary runs as long
32+
as a `python3` with `numpy` is on PATH. The `run` wrapper at the
33+
example root just runs the binary under `uv run` so the local venv's
34+
numpy is picked up.
35+
36+
You can also load `Main.lean` in VS Code and `#eval runDemo` directly
37+
— the same auto-discovery kicks in inside the Lean LSP. (If you're on
38+
a setup where `python3` on PATH doesn't have numpy installed, set
39+
`LEANPY_LIBPYTHON` and `PYTHONPATH` in your shell to override.)
3340

3441
## Layout
3542

examples/03_numpy_typed/run

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
#!/usr/bin/env bash
2-
# Build and run the typed-numpy Lean executable, picking up libpython
3-
# and numpy from the local uv venv.
2+
# Build and run the typed-numpy Lean executable, with the local uv venv
3+
# active so numpy is on python3's sys.path. The bridge now discovers
4+
# libpython itself by asking python3 — no env var needed in most cases.
45
set -euo pipefail
56

67
cd "$(dirname "$0")"
78
(cd lean && lake build)
8-
9-
# `uv run` activates the venv (so numpy is on sys.path); PYTHONHOME
10-
# stays unset on purpose — letting libpython use the venv's prefix.
11-
export LEANPY_LIBPYTHON=$(uv run -- python -c '
12-
import os, sysconfig
13-
libdir = sysconfig.get_config_var("LIBDIR")
14-
soname = sysconfig.get_config_var("INSTSONAME") or sysconfig.get_config_var("LDLIBRARY")
15-
print(os.path.join(libdir, soname) if soname else "")
16-
')
17-
189
exec uv run -- ./lean/.lake/build/bin/demo "$@"

0 commit comments

Comments
 (0)