Merge pull request #622 from stgmt/fix/linux-arm64-dotnet-runtime

feat: Add configurable runtime dependencies for C# Language Server (ARM64 support)
This commit is contained in:
Michael Panchenko authored and GitHub committed 2025-09-22 11:23:17 +02:00
commit 271e7ad31f
13 files changed
+170 -73

No files matched your search

+8
View File
@@ -102,6 +102,10 @@ jobs:
swiftly use 6.1.2
echo "~/.swiftly/bin" >> $GITHUB_PATH
echo "Swiftly installed successfully"
# Verify sourcekit-lsp is working before proceeding
echo "=== Verifying sourcekit-lsp installation ==="
which sourcekit-lsp || echo "Warning: sourcekit-lsp not found in PATH"
sourcekit-lsp --help || echo "Warning: sourcekit-lsp not responding"
- name: Install Swift with swiftly (Ubuntu)
if: runner.os == 'Linux'
run: |
@@ -119,6 +123,10 @@ jobs:
echo "=== Adding Swift toolchain to PATH ==="
echo "$HOME/.local/share/swiftly/bin" >> $GITHUB_PATH
echo "Swiftly installed successfully!"
# Verify sourcekit-lsp is working before proceeding
echo "=== Verifying sourcekit-lsp installation ==="
which sourcekit-lsp || echo "Warning: sourcekit-lsp not found in PATH"
sourcekit-lsp --help || echo "Warning: sourcekit-lsp not responding"
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
+1 -1
View File
@@ -309,6 +309,6 @@ class Project:
solidlsp_settings=SolidLSPSettings(
solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME,
project_data_relative_path=SERENA_MANAGED_DIR_NAME,
ls_specific_settings=ls_specific_settings,
ls_specific_settings=ls_specific_settings or {},
),
)
@@ -37,8 +37,9 @@ ls_specific_settings: {}
# Added on 23.08.2025
# Advanced configuration option allowing to configure language server implementation specific options. Maps the language
# (same entry as in project.yml) to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp to see which options are available.
# No documentation on options means no options are available
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
#
tool_timeout: 240
# timeout, in seconds, after which tool executions are terminated
+2 -2
View File
@@ -485,14 +485,14 @@ class LanguageServerSymbolRetriever:
return symbols
def get_document_symbols(self, relative_path: str) -> list[LanguageServerSymbol]:
symbol_dicts, roots = self._lang_server.request_document_symbols(relative_path, include_body=False)
symbol_dicts, _roots = self._lang_server.request_document_symbols(relative_path, include_body=False)
symbols = [LanguageServerSymbol(s) for s in symbol_dicts]
return symbols
def find_by_location(self, location: LanguageServerSymbolLocation) -> LanguageServerSymbol | None:
if location.relative_path is None:
return None
symbol_dicts, roots = self._lang_server.request_document_symbols(location.relative_path, include_body=False)
symbol_dicts, _roots = self._lang_server.request_document_symbols(location.relative_path, include_body=False)
for symbol_dict in symbol_dicts:
symbol = LanguageServerSymbol(symbol_dict)
if symbol.location == location:
+2 -2
View File
@@ -145,7 +145,7 @@ class FindFileTool(Tool):
filename = os.path.basename(abs_path)
return not fnmatch(filename, file_mask)
dirs, files = scan_directory(
_dirs, files = scan_directory(
path=dir_to_scan,
recursive=True,
is_ignored_dir=self.project.is_ignored_path,
@@ -378,7 +378,7 @@ class SearchForPatternTool(Tool):
if os.path.isfile(abs_path):
rel_paths_to_search = [relative_path]
else:
dirs, rel_paths_to_search = scan_directory(
_dirs, rel_paths_to_search = scan_directory(
path=abs_path,
recursive=True,
is_ignored_dir=self.project.is_ignored_path,
@@ -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)
+39 -13
View File
@@ -4,8 +4,9 @@ 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 dataclasses import dataclass, replace
from typing import Any, cast
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import FileUtils, PlatformUtils
@@ -33,23 +34,47 @@ 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:
self._id_and_platform_id_to_dep[override_key] = replace(base_dep, **dep_values_override)
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 +86,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 +132,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)",
@@ -107,6 +106,14 @@ RUNTIME_DEPENDENCIES = [
archive_type="tar.gz",
binary_name="dotnet",
),
RuntimeDependency(
id="DotNetRuntime",
description=".NET 9 Runtime for Linux (ARM64)",
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-arm64.tar.gz",
platform_id="linux-arm64",
archive_type="tar.gz",
binary_name="dotnet",
),
RuntimeDependency(
id="DotNetRuntime",
description=".NET 9 Runtime for macOS (x64)",
@@ -172,11 +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: will override the URL from 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__(
@@ -229,52 +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()
lang_server_dep, dotnet_runtime_dep = CSharpLanguageServer._get_runtime_dependencies(runtime_id)
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, {})
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:
logger.log(
f"Language-specific config for {language_enum}: {language_specific_config}",
logging.DEBUG,
)
runtime_dependencies = RuntimeDependencyCollection(
_RUNTIME_DEPENDENCIES,
overrides=runtime_dependency_overrides,
)
logger.log(
f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}",
logging.DEBUG,
)
# Find the dependencies for our platform
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}")
@staticmethod
def _get_runtime_dependencies(runtime_id: str) -> tuple[RuntimeDependency, RuntimeDependency]:
"""Get the language server and .NET runtime dependencies for the platform."""
lang_server_dep = None
dotnet_runtime_dep = None
for dep in RUNTIME_DEPENDENCIES:
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:
raise SolidLSPException(f"No C# language server dependency found for platform {runtime_id}")
if not dotnet_runtime_dep:
raise SolidLSPException(f"No .NET runtime dependency found for platform {runtime_id}")
return lang_server_dep, dotnet_runtime_dep
@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:
@@ -413,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:
+32 -5
View File
@@ -62,6 +62,7 @@ class SourceKitLSP(SolidLanguageServer):
self.server_ready = threading.Event()
self.request_id = 0
self._did_sleep_before_requesting_references = False
self._initialization_timestamp = None
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
@@ -345,14 +346,40 @@ class SourceKitLSP(SolidLanguageServer):
self.server_ready.set()
self.server_ready.wait()
# Mark initialization timestamp for smarter delay calculation
self._initialization_timestamp = time.time()
@override
def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
# SourceKit LSP needs a short initialization period after startup
# SourceKit LSP needs initialization + indexing time after startup
# before it can provide accurate reference information. This sleep
# prevents race conditions where references might not be available yet.
# Unfortunately, sourcekit doesn't send a signal when it's really ready
# CI environments need extra time for project indexing and cross-file analysis
if not self._did_sleep_before_requesting_references:
self.logger.log("Sleeping 5s before requesting references for the first time", logging.DEBUG)
time.sleep(5)
# Calculate minimum delay based on how much time has passed since initialization
if self._initialization_timestamp:
elapsed = time.time() - self._initialization_timestamp
# Increased CI delay for project indexing: 15s CI, 5s local
base_delay = 15 if os.getenv("CI") else 5
remaining_delay = max(2, base_delay - elapsed)
else:
# Fallback if initialization timestamp is missing
remaining_delay = 15 if os.getenv("CI") else 5
self.logger.log(
f"Sleeping {remaining_delay:.1f}s before requesting references for the first time (CI needs extra indexing time)",
logging.INFO,
)
time.sleep(remaining_delay)
self._did_sleep_before_requesting_references = True
return super().request_references(relative_file_path, line, column)
# Get references with retry logic for CI stability
references = super().request_references(relative_file_path, line, column)
# In CI, if no references found, retry once after additional delay
if os.getenv("CI") and not references:
self.logger.log("No references found in CI - retrying after additional 5s delay", logging.INFO)
time.sleep(5)
references = super().request_references(relative_file_path, line, column)
return references
@@ -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):
+24 -1
View File
@@ -1105,7 +1105,30 @@ class SolidLanguageServer(ABC):
if symbol["kind"] == ls_types.SymbolKind.File:
# For file symbols, process their children (top-level symbols)
for child in symbol["children"]:
path = Path(child["location"]["absolutePath"]).resolve().relative_to(self.repository_root_path)
# Handle cross-platform path resolution (fixes Docker/macOS path issues)
absolute_path = Path(child["location"]["absolutePath"]).resolve()
repository_root = Path(self.repository_root_path).resolve()
# Try pathlib first, fallback to alternative approach if paths are incompatible
try:
path = absolute_path.relative_to(repository_root)
except ValueError:
# If paths are from different roots (e.g., /workspaces vs /Users),
# use the relativePath from location if available, or extract from absolutePath
if "relativePath" in child["location"] and child["location"]["relativePath"]:
path = Path(child["location"]["relativePath"])
else:
# Extract relative path by finding common structure
# Example: /workspaces/.../test_repo/file.py -> test_repo/file.py
path_parts = absolute_path.parts
# Find the last common part or use a fallback
if "test_repo" in path_parts:
test_repo_idx = path_parts.index("test_repo")
path = Path(*path_parts[test_repo_idx:])
else:
# Last resort: use filename only
path = Path(absolute_path.name)
result[str(path)].append(child)
# For package/directory symbols, process their children
for child in symbol["children"]:
@@ -0,0 +1 @@
.gradle/