Introduce DownloadedDependency abstraction with URL-keyed hash database

* Add DownloadedDependency abstraction to facilitate handling of downloaded
  and hash-verified dependencies.
  In CI, missing hashes raise an exception.
  For users, unverified downloads log a warning.
* Add DownloadedDependencyHashDatabase which manages a URL-keyed database
  of sha256 hashes. It can be conveniently updated incrementally through a
  script (src/solidlsp/resources/downloaded_dependency_hashes.json),
  provided that language server implementations provide DownloadedDependency
  instances statically

Applied in EclipseJDTLS:
* Replace the untyped nested-dict runtime dependency descriptors with DownloadedDependency instances
* Move the JDTLS version-pinning constants into DependencyProvider
* Replace the platform dependency dicts with the VSCodeJavaConfig dataclass

Utils:
 * Add FileUtils.ArchiveType to type archive types that were previously plain str
 * Add is_running_in_ci() to ls_utils

Updated memory on adding a language server accordingly.
This commit is contained in:
Dominik Jain authored and Dr. Dominik Jain committed 2026-08-03 01:34:24 +02:00
1 parent c7dbe3bd11
commit 9fc8033ece
11 files changed
+401 -231

No files matched your search

@@ -66,6 +66,50 @@ To implement a new language server using the DependencyProvider pattern:
You should look at at least one existing implementation of each base class to understand how they work.
#### Downloading Runtime Dependencies
Use `DownloadedDependency` (in `solidlsp.dependency_provider`), which bundles the URL,
archive type, allowed hosts and checksum verification behind a single `download_to()` call:
```python
dep = DownloadedDependency(
url=f"https://example.org/foo-{version}-{platform}.zip",
archive_type="zip", # optional FileUtils.ArchiveType for extraction
allowed_hosts=FOO_ALLOWED_HOSTS, # optional list of allowed hosts
)
dep.download_to(target_dir)
```
Checksums for downloads live in a URL-keyed database, `src/solidlsp/resources/downloaded_dependency_hashes.json`,
managed by `DownloadedDependencyHashDatabase`.
Consequences for your implementation:
* Build each dependency in a factory classmethod (`_create_dep_*`) that takes an
optional version and falls back to the pinned `DEFAULT_*` constant.
* Add an `update_dep_hashes()` classmethod that constructs every dependency
and updates the hashes:
```python
@classmethod
def update_dep_hashes(cls) -> None:
deps = [cls._create_dep_foo(), cls_._create_dep_bar(), ...]
with DownloadedDependencyHashDatabase.get_instance().update_context() as db:
for dep in deps:
db.update(dep)
```
Hook a call to this method into `scripts/update_downloaded_dependency_hashes.py`, run that script,
and commit the resulting JSON changes.
* After bumping any pinned version, re-run the script. Add a NOTE comment next to
the version constants saying so; a stale database means unverified downloads
locally and a CI failure.
* Pass `verified=False` only for dependencies whose hash cannot be pinned by design
(e.g. a user-supplied version override).
Reference implementation: `EclipseJDTLS.DependencyProvider`
Note that several older language servers still define hashes locally in constants and
call `FileUtils.download_and_extract_archive_verified`directly. Do not apply this legacy approach.
### 1.2 LSP Initialization
Override `_create_base_initialize_params` to provide server-specific initialization
@@ -0,0 +1,7 @@
from sensai.util import logging
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
if __name__ == "__main__":
logging.configure()
EclipseJDTLS.DependencyProvider.update_dep_hashes()
+130 -2
View File
@@ -1,10 +1,16 @@
import json
import logging
import os
import shutil
import tempfile
import threading
from abc import ABC, abstractmethod
from collections.abc import Sequence
from collections.abc import Iterator, Sequence
from contextlib import contextmanager
from os import PathLike
from solidlsp.settings import SolidLSPSettings
from solidlsp.ls_utils import FileUtils, is_running_in_ci
from solidlsp.settings import SOLIDLSP_RESOURCES_DIR, SolidLSPSettings
log = logging.getLogger(__name__)
@@ -239,3 +245,125 @@ class LanguageServerDependencyProviderUvx(LanguageServerDependencyProviderBaseCo
def _create_launch_command_from_base_command(self, base_command: list[str]) -> list[str]:
return base_command + list(self._extra_args)
class DownloadedDependencyHashDatabase:
RESOURCE_FILE_NAME = "downloaded_dependency_hashes.json"
_instance: "DownloadedDependencyHashDatabase | None" = None
_instance_lock: threading.Lock = threading.Lock()
def __init__(self, _singleton: bool):
self._json_file = os.path.join(SOLIDLSP_RESOURCES_DIR, self.RESOURCE_FILE_NAME)
self._hashes = {}
if os.path.exists(self._json_file):
try:
with open(self._json_file, encoding="utf-8") as f:
self._hashes = json.load(f)
except:
log.warning("Error loading %s", self._json_file, exc_info=True)
@classmethod
def get_instance(cls) -> "DownloadedDependencyHashDatabase":
if cls._instance is None:
with cls._instance_lock:
if cls._instance is None:
cls._instance = DownloadedDependencyHashDatabase(True)
return cls._instance
def get_sha256(self, dep: "DownloadedDependency") -> str | None:
return self._hashes.get(dep.get_url())
class Updater:
def __init__(self, db: "DownloadedDependencyHashDatabase"):
self._db = db
def update(self, dep: "DownloadedDependency", force: bool = False) -> None:
"""
:param dep: the dependency to update the hash for
:param force: whether to force an update even if the hash already exists
"""
self._db._update(dep, force)
@contextmanager
def update_context(self) -> Iterator[Updater]:
try:
yield self.Updater(self)
finally:
self._save()
def _update(self, dep: "DownloadedDependency", force: bool = False) -> None:
if not force and dep.get_url() in self._hashes:
log.info("SHA256 for %s already exists, skipping update", dep.get_url())
return
try:
with tempfile.TemporaryDirectory() as temp_dir:
path = os.path.join(temp_dir, "downloaded_dependency")
log.info("Downloading %s for hash calculation", dep.get_url())
FileUtils.download_file(dep.get_url(), path)
sha = FileUtils.calculate_sha256(path)
log.info("SHA256 for %s: %s", dep.get_url(), sha)
self._hashes[dep.get_url()] = sha
except:
log.warning("Error updating hash for %s", dep.get_url(), exc_info=True)
def _save(self) -> None:
try:
with open(self._json_file, "w", encoding="utf-8") as f:
json.dump(self._hashes, f, indent=2)
except:
log.warning("Error saving %s", self._json_file, exc_info=True)
class DownloadedDependency:
"""
Represents a dependency that can be downloaded from a URL, optionally verified against a known SHA256 hash,
and optionally extracted if it is an archive.
"""
def __init__(
self,
url: str,
archive_type: FileUtils.ArchiveType | None = None,
allowed_hosts: Sequence[str] | None = None,
verified: bool = True,
):
"""
:param url: the URL of the file to be downloaded
:param archive_type: optional type of archive for the case where `url` points to an archive.
Use `None` or "binary" for a binary file (no extraction).
:param allowed_hosts: optional list of allowed hosts for the download URL
:param verified: whether to verify the downloaded file against a known SHA256 hash (if available).
If enabled and the hash for the URL is not known, will raise an exception in CI and issue a warning
in non-CI environments.
"""
self._url = url
self._allowed_hosts = allowed_hosts
self._archive_type = archive_type if archive_type is not None else "binary"
self._verified = verified
def get_url(self) -> str:
return self._url
def download_to(self, target_path: str | PathLike) -> None:
"""
Downloads the dependency to the specified target path.
If defined at construction, will apply hash verification and archive extraction.
:param target_path: the path to which the dependency should be downloaded (or extracted if applicable)
"""
if not isinstance(target_path, str):
target_path = str(target_path)
if self._verified:
sha256 = DownloadedDependencyHashDatabase.get_instance().get_sha256(self)
if sha256 is None:
if is_running_in_ci():
raise RuntimeError(
f"No SHA256 hash found for {self._url}. "
"Please update the hash database by running 'scripts/update_downloaded_dependency_hashes.py'."
)
log.warning("No SHA256 hash found for %s. The downloaded file will not be verified.", self._url)
else:
sha256 = None
FileUtils.download_and_extract_archive_verified(
self._url, target_path, archive_type=self._archive_type, expected_sha256=sha256, allowed_hosts=self._allowed_hosts
)
+1 -1
View File
@@ -24,7 +24,7 @@ class RuntimeDependency:
url: str | None = None
sha256: str | None = None
allowed_hosts: tuple[str, ...] | list[str] | None = None
archive_type: str | None = None
archive_type: FileUtils.ArchiveType | None = None
binary_name: str | None = None
command: str | list[str] | None = None
package_name: str | None = None
+185 -218
View File
@@ -13,18 +13,20 @@ import re
import shutil
import subprocess
import threading
from dataclasses import dataclass
from pathlib import Path, PurePath
from time import sleep
from typing import Any, cast
from typing import Any
from overrides import override
from solidlsp import ls_types
from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import UnifiedSymbolInformation
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.ls_utils import PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, SymbolInformation
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.subprocess_util import subprocess_run
@@ -32,7 +34,6 @@ from solidlsp.util.subprocess_util import subprocess_run
log = logging.getLogger(__name__)
GRADLE_ALLOWED_HOSTS = ("services.gradle.org", "github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
GRADLE_SHA256 = "7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999"
VSCODE_JAVA_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
INTELLICODE_ALLOWED_HOSTS = (
"visualstudioexptteam.gallery.vsassets.io",
@@ -41,30 +42,6 @@ INTELLICODE_ALLOWED_HOSTS = (
)
# Version pinning convention (read this before bumping anything below):
#
# INITIAL_* — the very first version we shipped runtime-dependency support for, paired with
# its SHA. NEVER edited. The legacy unversioned install directory (e.g. "vscode-java/")
# is reserved exclusively for this version, so users who installed under the original
# layout keep their existing cache forever.
#
# DEFAULT_* — the version (and matching SHA) used when the user does not override via
# custom_settings. THIS is what we edit to upgrade: copy the new version + new SHA over
# the existing DEFAULT_* literals, leave INITIAL_* alone.
#
# Resolution at install time:
# resolved_version = custom_settings.get("..._version", DEFAULT_*_VERSION)
# - resolved == INITIAL_* -> legacy unversioned dir, SHA = INITIAL_*_SHA256
# - resolved == DEFAULT_* -> versioned dir "{name}-{resolved}", SHA = DEFAULT_*_SHA256
# - any other version -> versioned dir "{name}-{resolved}", SHA verification skipped
#
# Consequences of this scheme:
# - Bumping DEFAULT_* makes new installs land in a new versioned subdir => no silent reuse
# of stale binaries (the bug this scheme exists to fix).
# - Previously-downloaded versions are not lost: a user can request any past version via
# custom_settings and the matching cached subdir is picked up without re-downloading.
# - INITIAL_* and DEFAULT_* hold identical literals when first introduced; they diverge on
# the first DEFAULT_* bump and stay independent thereafter.
@dataclasses.dataclass(frozen=True)
class VsixResourcePaths:
"""
@@ -81,40 +58,6 @@ class VsixResourcePaths:
equinox_launcher_basename: str # e.g. "org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar"
INITIAL_VSCODE_JAVA_VERSION = "1.42.0-561"
INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM = {
"osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933",
"osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f",
"linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1",
"linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9",
"win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3",
}
INITIAL_VSCODE_JAVA_PATHS = VsixResourcePaths(
jre_version="21.0.7",
lombok_jar_basename="lombok-1.18.36.jar",
equinox_launcher_basename="org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
)
# Bumped from 1.42.0-561 to surface Lombok-generated methods (#1432); brings JDTLS commit b2d8952
# (java.symbols.includeGeneratedCode), JRE 21.0.10, Lombok 1.18.39 and Equinox launcher 1.7.100.
DEFAULT_VSCODE_JAVA_VERSION = "1.54.0-923"
DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM = {
"osx-arm64": "c54c45cb0d2579d8e0a4ddeb24d4a9dd0b460d07d9366adea2b38a1da22a463c",
"osx-x64": "dfc98abc4e54165a78372e280242a039671729b1b03420608df3b10c6b629fb6",
"linux-arm64": "e2bb22c427d90da8dbb1afff72ff1e2dce38d50b76deb02d7bc313a330a1330c",
"linux-x64": "9d4b15da54e25a0192f9bac073f086c015397d3676623b68dbf83a5dbaf5132b",
"win-x64": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4",
}
DEFAULT_VSCODE_JAVA_PATHS = VsixResourcePaths(
jre_version="21.0.10",
lombok_jar_basename="lombok-1.18.39-4050.jar",
equinox_launcher_basename="org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar",
)
INITIAL_INTELLICODE_VERSION = "1.2.30"
INITIAL_INTELLICODE_SHA256 = "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251"
DEFAULT_INTELLICODE_VERSION = "1.2.30"
DEFAULT_INTELLICODE_SHA256 = "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251"
# Mapping from Serena's platform identifiers to upstream JDTLS config_<platform> directory names
JDTLS_CONFIG_DIR_BY_PLATFORM = {
"osx-arm64": "config_mac_arm",
@@ -150,6 +93,19 @@ class RuntimeDependencyPaths:
intellisense_members_path: str | None = None
@dataclass
class VSCodeJavaConfig:
"""
Represents a (platform-specific) configuration for VSCode Java
"""
jre_home_path: str
jre_path: str
lombok_jar_path: str
jdtls_launcher_jar_path: str
jdtls_readonly_config_path: str
class EclipseJDTLS(SolidLanguageServer):
r"""
The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class
@@ -271,6 +227,56 @@ class EclipseJDTLS(SolidLanguageServer):
return self.DependencyProvider(self._custom_settings, ls_resources_dir, self._solidlsp_settings, self.repository_root_path)
class DependencyProvider(LanguageServerDependencyProvider):
"""
Dependency provider for Eclipse JDTLS.
Version pinning convention (read this before bumping anything below):
INITIAL_* — the very first version we shipped runtime-dependency support for, paired with
its SHA. NEVER edited. The legacy unversioned install directory (e.g. "vscode-java/")
is reserved exclusively for this version, so users who installed under the original
layout keep their existing cache forever.
DEFAULT_* — the version (and matching SHA) used when the user does not override via
custom_settings. THIS is what we edit to upgrade: copy the new version + new SHA over
the existing DEFAULT_* literals, leave INITIAL_* alone.
Resolution at install time:
resolved_version = custom_settings.get("..._version", DEFAULT_*_VERSION)
- resolved == INITIAL_* -> legacy unversioned dir, SHA = INITIAL_*_SHA256
- resolved == DEFAULT_* -> versioned dir "{name}-{resolved}", SHA = DEFAULT_*_SHA256
- any other version -> versioned dir "{name}-{resolved}", SHA verification skipped
Consequences of this scheme:
- Bumping DEFAULT_* makes new installs land in a new versioned subdir => no silent reuse
of stale binaries (the bug this scheme exists to fix).
- Previously-downloaded versions are not lost: a user can request any past version via
custom_settings and the matching cached subdir is picked up without re-downloading.
- INITIAL_* and DEFAULT_* hold identical literals when first introduced; they diverge on
the first DEFAULT_* bump and stay independent thereafter.
"""
# versions used initially (which use default paths without version suffix)
INITIAL_VSCODE_JAVA_VERSION = "1.42.0-561"
INITIAL_INTELLICODE_VERSION = "1.2.30"
INITIAL_VSCODE_JAVA_PATHS = VsixResourcePaths(
jre_version="21.0.7",
lombok_jar_basename="lombok-1.18.36.jar",
equinox_launcher_basename="org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
)
# currently used default versions
# NOTE: After bumping any of these, also update the corresponding hashes by running
# scripts/update_downloaded_dependency_hashes.py and commit the resulting changes.
DEFAULT_GRADLE_VERSION = "8.14.2"
DEFAULT_VSCODE_JAVA_VERSION = "1.54.0-923"
DEFAULT_INTELLICODE_VERSION = "1.2.30"
DEFAULT_VSCODE_JAVA_PATHS = VsixResourcePaths(
jre_version="21.0.10",
lombok_jar_basename="lombok-1.18.39-4050.jar",
equinox_launcher_basename="org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar",
)
def __init__(
self,
custom_settings: SolidLSPSettings.CustomLSSettings,
@@ -283,9 +289,57 @@ class EclipseJDTLS(SolidLanguageServer):
self._repository_root_path = repository_root_path
self.runtime_dependency_paths = self._setup_runtime_dependencies(ls_resources_dir, custom_settings)
@staticmethod
@classmethod
def _create_dep_gradle(cls, gradle_version: str | None = None) -> DownloadedDependency:
gradle_version = gradle_version or cls.DEFAULT_GRADLE_VERSION
return DownloadedDependency(
url=f"https://services.gradle.org/distributions/gradle-{gradle_version}-bin.zip",
archive_type="zip",
allowed_hosts=GRADLE_ALLOWED_HOSTS,
verified=gradle_version == cls.DEFAULT_GRADLE_VERSION,
)
@classmethod
def _create_deps_vscode_java(cls, vscode_java_version: str | None = None) -> dict[str, DownloadedDependency]:
vscode_java_version = vscode_java_version or cls.DEFAULT_VSCODE_JAVA_VERSION
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
urls = {
"osx-arm64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"osx-x64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-x64-{vscode_java_version}.vsix",
"linux-arm64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-arm64-{vscode_java_version}.vsix",
"linux-x64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-x64-{vscode_java_version}.vsix",
"win-x64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-win32-x64-{vscode_java_version}.vsix",
}
deps = {}
for platform_key, url in urls.items():
deps[platform_key] = DownloadedDependency(
url=url,
archive_type="zip",
allowed_hosts=VSCODE_JAVA_ALLOWED_HOSTS,
verified=vscode_java_version == cls.DEFAULT_VSCODE_JAVA_VERSION,
)
return deps
@classmethod
def _create_dep_intellicode(cls, intellicode_version: str | None = None) -> DownloadedDependency:
intellicode_version = intellicode_version or cls.DEFAULT_INTELLICODE_VERSION
return DownloadedDependency(
url=f"https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/{intellicode_version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
archive_type="zip",
allowed_hosts=INTELLICODE_ALLOWED_HOSTS,
verified=intellicode_version == cls.DEFAULT_INTELLICODE_VERSION,
)
@classmethod
def update_dep_hashes(cls) -> None:
deps = [cls._create_dep_gradle(), cls._create_dep_intellicode()] + list(cls._create_deps_vscode_java().values())
with DownloadedDependencyHashDatabase.get_instance().update_context() as db:
for dep in deps:
db.update(dep)
@classmethod
def _setup_runtime_dependencies(
ls_resources_dir: str, custom_settings: SolidLSPSettings.CustomLSSettings
cls, ls_resources_dir: str, custom_settings: SolidLSPSettings.CustomLSSettings
) -> RuntimeDependencyPaths:
"""
Setup runtime dependencies for EclipseJDTLS and return the paths.
@@ -313,145 +367,72 @@ class EclipseJDTLS(SolidLanguageServer):
return EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(jdtls_path), str(lombok_path), custom_settings)
platformId = PlatformUtils.get_platform_id()
gradle_version = custom_settings.get("gradle_version", "8.14.2")
vscode_java_version = custom_settings.get("vscode_java_version", DEFAULT_VSCODE_JAVA_VERSION)
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
intellicode_version = custom_settings.get("intellicode_version", DEFAULT_INTELLICODE_VERSION)
default_gradle_version = gradle_version == "8.14.2"
gradle_version = custom_settings.get("gradle_version", cls.DEFAULT_GRADLE_VERSION)
vscode_java_version = custom_settings.get("vscode_java_version", cls.DEFAULT_VSCODE_JAVA_VERSION)
intellicode_version = custom_settings.get("intellicode_version", cls.DEFAULT_INTELLICODE_VERSION)
# install-dir name per the version-pinning convention (see module-level block):
# INITIAL -> legacy unversioned dir; everything else -> "{name}-{resolved}" subdir
vscode_java_dirname = (
"vscode-java" if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION else f"vscode-java-{vscode_java_version}"
"vscode-java" if vscode_java_version == cls.INITIAL_VSCODE_JAVA_VERSION else f"vscode-java-{vscode_java_version}"
)
intellicode_dirname = (
"intellicode" if intellicode_version == INITIAL_INTELLICODE_VERSION else f"intellicode-{intellicode_version}"
"intellicode" if intellicode_version == cls.INITIAL_INTELLICODE_VERSION else f"intellicode-{intellicode_version}"
)
# SHA is only known for our two pinned versions (INITIAL and current DEFAULT);
# for any other user-supplied version we skip verification (returns None)
def vscode_java_sha(platform_key: str) -> str | None:
if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION:
return INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key]
if vscode_java_version == DEFAULT_VSCODE_JAVA_VERSION:
return DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key]
return None
def intellicode_sha() -> str | None:
if intellicode_version == INITIAL_INTELLICODE_VERSION:
return INITIAL_INTELLICODE_SHA256
if intellicode_version == DEFAULT_INTELLICODE_VERSION:
return DEFAULT_INTELLICODE_SHA256
return None
# Resolve internal VSIX paths (JRE / Lombok / launcher filenames). For pinned versions
# these are known; for any other user-supplied version we bail out — guessing would
# silently produce broken paths at JDTLS launch time, which is a worse UX than failing
# fast here with a pointer to upstream-JDTLS mode (which doesn't need pinned paths).
if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION:
vsix_paths = INITIAL_VSCODE_JAVA_PATHS
elif vscode_java_version == DEFAULT_VSCODE_JAVA_VERSION:
vsix_paths = DEFAULT_VSCODE_JAVA_PATHS
if vscode_java_version == cls.INITIAL_VSCODE_JAVA_VERSION:
vsix_paths = cls.INITIAL_VSCODE_JAVA_PATHS
elif vscode_java_version == cls.DEFAULT_VSCODE_JAVA_VERSION:
vsix_paths = cls.DEFAULT_VSCODE_JAVA_PATHS
else:
raise SolidLSPException(
f"Resource paths inside the vscode-java {vscode_java_version} VSIX are not pinned in serena "
f"(known: {INITIAL_VSCODE_JAVA_VERSION}, {DEFAULT_VSCODE_JAVA_VERSION}). "
f"Either remove the 'vscode_java_version' override (defaults to {DEFAULT_VSCODE_JAVA_VERSION}), "
f"(known: {cls.INITIAL_VSCODE_JAVA_VERSION}, {cls.DEFAULT_VSCODE_JAVA_VERSION}). "
f"Either remove the 'vscode_java_version' override (defaults to {cls.DEFAULT_VSCODE_JAVA_VERSION}), "
f"or use upstream JDTLS mode by setting both 'jdtls_path' and 'lombok_path' in "
f"ls_specific_settings.java (no pinning required)."
)
runtime_dependencies: dict[str, dict[str, dict[str, object]]] = {
"gradle": {
"platform-agnostic": {
"url": f"https://services.gradle.org/distributions/gradle-{gradle_version}-bin.zip",
"archiveType": "zip",
"relative_extraction_path": ".",
"sha256": GRADLE_SHA256 if default_gradle_version else None,
"allowed_hosts": GRADLE_ALLOWED_HOSTS,
}
},
"vscode-java": {
"darwin-arm64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("osx-arm64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
},
"osx-arm64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("osx-arm64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-macosx-aarch64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-macosx-aarch64/bin/java",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_mac_arm",
},
"osx-x64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("osx-x64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-macosx-x86_64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-macosx-x86_64/bin/java",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_mac",
},
"linux-arm64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("linux-arm64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-linux-aarch64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-linux-aarch64/bin/java",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_linux_arm",
},
"linux-x64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("linux-x64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-linux-x86_64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-linux-x86_64/bin/java",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_linux",
},
"win-x64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-win32-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("win-x64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-win32-x86_64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-win32-x86_64/bin/java.exe",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_win",
},
},
"intellicode": {
"platform-agnostic": {
"url": f"https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/{intellicode_version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
"alternate_url": f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/{intellicode_version}/vspackage",
"archiveType": "zip",
"relative_extraction_path": intellicode_dirname,
"sha256": intellicode_sha(),
"allowed_hosts": INTELLICODE_ALLOWED_HOSTS,
"intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar",
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members",
}
},
vscode_java_configs: dict[str, VSCodeJavaConfig] = {
"osx-arm64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-macosx-aarch64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-macosx-aarch64/bin/java",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_mac_arm",
),
"osx-x64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-macosx-x86_64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-macosx-x86_64/bin/java",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_mac",
),
"linux-arm64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-linux-aarch64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-linux-aarch64/bin/java",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_linux_arm",
),
"linux-x64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-linux-x86_64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-linux-x86_64/bin/java",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_linux",
),
"win-x64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-win32-x86_64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-win32-x86_64/bin/java.exe",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_win",
),
}
gradle_path = str(
@@ -462,25 +443,20 @@ class EclipseJDTLS(SolidLanguageServer):
)
if not os.path.exists(gradle_path):
gradle_dependency = runtime_dependencies["gradle"]["platform-agnostic"]
FileUtils.download_and_extract_archive_verified(
cast(str, gradle_dependency["url"]),
str(PurePath(gradle_path).parent),
cast(str, gradle_dependency["archiveType"]),
expected_sha256=cast(str | None, gradle_dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], gradle_dependency["allowed_hosts"]),
)
gradle_dependency = cls._create_dep_gradle(gradle_version)
gradle_dependency.download_to(PurePath(gradle_path).parent)
assert os.path.exists(gradle_path)
dependency = runtime_dependencies["vscode-java"][platformId.value]
vscode_java_path = str(PurePath(ls_resources_dir, cast(str, dependency["relative_extraction_path"])))
vscode_java_dep = cls._create_deps_vscode_java(vscode_java_version)[platformId.value]
vscode_java_cfg = vscode_java_configs[platformId.value]
vscode_java_path = str(PurePath(ls_resources_dir, vscode_java_dirname))
os.makedirs(vscode_java_path, exist_ok=True)
jre_home_path = str(PurePath(vscode_java_path, cast(str, dependency["jre_home_path"])))
jre_path = str(PurePath(vscode_java_path, cast(str, dependency["jre_path"])))
lombok_jar_path = str(PurePath(vscode_java_path, cast(str, dependency["lombok_jar_path"])))
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, cast(str, dependency["jdtls_launcher_jar_path"])))
jdtls_readonly_config_path = str(PurePath(vscode_java_path, cast(str, dependency["jdtls_readonly_config_path"])))
jre_home_path = str(PurePath(vscode_java_path, vscode_java_cfg.jre_home_path))
jre_path = str(PurePath(vscode_java_path, vscode_java_cfg.jre_path))
lombok_jar_path = str(PurePath(vscode_java_path, vscode_java_cfg.lombok_jar_path))
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, vscode_java_cfg.jdtls_launcher_jar_path))
jdtls_readonly_config_path = str(PurePath(vscode_java_path, vscode_java_cfg.jdtls_readonly_config_path))
if not all(
[
os.path.exists(vscode_java_path),
@@ -491,13 +467,7 @@ class EclipseJDTLS(SolidLanguageServer):
os.path.exists(jdtls_readonly_config_path),
]
):
FileUtils.download_and_extract_archive_verified(
cast(str, dependency["url"]),
vscode_java_path,
cast(str, dependency["archiveType"]),
expected_sha256=cast(str | None, dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], dependency["allowed_hosts"]),
)
vscode_java_dep.download_to(vscode_java_path)
os.chmod(jre_path, 0o755)
@@ -508,11 +478,13 @@ class EclipseJDTLS(SolidLanguageServer):
assert os.path.exists(jdtls_launcher_jar_path)
assert os.path.exists(jdtls_readonly_config_path)
dependency = runtime_dependencies["intellicode"]["platform-agnostic"]
intellicode_directory_path = str(PurePath(ls_resources_dir, cast(str, dependency["relative_extraction_path"])))
intellicode_dep = cls._create_dep_intellicode(intellicode_version)
intellicode_directory_path = str(PurePath(ls_resources_dir, intellicode_dirname))
os.makedirs(intellicode_directory_path, exist_ok=True)
intellicode_jar_path = str(PurePath(intellicode_directory_path, cast(str, dependency["intellicode_jar_path"])))
intellisense_members_path = str(PurePath(intellicode_directory_path, cast(str, dependency["intellisense_members_path"])))
intellicode_jar_path = str(
PurePath(intellicode_directory_path, "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar")
)
intellisense_members_path = str(PurePath(intellicode_directory_path, "extension/dist/bundledModels/java_intellisense-members"))
if not all(
[
os.path.exists(intellicode_directory_path),
@@ -520,13 +492,7 @@ class EclipseJDTLS(SolidLanguageServer):
os.path.exists(intellisense_members_path),
]
):
FileUtils.download_and_extract_archive_verified(
cast(str, dependency["url"]),
intellicode_directory_path,
cast(str, dependency["archiveType"]),
expected_sha256=cast(str | None, dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], dependency["allowed_hosts"]),
)
intellicode_dep.download_to(intellicode_directory_path)
assert os.path.exists(intellicode_directory_path)
assert os.path.exists(intellicode_jar_path)
@@ -764,8 +730,9 @@ class EclipseJDTLS(SolidLanguageServer):
major = int(version_match.group(1))
return real_home, major
@staticmethod
@classmethod
def _compute_workspace_hash(
cls,
repository_root_path: str,
jdtls_launcher_jar_path: str,
custom_settings: SolidLSPSettings.CustomLSSettings,
@@ -802,7 +769,7 @@ class EclipseJDTLS(SolidLanguageServer):
is_legacy_initial = (
not custom_settings.get("jdtls_path")
and custom_settings.get("vscode_java_version", DEFAULT_VSCODE_JAVA_VERSION) == INITIAL_VSCODE_JAVA_VERSION
and custom_settings.get("vscode_java_version", cls.DEFAULT_VSCODE_JAVA_VERSION) == cls.INITIAL_VSCODE_JAVA_VERSION
and not workspace_settings
)
if is_legacy_initial:
@@ -11,6 +11,7 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import is_running_in_ci
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.subprocess_util import subprocess_run
@@ -160,7 +161,7 @@ class ErlangLanguageServer(SolidLanguageServer):
self.server.notify.initialized({})
# Wait for Erlang LS to be ready - adjust timeout based on environment
is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
is_ci = is_running_in_ci()
is_macos = os.uname().sysname == "Darwin" if hasattr(os, "uname") else False
# macOS in CI can be particularly slow for language server startup
@@ -6,6 +6,7 @@ import dataclasses
import logging
import os
import shlex
from typing import cast
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
@@ -184,7 +185,7 @@ class GroovyLanguageServer(SolidLanguageServer):
java_home_relative_path = java_dependency["java_home_path"]
java_relative_path = java_dependency["java_path"]
java_download_url = java_dependency["url"]
java_archive_type = java_dependency["archiveType"]
java_archive_type = cast(FileUtils.ArchiveType, java_dependency["archiveType"])
assert java_home_relative_path is not None
assert java_relative_path is not None
assert java_download_url is not None
+17 -5
View File
@@ -13,6 +13,7 @@ import tarfile
import tempfile
import uuid
import zipfile
from collections.abc import Sequence
from enum import Enum
from pathlib import Path, PurePath
from typing import Literal, cast
@@ -28,6 +29,15 @@ from solidlsp.util.subprocess_util import subprocess_run
log = logging.getLogger(__name__)
def is_running_in_ci() -> bool:
"""
Determines whether the current process is running in a Continuous Integration (CI) environment.
:return: True if running in CI, False otherwise
"""
return os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
class TextStepper:
r"""
A utility class for stepping through a text string line by line, keeping track of the current line and column numbers.
@@ -383,6 +393,8 @@ class FileUtils:
Utility functions for file operations.
"""
ArchiveType = Literal["tar", "gztar", "bztar", "xztar", "zip", "zip.gz", "gz", "binary"]
@staticmethod
def read_file(file_path: str, encoding: str) -> str:
"""
@@ -429,7 +441,7 @@ class FileUtils:
url: str,
target_path: str,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
allowed_hosts: Sequence[str] | None = None,
) -> None:
"""
Downloads a file from ``url`` to ``target_path`` with optional integrity and host validation.
@@ -468,7 +480,7 @@ class FileUtils:
Path.unlink(Path(temp_file_path))
@staticmethod
def download_and_extract_archive(url: str, target_path: str, archive_type: str) -> None:
def download_and_extract_archive(url: str, target_path: str, archive_type: ArchiveType) -> None:
"""
Downloads the archive from the given URL having format {archive_type} and extracts it to the given {target_path}
"""
@@ -478,9 +490,9 @@ class FileUtils:
def download_and_extract_archive_verified(
url: str,
target_path: str,
archive_type: str,
archive_type: ArchiveType,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
allowed_hosts: Sequence[str] | None = None,
) -> None:
"""
Downloads an archive from ``url`` and extracts it safely into ``target_path``.
@@ -560,7 +572,7 @@ class FileUtils:
raise SolidLSPException(f"Checksum verification failed for '{file_path}': expected {expected_sha256}, got {actual_sha256}")
@staticmethod
def _validate_download_host(url: str, allowed_hosts: tuple[str, ...] | list[str] | None) -> None:
def _validate_download_host(url: str, allowed_hosts: Sequence[str] | None) -> None:
"""
Validates that a download URL resolves to one of the configured hosts.
"""
@@ -0,0 +1,9 @@
{
"https://services.gradle.org/distributions/gradle-8.14.2-bin.zip": "7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999",
"https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage": "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-darwin-arm64-1.54.0-923.vsix": "c54c45cb0d2579d8e0a4ddeb24d4a9dd0b460d07d9366adea2b38a1da22a463c",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-darwin-x64-1.54.0-923.vsix": "dfc98abc4e54165a78372e280242a039671729b1b03420608df3b10c6b629fb6",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-arm64-1.54.0-923.vsix": "e2bb22c427d90da8dbb1afff72ff1e2dce38d50b76deb02d7bc313a330a1330c",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-x64-1.54.0-923.vsix": "9d4b15da54e25a0192f9bac073f086c015397d3676623b68dbf83a5dbaf5132b",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-win32-x64-1.54.0-923.vsix": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4"
}
+3
View File
@@ -16,6 +16,9 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
SOLIDLSP_RESOURCES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources")
@dataclass
class SolidLSPSettings:
"""
@@ -545,9 +545,7 @@ class TestComputeWorkspaceHash:
UPSTREAM_LAUNCHER = "/opt/homebrew/Cellar/jdtls/1.50.0/libexec/plugins/org.eclipse.equinox.launcher_1.7.0.jar"
def _initial_settings(self) -> "SolidLSPSettings.CustomLSSettings":
from solidlsp.language_servers.eclipse_jdtls import INITIAL_VSCODE_JAVA_VERSION
return SolidLSPSettings.CustomLSSettings({"vscode_java_version": INITIAL_VSCODE_JAVA_VERSION})
return SolidLSPSettings.CustomLSSettings({"vscode_java_version": EclipseJDTLS.DependencyProvider.INITIAL_VSCODE_JAVA_VERSION})
def test_initial_default_mode_matches_pre_upstream_format(self) -> None:
"""Legacy carve-out: INITIAL default-mode hash MUST equal md5(repository_root_path)."""