From 9ccd5f02a20da96d88fe63a240355311dc373b97 Mon Sep 17 00:00:00 2001 From: osmanrafiq Date: Mon, 6 Jul 2026 00:14:28 +0200 Subject: [PATCH] Fix Java Gradle import JAVA_HOME fallback (#1657) --- CHANGELOG.md | 2 + docs/02-usage/050_configuration.md | 4 +- .../language_servers/eclipse_jdtls.py | 60 +++++--- .../java/test_jdtls_path_resolution.py | 139 ++++++++++++++++++ 4 files changed, 185 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c951222..2d69a6db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 03f44761..29c6df7d 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -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. | diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index 320f7ed1..8c85998d 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -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: diff --git a/test/solidlsp/java/test_jdtls_path_resolution.py b/test/solidlsp/java/test_jdtls_path_resolution.py index 2154eb9b..3492134a 100644 --- a/test/solidlsp/java/test_jdtls_path_resolution.py +++ b/test/solidlsp/java/test_jdtls_path_resolution.py @@ -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 # ----------------------------------------------------------------------------