mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 18:16:10 +00:00
Use official .NET install scripts instead of manual downloads
Replace manual .NET runtime downloads with Microsoft's official dotnet-install scripts for better maintainability and automatic platform detection. Changes: - Remove 6 DotNetRuntime RuntimeDependency entries (42 lines removed) - Replace _ensure_dotnet_runtime_from_config with _install_dotnet_with_script - Use bash script (dotnet-install.sh) for Linux/macOS - Use PowerShell script (dotnet-install.ps1) for Windows - Update version check from .NET 9 to .NET 10+ (supports 10.x, 11.x, etc.) - Add comprehensive tests for install script behavior Benefits: - Simpler codebase (~80 lines of manual download code → ~60 lines of script invocation) - Automatic platform/architecture detection by Microsoft scripts - Built-in checksum verification and dependency handling - Official Microsoft-maintained approach - Easier version updates (just change version string) - Better compatibility with .NET 10 (required by roslyn-language-server packages) The language server packages from NuGet.org remain unchanged - this only affects how the .NET runtime is installed to execute the language server.
This commit is contained in:
1 parent
b238e1d0ce
commit
af0075c3e0
2 files changed
+210
-109
No files matched your search
@@ -7,10 +7,8 @@ import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import threading
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
@@ -96,54 +94,6 @@ _RUNTIME_DEPENDENCIES = [
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="tools/net10.0/linux-arm64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Windows (x64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x64.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="dotnet.exe",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Linux (x64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-x64.tar.gz",
|
||||
platform_id="linux-x64",
|
||||
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)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-osx-x64.tar.gz",
|
||||
platform_id="osx-x64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for macOS (ARM64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-osx-arm64.tar.gz",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Windows (ARM64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-arm64.zip",
|
||||
platform_id="win-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="dotnet.exe",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -279,30 +229,28 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
|
||||
# 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 = self._ensure_dotnet_runtime(dotnet_runtime_dep)
|
||||
dotnet_path = self._ensure_dotnet_runtime()
|
||||
server_dll_path = self._ensure_language_server(lang_server_dep)
|
||||
|
||||
return dotnet_path, server_dll_path
|
||||
|
||||
def _ensure_dotnet_runtime(self, dotnet_runtime_dep: RuntimeDependency) -> str:
|
||||
def _ensure_dotnet_runtime(self) -> 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:
|
||||
# Check if it's .NET 9
|
||||
# Check if it's .NET 10 or compatible
|
||||
try:
|
||||
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
if "Microsoft.NETCore.App 9." in result.stdout:
|
||||
log.info("Found system .NET 9 runtime")
|
||||
# Accept .NET 10 or higher (10.x, 11.x, etc.)
|
||||
if any(f"Microsoft.NETCore.App {v}." in result.stdout for v in range(10, 20)):
|
||||
log.info("Found system .NET 10+ runtime")
|
||||
return system_dotnet
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
# Download .NET 9 runtime using config
|
||||
return self._ensure_dotnet_runtime_from_config(dotnet_runtime_dep)
|
||||
# Install .NET 10 runtime using Microsoft's install script
|
||||
return self._install_dotnet_with_script()
|
||||
|
||||
def _ensure_language_server(self, lang_server_dep: RuntimeDependency) -> str:
|
||||
"""Ensure language server is available and return the DLL path."""
|
||||
@@ -397,74 +345,82 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
except Exception as e:
|
||||
raise SolidLSPException(f"Failed to download package {package_name} version {package_version} from NuGet.org: {e}") from e
|
||||
|
||||
def _ensure_dotnet_runtime_from_config(self, dotnet_runtime_dep: RuntimeDependency) -> str:
|
||||
def _install_dotnet_with_script(self, version: str = "10.0") -> str:
|
||||
"""
|
||||
Ensure .NET 9 runtime is available using runtime dependency configuration.
|
||||
Install .NET runtime using Microsoft's official install script.
|
||||
Returns the path to the dotnet executable.
|
||||
"""
|
||||
# TODO: use RuntimeDependency util methods instead of custom download logic
|
||||
dotnet_dir = Path(self._ls_resources_dir) / f"dotnet-runtime-{version}"
|
||||
|
||||
# Check if dotnet is already available on the system
|
||||
system_dotnet = shutil.which("dotnet")
|
||||
if system_dotnet:
|
||||
# Check if it's .NET 9
|
||||
try:
|
||||
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
if "Microsoft.NETCore.App 9." in result.stdout:
|
||||
log.info("Found system .NET 9 runtime")
|
||||
return system_dotnet
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
# Download .NET 9 runtime using config
|
||||
dotnet_dir = Path(self._ls_resources_dir) / "dotnet-runtime-9.0"
|
||||
assert dotnet_runtime_dep.binary_name is not None, "Runtime dependency must have a binary_name"
|
||||
dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name
|
||||
# Determine binary name based on platform
|
||||
is_windows = platform.system().lower() == "windows"
|
||||
dotnet_exe = dotnet_dir / ("dotnet.exe" if is_windows else "dotnet")
|
||||
|
||||
if dotnet_exe.exists():
|
||||
log.info(f"Using cached .NET runtime from {dotnet_exe}")
|
||||
log.info(f"Using cached .NET {version} runtime from {dotnet_exe}")
|
||||
return str(dotnet_exe)
|
||||
|
||||
# Download .NET runtime
|
||||
log.info("Downloading .NET 9 runtime...")
|
||||
# Download and run install script
|
||||
log.info(f"Installing .NET {version} runtime using official Microsoft install script...")
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
custom_dotnet_runtime_url = self._custom_settings.get("dotnet_runtime_url")
|
||||
if custom_dotnet_runtime_url is not None:
|
||||
log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}")
|
||||
url = custom_dotnet_runtime_url
|
||||
else:
|
||||
url = dotnet_runtime_dep.url
|
||||
|
||||
archive_type = dotnet_runtime_dep.archive_type
|
||||
|
||||
# Download the runtime
|
||||
download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
|
||||
try:
|
||||
log.debug(f"Downloading from {url}")
|
||||
urllib.request.urlretrieve(url, download_path)
|
||||
if is_windows:
|
||||
# PowerShell script for Windows
|
||||
script_url = "https://dot.net/v1/dotnet-install.ps1"
|
||||
script_path = dotnet_dir / "dotnet-install.ps1"
|
||||
urllib.request.urlretrieve(script_url, script_path)
|
||||
|
||||
# Extract the archive
|
||||
if archive_type == "zip":
|
||||
with zipfile.ZipFile(download_path, "r") as zip_ref:
|
||||
zip_ref.extractall(dotnet_dir)
|
||||
cmd = [
|
||||
"pwsh",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(script_path),
|
||||
"-Version",
|
||||
version,
|
||||
"-InstallDir",
|
||||
str(dotnet_dir),
|
||||
"-Runtime",
|
||||
"dotnet",
|
||||
"-NoPath",
|
||||
]
|
||||
else:
|
||||
# tar.gz
|
||||
with tarfile.open(download_path, "r:gz") as tar_ref:
|
||||
tar_ref.extractall(dotnet_dir)
|
||||
# Bash script for Linux/macOS
|
||||
script_url = "https://dot.net/v1/dotnet-install.sh"
|
||||
script_path = dotnet_dir / "dotnet-install.sh"
|
||||
urllib.request.urlretrieve(script_url, script_path)
|
||||
script_path.chmod(0o755)
|
||||
|
||||
# Remove the archive
|
||||
download_path.unlink()
|
||||
cmd = [
|
||||
"bash",
|
||||
str(script_path),
|
||||
"--version",
|
||||
version,
|
||||
"--install-dir",
|
||||
str(dotnet_dir),
|
||||
"--runtime",
|
||||
"dotnet",
|
||||
"--no-path",
|
||||
]
|
||||
|
||||
# Make dotnet executable on Unix
|
||||
if platform.system().lower() != "windows":
|
||||
dotnet_exe.chmod(0o755)
|
||||
# Run the install script
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
log.debug(f"Install script output: {result.stdout}")
|
||||
|
||||
log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}")
|
||||
if not dotnet_exe.exists():
|
||||
raise SolidLSPException(f"dotnet executable not found at {dotnet_exe} after installation")
|
||||
|
||||
log.info(f"Successfully installed .NET {version} runtime to {dotnet_exe}")
|
||||
return str(dotnet_exe)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SolidLSPException(
|
||||
f"Failed to install .NET {version} runtime using install script: {e.stderr if e.stderr else e}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e
|
||||
raise SolidLSPException(f"Failed to install .NET {version} runtime: {e}") from e
|
||||
|
||||
def _get_initialize_params(self) -> InitializeParams:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Tests for C# language server .NET runtime installation using official install scripts."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
@pytest.mark.csharp
|
||||
class TestDotNetInstallScript:
|
||||
"""Test .NET runtime installation using Microsoft's official install scripts."""
|
||||
|
||||
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.platform.system")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.subprocess.run")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.shutil.which")
|
||||
def test_install_dotnet_uses_bash_script_on_linux(self, mock_which, mock_urlretrieve, mock_subprocess, mock_platform, mock_ensure_ls):
|
||||
"""Test that Linux uses bash install script."""
|
||||
mock_platform.return_value = "Linux"
|
||||
mock_which.return_value = None # No system dotnet
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
def mock_retrieve(url, path):
|
||||
# Create the file that urlretrieve would create
|
||||
Path(path).touch()
|
||||
# Also create the dotnet executable after script download
|
||||
if "dotnet-install.sh" in str(path):
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet"
|
||||
dotnet_exe.touch()
|
||||
dotnet_exe.chmod(0o755)
|
||||
|
||||
mock_urlretrieve.side_effect = mock_retrieve
|
||||
mock_subprocess.return_value = Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
_ = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Verify bash script was downloaded
|
||||
assert mock_urlretrieve.called
|
||||
script_url = mock_urlretrieve.call_args[0][0]
|
||||
assert script_url == "https://dot.net/v1/dotnet-install.sh"
|
||||
|
||||
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.platform.system")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.subprocess.run")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.shutil.which")
|
||||
def test_install_dotnet_uses_powershell_script_on_windows(
|
||||
self, mock_which, mock_urlretrieve, mock_subprocess, mock_platform, mock_ensure_ls
|
||||
):
|
||||
"""Test that Windows uses PowerShell install script."""
|
||||
mock_platform.return_value = "Windows"
|
||||
mock_which.return_value = None # No system dotnet
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
def mock_retrieve(url, path):
|
||||
# Create the file that urlretrieve would create
|
||||
Path(path).touch()
|
||||
# Also create the dotnet executable after script download
|
||||
if "dotnet-install.ps1" in str(path):
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet.exe"
|
||||
dotnet_exe.touch()
|
||||
|
||||
mock_urlretrieve.side_effect = mock_retrieve
|
||||
mock_subprocess.return_value = Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
_ = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Verify PowerShell script was downloaded
|
||||
assert mock_urlretrieve.called
|
||||
script_url = mock_urlretrieve.call_args[0][0]
|
||||
assert script_url == "https://dot.net/v1/dotnet-install.ps1"
|
||||
|
||||
def test_uses_system_dotnet_10_if_available(self):
|
||||
"""Test that system .NET 10 is used if available instead of downloading."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
with patch("solidlsp.language_servers.csharp_language_server.shutil.which") as mock_which:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.subprocess.run") as mock_subprocess:
|
||||
with patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id") as mock_platform_id:
|
||||
with patch(
|
||||
"solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server"
|
||||
) as mock_ensure_ls:
|
||||
mock_which.return_value = "/usr/bin/dotnet"
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
|
||||
# Mock dotnet --list-runtimes output showing .NET 10
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "Microsoft.NETCore.App 10.0.1 [/usr/share/dotnet/shared/Microsoft.NETCore.App]"
|
||||
mock_result.returncode = 0
|
||||
mock_subprocess.return_value = mock_result
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should have used system dotnet
|
||||
assert dependency_provider._dotnet_path == "/usr/bin/dotnet"
|
||||
|
||||
def test_runtime_dependencies_no_longer_include_dotnet_downloads(self):
|
||||
"""Test that _RUNTIME_DEPENDENCIES no longer includes manual .NET download entries."""
|
||||
from solidlsp.language_servers.csharp_language_server import _RUNTIME_DEPENDENCIES
|
||||
|
||||
# Check that DotNetRuntime entries don't exist (we use install script instead)
|
||||
dotnet_deps = [dep for dep in _RUNTIME_DEPENDENCIES if dep.id == "DotNetRuntime"]
|
||||
|
||||
# After our changes, there should be no DotNetRuntime entries
|
||||
# (we'll use the install script instead)
|
||||
assert len(dotnet_deps) == 0, "DotNetRuntime dependencies should be removed in favor of install script"
|
||||
Reference in new issue
Block a user