Moved logic of handling updates to csharp dependencies to RuntimeDependencyCollection

Removed the RuntimeDependencyLoader
This commit is contained in:
MischaPanch authored and Michael Panchenko committed 2025-09-19 17:59:10 +02:00
1 parent a80fa38d3a
commit 96aec3f885
6 files changed
+76 -129

No files matched your search

@@ -89,7 +89,7 @@ class ClangdLanguageServer(SolidLanguageServer):
)
clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd")
dep = deps.single_for_current_platform()
dep = deps.get_single_dep_for_current_platform()
clangd_executable_path = deps.binary_path(clangd_ls_dir)
if not os.path.exists(clangd_executable_path):
logger.log(
+1 -1
View File
@@ -112,7 +112,7 @@ class ClojureLSP(SolidLanguageServer):
"""Setup runtime dependencies for clojure-lsp and return the command to start the server."""
verify_clojure_cli()
deps = ClojureLSP.runtime_dependencies
dependency = deps.single_for_current_platform()
dependency = deps.get_single_dep_for_current_platform()
clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings)
clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir)
+42 -13
View File
@@ -4,8 +4,10 @@ import logging
import os
import platform
import subprocess
from collections.abc import Sequence
from dataclasses import dataclass
from collections.abc import Iterable, Mapping, Sequence
from copy import copy
from dataclasses import dataclass, replace
from typing import Any, cast
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import FileUtils, PlatformUtils
@@ -33,23 +35,49 @@ class RuntimeDependency:
class RuntimeDependencyCollection:
"""Utility to handle installation of runtime dependencies."""
def __init__(self, dependencies: Sequence[RuntimeDependency]):
self._dependencies = list(dependencies)
def __init__(self, dependencies: Sequence[RuntimeDependency], overrides: Iterable[Mapping[str, Any]] = ()) -> None:
"""Initialize the collection with a list of dependencies and optional overrides.
def for_platform(self, platform_id: str) -> list[RuntimeDependency]:
return [d for d in self._dependencies if d.platform_id in (platform_id, "any", "platform-agnostic", None)]
:param dependencies: List of base RuntimeDependency instances. The combination of 'id' and 'platform_id' must be unique.
:param overrides: List of dictionaries which represent overrides or additions to the base dependencies.
Each entry must contain at least the 'id' key, and optionally 'platform_id' to uniquely identify the dependency to override.
"""
self._id_and_platform_id_to_dep: dict[tuple[str, str | None], RuntimeDependency] = {}
for dep in dependencies:
dep_key = (dep.id, dep.platform_id)
if dep_key in self._id_and_platform_id_to_dep:
raise ValueError(f"Duplicate runtime dependency with id '{dep.id}' and platform_id '{dep.platform_id}':\n{dep}")
self._id_and_platform_id_to_dep[dep_key] = dep
def for_current_platform(self) -> list[RuntimeDependency]:
return self.for_platform(PlatformUtils.get_platform_id().value)
for dep_values_override in overrides:
override_key = cast(tuple[str, str | None], (dep_values_override["id"], dep_values_override.get("platform_id")))
base_dep = self._id_and_platform_id_to_dep.get(override_key)
if base_dep is None:
new_runtime_dep = RuntimeDependency(**dep_values_override)
self._id_and_platform_id_to_dep[override_key] = new_runtime_dep
else:
updated_dep = copy(base_dep)
replace(updated_dep, **dep_values_override)
self._id_and_platform_id_to_dep[override_key] = updated_dep
def single_for_current_platform(self) -> RuntimeDependency:
deps = self.for_current_platform()
def get_dependencies_for_platform(self, platform_id: str) -> list[RuntimeDependency]:
return [d for d in self._id_and_platform_id_to_dep.values() if d.platform_id in (platform_id, "any", "platform-agnostic", None)]
def get_dependencies_for_current_platform(self) -> list[RuntimeDependency]:
return self.get_dependencies_for_platform(PlatformUtils.get_platform_id().value)
def get_single_dep_for_current_platform(self, dependency_id: str | None = None) -> RuntimeDependency:
deps = self.get_dependencies_for_current_platform()
if dependency_id is not None:
deps = [d for d in deps if d.id == dependency_id]
if len(deps) != 1:
raise RuntimeError(f"Expected exactly one runtime dependency for {PlatformUtils.get_platform_id().value}, found {len(deps)}")
raise RuntimeError(
f"Expected exactly one runtime dependency for platform-{PlatformUtils.get_platform_id().value} and {dependency_id=}, found {len(deps)}"
)
return deps[0]
def binary_path(self, target_dir: str) -> str:
dep = self.single_for_current_platform()
dep = self.get_single_dep_for_current_platform()
if not dep.binary_name:
return target_dir
return os.path.join(target_dir, dep.binary_name)
@@ -61,7 +89,7 @@ class RuntimeDependencyCollection:
"""
os.makedirs(target_dir, exist_ok=True)
results: dict[str, str] = {}
for dep in self.for_current_platform():
for dep in self.get_dependencies_for_current_platform():
if dep.url:
self._install_from_url(dep, logger, target_dir)
if dep.command:
@@ -107,6 +135,7 @@ class RuntimeDependencyCollection:
@staticmethod
def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None:
assert dep.url is not None
if dep.archive_type == "gz" and dep.binary_name:
dest = os.path.join(target_dir, dep.binary_name)
FileUtils.download_and_extract_archive(logger, dep.url, dest, dep.archive_type)
@@ -13,7 +13,7 @@ import threading
import urllib.request
import zipfile
from pathlib import Path
from typing import cast
from typing import Any, cast
from overrides import override
@@ -27,10 +27,9 @@ from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.zip import SafeZipExtractor
from .common import RuntimeDependency
from .common import RuntimeDependency, RuntimeDependencyCollection
# Runtime dependencies configuration
RUNTIME_DEPENDENCIES = [
_RUNTIME_DEPENDENCIES = [
RuntimeDependency(
id="CSharpLanguageServer",
description="Microsoft.CodeAnalysis.LanguageServer for Windows (x64)",
@@ -180,12 +179,22 @@ def find_solution_or_project_file(root_dir) -> str | None:
class CSharpLanguageServer(SolidLanguageServer):
"""
Provides C# specific instantiation of the LanguageServer class using Microsoft.CodeAnalysis.LanguageServer.
This is the official Roslyn-based language server from Microsoft.
Provides C# specific instantiation of the LanguageServer class using `Microsoft.CodeAnalysis.LanguageServer`,
the official Roslyn-based language server from Microsoft.
You can pass the following entries in ls_specific_settings["csharp"]:
- dotnet_runtime_url: overrides the .NET runtime download URL
- runtime_dependencies: list of dependency definitions merged with RUNTIME_DEPENDENCIES
You can pass a list of runtime dependency overrides in ls_specific_settings["csharp"]. This is a list of
dicts, each containing at least the "id" key, and optionally "platform_id" to uniquely identify the dependency to override.
For example, to override the URL of the .NET runtime on windows-x64, add the entry:
```
{
"id": "DotNetRuntime",
"platform_id": "win-x64",
"url": "https://example.com/custom-dotnet-runtime.zip"
}
```
See the `_RUNTIME_DEPENDENCIES` variable above for the available dependency ids and platform_ids.
"""
def __init__(
@@ -238,88 +247,43 @@ class CSharpLanguageServer(SolidLanguageServer):
Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
Returns a tuple of (dotnet_path, language_server_dll_path).
"""
runtime_id = CSharpLanguageServer._get_runtime_id()
from solidlsp.runtime_config import RuntimeDependencyLoader
language_enum = cls.get_language_enum_instance()
ls_specific_settings = solidlsp_settings.ls_specific_settings or {}
language_specific_config = ls_specific_settings.get(language_enum)
if language_specific_config is None:
language_specific_config = ls_specific_settings.get(str(language_enum))
runtime_overrides = None
if isinstance(language_specific_config, dict):
runtime_overrides = cast(list[dict[str, object]] | None, language_specific_config.get("runtime_dependencies"))
language_specific_config = ls_specific_settings.get(language_enum, {})
runtime_dependency_overrides = cast(list[dict[str, Any]], language_specific_config.get("runtime_dependencies", []))
logger.log("Resolving runtime dependencies", logging.DEBUG)
if language_specific_config is not None:
if language_specific_config:
logger.log(
f"Language-specific config for {language_enum}: {language_specific_config}",
logging.DEBUG,
)
all_dependencies = RuntimeDependencyLoader.load_dependencies(
base_dependencies=RUNTIME_DEPENDENCIES,
override_entries=runtime_overrides,
runtime_dependencies = RuntimeDependencyCollection(
_RUNTIME_DEPENDENCIES,
overrides=runtime_dependency_overrides,
)
logger.log(
f"Available runtime dependencies: {[(dep.id, dep.platform_id) for dep in all_dependencies]}",
f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}",
logging.DEBUG,
)
# Find the dependencies for our platform
lang_server_dep = None
dotnet_runtime_dep = None
for dep in all_dependencies:
logger.log(
f"Inspecting dependency {dep.id} for platform {dep.platform_id}",
logging.DEBUG,
)
if dep.id == "CSharpLanguageServer" and dep.platform_id == runtime_id:
lang_server_dep = dep
elif dep.id == "DotNetRuntime" and dep.platform_id == runtime_id:
dotnet_runtime_dep = dep
if not lang_server_dep:
logger.log(
f"No C# language server dependency matched runtime {runtime_id}",
logging.ERROR,
)
raise SolidLSPException(f"No C# language server dependency found for platform {runtime_id}")
if not dotnet_runtime_dep:
logger.log(
f"No .NET runtime dependency matched runtime {runtime_id}",
logging.ERROR,
)
raise SolidLSPException(f"No .NET runtime dependency found for platform {runtime_id}")
lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer")
dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime")
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep, solidlsp_settings)
server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep, solidlsp_settings)
return dotnet_path, server_dll_path
@staticmethod
def _get_runtime_id() -> str:
"""Determine the runtime ID based on the platform."""
system = platform.system().lower()
machine = platform.machine().lower()
if system == "windows":
return "win-x64" if machine in ["amd64", "x86_64"] else "win-arm64"
elif system == "darwin":
return "osx-x64" if machine in ["x86_64"] else "osx-arm64"
elif system == "linux":
return "linux-x64" if machine in ["x86_64", "amd64"] else "linux-arm64"
else:
raise SolidLSPException(f"Unsupported platform: {system} {machine}")
@classmethod
def _ensure_dotnet_runtime(
cls, logger: LanguageServerLogger, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
"""Ensure .NET runtime is available and return the dotnet executable path."""
# TODO: use RuntimeDependency util methods instead of custom validation/download logic
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
if system_dotnet:
@@ -458,6 +422,8 @@ class CSharpLanguageServer(SolidLanguageServer):
Ensure .NET 9 runtime is available using runtime dependency configuration.
Returns the path to the dotnet executable.
"""
# TODO: use RuntimeDependency util methods instead of custom download logic
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
if system_dotnet:
@@ -108,7 +108,7 @@ class TerraformLS(SolidLanguageServer):
),
]
)
dependency = deps.single_for_current_platform()
dependency = deps.get_single_dep_for_current_platform()
terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings))
if not os.path.exists(terraform_ls_executable_path):
-48
View File
@@ -1,48 +0,0 @@
"""Runtime dependency configuration loader for language servers."""
from __future__ import annotations
from collections.abc import Iterable, Mapping
from dataclasses import asdict
from typing import Optional
from .language_servers.common import RuntimeDependency
class RuntimeDependencyLoader:
"""Utility helpers to merge runtime dependency overrides."""
@staticmethod
def load_dependencies(
base_dependencies: Iterable[RuntimeDependency],
override_entries: Optional[Iterable[Mapping[str, object]]] = None,
) -> list[RuntimeDependency]:
"""Merge *override_entries* into *base_dependencies*.
Entries are matched by the tuple ``(id, platform_id)``. If an override provides only a
subset of the ``RuntimeDependency`` fields, the remaining values will fall back to the
corresponding base dependency.
"""
# Ensure we work on a copy in order to keep the base definition immutable for callers
merged: dict[tuple[str | None, str | None], RuntimeDependency] = {(dep.id, dep.platform_id): dep for dep in base_dependencies}
if not override_entries:
return list(merged.values())
for entry in override_entries:
entry_id = entry.get("id")
if entry_id is None:
raise ValueError("Runtime dependency override requires an 'id' value")
key = (entry_id, entry.get("platform_id"))
base_dep = merged.get(key)
if base_dep is not None:
dep_data = asdict(base_dep)
dep_data.update(entry)
else:
dep_data = dict(entry)
merged[key] = RuntimeDependency(**dep_data)
return list(merged.values())