Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## Unreleased
* Bug fix: JuliaCall initialization when the Julia executable is a symlink or
wrapper outside its installation directory.
* Bug fix: avoid precompilation cache miss when `check_bounds` option set.
* Bug fix: `juliacall` set the environment variable `JULIA_PYTHONCALL_EXECUTABLE`
instead of `JULIA_PYTHONCALL_EXE`, so child Julia processes resolved a new
Expand Down
11 changes: 5 additions & 6 deletions pysrc/juliacall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,15 @@ def args_from_config(config):
if (libpath is not None) and (exepath is None):
raise Exception("PYTHON_JULIACALL_EXE is required if PYTHON_JULIACALL_LIB is set.")

# Find the Julia library, if not specified.
if libpath is None:
# Discover the Julia library and binary directory if either is missing.
if libpath is None or bindir is None:
cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min',
'-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)']
libpath, found_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0')
CONFIG['libpath'] = libpath
found_libpath, found_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0')
if libpath is None:
CONFIG['libpath'] = libpath = found_libpath
if bindir is None:
CONFIG['bindir'] = bindir = found_bindir
if bindir is None:
bindir = os.path.dirname(exepath)
assert os.path.exists(libpath)
assert os.path.exists(bindir)

Expand Down
89 changes: 89 additions & 0 deletions pytest/test_issue_816.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import os
from pathlib import Path
import subprocess
import sys

import pytest


_CHILD = r"""
import os
from pathlib import Path
import shlex
import stat

import juliapkg

juliapkg.resolve()
real_executable = juliapkg.executable()
# Populate the normal JuliaPkg project and libjulia cache before changing only
# the executable result that JuliaCall will see.
juliapkg.project()
juliapkg.libjulia()

launcher = Path(os.environ["JULIACALL_TEST_LAUNCHER"])
mode = os.environ["JULIACALL_TEST_LAUNCHER_MODE"]
if mode == "symlink":
try:
launcher.symlink_to(real_executable)
except OSError as exc:
print(f"symlink unsupported: {exc}")
raise SystemExit(77)
elif mode == "wrapper":
launcher.write_text(
"#!/bin/sh\nexec " + shlex.quote(real_executable) + ' "$@"\n',
encoding="utf-8",
)
launcher.chmod(launcher.stat().st_mode | stat.S_IXUSR)
else:
raise AssertionError(mode)

juliapkg.executable = lambda: str(launcher)

from juliacall import Main

assert Main.seval("1 + 1") == 2
"""


def _run_launcher_case(
tmp_path: Path, mode: str
) -> subprocess.CompletedProcess[str]:
env = os.environ.copy()
for name in (
"PYTHON_JULIACALL_EXE",
"PYTHON_JULIACALL_PROJECT",
"PYTHON_JULIACALL_BINDIR",
"PYTHON_JULIACALL_LIB",
):
env.pop(name, None)
env["PYTHONPATH"] = str(Path(__file__).parents[1] / "pysrc")
env["JULIACALL_TEST_LAUNCHER"] = str(
tmp_path / "outside-julia-layout" / "julia"
)
env["JULIACALL_TEST_LAUNCHER_MODE"] = mode
Path(env["JULIACALL_TEST_LAUNCHER"]).parent.mkdir()
return subprocess.run(
[sys.executable, "-c", _CHILD],
cwd=Path(__file__).parents[1],
env=env,
capture_output=True,
text=True,
timeout=180,
)


@pytest.mark.parametrize("mode", ["symlink", "wrapper"])
def test_issue_816_launcher_outside_julia_bindir(tmp_path, mode):
"""https://github.com/JuliaPy/PythonCall.jl/issues/816"""
if mode == "wrapper" and os.name != "posix":
pytest.skip("the executable wrapper requires POSIX sh")

result = _run_launcher_case(tmp_path, mode)
if mode == "symlink" and result.returncode == 77:
pytest.skip(result.stdout.strip())
assert result.returncode == 0, (
f"child exited with {result.returncode}\n"
f"stdout:\n{result.stdout}\n"
f"stderr:\n{result.stderr}"
)
Loading