Fix Java Gradle import JAVA_HOME fallback (#1657)

This commit is contained in:
osmanrafiq committed 2026-07-06 00:14:28 +02:00
1 parent 6171c484d6
commit 9ccd5f02a2
4 files changed
+185 -20

No files matched your search

+2
View File
@@ -62,6 +62,8 @@ Status of the `main` branch. Changes prior to the next official version change w
- `JuliaLanguageServer`: Fix the stdio MCP server exiting right after `initialize` ("tools fetch failed")
when `julia` is enabled. #1577
- `Java`: invalidate JDTLS workspace cache when Java import settings change #1576
- `Java`: use `JAVA_HOME` for Gradle import when `use_system_java_home` is enabled and `gradle_java_home`
is unset. #1657
- Improve quoting of arguments in shell executions
- Add **LaTeX** support (experimental) via [texlab](https://github.com/latex-lsp/texlab).
- PHP: add support for PHPantom as alternative to the already supported PHP LS #1554.
+2 -2
View File
@@ -695,8 +695,8 @@ The following settings are supported for the Java language server:
| `maven_user_settings` | `~/.m2/settings.xml` | Path to Maven `settings.xml` |
| `gradle_user_home` | `~/.gradle` | Path to Gradle user home directory |
| `gradle_wrapper_enabled` | `false` | Use the project's Gradle wrapper (`gradlew`) instead of the bundled Gradle distribution. Enable this for projects with custom plugins or repositories. |
| `gradle_java_home` | `null` | Path to the JDK used by Gradle. When unset, Gradle uses the bundled JRE. |
| `use_system_java_home` | `false` | Use the system's `JAVA_HOME` environment variable for JDTLS itself. Enable this if your project requires a specific JDK vendor or version for Gradle's JDK checks. |
| `gradle_java_home` | `null` | Path to the JDK used by Gradle. When unset, Gradle uses `JAVA_HOME` if `use_system_java_home` is enabled and `JAVA_HOME` is set; otherwise it falls back to Serena's bundled JRE. |
| `use_system_java_home` | `false` | Use the system's `JAVA_HOME` environment variable for JDTLS itself and, when `gradle_java_home` is unset, Gradle import. Enable this if your project requires a specific JDK vendor or version for Gradle's JDK checks. |
| `gradle_version` | `8.14.2` | (vscode-java mode only) Override the Gradle distribution version Serena downloads by default. |
| `vscode_java_version` | `1.54.0-923` | (vscode-java mode only) Override the bundled `vscode-java` runtime bundle version Serena downloads by default. |
| `intellicode_version` | `1.2.30` | (vscode-java mode only) Override the IntelliCode VSIX version Serena downloads by default. |
+42 -18
View File
@@ -180,8 +180,10 @@ class EclipseJDTLS(SolidLanguageServer):
- maven_user_settings: Path to Maven settings.xml file (default: ~/.m2/settings.xml)
- gradle_user_home: Path to Gradle user home directory (default: ~/.gradle)
- gradle_wrapper_enabled: Whether to use the project's Gradle wrapper (default: false)
- gradle_java_home: Path to JDK for Gradle (default: null, uses bundled JRE)
- use_system_java_home: Whether to use the system's JAVA_HOME for JDTLS itself (default: false)
- gradle_java_home: Path to JDK for Gradle (default: null, falls back to JAVA_HOME when
use_system_java_home is true, then the bundled JRE)
- use_system_java_home: Whether to use the system's JAVA_HOME for JDTLS itself and, when
gradle_java_home is unset, Gradle import (default: false)
- jdtls_xmx: Maximum heap size for the JDTLS server JVM (default: "3G")
- jdtls_xms: Initial heap size for the JDTLS server JVM (default: "100m")
- intellicode_xmx: Maximum heap size for the IntelliCode embedded JVM (default: "1G")
@@ -217,7 +219,7 @@ class EclipseJDTLS(SolidLanguageServer):
# gradle_user_home: 'C:\\Users\\YourName\\.gradle' # Windows (use single quotes!)
gradle_wrapper_enabled: true # set to true for projects with custom plugins/repositories
gradle_java_home: "/path/to/jdk" # set to override Gradle's JDK
use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS
use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS and Gradle fallback
jdtls_xmx: "3G" # maximum heap size for the JDTLS server JVM
jdtls_xms: "100m" # initial heap size for the JDTLS server JVM
intellicode_xmx: "1G" # maximum heap size for the IntelliCode embedded JVM
@@ -904,6 +906,40 @@ class EclipseJDTLS(SolidLanguageServer):
log.info(f"Using bundled JRE for JDTLS: {java_home}")
return {"syntaxserver": "false", "JAVA_HOME": java_home}
def _resolve_gradle_java_home(self) -> str:
"""
Resolve the Java home configured for JDTLS Gradle import settings.
:return: Java home selected from explicit Gradle override, system ``JAVA_HOME`` when enabled, or bundled runtime.
:raises FileNotFoundError: If an explicitly configured ``gradle_java_home`` path does not exist.
"""
# explicit Gradle override...
gradle_java_home = self._custom_settings.get("gradle_java_home")
if gradle_java_home is not None:
if not os.path.exists(gradle_java_home):
error_msg = (
f"Gradle Java home not found: {gradle_java_home}. "
f"Fix: update path in ~/.serena/serena_config.yml (ls_specific_settings -> java -> gradle_java_home), "
f"or remove the setting to use JAVA_HOME or the bundled JRE fallback"
)
log.error(error_msg)
raise FileNotFoundError(error_msg)
log.info(f"Using Gradle Java home from custom location: {gradle_java_home}")
return gradle_java_home
# system JAVA_HOME fallback...
use_system_java_home = self._custom_settings.get("use_system_java_home", False)
if use_system_java_home:
system_java_home = os.environ.get("JAVA_HOME")
if system_java_home:
log.info(f"Using system JAVA_HOME for Gradle: {system_java_home}")
return system_java_home
log.warning("use_system_java_home is set but JAVA_HOME is not set in environment, falling back to bundled JRE for Gradle")
# bundled runtime fallback...
log.info(f"Using bundled JRE for Gradle: {self.runtime_dependency_paths.jre_path}")
return self.runtime_dependency_paths.jre_path
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize parameters for the EclipseJDTLS server.
@@ -977,20 +1013,8 @@ class EclipseJDTLS(SolidLanguageServer):
f"Gradle wrapper {'enabled' if gradle_wrapper_enabled else 'disabled'} (configurable via ls_specific_settings -> java -> gradle_wrapper_enabled)"
)
# Gradle Java home: default to None, which means the bundled JRE is used
gradle_java_home = self._custom_settings.get("gradle_java_home")
if gradle_java_home is not None:
if not os.path.exists(gradle_java_home):
error_msg = (
f"Gradle Java home not found: {gradle_java_home}. "
f"Fix: update path in ~/.serena/serena_config.yml (ls_specific_settings -> java -> gradle_java_home), "
f"or remove the setting to use the bundled JRE"
)
log.error(error_msg)
raise FileNotFoundError(error_msg)
log.info(f"Using Gradle Java home from custom location: {gradle_java_home}")
else:
log.info(f"Using bundled JRE for Gradle: {self.runtime_dependency_paths.jre_path}")
# Gradle Java home: explicit setting, system JAVA_HOME when requested, then bundled runtime.
gradle_java_home = self._resolve_gradle_java_home()
initialize_params = {
"locale": "en",
@@ -1312,7 +1336,7 @@ class EclipseJDTLS(SolidLanguageServer):
# ./gradlew wrapper or a system-installed Gradle via its standard discovery rules.
if self.runtime_dependency_paths.gradle_path is not None:
gradle_settings["home"] = self.runtime_dependency_paths.gradle_path
gradle_settings["java"] = {"home": gradle_java_home if gradle_java_home is not None else self.runtime_dependency_paths.jre_path}
gradle_settings["java"] = {"home": gradle_java_home}
return initialize_params
def _start_server(self) -> None:
@@ -21,6 +21,7 @@ from solidlsp.language_servers.eclipse_jdtls import (
JDTLS_CONFIG_DIR_BY_PLATFORM,
JDTLS_MIN_JDK_VERSION,
EclipseJDTLS,
RuntimeDependencyPaths,
)
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.settings import SolidLSPSettings
@@ -54,6 +55,144 @@ def _make_fake_jdtls_install(
return root
def _make_runtime_dependency_paths(root: Path) -> RuntimeDependencyPaths:
"""
Build minimal runtime dependency paths for initialize-parameter tests.
:param root: Root directory under which fake runtime files are created.
:return: Runtime paths sufficient for ``EclipseJDTLS._create_base_initialize_params``.
"""
jre_home = root / "jre-home"
jre_home.mkdir(parents=True)
jre_bin = jre_home / "bin"
jre_bin.mkdir()
jre_path = jre_bin / _JAVA_EXE_NAME
jre_path.touch()
gradle_path = root / "gradle"
gradle_path.mkdir()
launcher = root / "jdtls-launcher.jar"
launcher.touch()
config = root / "config"
config.mkdir()
lombok = root / "lombok.jar"
lombok.touch()
return RuntimeDependencyPaths(
jre_path=str(jre_path),
jre_home_path=str(jre_home),
jdtls_launcher_jar_path=str(launcher),
jdtls_readonly_config_path=str(config),
lombok_jar_path=str(lombok),
gradle_path=str(gradle_path),
)
def _make_uninitialized_jdtls(
repository_root: Path, custom_settings: dict[str, object], runtime_dependency_paths: RuntimeDependencyPaths
) -> EclipseJDTLS:
"""
Build an EclipseJDTLS instance without starting dependency setup or JDTLS.
:param repository_root: Repository root reported to initialization-parameter generation.
:param custom_settings: Java language-server settings to attach to the instance.
:param runtime_dependency_paths: Runtime paths to attach to the instance.
:return: Partially initialized server suitable for pure parameter-generation tests.
"""
server = object.__new__(EclipseJDTLS)
server.repository_root_path = str(repository_root)
server._custom_settings = SolidLSPSettings.CustomLSSettings(custom_settings)
server.runtime_dependency_paths = runtime_dependency_paths
return server
def _gradle_java_home(initialize_params: dict) -> str:
"""
Return the Gradle Java home from initialize parameters.
:param initialize_params: JDTLS initialize-parameter payload.
:return: Configured Gradle Java home.
"""
return initialize_params["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"]["home"]
# ----------------------------------------------------------------------------
# Gradle Java-home initialize settings
# ----------------------------------------------------------------------------
class TestGradleJavaHomeInitializeSettings:
def test_explicit_gradle_java_home_takes_precedence_over_java_home(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Verify explicit Gradle Java home wins over system ``JAVA_HOME``."""
repo = tmp_path / "repo"
repo.mkdir()
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
explicit_gradle_jdk = tmp_path / "explicit-gradle-jdk"
explicit_gradle_jdk.mkdir()
env_jdk = tmp_path / "env-jdk"
env_jdk.mkdir()
monkeypatch.setenv("JAVA_HOME", str(env_jdk))
server = _make_uninitialized_jdtls(
repo,
{"gradle_java_home": str(explicit_gradle_jdk), "use_system_java_home": True},
runtime_paths,
)
assert _gradle_java_home(server._create_base_initialize_params()) == str(explicit_gradle_jdk)
def test_uses_java_home_for_gradle_when_system_java_home_enabled(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Verify Gradle import uses system ``JAVA_HOME`` when the system setting is enabled."""
repo = tmp_path / "repo"
repo.mkdir()
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
env_jdk = tmp_path / "env-jdk"
env_jdk.mkdir()
monkeypatch.setenv("JAVA_HOME", str(env_jdk))
server = _make_uninitialized_jdtls(repo, {"use_system_java_home": True}, runtime_paths)
assert _gradle_java_home(server._create_base_initialize_params()) == str(env_jdk)
def test_uses_bundled_runtime_for_gradle_when_system_java_home_disabled(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Verify Gradle import uses the bundled runtime when system Java home is disabled."""
repo = tmp_path / "repo"
repo.mkdir()
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
env_jdk = tmp_path / "env-jdk"
env_jdk.mkdir()
monkeypatch.setenv("JAVA_HOME", str(env_jdk))
server = _make_uninitialized_jdtls(repo, {"use_system_java_home": False}, runtime_paths)
assert _gradle_java_home(server._create_base_initialize_params()) == runtime_paths.jre_path
def test_uses_bundled_runtime_for_gradle_when_requested_java_home_is_unset(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Verify Gradle import falls back to the bundled runtime when ``JAVA_HOME`` is unset."""
repo = tmp_path / "repo"
repo.mkdir()
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
monkeypatch.delenv("JAVA_HOME", raising=False)
server = _make_uninitialized_jdtls(repo, {"use_system_java_home": True}, runtime_paths)
assert _gradle_java_home(server._create_base_initialize_params()) == runtime_paths.jre_path
def test_missing_explicit_gradle_java_home_raises(self, tmp_path: Path) -> None:
"""Verify missing explicit Gradle Java home remains a configuration error."""
repo = tmp_path / "repo"
repo.mkdir()
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
server = _make_uninitialized_jdtls(repo, {"gradle_java_home": str(tmp_path / "missing-jdk")}, runtime_paths)
with pytest.raises(FileNotFoundError, match="Gradle Java home not found"):
server._create_base_initialize_params()
# ----------------------------------------------------------------------------
# _resolve_launcher_jar
# ----------------------------------------------------------------------------