diff --git a/src/solidlsp/language_servers/al_language_server.py b/src/solidlsp/language_servers/al_language_server.py index ed3cf9c7..438b1d3d 100644 --- a/src/solidlsp/language_servers/al_language_server.py +++ b/src/solidlsp/language_servers/al_language_server.py @@ -28,11 +28,28 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) -AL_EXTENSION_VERSION = "18.0.2242655" -AL_EXTENSION_URL = ( - f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/vsextensions/al/{AL_EXTENSION_VERSION}/vspackage" -) -AL_EXTENSION_SHA256 = "3971995e61a59dc4fcce4a65053072a67991ed624a16635c4f2911f12564b2b9" +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_AL_EXTENSION_VERSION = "18.0.2242655" +INITIAL_AL_EXTENSION_SHA256 = "3971995e61a59dc4fcce4a65053072a67991ed624a16635c4f2911f12564b2b9" +DEFAULT_AL_EXTENSION_VERSION = "18.0.2242655" +DEFAULT_AL_EXTENSION_SHA256 = "3971995e61a59dc4fcce4a65053072a67991ed624a16635c4f2911f12564b2b9" + + +def _al_extension_sha(version: str) -> str | None: + if version == INITIAL_AL_EXTENSION_VERSION: + return INITIAL_AL_EXTENSION_SHA256 + if version == DEFAULT_AL_EXTENSION_VERSION: + return DEFAULT_AL_EXTENSION_SHA256 + return None + + +def _al_extension_dirname(version: str) -> str: + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + return "al-extension" if version == INITIAL_AL_EXTENSION_VERSION else f"al-extension-{version}" + + AL_EXTENSION_ALLOWED_HOSTS = ("marketplace.visualstudio.com",) @@ -131,21 +148,13 @@ class ALLanguageServer(SolidLanguageServer): return path.replace("\\", "/") @classmethod - def _download_al_extension(cls, url: str, target_dir: str) -> bool: + def _download_al_extension(cls, url: str, target_dir: str, expected_sha256: str | None) -> bool: """ Download and extract the AL extension from VS Code marketplace. The VS Code marketplace packages extensions as .vsix files (which are ZIP archives). This method downloads the VSIX file and extracts it to get the language server binaries. - Args: - logger: Logger for tracking download progress - url: VS Code marketplace URL for the AL extension - target_dir: Directory where the extension will be extracted - - Returns: - True if successful, False otherwise - Note: The download includes progress tracking and proper user-agent headers to ensure compatibility with the VS Code marketplace. @@ -158,7 +167,7 @@ class ALLanguageServer(SolidLanguageServer): url, target_dir, "zip", - expected_sha256=AL_EXTENSION_SHA256 if url == AL_EXTENSION_URL else None, + expected_sha256=expected_sha256, allowed_hosts=AL_EXTENSION_ALLOWED_HOSTS, ) log.info("AL extension extracted successfully") @@ -231,8 +240,10 @@ class ALLanguageServer(SolidLanguageServer): elif env_path: log.warning(f"AL_EXTENSION_PATH set but directory not found: {env_path}") - # Check default download location - default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), "al-extension", "extension") + # Check the resolved-version download location (versioned for non-INITIAL, legacy "al-extension" for INITIAL) + al_settings = solidlsp_settings.get_ls_specific_settings(Language.AL) + al_extension_version = al_settings.get("al_extension_version", DEFAULT_AL_EXTENSION_VERSION) + default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), _al_extension_dirname(al_extension_version), "extension") if os.path.exists(default_path): log.debug(f"Found AL extension in default location: {default_path}") return default_path @@ -255,9 +266,9 @@ class ALLanguageServer(SolidLanguageServer): Path to installed extension or None if download failed """ - al_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "al-extension") al_settings = solidlsp_settings.get_ls_specific_settings(Language.AL) - al_extension_version = al_settings.get("al_extension_version", AL_EXTENSION_VERSION) + al_extension_version = al_settings.get("al_extension_version", DEFAULT_AL_EXTENSION_VERSION) + al_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), _al_extension_dirname(al_extension_version)) al_extension_url = ( "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/" f"vsextensions/al/{al_extension_version}/vspackage" @@ -265,7 +276,7 @@ class ALLanguageServer(SolidLanguageServer): log.info(f"Downloading AL extension from: {al_extension_url}") - if cls._download_al_extension(al_extension_url, al_extension_dir): + if cls._download_al_extension(al_extension_url, al_extension_dir, _al_extension_sha(al_extension_version)): extension_path = os.path.join(al_extension_dir, "extension") if os.path.exists(extension_path): log.info("AL extension downloaded and installed successfully") diff --git a/src/solidlsp/language_servers/ansible_language_server.py b/src/solidlsp/language_servers/ansible_language_server.py index 2b3da993..3fb1a3b6 100644 --- a/src/solidlsp/language_servers/ansible_language_server.py +++ b/src/solidlsp/language_servers/ansible_language_server.py @@ -20,6 +20,12 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_ANSIBLE_LANGUAGE_SERVER_VERSION = "1.2.3" +DEFAULT_ANSIBLE_LANGUAGE_SERVER_VERSION = "1.2.3" + def _deep_merge(base: dict, override: dict) -> None: """Recursively merge *override* into *base*, modifying *base* in-place.""" @@ -162,7 +168,9 @@ class AnsibleLanguageServer(SolidLanguageServer): assert is_node_installed, "node is not installed or isn't in PATH. Please install Node.js and try again." is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - ansible_language_server_version = self._custom_settings.get("ansible_language_server_version", "1.2.3") + ansible_language_server_version = self._custom_settings.get( + "ansible_language_server_version", DEFAULT_ANSIBLE_LANGUAGE_SERVER_VERSION + ) npm_registry = self._custom_settings.get("npm_registry") deps = RuntimeDependencyCollection( @@ -180,8 +188,13 @@ class AnsibleLanguageServer(SolidLanguageServer): ] ) - # install ansible-language-server if not already installed - ansible_ls_dir = os.path.join(self._ls_resources_dir, "ansible-lsp") + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + ls_dirname = ( + "ansible-lsp" + if ansible_language_server_version == INITIAL_ANSIBLE_LANGUAGE_SERVER_VERSION + else f"ansible-lsp-{ansible_language_server_version}" + ) + ansible_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname) ansible_executable_path = os.path.join(ansible_ls_dir, "node_modules", ".bin", "ansible-language-server") # handle Windows executable extension diff --git a/src/solidlsp/language_servers/bash_language_server.py b/src/solidlsp/language_servers/bash_language_server.py index 9dfc9356..0338a5ef 100644 --- a/src/solidlsp/language_servers/bash_language_server.py +++ b/src/solidlsp/language_servers/bash_language_server.py @@ -28,6 +28,14 @@ log = logging.getLogger(__name__) # ShellCheck binary release info; download directly from upstream GitHub releases rather than # relying on the `shellcheck` npm wrapper (which lazily downloads on first invocation and # turned the bash LS install into a fragile, network-dependent step). +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_BASH_LANGUAGE_SERVER_VERSION = "5.6.0" +DEFAULT_BASH_LANGUAGE_SERVER_VERSION = "5.6.0" + +# ShellCheck binary path already encodes _SHELLCHECK_VERSION (see _shellcheck_binary_path), +# so version bumps trigger reinstall correctly without further intervention. _SHELLCHECK_VERSION = "0.10.0" _SHELLCHECK_RELEASE_BASE = f"https://github.com/koalaman/shellcheck/releases/download/v{_SHELLCHECK_VERSION}" _SHELLCHECK_ALLOWED_HOSTS = ( @@ -110,10 +118,10 @@ class BashLanguageServer(SolidLanguageServer): assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - bash_language_server_version = self._custom_settings.get("bash_language_server_version", "5.6.0") + bash_language_server_version = self._custom_settings.get("bash_language_server_version", DEFAULT_BASH_LANGUAGE_SERVER_VERSION) npm_registry = self._custom_settings.get("npm_registry") - bash_ls_dir = os.path.join(self._ls_resources_dir, "bash-lsp") + bash_ls_dir = self._resolve_bash_ls_dir(bash_language_server_version) managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin") bash_executable_path = os.path.join(managed_bin_dir, "bash-language-server") if os.name == "nt": @@ -180,13 +188,23 @@ class BashLanguageServer(SolidLanguageServer): os.chmod(binary_path, current | 0o111) def create_launch_command_env(self) -> dict[str, str]: - bash_ls_dir = os.path.join(self._ls_resources_dir, "bash-lsp") + bash_language_server_version = self._custom_settings.get("bash_language_server_version", DEFAULT_BASH_LANGUAGE_SERVER_VERSION) + bash_ls_dir = self._resolve_bash_ls_dir(bash_language_server_version) managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin") return { "PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", ""), "SHELLCHECK_PATH": _shellcheck_binary_path(bash_ls_dir), } + def _resolve_bash_ls_dir(self, bash_language_server_version: str) -> str: + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + ls_dirname = ( + "bash-lsp" + if bash_language_server_version == INITIAL_BASH_LANGUAGE_SERVER_VERSION + else f"bash-lsp-{bash_language_server_version}" + ) + return os.path.join(self._ls_resources_dir, ls_dirname) + def _create_launch_command(self, core_path: str) -> list[str]: return [core_path, "start"] diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index 4fdc3ce9..dc238256 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -21,7 +21,35 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) -CLOJURE_LSP_VERSION = "2026.02.20-16.08.58" +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_CLOJURE_LSP_VERSION = "2026.02.20-16.08.58" +INITIAL_CLOJURE_LSP_SHA256_BY_PLATFORM = { + "osx-arm64": "a14d4db074f665378214e2dc888472e186c228dfa065c777b0534bfda5571669", + "osx-x64": "5507434c27104ab816e096d3336d8191641de8a65b57d76afb585d07167a3cf2", + "linux-arm64": "f8f09fa07dd4b6743b5c57270ccf1ee5cdbc5fca09dbca8b6a3b22705b5da4e1", + "linux-x64": "52e8bf4fd4cf171df0a3077c8bb5a3bf598d4c621e94b4876dab943a61267309", + "win-x64": "817b1271288817c954fb9e595278b1f25003827ce31f8785f253dc4ac911041f", +} +DEFAULT_CLOJURE_LSP_VERSION = "2026.02.20-16.08.58" +DEFAULT_CLOJURE_LSP_SHA256_BY_PLATFORM = { + "osx-arm64": "a14d4db074f665378214e2dc888472e186c228dfa065c777b0534bfda5571669", + "osx-x64": "5507434c27104ab816e096d3336d8191641de8a65b57d76afb585d07167a3cf2", + "linux-arm64": "f8f09fa07dd4b6743b5c57270ccf1ee5cdbc5fca09dbca8b6a3b22705b5da4e1", + "linux-x64": "52e8bf4fd4cf171df0a3077c8bb5a3bf598d4c621e94b4876dab943a61267309", + "win-x64": "817b1271288817c954fb9e595278b1f25003827ce31f8785f253dc4ac911041f", +} + + +def _clojure_lsp_sha(version: str, platform_key: str) -> str | None: + if version == INITIAL_CLOJURE_LSP_VERSION: + return INITIAL_CLOJURE_LSP_SHA256_BY_PLATFORM[platform_key] + if version == DEFAULT_CLOJURE_LSP_VERSION: + return DEFAULT_CLOJURE_LSP_SHA256_BY_PLATFORM[platform_key] + return None + + CLOJURE_LSP_ALLOWED_HOSTS = ( "github.com", "release-assets.githubusercontent.com", @@ -58,7 +86,6 @@ class ClojureLSP(SolidLanguageServer): by Serena (default: the bundled Serena version). """ - CLOJURE_LSP_VERSION = CLOJURE_LSP_VERSION CLOJURE_LSP_ALLOWED_HOSTS = CLOJURE_LSP_ALLOWED_HOSTS @override @@ -69,7 +96,6 @@ class ClojureLSP(SolidLanguageServer): @classmethod def _runtime_dependencies(cls, version: str) -> RuntimeDependencyCollection: clojure_lsp_releases = f"https://github.com/clojure-lsp/clojure-lsp/releases/download/{version}" - default_version = version == cls.CLOJURE_LSP_VERSION return RuntimeDependencyCollection( [ RuntimeDependency( @@ -78,7 +104,7 @@ class ClojureLSP(SolidLanguageServer): platform_id="osx-arm64", archive_type="zip", binary_name="clojure-lsp", - sha256="a14d4db074f665378214e2dc888472e186c228dfa065c777b0534bfda5571669" if default_version else None, + sha256=_clojure_lsp_sha(version, "osx-arm64"), allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, ), RuntimeDependency( @@ -87,7 +113,7 @@ class ClojureLSP(SolidLanguageServer): platform_id="osx-x64", archive_type="zip", binary_name="clojure-lsp", - sha256="5507434c27104ab816e096d3336d8191641de8a65b57d76afb585d07167a3cf2" if default_version else None, + sha256=_clojure_lsp_sha(version, "osx-x64"), allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, ), RuntimeDependency( @@ -96,7 +122,7 @@ class ClojureLSP(SolidLanguageServer): platform_id="linux-arm64", archive_type="zip", binary_name="clojure-lsp", - sha256="f8f09fa07dd4b6743b5c57270ccf1ee5cdbc5fca09dbca8b6a3b22705b5da4e1" if default_version else None, + sha256=_clojure_lsp_sha(version, "linux-arm64"), allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, ), RuntimeDependency( @@ -105,7 +131,7 @@ class ClojureLSP(SolidLanguageServer): platform_id="linux-x64", archive_type="zip", binary_name="clojure-lsp", - sha256="52e8bf4fd4cf171df0a3077c8bb5a3bf598d4c621e94b4876dab943a61267309" if default_version else None, + sha256=_clojure_lsp_sha(version, "linux-x64"), allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, ), RuntimeDependency( @@ -114,7 +140,7 @@ class ClojureLSP(SolidLanguageServer): platform_id="win-x64", archive_type="zip", binary_name="clojure-lsp.exe", - sha256="817b1271288817c954fb9e595278b1f25003827ce31f8785f253dc4ac911041f" if default_version else None, + sha256=_clojure_lsp_sha(version, "win-x64"), allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, ), ] @@ -143,16 +169,22 @@ class ClojureLSP(SolidLanguageServer): def _get_or_install_core_dependency(self) -> str: """Setup runtime dependencies for clojure-lsp and return the path to the executable.""" verify_clojure_cli() - clojure_lsp_version = self._custom_settings.get("clojure_lsp_version", ClojureLSP.CLOJURE_LSP_VERSION) + clojure_lsp_version = self._custom_settings.get("clojure_lsp_version", DEFAULT_CLOJURE_LSP_VERSION) deps = ClojureLSP._runtime_dependencies(clojure_lsp_version) dependency = deps.get_single_dep_for_current_platform() - clojurelsp_executable_path = deps.binary_path(self._ls_resources_dir) + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + install_dir = ( + self._ls_resources_dir + if clojure_lsp_version == INITIAL_CLOJURE_LSP_VERSION + else os.path.join(self._ls_resources_dir, f"clojure-lsp-{clojure_lsp_version}") + ) + clojurelsp_executable_path = deps.binary_path(install_dir) if not os.path.exists(clojurelsp_executable_path): log.info( - f"Downloading and extracting clojure-lsp from {dependency.url} to {self._ls_resources_dir}", + f"Downloading and extracting clojure-lsp from {dependency.url} to {install_dir}", ) - deps.install(self._ls_resources_dir) + deps.install(install_dir) if not os.path.exists(clojurelsp_executable_path): raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}") os.chmod(clojurelsp_executable_path, 0o755) diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index f82627c6..800f69eb 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -18,6 +18,34 @@ log = logging.getLogger(__name__) DART_ALLOWED_HOSTS = ("storage.googleapis.com",) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_DART_SDK_VERSION = "3.7.1" +INITIAL_DART_SDK_SHA256_BY_PLATFORM = { + "linux-x64": "2813959e7d9650334015b927cc533f5beadfbf7fa48248beec471f8942a0ee71", + "win-x64": "f56c03122e17abe5be1429eee0a975fb8ed511b6731ec90c6475992d3dee4ea5", + "win-arm64": "fada411c6538d0ac24c35d6360767241f1298f64cbc5e88716387d54757a105a", + "osx-x64": "a2765917b6ae49d1ac119553df9584989f9c441a46e8f18c129ba52489658d2e", + "osx-arm64": "f57c25163092bac818f8ca6250a0d8b2c56344c6a075a1bd7c60da7ac28b32a4", +} +DEFAULT_DART_SDK_VERSION = "3.7.1" +DEFAULT_DART_SDK_SHA256_BY_PLATFORM = { + "linux-x64": "2813959e7d9650334015b927cc533f5beadfbf7fa48248beec471f8942a0ee71", + "win-x64": "f56c03122e17abe5be1429eee0a975fb8ed511b6731ec90c6475992d3dee4ea5", + "win-arm64": "fada411c6538d0ac24c35d6360767241f1298f64cbc5e88716387d54757a105a", + "osx-x64": "a2765917b6ae49d1ac119553df9584989f9c441a46e8f18c129ba52489658d2e", + "osx-arm64": "f57c25163092bac818f8ca6250a0d8b2c56344c6a075a1bd7c60da7ac28b32a4", +} + + +def _dart_sdk_sha(version: str, platform_key: str) -> str | None: + if version == INITIAL_DART_SDK_VERSION: + return INITIAL_DART_SDK_SHA256_BY_PLATFORM[platform_key] + if version == DEFAULT_DART_SDK_VERSION: + return DEFAULT_DART_SDK_SHA256_BY_PLATFORM[platform_key] + return None + class DartLanguageServer(SolidLanguageServer): """ @@ -49,7 +77,7 @@ class DartLanguageServer(SolidLanguageServer): @classmethod def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str: dart_settings = solidlsp_settings.get_ls_specific_settings(Language.DART) - dart_sdk_version = dart_settings.get("dart_sdk_version", "3.7.1") + dart_sdk_version = dart_settings.get("dart_sdk_version", DEFAULT_DART_SDK_VERSION) deps = RuntimeDependencyCollection( [ RuntimeDependency( @@ -59,7 +87,7 @@ class DartLanguageServer(SolidLanguageServer): platform_id="linux-x64", archive_type="zip", binary_name="dart-sdk/bin/dart", - sha256="2813959e7d9650334015b927cc533f5beadfbf7fa48248beec471f8942a0ee71" if dart_sdk_version == "3.7.1" else None, + sha256=_dart_sdk_sha(dart_sdk_version, "linux-x64"), allowed_hosts=DART_ALLOWED_HOSTS, ), RuntimeDependency( @@ -69,7 +97,7 @@ class DartLanguageServer(SolidLanguageServer): platform_id="win-x64", archive_type="zip", binary_name="dart-sdk/bin/dart.exe", - sha256="f56c03122e17abe5be1429eee0a975fb8ed511b6731ec90c6475992d3dee4ea5" if dart_sdk_version == "3.7.1" else None, + sha256=_dart_sdk_sha(dart_sdk_version, "win-x64"), allowed_hosts=DART_ALLOWED_HOSTS, ), RuntimeDependency( @@ -79,7 +107,7 @@ class DartLanguageServer(SolidLanguageServer): platform_id="win-arm64", archive_type="zip", binary_name="dart-sdk/bin/dart.exe", - sha256="fada411c6538d0ac24c35d6360767241f1298f64cbc5e88716387d54757a105a" if dart_sdk_version == "3.7.1" else None, + sha256=_dart_sdk_sha(dart_sdk_version, "win-arm64"), allowed_hosts=DART_ALLOWED_HOSTS, ), RuntimeDependency( @@ -89,7 +117,7 @@ class DartLanguageServer(SolidLanguageServer): platform_id="osx-x64", archive_type="zip", binary_name="dart-sdk/bin/dart", - sha256="a2765917b6ae49d1ac119553df9584989f9c441a46e8f18c129ba52489658d2e" if dart_sdk_version == "3.7.1" else None, + sha256=_dart_sdk_sha(dart_sdk_version, "osx-x64"), allowed_hosts=DART_ALLOWED_HOSTS, ), RuntimeDependency( @@ -99,13 +127,18 @@ class DartLanguageServer(SolidLanguageServer): platform_id="osx-arm64", archive_type="zip", binary_name="dart-sdk/bin/dart", - sha256="f57c25163092bac818f8ca6250a0d8b2c56344c6a075a1bd7c60da7ac28b32a4" if dart_sdk_version == "3.7.1" else None, + sha256=_dart_sdk_sha(dart_sdk_version, "osx-arm64"), allowed_hosts=DART_ALLOWED_HOSTS, ), ] ) - dart_ls_dir = cls.ls_resources_dir(solidlsp_settings) + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + dart_ls_dir = ( + cls.ls_resources_dir(solidlsp_settings) + if dart_sdk_version == INITIAL_DART_SDK_VERSION + else os.path.join(cls.ls_resources_dir(solidlsp_settings), f"dart-sdk-{dart_sdk_version}") + ) dart_executable_path = deps.binary_path(dart_ls_dir) if not os.path.exists(dart_executable_path): diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index 50deb1b0..646174a5 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -32,19 +32,57 @@ 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") -VSCODE_JAVA_SHA256_BY_PLATFORM = { +INTELLICODE_ALLOWED_HOSTS = ( + "visualstudioexptteam.gallery.vsassets.io", + "marketplace.visualstudio.com", + "download.visualstudio.microsoft.com", +) + +# 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. +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", } -INTELLICODE_ALLOWED_HOSTS = ( - "visualstudioexptteam.gallery.vsassets.io", - "marketplace.visualstudio.com", - "download.visualstudio.microsoft.com", -) -INTELLICODE_SHA256 = "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251" +DEFAULT_VSCODE_JAVA_VERSION = "1.42.0-561" +DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM = { + "osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933", + "osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f", + "linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1", + "linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9", + "win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3", +} + +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_ directory names JDTLS_CONFIG_DIR_BY_PLATFORM = { @@ -237,12 +275,35 @@ class EclipseJDTLS(SolidLanguageServer): platformId = PlatformUtils.get_platform_id() gradle_version = custom_settings.get("gradle_version", "8.14.2") - vscode_java_version = custom_settings.get("vscode_java_version", "1.42.0-561") + 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", "1.2.30") + intellicode_version = custom_settings.get("intellicode_version", DEFAULT_INTELLICODE_VERSION) default_gradle_version = gradle_version == "8.14.2" - default_vscode_java_version = vscode_java_version == "1.42.0-561" - default_intellicode_version = intellicode_version == "1.2.30" + + # 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}" + ) + intellicode_dirname = ( + "intellicode" if intellicode_version == 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 runtime_dependencies: dict[str, dict[str, dict[str, object]]] = { "gradle": { @@ -258,15 +319,15 @@ class EclipseJDTLS(SolidLanguageServer): "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", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-arm64"] if default_vscode_java_version else None, + "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", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-arm64"] if default_vscode_java_version else None, + "relative_extraction_path": vscode_java_dirname, + "sha256": vscode_java_sha("osx-arm64"), "allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS, "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", @@ -277,8 +338,8 @@ class EclipseJDTLS(SolidLanguageServer): "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", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-x64"] if default_vscode_java_version else None, + "relative_extraction_path": vscode_java_dirname, + "sha256": vscode_java_sha("osx-x64"), "allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS, "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", @@ -289,8 +350,8 @@ class EclipseJDTLS(SolidLanguageServer): "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", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["linux-arm64"] if default_vscode_java_version else None, + "relative_extraction_path": vscode_java_dirname, + "sha256": vscode_java_sha("linux-arm64"), "allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS, "jre_home_path": "extension/jre/21.0.7-linux-aarch64", "jre_path": "extension/jre/21.0.7-linux-aarch64/bin/java", @@ -301,8 +362,8 @@ class EclipseJDTLS(SolidLanguageServer): "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", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["linux-x64"] if default_vscode_java_version else None, + "relative_extraction_path": vscode_java_dirname, + "sha256": vscode_java_sha("linux-x64"), "allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS, "jre_home_path": "extension/jre/21.0.7-linux-x86_64", "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", @@ -313,8 +374,8 @@ class EclipseJDTLS(SolidLanguageServer): "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", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["win-x64"] if default_vscode_java_version else None, + "relative_extraction_path": vscode_java_dirname, + "sha256": vscode_java_sha("win-x64"), "allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS, "jre_home_path": "extension/jre/21.0.7-win32-x86_64", "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", @@ -328,8 +389,8 @@ class EclipseJDTLS(SolidLanguageServer): "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", - "sha256": INTELLICODE_SHA256 if default_intellicode_version else None, + "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", diff --git a/src/solidlsp/language_servers/elm_language_server.py b/src/solidlsp/language_servers/elm_language_server.py index a86a1d2d..e5823811 100644 --- a/src/solidlsp/language_servers/elm_language_server.py +++ b/src/solidlsp/language_servers/elm_language_server.py @@ -22,6 +22,14 @@ from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_in log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_ELM_LANGUAGE_SERVER_VERSION = "2.8.0" +DEFAULT_ELM_LANGUAGE_SERVER_VERSION = "2.8.0" +INITIAL_ELM_COMPILER_VERSION = "0.19.1-6" +DEFAULT_ELM_COMPILER_VERSION = "0.19.1-6" + class ElmLanguageServer(SolidLanguageServer): """ @@ -68,8 +76,8 @@ class ElmLanguageServer(SolidLanguageServer): Setup runtime dependencies for Elm Language Server and return the command to start the server. """ elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM) - elm_language_server_version = elm_config.get("elm_language_server_version", "2.8.0") - elm_compiler_version = elm_config.get("elm_compiler_version", "0.19.1-6") + elm_language_server_version = elm_config.get("elm_language_server_version", DEFAULT_ELM_LANGUAGE_SERVER_VERSION) + elm_compiler_version = elm_config.get("elm_compiler_version", DEFAULT_ELM_COMPILER_VERSION) npm_registry = elm_config.get("npm_registry") # Check if elm-language-server and elm are already installed globally @@ -101,8 +109,12 @@ class ElmLanguageServer(SolidLanguageServer): ] ) - # Install elm-language-server if not already installed - elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "elm-lsp") + # legacy unversioned dir reserved for INITIAL pair; any other version combination goes into a versioned subdir + is_initial = ( + elm_language_server_version == INITIAL_ELM_LANGUAGE_SERVER_VERSION and elm_compiler_version == INITIAL_ELM_COMPILER_VERSION + ) + ls_dirname = "elm-lsp" if is_initial else f"elm-lsp-{elm_language_server_version}-{elm_compiler_version}" + elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), ls_dirname) elm_ls_executable_path = os.path.join(elm_ls_dir, "node_modules", ".bin", "elm-language-server") if not os.path.exists(elm_ls_executable_path): log.info(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...") diff --git a/src/solidlsp/language_servers/fsharp_language_server.py b/src/solidlsp/language_servers/fsharp_language_server.py index 79970bae..a8cd6847 100644 --- a/src/solidlsp/language_servers/fsharp_language_server.py +++ b/src/solidlsp/language_servers/fsharp_language_server.py @@ -22,7 +22,12 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) -FSAUTOCOMPLETE_VERSION = "0.83.0" +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_FSAUTOCOMPLETE_VERSION = "0.83.0" +DEFAULT_FSAUTOCOMPLETE_VERSION = "0.83.0" +FSAUTOCOMPLETE_VERSION = DEFAULT_FSAUTOCOMPLETE_VERSION class FSharpLanguageServer(SolidLanguageServer): @@ -69,7 +74,7 @@ class FSharpLanguageServer(SolidLanguageServer): Setup runtime dependencies for F# Language Server and return the command to start the server. """ fsharp_settings = solidlsp_settings.get_ls_specific_settings(Language.FSHARP) - fsautocomplete_version = fsharp_settings.get("fsautocomplete_version", FSAUTOCOMPLETE_VERSION) + fsautocomplete_version = fsharp_settings.get("fsautocomplete_version", DEFAULT_FSAUTOCOMPLETE_VERSION) dotnet_exe = DotNETUtil("8.0", allow_higher_version=True).get_dotnet_path_or_raise() RuntimeDependencyCollection( @@ -84,7 +89,9 @@ class FSharpLanguageServer(SolidLanguageServer): ) # Install FsAutoComplete if not already installed - fsharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "fsharp-lsp") + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + ls_dirname = "fsharp-lsp" if fsautocomplete_version == INITIAL_FSAUTOCOMPLETE_VERSION else f"fsharp-lsp-{fsautocomplete_version}" + fsharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), ls_dirname) fsautocomplete_path = os.path.join(fsharp_ls_dir, "fsautocomplete") # Handle Windows executable extension diff --git a/src/solidlsp/language_servers/groovy_language_server.py b/src/solidlsp/language_servers/groovy_language_server.py index 66bcacc7..2370e128 100644 --- a/src/solidlsp/language_servers/groovy_language_server.py +++ b/src/solidlsp/language_servers/groovy_language_server.py @@ -18,13 +18,34 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) VSCODE_JAVA_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") -VSCODE_JAVA_SHA256_BY_PLATFORM = { + +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_VSCODE_JAVA_VERSION = "1.42.0-561" +INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM = { "win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3", "linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9", "linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1", "osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f", "osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933", } +DEFAULT_VSCODE_JAVA_VERSION = "1.42.0-561" +DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM = { + "win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3", + "linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9", + "linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1", + "osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f", + "osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933", +} + + +def _vscode_java_sha(version: str, platform_key: str) -> str | None: + if version == INITIAL_VSCODE_JAVA_VERSION: + return INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key] + if version == DEFAULT_VSCODE_JAVA_VERSION: + return DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key] + return None @dataclasses.dataclass @@ -93,7 +114,7 @@ class GroovyLanguageServer(SolidLanguageServer): """ platform_id = PlatformUtils.get_platform_id() groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY) - vscode_java_version = groovy_settings.get("vscode_java_version", "1.42.0-561") + vscode_java_version = groovy_settings.get("vscode_java_version", DEFAULT_VSCODE_JAVA_VERSION) vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}" # Verify platform support @@ -128,35 +149,35 @@ class GroovyLanguageServer(SolidLanguageServer): "archiveType": "zip", "java_home_path": "extension/jre/21.0.7-win32-x86_64", "java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["win-x64"] if vscode_java_version == "1.42.0-561" else None, + "sha256": _vscode_java_sha(vscode_java_version, "win-x64"), }, "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", "java_home_path": "extension/jre/21.0.7-linux-x86_64", "java_path": "extension/jre/21.0.7-linux-x86_64/bin/java", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["linux-x64"] if vscode_java_version == "1.42.0-561" else None, + "sha256": _vscode_java_sha(vscode_java_version, "linux-x64"), }, "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", "java_home_path": "extension/jre/21.0.7-linux-aarch64", "java_path": "extension/jre/21.0.7-linux-aarch64/bin/java", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["linux-arm64"] if vscode_java_version == "1.42.0-561" else None, + "sha256": _vscode_java_sha(vscode_java_version, "linux-arm64"), }, "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", "java_home_path": "extension/jre/21.0.7-macosx-x86_64", "java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-x64"] if vscode_java_version == "1.42.0-561" else None, + "sha256": _vscode_java_sha(vscode_java_version, "osx-x64"), }, "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", "java_home_path": "extension/jre/21.0.7-macosx-aarch64", "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", - "sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-arm64"] if vscode_java_version == "1.42.0-561" else None, + "sha256": _vscode_java_sha(vscode_java_version, "osx-arm64"), }, }, } @@ -174,7 +195,9 @@ class GroovyLanguageServer(SolidLanguageServer): static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "groovy_language_server") os.makedirs(static_dir, exist_ok=True) - java_dir = os.path.join(static_dir, "java") + # legacy unversioned "java" dir reserved for INITIAL; every other version goes into a versioned subdir + java_dirname = "java" if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION else f"java-{vscode_java_version}" + java_dir = os.path.join(static_dir, java_dirname) os.makedirs(java_dir, exist_ok=True) java_home_path = os.path.join(java_dir, java_home_relative_path) diff --git a/src/solidlsp/language_servers/haxe_language_server.py b/src/solidlsp/language_servers/haxe_language_server.py index cf48c78a..24ca6fe7 100644 --- a/src/solidlsp/language_servers/haxe_language_server.py +++ b/src/solidlsp/language_servers/haxe_language_server.py @@ -27,6 +27,22 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_VSHAXE_VERSION = "2.34.2" +INITIAL_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2" +DEFAULT_VSHAXE_VERSION = "2.34.2" +DEFAULT_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2" + + +def _vshaxe_sha(version: str) -> str | None: + if version == INITIAL_VSHAXE_VERSION: + return INITIAL_VSHAXE_SHA256 + if version == DEFAULT_VSHAXE_VERSION: + return DEFAULT_VSHAXE_SHA256 + return None + class HaxeLanguageServer(SolidLanguageServer): """Haxe language server integration using vshaxe/haxe-language-server. @@ -58,8 +74,6 @@ class HaxeLanguageServer(SolidLanguageServer): class DependencyProvider(LanguageServerDependencyProviderSinglePath): # Downloaded from Open VSX (not the VS Code Marketplace) because Open VSX # provides stable versioned URLs and SHA256 checksums for integrity verification. - _DEFAULT_VSHAXE_VERSION = "2.34.2" - _DEFAULT_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2" @override def _get_or_install_core_dependency(self) -> str: @@ -77,7 +91,9 @@ class HaxeLanguageServer(SolidLanguageServer): return vscode_server_path # 3. Check resource dir / download from Open VSX - haxe_ls_dir = os.path.join(self._ls_resources_dir, "haxe-lsp") + version = self._custom_settings.get("version", DEFAULT_VSHAXE_VERSION) + ls_dirname = "haxe-lsp" if version == INITIAL_VSHAXE_VERSION else f"haxe-lsp-{version}" + haxe_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname) server_js_path = os.path.join(haxe_ls_dir, "bin", "server.js") if os.path.exists(server_js_path): log.info(f"Found Haxe Language Server at {server_js_path}") @@ -92,7 +108,6 @@ class HaxeLanguageServer(SolidLanguageServer): " 3. Set ls_path in serena_config.yml under ls_specific_settings.haxe" ) - version = self._custom_settings.get("version", self._DEFAULT_VSHAXE_VERSION) downloaded_path = self._download_from_open_vsx(haxe_ls_dir, version) if downloaded_path: return downloaded_path @@ -129,16 +144,17 @@ class HaxeLanguageServer(SolidLanguageServer): vsix_path = os.path.join(tempfile.gettempdir(), "vshaxe.vsix") urllib.request.urlretrieve(download_url, vsix_path) - # Verify SHA256 checksum only for the default (pinned) version - if version == cls._DEFAULT_VSHAXE_VERSION: + # Verify SHA256 checksum only when the resolved version is one of our pinned ones (INITIAL or current DEFAULT) + expected_sha = _vshaxe_sha(version) + if expected_sha is not None: sha256 = hashlib.sha256() with open(vsix_path, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): sha256.update(chunk) - if sha256.hexdigest().lower() != cls._DEFAULT_VSHAXE_SHA256: + if sha256.hexdigest().lower() != expected_sha: os.remove(vsix_path) raise RuntimeError( - f"SHA256 checksum mismatch for vshaxe VSIX. Expected {cls._DEFAULT_VSHAXE_SHA256}, " + f"SHA256 checksum mismatch for vshaxe VSIX. Expected {expected_sha}, " f"got {sha256.hexdigest()}. The file may be corrupted or tampered with." ) log.info("SHA256 checksum verified") diff --git a/src/solidlsp/language_servers/hlsl_language_server.py b/src/solidlsp/language_servers/hlsl_language_server.py index b6eb7e05..cab201dd 100644 --- a/src/solidlsp/language_servers/hlsl_language_server.py +++ b/src/solidlsp/language_servers/hlsl_language_server.py @@ -25,15 +25,32 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) -# GitHub release version to download when not installed locally -_DEFAULT_VERSION = "1.3.1" _GITHUB_RELEASE_BASE = "https://github.com/antaalt/shader-sense/releases/download" _HLSL_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") -_HLSL_SHA256_BY_ASSET = { + +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# _INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# _DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +_INITIAL_VERSION = "1.3.1" +_INITIAL_HLSL_SHA256_BY_ASSET = { "shader-language-server-x86_64-pc-windows-msvc.zip": "49081c5547ddde1b8b3b17295282a80ddacbca1d6f5dcd834e2788c02bafa997", "shader-language-server-x86_64-unknown-linux-gnu.zip": "61710df7ca17a2d063b598936c57c56c49fbf837707a1aa886f9b0193a35be3c", "shader-language-server-aarch64-pc-windows-msvc.zip": "a3b3799affe2cad27652e788376b46fe76e1a6c2ce45946a486dcb26c9091412", } +_DEFAULT_VERSION = "1.3.1" +_DEFAULT_HLSL_SHA256_BY_ASSET = { + "shader-language-server-x86_64-pc-windows-msvc.zip": "49081c5547ddde1b8b3b17295282a80ddacbca1d6f5dcd834e2788c02bafa997", + "shader-language-server-x86_64-unknown-linux-gnu.zip": "61710df7ca17a2d063b598936c57c56c49fbf837707a1aa886f9b0193a35be3c", + "shader-language-server-aarch64-pc-windows-msvc.zip": "a3b3799affe2cad27652e788376b46fe76e1a6c2ce45946a486dcb26c9091412", +} + + +def _hlsl_sha(version: str, asset_name: str) -> str | None: + if version == _INITIAL_VERSION: + return _INITIAL_HLSL_SHA256_BY_ASSET.get(asset_name) + if version == _DEFAULT_VERSION: + return _DEFAULT_HLSL_SHA256_BY_ASSET.get(asset_name) + return None class HlslLanguageServer(SolidLanguageServer): @@ -78,9 +95,7 @@ class HlslLanguageServer(SolidLanguageServer): platform_id="win-x64", archive_type="zip", binary_name="shader-language-server.exe", - sha256=_HLSL_SHA256_BY_ASSET["shader-language-server-x86_64-pc-windows-msvc.zip"] - if version == _DEFAULT_VERSION - else None, + sha256=_hlsl_sha(version, "shader-language-server-x86_64-pc-windows-msvc.zip"), allowed_hosts=_HLSL_ALLOWED_HOSTS, ), RuntimeDependency( @@ -90,9 +105,7 @@ class HlslLanguageServer(SolidLanguageServer): platform_id="linux-x64", archive_type="zip", binary_name="shader-language-server", - sha256=_HLSL_SHA256_BY_ASSET["shader-language-server-x86_64-unknown-linux-gnu.zip"] - if version == _DEFAULT_VERSION - else None, + sha256=_hlsl_sha(version, "shader-language-server-x86_64-unknown-linux-gnu.zip"), allowed_hosts=_HLSL_ALLOWED_HOSTS, ), RuntimeDependency( @@ -102,9 +115,7 @@ class HlslLanguageServer(SolidLanguageServer): platform_id="win-arm64", archive_type="zip", binary_name="shader-language-server.exe", - sha256=_HLSL_SHA256_BY_ASSET["shader-language-server-aarch64-pc-windows-msvc.zip"] - if version == _DEFAULT_VERSION - else None, + sha256=_hlsl_sha(version, "shader-language-server-aarch64-pc-windows-msvc.zip"), allowed_hosts=_HLSL_ALLOWED_HOSTS, ), RuntimeDependency( @@ -139,7 +150,9 @@ class HlslLanguageServer(SolidLanguageServer): "See https://github.com/antaalt/shader-sense for more details." ) - install_dir = os.path.join(self._ls_resources_dir, "shader-language-server") + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + ls_dirname = "shader-language-server" if version == _INITIAL_VERSION else f"shader-language-server-{version}" + install_dir = os.path.join(self._ls_resources_dir, ls_dirname) executable_path = deps.binary_path(install_dir) if not os.path.exists(executable_path): diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index 0c77af6d..47212695 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -21,6 +21,12 @@ from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_in log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_INTELEPHENSE_VERSION = "1.14.4" +DEFAULT_INTELEPHENSE_VERSION = "1.14.4" + class Intelephense(SolidLanguageServer): """ @@ -59,11 +65,12 @@ class Intelephense(SolidLanguageServer): assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - intelephense_version = self._custom_settings.get("intelephense_version", "1.14.4") + intelephense_version = self._custom_settings.get("intelephense_version", DEFAULT_INTELEPHENSE_VERSION) npm_registry = self._custom_settings.get("npm_registry") - # Install intelephense if not already installed - intelephense_ls_dir = os.path.join(self._ls_resources_dir, "php-lsp") + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + ls_dirname = "php-lsp" if intelephense_version == INITIAL_INTELEPHENSE_VERSION else f"php-lsp-{intelephense_version}" + intelephense_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname) os.makedirs(intelephense_ls_dir, exist_ok=True) intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") if not os.path.exists(intelephense_executable_path): diff --git a/src/solidlsp/language_servers/json_language_server.py b/src/solidlsp/language_servers/json_language_server.py index bf46992a..f764418e 100644 --- a/src/solidlsp/language_servers/json_language_server.py +++ b/src/solidlsp/language_servers/json_language_server.py @@ -17,6 +17,12 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_JSON_LANGUAGE_SERVER_VERSION = "1.3.4" +DEFAULT_JSON_LANGUAGE_SERVER_VERSION = "1.3.4" + class JsonLanguageServer(SolidLanguageServer): """ @@ -54,7 +60,7 @@ class JsonLanguageServer(SolidLanguageServer): is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - json_language_server_version = self._custom_settings.get("json_language_server_version", "1.3.4") + json_language_server_version = self._custom_settings.get("json_language_server_version", DEFAULT_JSON_LANGUAGE_SERVER_VERSION) npm_registry = self._custom_settings.get("npm_registry") deps = RuntimeDependencyCollection( @@ -68,7 +74,13 @@ class JsonLanguageServer(SolidLanguageServer): ] ) - json_ls_dir = os.path.join(self._ls_resources_dir, "json-lsp") + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + ls_dirname = ( + "json-lsp" + if json_language_server_version == INITIAL_JSON_LANGUAGE_SERVER_VERSION + else f"json-lsp-{json_language_server_version}" + ) + json_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname) json_executable_path = os.path.join(json_ls_dir, "node_modules", ".bin", "vscode-json-languageserver") if os.name == "nt": diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py index bd2d3035..3998dafa 100644 --- a/src/solidlsp/language_servers/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -41,16 +41,36 @@ log = logging.getLogger(__name__) # -Xmx2G: 2GB heap is sufficient for most projects; override via ls_specific_settings for large codebases DEFAULT_KOTLIN_JVM_OPTIONS = "-Xmx2G" -# Default Kotlin Language Server version (can be overridden via ls_specific_settings) -DEFAULT_KOTLIN_LSP_VERSION = "261.13587.0" KOTLIN_LSP_ALLOWED_HOSTS = ("download-cdn.jetbrains.com",) -KOTLIN_LSP_SHA256_BY_SUFFIX = { + +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_KOTLIN_LSP_VERSION = "261.13587.0" +INITIAL_KOTLIN_LSP_SHA256_BY_SUFFIX = { "win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad", "linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec", "linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651", "mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3", "mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c", } +DEFAULT_KOTLIN_LSP_VERSION = "261.13587.0" +DEFAULT_KOTLIN_LSP_SHA256_BY_SUFFIX = { + "win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad", + "linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec", + "linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651", + "mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3", + "mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c", +} + + +def _kotlin_lsp_sha(version: str, kotlin_suffix: str) -> str | None: + if version == INITIAL_KOTLIN_LSP_VERSION: + return INITIAL_KOTLIN_LSP_SHA256_BY_SUFFIX.get(kotlin_suffix) + if version == DEFAULT_KOTLIN_LSP_VERSION: + return DEFAULT_KOTLIN_LSP_SHA256_BY_SUFFIX.get(kotlin_suffix) + return None + # Platform-specific Kotlin LSP download suffixes PLATFORM_KOTLIN_SUFFIX = { @@ -108,8 +128,14 @@ class KotlinLanguageServer(SolidLanguageServer): kotlin_suffix = PLATFORM_KOTLIN_SUFFIX.get(platform_id.value) assert kotlin_suffix, f"Unsupported platform for Kotlin LSP: {platform_id.value}" - # Setup paths for dependencies - static_dir = os.path.join(self._ls_resources_dir, "kotlin_language_server") + # Setup paths for dependencies; legacy unversioned dir reserved for INITIAL only + kotlin_lsp_version = self._custom_settings.get("kotlin_lsp_version", DEFAULT_KOTLIN_LSP_VERSION) + ls_dirname = ( + "kotlin_language_server" + if kotlin_lsp_version == INITIAL_KOTLIN_LSP_VERSION + else f"kotlin_language_server-{kotlin_lsp_version}" + ) + static_dir = os.path.join(self._ls_resources_dir, ls_dirname) os.makedirs(static_dir, exist_ok=True) # Setup Kotlin Language Server @@ -117,11 +143,8 @@ class KotlinLanguageServer(SolidLanguageServer): kotlin_script = os.path.join(static_dir, kotlin_script_name) if not os.path.exists(kotlin_script): - kotlin_lsp_version = self._custom_settings.get("kotlin_lsp_version", DEFAULT_KOTLIN_LSP_VERSION) kotlin_url = f"https://download-cdn.jetbrains.com/kotlin-lsp/{kotlin_lsp_version}/kotlin-lsp-{kotlin_lsp_version}-{kotlin_suffix}.zip" - expected_sha256 = None - if kotlin_lsp_version == DEFAULT_KOTLIN_LSP_VERSION: - expected_sha256 = KOTLIN_LSP_SHA256_BY_SUFFIX[kotlin_suffix] + expected_sha256 = _kotlin_lsp_sha(kotlin_lsp_version, kotlin_suffix) log.info("Downloading Kotlin Language Server...") FileUtils.download_and_extract_archive_verified( kotlin_url, diff --git a/src/solidlsp/language_servers/lua_ls.py b/src/solidlsp/language_servers/lua_ls.py index ef5e8674..3176e41f 100644 --- a/src/solidlsp/language_servers/lua_ls.py +++ b/src/solidlsp/language_servers/lua_ls.py @@ -26,15 +26,42 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) -LUA_LS_VERSION = "3.15.0" LUA_LS_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") -LUA_LS_SHA256_BY_ASSET = { + +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_LUA_LS_VERSION = "3.15.0" +INITIAL_LUA_LS_SHA256_BY_ASSET = { "lua-language-server-3.15.0-linux-x64.tar.gz": "4877b874c52fb7587707898da9026cc3a6c854d9bbab115ef49ac4e6a1b88007", "lua-language-server-3.15.0-linux-arm64.tar.gz": "7dff8edfed4f34cf6325ff384791287d95f9a8dd9615a5279c7c6af81cf8c45d", "lua-language-server-3.15.0-darwin-x64.tar.gz": "01d28a31e264434e51662814a68f584af068393caecfa158c4df5f7fdc3ca2f7", "lua-language-server-3.15.0-darwin-arm64.tar.gz": "050f5f493f65112afc116e31281a9f73918546782d3696485dc052724838f58b", "lua-language-server-3.15.0-win32-x64.zip": "76a10c05e8c947a448f00a61acead4240484cd1e2e8c66d54401c67d99b77535", } +DEFAULT_LUA_LS_VERSION = "3.15.0" +DEFAULT_LUA_LS_SHA256_BY_ASSET = { + "lua-language-server-3.15.0-linux-x64.tar.gz": "4877b874c52fb7587707898da9026cc3a6c854d9bbab115ef49ac4e6a1b88007", + "lua-language-server-3.15.0-linux-arm64.tar.gz": "7dff8edfed4f34cf6325ff384791287d95f9a8dd9615a5279c7c6af81cf8c45d", + "lua-language-server-3.15.0-darwin-x64.tar.gz": "01d28a31e264434e51662814a68f584af068393caecfa158c4df5f7fdc3ca2f7", + "lua-language-server-3.15.0-darwin-arm64.tar.gz": "050f5f493f65112afc116e31281a9f73918546782d3696485dc052724838f58b", + "lua-language-server-3.15.0-win32-x64.zip": "76a10c05e8c947a448f00a61acead4240484cd1e2e8c66d54401c67d99b77535", +} + + +def _lua_ls_sha(version: str, asset_name: str) -> str | None: + if version == INITIAL_LUA_LS_VERSION: + return INITIAL_LUA_LS_SHA256_BY_ASSET.get(asset_name) + if version == DEFAULT_LUA_LS_VERSION: + return DEFAULT_LUA_LS_SHA256_BY_ASSET.get(asset_name) + return None + + +def _lua_ls_install_dir(ls_resources_dir: str, version: str) -> Path: + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + if version == INITIAL_LUA_LS_VERSION: + return Path(ls_resources_dir) / "lua" + return Path(ls_resources_dir) / f"lua-{version}" class LuaLanguageServer(SolidLanguageServer): @@ -67,7 +94,9 @@ class LuaLanguageServer(SolidLanguageServer): ] if solidlsp_settings is not None: - ls_resource_dir = Path(LuaLanguageServer.ls_resources_dir(solidlsp_settings)) / "lua" + lua_settings = solidlsp_settings.get_ls_specific_settings(Language.LUA) + lua_ls_version = lua_settings.get("lua_language_server_version", DEFAULT_LUA_LS_VERSION) + ls_resource_dir = _lua_ls_install_dir(LuaLanguageServer.ls_resources_dir(solidlsp_settings), lua_ls_version) possible_paths.extend( [ ls_resource_dir / "bin" / "lua-language-server", @@ -93,7 +122,7 @@ class LuaLanguageServer(SolidLanguageServer): def _download_lua_ls(solidlsp_settings: SolidLSPSettings) -> str: """Download and install lua-language-server if not present.""" lua_settings = solidlsp_settings.get_ls_specific_settings(Language.LUA) - lua_ls_version = lua_settings.get("lua_language_server_version", LUA_LS_VERSION) + lua_ls_version = lua_settings.get("lua_language_server_version", DEFAULT_LUA_LS_VERSION) system = platform.system() machine = platform.machine().lower() @@ -122,7 +151,7 @@ class LuaLanguageServer(SolidLanguageServer): download_url = f"https://github.com/LuaLS/lua-language-server/releases/download/{lua_ls_version}/{download_name}" - install_dir = Path(LuaLanguageServer.ls_resources_dir(solidlsp_settings)) / "lua" + install_dir = _lua_ls_install_dir(LuaLanguageServer.ls_resources_dir(solidlsp_settings), lua_ls_version) install_dir.mkdir(parents=True, exist_ok=True) log.info("Downloading lua-language-server from %s", download_url) @@ -131,7 +160,7 @@ class LuaLanguageServer(SolidLanguageServer): download_url, str(install_dir), archive_type, - expected_sha256=LUA_LS_SHA256_BY_ASSET.get(download_name) if lua_ls_version == LUA_LS_VERSION else None, + expected_sha256=_lua_ls_sha(lua_ls_version, download_name), allowed_hosts=LUA_LS_ALLOWED_HOSTS, ) diff --git a/src/solidlsp/language_servers/luau_lsp.py b/src/solidlsp/language_servers/luau_lsp.py index 4150374d..f0cb06bf 100644 --- a/src/solidlsp/language_servers/luau_lsp.py +++ b/src/solidlsp/language_servers/luau_lsp.py @@ -37,15 +37,34 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) -# Pin to a known stable release -LUAU_LSP_VERSION = "1.63.0" LUAU_LSP_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") -LUAU_LSP_SHA256_BY_ASSET = { + +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_LUAU_LSP_VERSION = "1.63.0" +INITIAL_LUAU_LSP_SHA256_BY_ASSET = { "luau-lsp-linux-x86_64.zip": "e4b633ad9a2c15437f60f9e721263f79aa0da606867d8458f0e159a325bf2db8", "luau-lsp-linux-arm64.zip": "355be010f337a6772df6255c92e1fb28a59d194abe5c570453f4186472244355", "luau-lsp-macos.zip": "01c1d6dd5fee27295b2968915dabb08c192192c46d9fe9c97bf31a130c96b8cb", "luau-lsp-win64.zip": "eea596d47dc1c94a61ba1b78e6472bb4445bc3309780751515e6ab0a0abba57d", } +DEFAULT_LUAU_LSP_VERSION = "1.63.0" +DEFAULT_LUAU_LSP_SHA256_BY_ASSET = { + "luau-lsp-linux-x86_64.zip": "e4b633ad9a2c15437f60f9e721263f79aa0da606867d8458f0e159a325bf2db8", + "luau-lsp-linux-arm64.zip": "355be010f337a6772df6255c92e1fb28a59d194abe5c570453f4186472244355", + "luau-lsp-macos.zip": "01c1d6dd5fee27295b2968915dabb08c192192c46d9fe9c97bf31a130c96b8cb", + "luau-lsp-win64.zip": "eea596d47dc1c94a61ba1b78e6472bb4445bc3309780751515e6ab0a0abba57d", +} + + +def _luau_lsp_sha(version: str, asset_name: str) -> str | None: + if version == INITIAL_LUAU_LSP_VERSION: + return INITIAL_LUAU_LSP_SHA256_BY_ASSET.get(asset_name) + if version == DEFAULT_LUAU_LSP_VERSION: + return DEFAULT_LUAU_LSP_SHA256_BY_ASSET.get(asset_name) + return None + # Luau built-in docs CDN LUAU_DOCS_URL = "https://luau-lsp.pages.dev/api-docs/luau-en-us.json" @@ -98,14 +117,19 @@ class LuauLanguageServer(SolidLanguageServer): return cmd def _download_luau_lsp(self) -> str: - install_dir = Path(self._ls_resources_dir) + luau_lsp_version = self._custom_settings.get("luau_lsp_version", DEFAULT_LUAU_LSP_VERSION) + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + install_dir = ( + Path(self._ls_resources_dir) + if luau_lsp_version == INITIAL_LUAU_LSP_VERSION + else Path(self._ls_resources_dir) / f"luau-lsp-{luau_lsp_version}" + ) install_dir.mkdir(parents=True, exist_ok=True) binary_path = self._find_existing_binary(install_dir) if binary_path is not None: return binary_path - luau_lsp_version = self._custom_settings.get("luau_lsp_version", LUAU_LSP_VERSION) asset_name = self._get_luau_lsp_asset_name() download_url = f"https://github.com/JohnnyMorganz/luau-lsp/releases/download/{luau_lsp_version}/{asset_name}" @@ -114,7 +138,7 @@ class LuauLanguageServer(SolidLanguageServer): download_url, str(install_dir), "zip", - expected_sha256=LUAU_LSP_SHA256_BY_ASSET.get(asset_name) if luau_lsp_version == LUAU_LSP_VERSION else None, + expected_sha256=_luau_lsp_sha(luau_lsp_version, asset_name), allowed_hosts=LUAU_LSP_ALLOWED_HOSTS, ) diff --git a/src/solidlsp/language_servers/marksman.py b/src/solidlsp/language_servers/marksman.py index affe2dcb..13c32147 100644 --- a/src/solidlsp/language_servers/marksman.py +++ b/src/solidlsp/language_servers/marksman.py @@ -28,6 +28,34 @@ log = logging.getLogger(__name__) MARKSMAN_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_MARKSMAN_VERSION = "2024-12-18" +INITIAL_MARKSMAN_SHA256_BY_PLATFORM = { + "linux-x64": "b9cb666c643dfd9b699811fdfc445ed4c56be65c1d878c21d46847f0d7b0e475", + "linux-arm64": "b8d6972a56f3f9b7bbbf7c77ef8998e3b66fa82fb03c01398e224144486c9e73", + "osx-x64": "7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83", + "osx-arm64": "7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83", + "win-x64": "39de9df039c8b0d627ac5918a9d8792ad20fc49e2461d1f5c906975c016799ec", +} +DEFAULT_MARKSMAN_VERSION = "2024-12-18" +DEFAULT_MARKSMAN_SHA256_BY_PLATFORM = { + "linux-x64": "b9cb666c643dfd9b699811fdfc445ed4c56be65c1d878c21d46847f0d7b0e475", + "linux-arm64": "b8d6972a56f3f9b7bbbf7c77ef8998e3b66fa82fb03c01398e224144486c9e73", + "osx-x64": "7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83", + "osx-arm64": "7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83", + "win-x64": "39de9df039c8b0d627ac5918a9d8792ad20fc49e2461d1f5c906975c016799ec", +} + + +def _marksman_sha(version: str, platform_key: str) -> str | None: + if version == INITIAL_MARKSMAN_VERSION: + return INITIAL_MARKSMAN_SHA256_BY_PLATFORM[platform_key] + if version == DEFAULT_MARKSMAN_VERSION: + return DEFAULT_MARKSMAN_SHA256_BY_PLATFORM[platform_key] + return None + class Marksman(SolidLanguageServer): """ @@ -39,12 +67,9 @@ class Marksman(SolidLanguageServer): """ class DependencyProvider(LanguageServerDependencyProviderSinglePath): - DEFAULT_MARKSMAN_VERSION = "2024-12-18" - @classmethod def _runtime_dependencies(cls, version: str) -> RuntimeDependencyCollection: marksman_releases = f"https://github.com/artempyanykh/marksman/releases/download/{version}" - default_version = version == cls.DEFAULT_MARKSMAN_VERSION return RuntimeDependencyCollection( [ RuntimeDependency( @@ -53,7 +78,7 @@ class Marksman(SolidLanguageServer): platform_id="linux-x64", archive_type="binary", binary_name="marksman", - sha256="b9cb666c643dfd9b699811fdfc445ed4c56be65c1d878c21d46847f0d7b0e475" if default_version else None, + sha256=_marksman_sha(version, "linux-x64"), allowed_hosts=MARKSMAN_ALLOWED_HOSTS, ), RuntimeDependency( @@ -62,7 +87,7 @@ class Marksman(SolidLanguageServer): platform_id="linux-arm64", archive_type="binary", binary_name="marksman", - sha256="b8d6972a56f3f9b7bbbf7c77ef8998e3b66fa82fb03c01398e224144486c9e73" if default_version else None, + sha256=_marksman_sha(version, "linux-arm64"), allowed_hosts=MARKSMAN_ALLOWED_HOSTS, ), RuntimeDependency( @@ -71,7 +96,7 @@ class Marksman(SolidLanguageServer): platform_id="osx-x64", archive_type="binary", binary_name="marksman", - sha256="7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83" if default_version else None, + sha256=_marksman_sha(version, "osx-x64"), allowed_hosts=MARKSMAN_ALLOWED_HOSTS, ), RuntimeDependency( @@ -80,7 +105,7 @@ class Marksman(SolidLanguageServer): platform_id="osx-arm64", archive_type="binary", binary_name="marksman", - sha256="7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83" if default_version else None, + sha256=_marksman_sha(version, "osx-arm64"), allowed_hosts=MARKSMAN_ALLOWED_HOSTS, ), RuntimeDependency( @@ -89,7 +114,7 @@ class Marksman(SolidLanguageServer): platform_id="win-x64", archive_type="binary", binary_name="marksman.exe", - sha256="39de9df039c8b0d627ac5918a9d8792ad20fc49e2461d1f5c906975c016799ec" if default_version else None, + sha256=_marksman_sha(version, "win-x64"), allowed_hosts=MARKSMAN_ALLOWED_HOSTS, ), ] @@ -97,11 +122,16 @@ class Marksman(SolidLanguageServer): def _get_or_install_core_dependency(self) -> str: """Setup runtime dependencies for marksman and return the command to start the server.""" - marksman_version = self._custom_settings.get("marksman_version", self.DEFAULT_MARKSMAN_VERSION) + marksman_version = self._custom_settings.get("marksman_version", DEFAULT_MARKSMAN_VERSION) deps = self._runtime_dependencies(marksman_version) dependency = deps.get_single_dep_for_current_platform() - marksman_ls_dir = self._ls_resources_dir + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + marksman_ls_dir = ( + self._ls_resources_dir + if marksman_version == INITIAL_MARKSMAN_VERSION + else os.path.join(self._ls_resources_dir, f"marksman-{marksman_version}") + ) marksman_executable_path = deps.binary_path(marksman_ls_dir) if not os.path.exists(marksman_executable_path): log.info( diff --git a/src/solidlsp/language_servers/matlab_language_server.py b/src/solidlsp/language_servers/matlab_language_server.py index d31c2224..5a83a75f 100644 --- a/src/solidlsp/language_servers/matlab_language_server.py +++ b/src/solidlsp/language_servers/matlab_language_server.py @@ -50,11 +50,29 @@ log = logging.getLogger(__name__) # Environment variable for MATLAB installation path MATLAB_PATH_ENV_VAR = "MATLAB_PATH" -MATLAB_EXTENSION_VERSION = "1.3.9" -MATLAB_EXTENSION_URL = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/vsextensions/language-matlab/{MATLAB_EXTENSION_VERSION}/vspackage" -MATLAB_EXTENSION_SHA256 = "1da3add2c3a593fa0ebcdf1d15231faee8014de10f549c36915ab9d4f18390f2" MATLAB_EXTENSION_ALLOWED_HOSTS = ("marketplace.visualstudio.com",) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_MATLAB_EXTENSION_VERSION = "1.3.9" +INITIAL_MATLAB_EXTENSION_SHA256 = "1da3add2c3a593fa0ebcdf1d15231faee8014de10f549c36915ab9d4f18390f2" +DEFAULT_MATLAB_EXTENSION_VERSION = "1.3.9" +DEFAULT_MATLAB_EXTENSION_SHA256 = "1da3add2c3a593fa0ebcdf1d15231faee8014de10f549c36915ab9d4f18390f2" + + +def _matlab_extension_sha(version: str) -> str | None: + if version == INITIAL_MATLAB_EXTENSION_VERSION: + return INITIAL_MATLAB_EXTENSION_SHA256 + if version == DEFAULT_MATLAB_EXTENSION_VERSION: + return DEFAULT_MATLAB_EXTENSION_SHA256 + return None + + +def _matlab_extension_dirname(version: str) -> str: + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + return "matlab-extension" if version == INITIAL_MATLAB_EXTENSION_VERSION else f"matlab-extension-{version}" + class MatlabLanguageServer(SolidLanguageServer): """ @@ -102,20 +120,12 @@ class MatlabLanguageServer(SolidLanguageServer): self._matlab_path: str | None = None @classmethod - def _download_matlab_extension(cls, url: str, target_dir: str) -> bool: + def _download_matlab_extension(cls, url: str, target_dir: str, expected_sha256: str | None) -> bool: """ Download and extract the MATLAB extension from VS Code marketplace. The VS Code marketplace packages extensions as .vsix files (which are ZIP archives). This method downloads the VSIX file and extracts it to get the language server. - - Args: - url: VS Code marketplace URL for the MATLAB extension - target_dir: Directory where the extension will be extracted - - Returns: - True if successful, False otherwise - """ try: log.info(f"Downloading MATLAB extension from {url}") @@ -124,7 +134,7 @@ class MatlabLanguageServer(SolidLanguageServer): url, target_dir, "zip", - expected_sha256=MATLAB_EXTENSION_SHA256 if url == MATLAB_EXTENSION_URL else None, + expected_sha256=expected_sha256, allowed_hosts=MATLAB_EXTENSION_ALLOWED_HOSTS, ) log.info("MATLAB extension extracted successfully") @@ -155,8 +165,9 @@ class MatlabLanguageServer(SolidLanguageServer): elif env_path: log.warning(f"MATLAB_EXTENSION_PATH set but directory not found: {env_path}") - # Check default download location - default_path = os.path.join(self._ls_resources_dir, "matlab-extension", "extension") + # Check the resolved-version download location (versioned for non-INITIAL, legacy "matlab-extension" for INITIAL) + matlab_extension_version = self._custom_settings.get("matlab_extension_version", DEFAULT_MATLAB_EXTENSION_VERSION) + default_path = os.path.join(self._ls_resources_dir, _matlab_extension_dirname(matlab_extension_version), "extension") if os.path.exists(default_path): log.debug(f"Found MATLAB extension in default location: {default_path}") return default_path @@ -182,8 +193,8 @@ class MatlabLanguageServer(SolidLanguageServer): Path to installed extension or None if download failed """ - matlab_extension_dir = os.path.join(self._ls_resources_dir, "matlab-extension") - matlab_extension_version = self._custom_settings.get("matlab_extension_version", MATLAB_EXTENSION_VERSION) + matlab_extension_version = self._custom_settings.get("matlab_extension_version", DEFAULT_MATLAB_EXTENSION_VERSION) + matlab_extension_dir = os.path.join(self._ls_resources_dir, _matlab_extension_dirname(matlab_extension_version)) matlab_extension_url = ( "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/" f"vsextensions/language-matlab/{matlab_extension_version}/vspackage" @@ -191,7 +202,7 @@ class MatlabLanguageServer(SolidLanguageServer): log.info(f"Downloading MATLAB extension from: {matlab_extension_url}") - if self._download_matlab_extension(matlab_extension_url, matlab_extension_dir): + if self._download_matlab_extension(matlab_extension_url, matlab_extension_dir, _matlab_extension_sha(matlab_extension_version)): extension_path = os.path.join(matlab_extension_dir, "extension") if os.path.exists(extension_path): log.info("MATLAB extension downloaded and installed successfully") diff --git a/src/solidlsp/language_servers/omnisharp.py b/src/solidlsp/language_servers/omnisharp.py index ddd21698..f4ece965 100644 --- a/src/solidlsp/language_servers/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp.py @@ -22,6 +22,11 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) OMNISHARP_ALLOWED_HOSTS = ("roslynomnisharp.blob.core.windows.net", "download.visualstudio.microsoft.com") +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_OMNISHARP_VERSION = "1.39.10" +INITIAL_RAZOR_OMNISHARP_VERSION = "7.0.0-preview.23363.1" DEFAULT_OMNISHARP_VERSION = "1.39.10" DEFAULT_RAZOR_OMNISHARP_VERSION = "7.0.0-preview.23363.1" @@ -168,11 +173,11 @@ class OmniSharp(SolidLanguageServer): dependency["installPath"] = dependency["installPath"].replace(DEFAULT_OMNISHARP_VERSION, omnisharp_version) if "installTestPath" in dependency: dependency["installTestPath"] = dependency["installTestPath"].replace(DEFAULT_OMNISHARP_VERSION, omnisharp_version) - if omnisharp_version != DEFAULT_OMNISHARP_VERSION: + if omnisharp_version not in (INITIAL_OMNISHARP_VERSION, DEFAULT_OMNISHARP_VERSION): dependency["integrity"] = None elif dependency["id"] == "RazorOmnisharp": dependency["url"] = dependency["url"].replace(DEFAULT_RAZOR_OMNISHARP_VERSION, razor_omnisharp_version) - if razor_omnisharp_version != DEFAULT_RAZOR_OMNISHARP_VERSION: + if razor_omnisharp_version not in (INITIAL_RAZOR_OMNISHARP_VERSION, DEFAULT_RAZOR_OMNISHARP_VERSION): dependency["integrity"] = None assert platform_id in [ @@ -207,7 +212,9 @@ class OmniSharp(SolidLanguageServer): assert "OmniSharp" in runtime_dependencies assert "RazorOmnisharp" in runtime_dependencies - omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp") + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + omnisharp_dirname = "OmniSharp" if omnisharp_version == INITIAL_OMNISHARP_VERSION else f"OmniSharp-{omnisharp_version}" + omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), omnisharp_dirname) if not os.path.exists(omnisharp_ls_dir): os.makedirs(omnisharp_ls_dir) FileUtils.download_and_extract_archive_verified( @@ -221,7 +228,10 @@ class OmniSharp(SolidLanguageServer): assert os.path.exists(omnisharp_executable_path) os.chmod(omnisharp_executable_path, 0o755) - razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp") + razor_dirname = ( + "RazorOmnisharp" if razor_omnisharp_version == INITIAL_RAZOR_OMNISHARP_VERSION else f"RazorOmnisharp-{razor_omnisharp_version}" + ) + razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), razor_dirname) if not os.path.exists(razor_omnisharp_ls_dir): os.makedirs(razor_omnisharp_ls_dir) FileUtils.download_and_extract_archive_verified( diff --git a/src/solidlsp/language_servers/pascal_server.py b/src/solidlsp/language_servers/pascal_server.py index 1f2b7de7..34c6ff73 100644 --- a/src/solidlsp/language_servers/pascal_server.py +++ b/src/solidlsp/language_servers/pascal_server.py @@ -69,7 +69,34 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) -PASLS_VERSION = "v0.2.0" +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_PASLS_VERSION = "v0.2.0" +INITIAL_PASLS_SHA256_BY_PLATFORM = { + "linux-x64": "517259395b0a385a5e848cf48b967645a984be3dd456118bc08771283a822a5b", + "linux-arm64": "cb4986941cfdcf9cb74ece6bbb53a443390a908e880108a37f4ccf82b2d6c502", + "osx-x64": "0abfcd98f63f77dba74094339a40d4407b69317c1c77b13a26b9b7dbdfd885f1", + "osx-arm64": "d4c2411e406af96ceae12b11e77fdb0c684ca15a68bfd8b4f9c6fe1fbdf515a7", + "win-x64": "1493c31552e6f90a59800b2d44669e01fc4551d3647f3cea5dd105a9f6bc73e5", +} +DEFAULT_PASLS_VERSION = "v0.2.0" +DEFAULT_PASLS_SHA256_BY_PLATFORM = { + "linux-x64": "517259395b0a385a5e848cf48b967645a984be3dd456118bc08771283a822a5b", + "linux-arm64": "cb4986941cfdcf9cb74ece6bbb53a443390a908e880108a37f4ccf82b2d6c502", + "osx-x64": "0abfcd98f63f77dba74094339a40d4407b69317c1c77b13a26b9b7dbdfd885f1", + "osx-arm64": "d4c2411e406af96ceae12b11e77fdb0c684ca15a68bfd8b4f9c6fe1fbdf515a7", + "win-x64": "1493c31552e6f90a59800b2d44669e01fc4551d3647f3cea5dd105a9f6bc73e5", +} +PASLS_VERSION = DEFAULT_PASLS_VERSION + + +def _pasls_sha(version: str, platform_key: str) -> str | None: + if version == INITIAL_PASLS_VERSION: + return INITIAL_PASLS_SHA256_BY_PLATFORM.get(platform_key) + if version == DEFAULT_PASLS_VERSION: + return DEFAULT_PASLS_SHA256_BY_PLATFORM.get(platform_key) + return None class PascalLanguageServer(SolidLanguageServer): @@ -629,7 +656,12 @@ class PascalLanguageServer(SolidLanguageServer): log.info(f"Found pasls in PATH: {pasls_in_path}") return quote_windows_path(pasls_in_path) - pasls_dir = cls.ls_resources_dir(solidlsp_settings) + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + pasls_dir = ( + cls.ls_resources_dir(solidlsp_settings) + if pasls_version == INITIAL_PASLS_VERSION + else os.path.join(cls.ls_resources_dir(solidlsp_settings), f"pasls-{pasls_version}") + ) os.makedirs(pasls_dir, exist_ok=True) # Clean up old files from previous sessions @@ -647,7 +679,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="linux-x64", archive_type="gztar", binary_name="pasls", - sha256="517259395b0a385a5e848cf48b967645a984be3dd456118bc08771283a822a5b", + sha256=_pasls_sha(pasls_version, "linux-x64"), ), RuntimeDependency( id="PascalLanguageServer", @@ -656,7 +688,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="linux-arm64", archive_type="gztar", binary_name="pasls", - sha256="cb4986941cfdcf9cb74ece6bbb53a443390a908e880108a37f4ccf82b2d6c502", + sha256=_pasls_sha(pasls_version, "linux-arm64"), ), RuntimeDependency( id="PascalLanguageServer", @@ -665,7 +697,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="osx-x64", archive_type="zip", binary_name="pasls", - sha256="0abfcd98f63f77dba74094339a40d4407b69317c1c77b13a26b9b7dbdfd885f1", + sha256=_pasls_sha(pasls_version, "osx-x64"), ), RuntimeDependency( id="PascalLanguageServer", @@ -674,7 +706,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="osx-arm64", archive_type="zip", binary_name="pasls", - sha256="d4c2411e406af96ceae12b11e77fdb0c684ca15a68bfd8b4f9c6fe1fbdf515a7", + sha256=_pasls_sha(pasls_version, "osx-arm64"), ), RuntimeDependency( id="PascalLanguageServer", @@ -683,7 +715,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="win-x64", archive_type="zip", binary_name="pasls.exe", - sha256="1493c31552e6f90a59800b2d44669e01fc4551d3647f3cea5dd105a9f6bc73e5", + sha256=_pasls_sha(pasls_version, "win-x64"), ), ] ) diff --git a/src/solidlsp/language_servers/phpactor.py b/src/solidlsp/language_servers/phpactor.py index 80805c0d..181cb3ee 100644 --- a/src/solidlsp/language_servers/phpactor.py +++ b/src/solidlsp/language_servers/phpactor.py @@ -20,11 +20,24 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) -PHPACTOR_VERSION = "2025.12.21.1" -PHPACTOR_PHAR_URL = f"https://github.com/phpactor/phpactor/releases/download/{PHPACTOR_VERSION}/phpactor.phar" -PHPACTOR_PHAR_SHA256 = "53bbe9625cd9b5e9b394bc2f595fbad13dbbe6dfc96950c56dea3b5d9a246cc3" PHPACTOR_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_PHPACTOR_VERSION = "2025.12.21.1" +INITIAL_PHPACTOR_PHAR_SHA256 = "53bbe9625cd9b5e9b394bc2f595fbad13dbbe6dfc96950c56dea3b5d9a246cc3" +DEFAULT_PHPACTOR_VERSION = "2025.12.21.1" +DEFAULT_PHPACTOR_PHAR_SHA256 = "53bbe9625cd9b5e9b394bc2f595fbad13dbbe6dfc96950c56dea3b5d9a246cc3" + + +def _phpactor_sha(version: str) -> str | None: + if version == INITIAL_PHPACTOR_VERSION: + return INITIAL_PHPACTOR_PHAR_SHA256 + if version == DEFAULT_PHPACTOR_VERSION: + return DEFAULT_PHPACTOR_PHAR_SHA256 + return None + class PhpactorServer(SolidLanguageServer): """ @@ -48,7 +61,7 @@ class PhpactorServer(SolidLanguageServer): """ Setup runtime dependencies for Phpactor and return the path to the PHAR file. """ - phpactor_version = self._custom_settings.get("phpactor_version", PHPACTOR_VERSION) + phpactor_version = self._custom_settings.get("phpactor_version", DEFAULT_PHPACTOR_VERSION) phpactor_phar_url = f"https://github.com/phpactor/phpactor/releases/download/{phpactor_version}/phpactor.phar" # Verify PHP is installed php_path = shutil.which("php") @@ -68,15 +81,20 @@ class PhpactorServer(SolidLanguageServer): else: log.warning("Could not parse PHP version from output. Continuing anyway.") - phpactor_phar_path = os.path.join(self._ls_resources_dir, "phpactor.phar") + # legacy unversioned phar at root reserved for INITIAL; every other version goes into a versioned subdir + if phpactor_version == INITIAL_PHPACTOR_VERSION: + phar_dir = self._ls_resources_dir + else: + phar_dir = os.path.join(self._ls_resources_dir, f"phpactor-{phpactor_version}") + phpactor_phar_path = os.path.join(phar_dir, "phpactor.phar") if not os.path.exists(phpactor_phar_path): - os.makedirs(self._ls_resources_dir, exist_ok=True) + os.makedirs(phar_dir, exist_ok=True) log.info(f"Downloading phpactor PHAR from {phpactor_phar_url}") FileUtils.download_and_extract_archive_verified( phpactor_phar_url, phpactor_phar_path, "binary", - expected_sha256=PHPACTOR_PHAR_SHA256 if phpactor_version == PHPACTOR_VERSION else None, + expected_sha256=_phpactor_sha(phpactor_version), allowed_hosts=PHPACTOR_ALLOWED_HOSTS, ) diff --git a/src/solidlsp/language_servers/powershell_language_server.py b/src/solidlsp/language_servers/powershell_language_server.py index e737b09a..70a06d1a 100644 --- a/src/solidlsp/language_servers/powershell_language_server.py +++ b/src/solidlsp/language_servers/powershell_language_server.py @@ -34,8 +34,30 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) # PowerShell Editor Services version to download -PSES_VERSION = "4.4.0" -PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc" +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_PSES_VERSION = "4.4.0" +INITIAL_PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc" +DEFAULT_PSES_VERSION = "4.4.0" +DEFAULT_PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc" + + +def _pses_sha(version: str) -> str | None: + if version == INITIAL_PSES_VERSION: + return INITIAL_PSES_SHA256 + if version == DEFAULT_PSES_VERSION: + return DEFAULT_PSES_SHA256 + return None + + +def _pses_install_dir(ls_resources_dir: str, version: str) -> Path: + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + if version == INITIAL_PSES_VERSION: + return Path(ls_resources_dir) / "powershell" + return Path(ls_resources_dir) / f"powershell-{version}" + + PSES_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") PSSCRIPTANALYZER_VERSION = "1.25.0" @@ -99,7 +121,9 @@ class PowerShellLanguageServer(SolidLanguageServer): @classmethod def _get_pses_path(cls, solidlsp_settings: SolidLSPSettings) -> str | None: """Get the path to PowerShell Editor Services installation.""" - install_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "powershell" + ps_settings = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL) + pses_version = ps_settings.get("pses_version", DEFAULT_PSES_VERSION) + install_dir = _pses_install_dir(cls.ls_resources_dir(solidlsp_settings), pses_version) start_script = install_dir / "PowerShellEditorServices" / "Start-EditorServices.ps1" if start_script.exists(): @@ -111,13 +135,13 @@ class PowerShellLanguageServer(SolidLanguageServer): def _download_pses(cls, solidlsp_settings: SolidLSPSettings) -> str: """Download and install PowerShell Editor Services.""" ps_settings = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL) - pses_version = ps_settings.get("pses_version", PSES_VERSION) + pses_version = ps_settings.get("pses_version", DEFAULT_PSES_VERSION) download_url = ( f"https://github.com/PowerShell/PowerShellEditorServices/releases/download/v{pses_version}/PowerShellEditorServices.zip" ) - # Create installation directory - install_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "powershell" + # Create installation directory; legacy unversioned dir reserved for INITIAL only + install_dir = _pses_install_dir(cls.ls_resources_dir(solidlsp_settings), pses_version) install_dir.mkdir(parents=True, exist_ok=True) log.info(f"Downloading PowerShell Editor Services from {download_url}...") @@ -125,7 +149,7 @@ class PowerShellLanguageServer(SolidLanguageServer): download_url, str(install_dir), "zip", - expected_sha256=PSES_SHA256 if pses_version == PSES_VERSION else None, + expected_sha256=_pses_sha(pses_version), allowed_hosts=PSES_ALLOWED_HOSTS, ) diff --git a/src/solidlsp/language_servers/solidity_language_server.py b/src/solidlsp/language_servers/solidity_language_server.py index a41deb89..d0c34053 100644 --- a/src/solidlsp/language_servers/solidity_language_server.py +++ b/src/solidlsp/language_servers/solidity_language_server.py @@ -24,6 +24,14 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_SOLIDITY_LANGUAGE_SERVER_VERSION = "0.8.4" +DEFAULT_SOLIDITY_LANGUAGE_SERVER_VERSION = "0.8.4" +INITIAL_FORGE_VERSION = "1.5.1" +DEFAULT_FORGE_VERSION = "1.5.1" + class SolidityLanguageServer(SolidLanguageServer): """ @@ -73,8 +81,10 @@ class SolidityLanguageServer(SolidLanguageServer): assert is_node_installed, "node is not installed or isn't in PATH. Please install Node.js and try again." is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - solidity_language_server_version = self._custom_settings.get("solidity_language_server_version", "0.8.4") - forge_version = self._custom_settings.get("forge_version", "1.5.1") + solidity_language_server_version = self._custom_settings.get( + "solidity_language_server_version", DEFAULT_SOLIDITY_LANGUAGE_SERVER_VERSION + ) + forge_version = self._custom_settings.get("forge_version", DEFAULT_FORGE_VERSION) npm_registry = self._custom_settings.get("npm_registry") deps = RuntimeDependencyCollection( @@ -101,7 +111,7 @@ class SolidityLanguageServer(SolidLanguageServer): ] ) - solidity_ls_dir = os.path.join(self._ls_resources_dir, "solidity-lsp") + solidity_ls_dir = self._resolve_solidity_ls_dir(solidity_language_server_version, forge_version) managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin") solidity_executable_path = os.path.join(managed_bin_dir, "nomicfoundation-solidity-language-server") forge_executable_path = os.path.join(managed_bin_dir, "forge") @@ -146,10 +156,22 @@ class SolidityLanguageServer(SolidLanguageServer): return f"@foundry-rs/forge-linux-{arch}" def create_launch_command_env(self) -> dict[str, str]: - solidity_ls_dir = os.path.join(self._ls_resources_dir, "solidity-lsp") + solidity_language_server_version = self._custom_settings.get( + "solidity_language_server_version", DEFAULT_SOLIDITY_LANGUAGE_SERVER_VERSION + ) + forge_version = self._custom_settings.get("forge_version", DEFAULT_FORGE_VERSION) + solidity_ls_dir = self._resolve_solidity_ls_dir(solidity_language_server_version, forge_version) managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin") return {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")} + def _resolve_solidity_ls_dir(self, solidity_language_server_version: str, forge_version: str) -> str: + # legacy unversioned dir reserved for INITIAL pair; any other combination goes into a versioned subdir + is_initial = ( + solidity_language_server_version == INITIAL_SOLIDITY_LANGUAGE_SERVER_VERSION and forge_version == INITIAL_FORGE_VERSION + ) + ls_dirname = "solidity-lsp" if is_initial else f"solidity-lsp-{solidity_language_server_version}-{forge_version}" + return os.path.join(self._ls_resources_dir, ls_dirname) + def _create_launch_command(self, core_path: str) -> list[str]: return [core_path, "--stdio"] diff --git a/src/solidlsp/language_servers/taplo_server.py b/src/solidlsp/language_servers/taplo_server.py index 410edde1..ee9be952 100644 --- a/src/solidlsp/language_servers/taplo_server.py +++ b/src/solidlsp/language_servers/taplo_server.py @@ -24,15 +24,24 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) -# Taplo release version and download URLs -TAPLO_VERSION = "0.10.0" -TAPLO_DOWNLOAD_BASE = f"https://github.com/tamasfe/taplo/releases/download/{TAPLO_VERSION}" TAPLO_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") -# SHA256 checksums for Taplo releases (verified from official GitHub releases) -# Source: https://github.com/tamasfe/taplo/releases/tag/0.10.0 -# To update: download each release file and run: sha256sum -TAPLO_SHA256_CHECKSUMS: dict[str, str] = { +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +# To update: download each release file and run: sha256sum , copy values into DEFAULT_*. +INITIAL_TAPLO_VERSION = "0.10.0" +INITIAL_TAPLO_SHA256_CHECKSUMS: dict[str, str] = { + "taplo-windows-x86_64.zip": "1615eed140039bd58e7089109883b1c434de5d6de8f64a993e6e8c80ca57bdf9", + "taplo-windows-x86.zip": "b825701daab10dcfc0251e6d668cd1a9c0e351e7f6762dd20844c3f3f3553aa0", + "taplo-darwin-x86_64.gz": "898122cde3a0b1cd1cbc2d52d3624f23338218c91b5ddb71518236a4c2c10ef2", + "taplo-darwin-aarch64.gz": "713734314c3e71894b9e77513c5349835eefbd52908445a0d73b0c7dc469347d", + "taplo-linux-x86_64.gz": "8fe196b894ccf9072f98d4e1013a180306e17d244830b03986ee5e8eabeb6156", + "taplo-linux-aarch64.gz": "033681d01eec8376c3fd38fa3703c79316f5e14bb013d859943b60a07bccdcc3", + "taplo-linux-armv7.gz": "6b728896afe2573522f38b8e668b1ff40eb5928fd9d6d0c253ecae508274d417", +} +DEFAULT_TAPLO_VERSION = "0.10.0" +DEFAULT_TAPLO_SHA256_CHECKSUMS: dict[str, str] = { "taplo-windows-x86_64.zip": "1615eed140039bd58e7089109883b1c434de5d6de8f64a993e6e8c80ca57bdf9", "taplo-windows-x86.zip": "b825701daab10dcfc0251e6d668cd1a9c0e351e7f6762dd20844c3f3f3553aa0", "taplo-darwin-x86_64.gz": "898122cde3a0b1cd1cbc2d52d3624f23338218c91b5ddb71518236a4c2c10ef2", @@ -43,7 +52,15 @@ TAPLO_SHA256_CHECKSUMS: dict[str, str] = { } -def _get_taplo_download_url(version: str = TAPLO_VERSION) -> tuple[str, str]: +def _taplo_sha(version: str, archive_filename: str) -> str | None: + if version == INITIAL_TAPLO_VERSION: + return INITIAL_TAPLO_SHA256_CHECKSUMS.get(archive_filename) + if version == DEFAULT_TAPLO_VERSION: + return DEFAULT_TAPLO_SHA256_CHECKSUMS.get(archive_filename) + return None + + +def _get_taplo_download_url(version: str = DEFAULT_TAPLO_VERSION) -> tuple[str, str]: """ Get the appropriate Taplo download URL for the current platform. @@ -130,11 +147,11 @@ class TaploServer(SolidLanguageServer): log.info(f"Using system-installed Taplo at: {system_taplo}") return system_taplo - # Setup local installation directory - taplo_dir = os.path.join(self._ls_resources_dir, "taplo") + # Setup local installation directory; legacy unversioned dir reserved for INITIAL only + taplo_version = self._custom_settings.get("taplo_version", DEFAULT_TAPLO_VERSION) + ls_dirname = "taplo" if taplo_version == INITIAL_TAPLO_VERSION else f"taplo-{taplo_version}" + taplo_dir = os.path.join(self._ls_resources_dir, ls_dirname) os.makedirs(taplo_dir, exist_ok=True) - - taplo_version = self._custom_settings.get("taplo_version", TAPLO_VERSION) _, executable_name = _get_taplo_download_url(taplo_version) taplo_executable = os.path.join(taplo_dir, executable_name) @@ -158,12 +175,13 @@ class TaploServer(SolidLanguageServer): return [core_path, "lsp", "stdio"] @classmethod - def _download_taplo(cls, install_dir: str, executable_path: str, version: str = TAPLO_VERSION) -> None: + def _download_taplo(cls, install_dir: str, executable_path: str, version: str = DEFAULT_TAPLO_VERSION) -> None: """Download and extract Taplo binary using the shared verified download helper.""" download_url, _ = _get_taplo_download_url(version) archive_filename = os.path.basename(download_url) - expected_hash = TAPLO_SHA256_CHECKSUMS.get(archive_filename) if version == TAPLO_VERSION else None - if expected_hash is None and version == TAPLO_VERSION: + # only verify the SHA when the resolved version is one of our pinned ones (INITIAL or current DEFAULT) + expected_hash = _taplo_sha(version, archive_filename) + if expected_hash is None and version in (INITIAL_TAPLO_VERSION, DEFAULT_TAPLO_VERSION): raise RuntimeError(f"No SHA256 checksum configured for Taplo archive: {archive_filename}") try: diff --git a/src/solidlsp/language_servers/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls.py index ec26126d..c654fd95 100644 --- a/src/solidlsp/language_servers/terraform_ls.py +++ b/src/solidlsp/language_servers/terraform_ls.py @@ -18,6 +18,34 @@ log = logging.getLogger(__name__) TERRAFORM_LS_ALLOWED_HOSTS = ("releases.hashicorp.com",) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_TERRAFORM_LS_VERSION = "0.36.5" +INITIAL_TERRAFORM_LS_SHA256_BY_PLATFORM = { + "osx-arm64": "fee8743aa71fe2d8b0b9b91283b844cfa57d58457306a62e53a8f38d143cec8c", + "osx-x64": "17c5c480f8eec7e528292565f1c05d5097a41edf7ef8ee2a9f3a18d288a1415a", + "linux-arm64": "724f45029f32d02d88b1952c7d1526c59fc8cd5dae49e31b9fed676a83f6cae7", + "linux-x64": "37e645cc54fd03e863157e2a3e773e7a5ff1d6cb3d045e4c20860cac1f550a44", + "win-x64": "a9223462cac9e1c0e6ba33043fbf9fb4483609b6970b5681a6306b04366698ec", +} +DEFAULT_TERRAFORM_LS_VERSION = "0.36.5" +DEFAULT_TERRAFORM_LS_SHA256_BY_PLATFORM = { + "osx-arm64": "fee8743aa71fe2d8b0b9b91283b844cfa57d58457306a62e53a8f38d143cec8c", + "osx-x64": "17c5c480f8eec7e528292565f1c05d5097a41edf7ef8ee2a9f3a18d288a1415a", + "linux-arm64": "724f45029f32d02d88b1952c7d1526c59fc8cd5dae49e31b9fed676a83f6cae7", + "linux-x64": "37e645cc54fd03e863157e2a3e773e7a5ff1d6cb3d045e4c20860cac1f550a44", + "win-x64": "a9223462cac9e1c0e6ba33043fbf9fb4483609b6970b5681a6306b04366698ec", +} + + +def _terraform_ls_sha(version: str, platform_key: str) -> str | None: + if version == INITIAL_TERRAFORM_LS_VERSION: + return INITIAL_TERRAFORM_LS_SHA256_BY_PLATFORM[platform_key] + if version == DEFAULT_TERRAFORM_LS_VERSION: + return DEFAULT_TERRAFORM_LS_SHA256_BY_PLATFORM[platform_key] + return None + class TerraformLS(SolidLanguageServer): """ @@ -99,7 +127,7 @@ class TerraformLS(SolidLanguageServer): """ cls._ensure_tf_command_available() terraform_settings = solidlsp_settings.get_ls_specific_settings(Language.TERRAFORM) - terraform_ls_version = terraform_settings.get("terraform_ls_version", "0.36.5") + terraform_ls_version = terraform_settings.get("terraform_ls_version", DEFAULT_TERRAFORM_LS_VERSION) platform_id = PlatformUtils.get_platform_id() deps = RuntimeDependencyCollection( [ @@ -110,7 +138,7 @@ class TerraformLS(SolidLanguageServer): platform_id="osx-arm64", archive_type="zip", binary_name="terraform-ls", - sha256="fee8743aa71fe2d8b0b9b91283b844cfa57d58457306a62e53a8f38d143cec8c" if terraform_ls_version == "0.36.5" else None, + sha256=_terraform_ls_sha(terraform_ls_version, "osx-arm64"), allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), RuntimeDependency( @@ -120,7 +148,7 @@ class TerraformLS(SolidLanguageServer): platform_id="osx-x64", archive_type="zip", binary_name="terraform-ls", - sha256="17c5c480f8eec7e528292565f1c05d5097a41edf7ef8ee2a9f3a18d288a1415a" if terraform_ls_version == "0.36.5" else None, + sha256=_terraform_ls_sha(terraform_ls_version, "osx-x64"), allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), RuntimeDependency( @@ -130,7 +158,7 @@ class TerraformLS(SolidLanguageServer): platform_id="linux-arm64", archive_type="zip", binary_name="terraform-ls", - sha256="724f45029f32d02d88b1952c7d1526c59fc8cd5dae49e31b9fed676a83f6cae7" if terraform_ls_version == "0.36.5" else None, + sha256=_terraform_ls_sha(terraform_ls_version, "linux-arm64"), allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), RuntimeDependency( @@ -140,7 +168,7 @@ class TerraformLS(SolidLanguageServer): platform_id="linux-x64", archive_type="zip", binary_name="terraform-ls", - sha256="37e645cc54fd03e863157e2a3e773e7a5ff1d6cb3d045e4c20860cac1f550a44" if terraform_ls_version == "0.36.5" else None, + sha256=_terraform_ls_sha(terraform_ls_version, "linux-x64"), allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), RuntimeDependency( @@ -150,17 +178,23 @@ class TerraformLS(SolidLanguageServer): platform_id="win-x64", archive_type="zip", binary_name="terraform-ls.exe", - sha256="a9223462cac9e1c0e6ba33043fbf9fb4483609b6970b5681a6306b04366698ec" if terraform_ls_version == "0.36.5" else None, + sha256=_terraform_ls_sha(terraform_ls_version, "win-x64"), allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), ] ) dependency = deps.get_single_dep_for_current_platform() - terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings)) + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + install_dir = ( + cls.ls_resources_dir(solidlsp_settings) + if terraform_ls_version == INITIAL_TERRAFORM_LS_VERSION + else os.path.join(cls.ls_resources_dir(solidlsp_settings), f"terraform-ls-{terraform_ls_version}") + ) + terraform_ls_executable_path = deps.binary_path(install_dir) if not os.path.exists(terraform_ls_executable_path): log.info(f"Downloading terraform-ls from {dependency.url}") - deps.install(cls.ls_resources_dir(solidlsp_settings)) + deps.install(install_dir) assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}" diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index 6f3a443b..77f919d8 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -23,6 +23,14 @@ from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_in log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_TYPESCRIPT_VERSION = "5.9.3" +DEFAULT_TYPESCRIPT_VERSION = "5.9.3" +INITIAL_TYPESCRIPT_LANGUAGE_SERVER_VERSION = "5.1.3" +DEFAULT_TYPESCRIPT_LANGUAGE_SERVER_VERSION = "5.1.3" + # Platform-specific imports if os.name != "nt": # Unix-like systems import pwd @@ -154,8 +162,10 @@ class TypeScriptLanguageServer(SolidLanguageServer): # Get version settings from ls_specific_settings or use defaults language_specific_config = self._custom_settings - typescript_version = language_specific_config.get("typescript_version", "5.9.3") - typescript_language_server_version = language_specific_config.get("typescript_language_server_version", "5.1.3") + typescript_version = language_specific_config.get("typescript_version", DEFAULT_TYPESCRIPT_VERSION) + typescript_language_server_version = language_specific_config.get( + "typescript_language_server_version", DEFAULT_TYPESCRIPT_LANGUAGE_SERVER_VERSION + ) npm_registry = language_specific_config.get("npm_registry") deps = RuntimeDependencyCollection( @@ -181,38 +191,19 @@ class TypeScriptLanguageServer(SolidLanguageServer): is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - # Install typescript and typescript-language-server if not already installed or version mismatch - tsserver_ls_dir = os.path.join(self._ls_resources_dir, "ts-lsp") + # legacy unversioned dir reserved for INITIAL pair; any other version combination goes into a versioned subdir + is_initial = ( + typescript_version == INITIAL_TYPESCRIPT_VERSION + and typescript_language_server_version == INITIAL_TYPESCRIPT_LANGUAGE_SERVER_VERSION + ) + ls_dirname = "ts-lsp" if is_initial else f"ts-lsp-{typescript_version}-{typescript_language_server_version}" + tsserver_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname) tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") - # Check if installation is needed based on executable AND version - version_file = os.path.join(tsserver_ls_dir, ".installed_version") - expected_version = f"{typescript_version}_{typescript_language_server_version}" - - needs_install = False if not os.path.exists(tsserver_executable_path): - log.info(f"Typescript Language Server executable not found at {tsserver_executable_path}.") - needs_install = True - elif os.path.exists(version_file): - with open(version_file) as f: - installed_version = f.read().strip() - if installed_version != expected_version: - log.info( - f"TypeScript Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..." - ) - needs_install = True - else: - # No version file exists, assume old installation needs refresh - log.info("TypeScript Language Server version file not found. Reinstalling to ensure correct version...") - needs_install = True - - if needs_install: - log.info("Installing TypeScript Language Server dependencies...") + log.info(f"Typescript Language Server executable not found at {tsserver_executable_path}. Installing...") with LogTime("Installation of TypeScript language server dependencies", logger=log): deps.install(tsserver_ls_dir) - # Write version marker file - with open(version_file, "w") as f: - f.write(expected_version) log.info("TypeScript language server dependencies installed successfully") if not os.path.exists(tsserver_executable_path): diff --git a/src/solidlsp/language_servers/vts_language_server.py b/src/solidlsp/language_servers/vts_language_server.py index dc6db249..2464338a 100644 --- a/src/solidlsp/language_servers/vts_language_server.py +++ b/src/solidlsp/language_servers/vts_language_server.py @@ -24,6 +24,12 @@ from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_in log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_VTSLS_VERSION = "0.2.9" +DEFAULT_VTSLS_VERSION = "0.2.9" + class VtsLanguageServer(SolidLanguageServer): """ @@ -73,7 +79,7 @@ class VtsLanguageServer(SolidLanguageServer): ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for vtsls at the moment" vts_config = solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT_VTS) - vtsls_version = vts_config.get("vtsls_version", "0.2.9") + vtsls_version = vts_config.get("vtsls_version", DEFAULT_VTSLS_VERSION) npm_registry = vts_config.get("npm_registry") deps = RuntimeDependencyCollection( @@ -86,7 +92,9 @@ class VtsLanguageServer(SolidLanguageServer): ), ] ) - vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vts-lsp") + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + ls_dirname = "vts-lsp" if vtsls_version == INITIAL_VTSLS_VERSION else f"vts-lsp-{vtsls_version}" + vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), ls_dirname) vts_executable_path = os.path.join(vts_ls_dir, "vtsls") # Verify both node and npm are installed diff --git a/src/solidlsp/language_servers/yaml_language_server.py b/src/solidlsp/language_servers/yaml_language_server.py index 8a471921..b52c760e 100644 --- a/src/solidlsp/language_servers/yaml_language_server.py +++ b/src/solidlsp/language_servers/yaml_language_server.py @@ -17,6 +17,12 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +# Version pinning convention (see eclipse_jdtls.py for the full spec): +# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. +# DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +INITIAL_YAML_LANGUAGE_SERVER_VERSION = "1.19.2" +DEFAULT_YAML_LANGUAGE_SERVER_VERSION = "1.19.2" + class YamlLanguageServer(SolidLanguageServer): """ @@ -66,7 +72,7 @@ class YamlLanguageServer(SolidLanguageServer): assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - yaml_language_server_version = self._custom_settings.get("yaml_language_server_version", "1.19.2") + yaml_language_server_version = self._custom_settings.get("yaml_language_server_version", DEFAULT_YAML_LANGUAGE_SERVER_VERSION) npm_registry = self._custom_settings.get("npm_registry") deps = RuntimeDependencyCollection( @@ -80,8 +86,13 @@ class YamlLanguageServer(SolidLanguageServer): ] ) - # Install yaml-language-server if not already installed - yaml_ls_dir = os.path.join(self._ls_resources_dir, "yaml-lsp") + # legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir + ls_dirname = ( + "yaml-lsp" + if yaml_language_server_version == INITIAL_YAML_LANGUAGE_SERVER_VERSION + else f"yaml-lsp-{yaml_language_server_version}" + ) + yaml_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname) yaml_executable_path = os.path.join(yaml_ls_dir, "node_modules", ".bin", "yaml-language-server") # Handle Windows executable extension