Skip to content

Commit 284ccdf

Browse files
committed
feat: Add comprehensive Java auto-detection
Implement robust Java installation detection that works across platforms: - Add JavaConfig class to hold detection results - Add detect_java_config() function with multi-step detection: 1. Check JAVA_HOME environment variable 2. Find javac executable and derive JAVA_HOME 3. Search common installation directories by platform - Support Linux, macOS, and Windows installation paths - Properly configure JNI include paths (jni.h, jni_md.h) - Find and link libjvm shared library - Get Java version for logging Changes to SConstruct: - Change --without-java default to False (enable auto-detection) - Use detect_java_config() for robust detection - Only enable Java features if detection succeeds - Add proper imports for os and subprocess modules This enables Java plugin support to work out-of-the-box when a JDK is installed, without requiring manual JAVA_HOME configuration.
1 parent ce5347e commit 284ccdf

File tree

2 files changed

+252
-26
lines changed

2 files changed

+252
-26
lines changed

SConstruct

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ from os import environ, getenv
1515
from os.path import relpath, abspath
1616
import logging
1717
import re
18+
import subprocess
1819
from subprocess import call
1920
import sys
2021
import platform
22+
import os
2123

2224
from build_config import *
2325
from build_support import *
@@ -95,8 +97,8 @@ AddOption(
9597
"--without-java",
9698
dest="without-java",
9799
action="store_true",
98-
default=True,
99-
help="Disable Java plugin compilation",
100+
default=False,
101+
help="Disable Java plugin support (auto-detected by default)",
100102
)
101103

102104
AddOption(
@@ -312,28 +314,18 @@ if not env.GetOption("without-r"):
312314
]
313315
)
314316

317+
java_enabled = False
315318
if not GetOption("without-java"):
316-
java_home = getenv('JAVA_HOME');
317-
318-
if not java_home:
319-
java_home = subprocess.check_output(["readlink", "-f", "/usr/bin/java"]).replace("/bin/java", "")
320-
321-
config.env.AppendUnique(
322-
CPPPATH=[
323-
Dir(java_home + "/include"),
324-
Dir(java_home + "/include/" + sys.platform.lower())
325-
],
326-
LIBPATH=[
327-
Dir(java_home + "/include"),
328-
Dir(java_home + "/include/" + sys.platform.lower())
329-
]
330-
)
331-
332-
if not config.CheckHeader("jni.h"):
333-
logging.error("!! Could not find `jni.h`")
334-
Exit(1)
335-
336-
config.env.Append(CCPDEFINES=["HAVE_JAVA"])
319+
java_config = detect_java_config()
320+
if java_config.is_valid:
321+
config.env.AppendUnique(CPPPATH=[Dir(p) for p in java_config.include_paths])
322+
config.env.AppendUnique(LIBPATH=[Dir(p) for p in java_config.lib_paths])
323+
config.env.Append(LIBS=["jvm"])
324+
config.env.Append(CPPDEFINES=["HAVE_JAVA"])
325+
java_enabled = True
326+
logging.info(f"Java support enabled (JAVA_HOME: {java_config.java_home})")
327+
else:
328+
logging.warning("Java support requested but Java installation could not be found.")
337329

338330
if GetOption("with-rust"):
339331
config.CheckProg("rustc")
@@ -393,7 +385,7 @@ if not env.GetOption("without-r"):
393385
"swig -r -c++ -module $TARGET -o ${TARGET}_wrap.cxx $SOURCE"
394386
)
395387

