Skip to content

Commit 296dd84

Browse files
ctruedenclaude
andcommitted
Handle JPype's macOS java_home fallback path
When JPype finds no JVM library via JAVA_HOME or its search locations, it falls back to /usr/libexec/java_home, whose output it returns as the bytes of a Java home directory rather than a JVM library path. This broke jvm_version() with a TypeError, and jpype.startJVM() cannot start from that directory either. Decode the path and look for bin/java when it is a directory, and point JAVA_HOME at that directory before starting the JVM, so that JPype then finds the JVM library inside it. Fixes #95. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3202163 commit 296dd84

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

‎src/scyjava/_jvm.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ def jvm_version() -> tuple[int, ...]:
7070
default_jvm_path = jpype.getDefaultJVMPath()
7171
if not default_jvm_path:
7272
raise RuntimeError("Cannot glean the default JVM path")
73+
# Note: On macOS, JPype may fall back to /usr/libexec/java_home, which
74+
# yields a Java home directory as bytes, rather than a JVM library path.
75+
default_jvm_path = os.fsdecode(default_jvm_path)
7376
_logger.debug(f"Default JVM path from JPype: {default_jvm_path}")
7477

7578
# Good ol' macOS! Nothing beats macOS.
@@ -104,6 +107,13 @@ def jvm_version() -> tuple[int, ...]:
104107
if java is None:
105108
raise RuntimeError(f"Invalid default JVM path: {p}")
106109

110+
if java is None and p.is_dir():
111+
java = p / "bin" / "java"
112+
if os.name == "nt":
113+
java = java.with_suffix(".exe")
114+
if not java.is_file():
115+
raise RuntimeError(f"No bin/java found inside: {p}")
116+
107117
if java is None:
108118
for _ in range(3): # The bin folder is always <=3 levels up from libjvm.
109119
p = p.parent
@@ -181,6 +191,17 @@ def start_jvm(options: Sequence[str] | None = None) -> None:
181191
# download Java as appropriate
182192
resolve_java()
183193

194+
# Note: When JPype finds no JVM library via JAVA_HOME (e.g. JAVA_HOME=/usr
195+
# from macOS's /usr/bin/java stub), it falls back to /usr/libexec/java_home,
196+
# which yields a Java home directory rather than a JVM library path, and
197+
# from which JPype cannot start the JVM. But given that directory as
198+
# JAVA_HOME, JPype finds the JVM library inside it.
199+
default_jvm_path = jpype.getDefaultJVMPath()
200+
if default_jvm_path and os.path.isdir(default_jvm_path):
201+
java_home = os.fsdecode(default_jvm_path)
202+
_logger.debug(f"Detected JAVA_HOME: {java_home}")
203+
os.environ["JAVA_HOME"] = java_home
204+
184205
# Fail fast if Java version is too old. JPype 1.6+ dropped Java 8 support.
185206
try:
186207
ver = jvm_version()

‎tests/test_jvm_version.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
Tests for functions in _versions submodule.
33
"""
44

5-
from scyjava._jvm import _jvm_version_str_to_tuple
5+
import os
6+
import subprocess
7+
from unittest import mock
8+
9+
import jpype
10+
11+
from scyjava._jvm import _jvm_version_str_to_tuple, jvm_version
612

713

814
def test_jvm_version():
@@ -20,3 +26,20 @@ def test_jvm_version():
2026
)
2127
assert _jvm_version_str_to_tuple(' version "1.8.0_312"', "java") == (1, 8, 0)
2228
assert _jvm_version_str_to_tuple(' version "25"', "java") == (25,)
29+
30+
31+
def test_jvm_version_from_java_home_bytes(tmp_path):
32+
# JPype's macOS java_home fallback yields a Java home directory as bytes.
33+
java = tmp_path / "bin" / ("java.exe" if os.name == "nt" else "java")
34+
java.parent.mkdir()
35+
java.touch()
36+
output = b'openjdk version "25.0.1" 2025-10-21'
37+
with (
38+
mock.patch.object(jpype, "getJVMVersion", return_value=(0, 0, 0)),
39+
mock.patch.object(
40+
jpype, "getDefaultJVMPath", return_value=os.fsencode(tmp_path)
41+
),
42+
mock.patch.object(subprocess, "check_output", return_value=output) as co,
43+
):
44+
assert jvm_version() == (25, 0, 1)
45+
assert co.call_args.args[0] == [str(java), "-version"]

0 commit comments

Comments
 (0)