396-
if not env.GetOption("without-java"):
388+
if java_enabled:
397389
env.Command(
398390
"JavaPluMA",
399391
"src/PluginWrapper.i",
@@ -458,7 +450,7 @@ if not env.GetOption('without-r'):
458450

459451
###################################################################
460452
# JAVA PLUGINS
461-
if not env.GetOption('without-java'):
453+
if java_enabled:
462454
env.SharedObject(
463455
source="JavaPluMA_wrap.cxx",
464456
target=ObjectPath("JavaPluMA_wrap.os"),

build_support.py

Lines changed: 235 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
#!python
2-
# Copyright (C) 2017, 2019-2020 FIUBioRG
2+
# Copyright (C) 2017, 2019-2020, 2026 FIUBioRG
33
# SPDX-License-Identifier: MIT
44

55
import os
66
from os import path
77
import subprocess
88
from subprocess import Popen, PIPE
99
import sys
10+
import glob
11+
import logging
1012

1113
python_version = ".".join(map(str, sys.version_info[0:2]))
1214

@@ -82,3 +84,235 @@ def CheckPerl(ctx):
8284
os.unlink(".perltest.pl")
8385

8486
return retcode == 0
87+
88+
89+
###################################################################
90+
# JAVA DETECTION
91+
###################################################################
92+
93+
class JavaConfig:
94+
"""Configuration holder for Java installation details."""
95+
def __init__(self):
96+
self.java_home = None
97+
self.include_paths = []
98+
self.lib_paths = []
99+
self.libjvm_path = None
100+
self.is_valid = False
101+
self.version = None
102+
103+
def __repr__(self):
104+
return f"JavaConfig(java_home={self.java_home}, valid={self.is_valid})"
105+
106+
107+
def detect_java_config():
108+
"""
109+
Detect Java installation and return JavaConfig with paths.
110+
111+
Detection order:
112+
1. JAVA_HOME environment variable
113+
2. Location of javac executable
114+
3. Common installation directories
115+
116+
Returns:
117+
JavaConfig: Configuration object with detected paths
118+
"""
119+
config = JavaConfig()
120+
121+
# Step 1: Check JAVA_HOME
122+
java_home = os.environ.get("JAVA_HOME")
123+
124+
# Step 2: Try to find from javac location
125+
if not java_home or not os.path.isdir(java_home):
126+
java_home = _find_java_home_from_javac()
127+
128+
# Step 3: Search common installation directories
129+
if not java_home or not os.path.isdir(java_home):
130+
java_home = _search_common_java_locations()
131+
132+
if not java_home or not os.path.isdir(java_home):
133+
logging.warning("Java installation not found")
134+
return config
135+
136+
config.java_home = java_home
137+
138+
# Determine platform-specific include subdirectory
139+
if sys.platform.startswith("linux"):
140+
platform_dir = "linux"
141+
elif sys.platform.startswith("darwin"):
142+
platform_dir = "darwin"
143+
elif sys.platform.startswith("win"):
144+
platform_dir = "win32"
145+
else:
146+
platform_dir = sys.platform
147+
148+
# Configure include paths
149+
include_dir = os.path.join(java_home, "include")
150+
if os.path.isdir(include_dir):
151+
config.include_paths.append(include_dir)
152+
platform_include = os.path.join(include_dir, platform_dir)
153+
if os.path.isdir(platform_include):
154+
config.include_paths.append(platform_include)
155+
156+
# Configure library paths and find libjvm
157+
lib_paths_to_check = [
158+
os.path.join(java_home, "lib", "server"),
159+
os.path.join(java_home, "lib", "client"),
160+
os.path.join(java_home, "lib"),
161+
os.path.join(java_home, "jre", "lib", "server"),
162+
os.path.join(java_home, "jre", "lib", "amd64", "server"),
163+
os.path.join(java_home, "jre", "lib", "i386", "server"),
164+
]
165+
166+
# macOS-specific paths
167+
if sys.platform.startswith("darwin"):
168+
lib_paths_to_check.extend([
169+
os.path.join(java_home, "lib", "jli"),
170+
os.path.join(java_home, "jre", "lib", "jli"),
171+
])
172+
173+
for lib_path in lib_paths_to_check:
174+
if os.path.isdir(lib_path):
175+
config.lib_paths.append(lib_path)
176+
# Check for libjvm
177+
if sys.platform.startswith("win"):
178+
libjvm = os.path.join(lib_path, "jvm.dll")
179+
elif sys.platform.startswith("darwin"):
180+
libjvm = os.path.join(lib_path, "libjvm.dylib")
181+
else:
182+
libjvm = os.path.join(lib_path, "libjvm.so")
183+
184+
if os.path.isfile(libjvm):
185+
config.libjvm_path = libjvm
186+
187+
# Validate: we need include paths and libjvm
188+
jni_header = os.path.join(include_dir, "jni.h") if include_dir else None
189+
if config.include_paths and (config.libjvm_path or config.lib_paths):
190+
if jni_header and os.path.isfile(jni_header):
191+
config.is_valid = True
192+
193+
# Try to get Java version
194+
config.version = _get_java_version(java_home)
195+
196+
if config.is_valid:
197+
logging.info(f"Java detected: {java_home} (version: {config.version})")
198+
else:
199+
logging.warning(f"Java installation at {java_home} is incomplete")
200+
201+
return config
202+
203+
204+
def _find_java_home_from_javac():
205+
"""Find JAVA_HOME by locating the javac executable."""
206+
try:
207+
# Try 'which javac' on Unix-like systems
208+
if not sys.platform.startswith("win"):
209+
result = subprocess.run(
210+
["which", "javac"],
211+
capture_output=True,
212+
text=True,
213+
timeout=5
214+
)
215+
if result.returncode == 0:
216+
javac_path = result.stdout.strip()
217+
if javac_path:
218+
# Resolve symlinks
219+
real_path = os.path.realpath(javac_path)
220+
# javac is typically in JAVA_HOME/bin/javac
221+
java_home = os.path.dirname(os.path.dirname(real_path))
222+
if os.path.isdir(java_home):
223+
return java_home
224+
else:
225+
# Windows: try 'where javac'
226+
result = subprocess.run(
227+
["where", "javac"],
228+
capture_output=True,
229+
text=True,
230+
timeout=5
231+
)
232+
if result.returncode == 0:
233+
javac_path = result.stdout.strip().split('\n')[0]
234+
if javac_path:
235+
java_home = os.path.dirname(os.path.dirname(javac_path))
236+
if os.path.isdir(java_home):
237+
return java_home
238+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
239+
pass
240+
241+
return None
242+
243+
244+
def _search_common_java_locations():
245+
"""Search common Java installation directories."""
246+
search_patterns = []
247+
248+
if sys.platform.startswith("linux"):
249+
search_patterns = [
250+
"/usr/lib/jvm/java-*-openjdk*",
251+
"/usr/lib/jvm/java-*-oracle*",
252+
"/usr/lib/jvm/jdk-*",
253+
"/usr/lib/jvm/java-*",
254+
"/usr/lib/jvm/default-java",
255+
"/usr/java/jdk*",
256+
"/usr/java/latest",
257+
"/opt/java/jdk*",
258+
"/opt/jdk*",
259+
]
260+
elif sys.platform.startswith("darwin"):
261+
search_patterns = [
262+
"/Library/Java/JavaVirtualMachines/*/Contents/Home",
263+
"/System/Library/Java/JavaVirtualMachines/*/Contents/Home",
264+
"/usr/local/opt/openjdk*/libexec/openjdk.jdk/Contents/Home",
265+
"/opt/homebrew/opt/openjdk*/libexec/openjdk.jdk/Contents/Home",
266+
]
267+
elif sys.platform.startswith("win"):
268+
search_patterns = [
269+
"C:/Program Files/Java/jdk*",
270+
"C:/Program Files (x86)/Java/jdk*",
271+
"C:/Program Files/Eclipse Adoptium/jdk*",
272+
"C:/Program Files/Microsoft/jdk*",
273+
]
274+
275+
candidates = []
276+
for pattern in search_patterns:
277+
matches = glob.glob(pattern)
278+
candidates.extend(matches)
279+
280+
# Sort by version (prefer newer versions)
281+
candidates.sort(reverse=True)
282+
283+
# Return first valid candidate with include directory
284+
for candidate in candidates:
285+
include_dir = os.path.join(candidate, "include")
286+
if os.path.isdir(include_dir) and os.path.isfile(os.path.join(include_dir, "jni.h")):
287+
return candidate
288+
289+
return None
290+
291+
292+
def _get_java_version(java_home):
293+
"""Get Java version from the installation."""
294+
try:
295+
java_bin = os.path.join(java_home, "bin", "java")
296+
if sys.platform.startswith("win"):
297+
java_bin += ".exe"
298+
299+
if os.path.isfile(java_bin):
300+
result = subprocess.run(
301+
[java_bin, "-version"],
302+
capture_output=True,
303+
text=True,
304+
timeout=5
305+
)
306+
# Java outputs version to stderr
307+
output = result.stderr or result.stdout
308+
if output:
309+
# Parse version from output like: openjdk version "11.0.11" or java version "1.8.0_291"
310+
for line in output.split('\n'):
311+
if 'version' in line.lower():
312+
parts = line.split('"')
313+
if len(parts) >= 2:
314+
return parts[1]
315+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
316+
pass
317+
318+
return "unknown"

0 commit comments

Comments
 (0)