From f00a992e5dae56f815637b5208185fa10de96480 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 25 Mar 2026 15:37:26 +0100 Subject: [PATCH] Security enhancement: pinned all runtime deps, verify checksums Extended documentation of ls_specific_settings and of our security approach --- docs/02-usage/050_configuration.md | 432 +++++++++++++++++- docs/02-usage/070_security.md | 155 ++++++- .../language_servers/al_language_server.py | 80 ++-- .../ansible_language_server.py | 10 +- .../language_servers/bash_language_server.py | 6 +- .../clangd_language_server.py | 35 +- src/solidlsp/language_servers/clojure_lsp.py | 112 +++-- src/solidlsp/language_servers/common.py | 27 +- .../csharp_language_server.py | 92 ++-- .../language_servers/dart_language_server.py | 30 +- .../language_servers/eclipse_jdtls.py | 119 +++-- .../elixir_tools/elixir_tools.py | 57 ++- .../language_servers/elm_language_server.py | 9 +- .../fsharp_language_server.py | 14 +- .../groovy_language_server.py | 54 ++- .../language_servers/hlsl_language_server.py | 24 +- src/solidlsp/language_servers/intelephense.py | 6 +- .../kotlin_language_server.py | 19 +- src/solidlsp/language_servers/lua_ls.py | 83 ++-- src/solidlsp/language_servers/luau_lsp.py | 43 +- src/solidlsp/language_servers/marksman.py | 104 +++-- .../matlab_language_server.py | 67 +-- src/solidlsp/language_servers/omnisharp.py | 42 +- .../omnisharp/runtime_dependencies.json | 29 +- .../language_servers/pascal_server.py | 59 ++- src/solidlsp/language_servers/phpactor.py | 16 +- .../powershell_language_server.py | 38 +- src/solidlsp/language_servers/ruby_lsp.py | 33 +- .../solidity_language_server.py | 10 +- .../language_servers/systemverilog_server.py | 16 + src/solidlsp/language_servers/taplo_server.py | 99 ++-- src/solidlsp/language_servers/terraform_ls.py | 30 +- .../typescript_language_server.py | 7 +- .../language_servers/vts_language_server.py | 9 +- .../language_servers/vue_language_server.py | 15 +- .../language_servers/yaml_language_server.py | 6 +- src/solidlsp/ls_utils.py | 190 +++++++- .../csharp/test_csharp_nuget_download.py | 62 ++- .../luau/test_luau_dependency_provider.py | 57 +-- test/solidlsp/util/test_ls_utils.py | 42 ++ 40 files changed, 1724 insertions(+), 614 deletions(-) create mode 100644 test/solidlsp/util/test_ls_utils.py diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 4f52f1ec..3d9b41ff 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -226,16 +226,10 @@ ls_specific_settings: # language-server-specific keys ``` -:::{attention} -Most settings are currently undocumented. Please refer to the -[source code of the respective language server](https://github.com/oraios/serena/tree/main/src/solidlsp/language_servers) -implementation to determine supported settings. -::: - (override-ls-path)= #### Overriding the Language Server Path -Some language servers, particularly those that use a single core path for the language server (e.g. the main executable), +Most of Serena's language servers, particularly those that use a single core path for the language server (e.g. the main executable), support overriding that path via the `ls_path` setting. Therefore, if you have installed the language server yourself and want to use your installation instead of Serena's managed installation, you can set the `ls_path` setting as follows: @@ -246,9 +240,87 @@ ls_specific_settings: ls_path: "/path/to/language-server" ``` -This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`. -Currently, this includes the following languages: `bash`, `clojure`, `cpp`, `kotlin`, `markdown`, `php`, `php_phpactor`, `python`, `rust`, `toml`, `typescript`, `yaml`. -We will add support for more languages over time. +This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`, +and by some additional wrappers that explicitly expose `ls_path`. +Common examples include: `ansible`, `bash`, `clojure`, `cpp`, `cpp_ccls`, `hlsl`, `kotlin`, `lean4`, `luau`, `markdown`, `php`, +`php_phpactor`, `python`, `rust`, `solidity`, `systemverilog`, `toml`, `typescript`, and `yaml`. + +If a language server supports `ls_path`, setting it bypasses Serena's managed download or install for that server. +In that case, any server-specific version or registry settings only apply when `ls_path` is not set. + +#### AL + +Serena uses the AL language server bundled in the Microsoft Dynamics 365 Business Central VS Code extension. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `al_extension_version` | `18.0.2242655` | Override the AL VS Code extension version Serena downloads from the VS Code Marketplace. | + +#### Ansible + +Serena uses `@ansible/ansible-language-server` for the `ansible` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed install | Override the `ansible-language-server` executable path. | +| `ansible_language_server_version` | `1.2.3` | Override the npm package version Serena installs when `ls_path` is not set. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | +| `ansible_path` | `"ansible"` | Path to the `ansible` executable forwarded to the language server. | +| `ansible_settings` | `null` | Full Ansible LS settings dict, deep-merged on top of Serena's defaults. | +| `lint_enabled` | `false` | Enable `ansible-lint` integration. | +| `lint_path` | `"ansible-lint"` | Path to the `ansible-lint` executable. | +| `python_interpreter_path` | `"python3"` | Python interpreter path forwarded to the language server. | +| `python_activation_script` | `""` | Virtualenv activation script forwarded to the language server. | + +#### Bash + +Serena uses `bash-language-server` for Bash support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed install | Override the `bash-language-server` executable path. | +| `bash_language_server_version` | `5.6.0` | Override the npm package version Serena installs when `ls_path` is not set. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | + +#### Clojure + +Serena uses `clojure-lsp` for Clojure support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed download | Override the `clojure-lsp` executable path. | +| `clojure_lsp_version` | `2026.02.20-16.08.58` | Override the `clojure-lsp` release version Serena downloads when `ls_path` is not set. | + +#### C/C++ (`clangd`) + +Serena uses `clangd` for the `cpp` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed download | Override the `clangd` executable path. | +| `compile_commands_dir` | `.serena` | Directory where Serena writes a transformed `compile_commands.json` if the project's original database uses relative `directory` entries. | +| `clangd_version` | `19.1.2` | Override the `clangd` version Serena downloads when `ls_path` is not set. | + +#### C/C++ via `ccls` + +Serena uses the `cpp_ccls` language key for `ccls`. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | `ccls` from PATH | Override the `ccls` executable path. Serena does not manage `ccls` downloads or installs. | + #### C# (Roslyn Language Server) @@ -266,12 +338,14 @@ Automatic download is supported for: Windows (x64, ARM64), macOS (x64, ARM64), L **Configuration:** The `runtime_dependencies` setting allows you to override the download URLs for the Roslyn Language Server. This is useful if you need to use a private package mirror or a specific version. +For the common case of changing only the package version, use `csharp_language_server_version`. Example configuration to override the language server download URL: ```yaml ls_specific_settings: csharp: + csharp_language_server_version: "5.5.0-2.26078.4" runtime_dependencies: - id: "CSharpLanguageServer" platform_id: "linux-x64" # or win-x64, win-arm64, osx-x64, osx-arm64, linux-arm64 @@ -294,6 +368,59 @@ Notes: - The language server package is a `.nupkg` file (ZIP format) downloaded from NuGet.org by default. - If you have .NET 10+ already installed, Serena will use your system installation. +#### C# (`OmniSharp`) + +Serena uses the `csharp_omnisharp` language key for OmniSharp. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `omnisharp_version` | `1.39.10` | Override the OmniSharp version Serena downloads. | +| `razor_omnisharp_version` | `7.0.0-preview.23363.1` | Override the Razor OmniSharp plugin version Serena downloads. | + +#### Dart + +Serena uses the Dart SDK's built-in language server for Dart support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `dart_sdk_version` | `3.7.1` | Override the Dart SDK version Serena downloads. | + +#### Elixir + +Serena uses [Expert](https://github.com/elixir-lang/expert) for Elixir support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `expert_version` | `v0.1.0-rc.6` | Override the Expert version Serena downloads when it does not use an `expert` executable already found in PATH. | + +#### Elm + +Serena uses `@elm-tooling/elm-language-server` for Elm support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `elm_language_server_version` | `2.8.0` | Override the npm package version Serena installs when no system `elm-language-server` is found. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | + +#### F# + +Serena uses FsAutoComplete (Ionide LSP) for F# support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `fsautocomplete_version` | `0.83.0` | Override the FsAutoComplete version Serena installs as a .NET tool. | + + #### Go (`gopls`) Serena forwards `ls_specific_settings.go.gopls_settings` to `gopls` as LSP `initializationOptions` when the Go language server is started. @@ -318,6 +445,45 @@ Notes: - `GOFLAGS` (from the environment you start Serena in) may also affect the Go build context. Prefer `buildFlags` for tags. - Build context changes are only picked up when `gopls` starts. After changing `gopls_settings` (or relevant env vars like `GOFLAGS`), restart the Serena process (or server) that hosts the Go language server, or use your client's "Restart language server" action if it causes `gopls` to restart. +#### Groovy + +Serena uses a user-provided Groovy Language Server JAR for Groovy support. If `ls_java_home_path` is not set, Serena downloads +a bundled Java runtime for launching that JAR. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_jar_path` | required | Path to the Groovy Language Server JAR | +| `ls_java_home_path` | `null` | Path to a Java installation to use instead of Serena's managed runtime | +| `ls_jar_options` | `""` | Additional options passed when launching the Groovy LS JAR | +| `vscode_java_version` | `1.42.0-561` | Override the bundled Java runtime bundle version Serena downloads by default | + +Note: +- When overriding `vscode_java_version`, Serena still assumes that the downloaded runtime bundle keeps the same internal + directory layout and file names as the bundled default version. + +Example: + +```yaml +ls_specific_settings: + groovy: + ls_jar_path: "/path/to/groovy-language-server-all.jar" + vscode_java_version: "1.42.0-561" +``` + +#### HLSL + +Serena uses `shader-language-server` for the `hlsl` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed install or build | Override the `shader-language-server` executable path. | +| `version` | `1.3.0` | Override the bundled version Serena downloads, or builds from source on macOS, when `ls_path` is not set. | + + #### Java (`eclipse.jdt.ls`) The following settings are supported for the Java language server: @@ -329,6 +495,13 @@ The following settings are supported for the Java language server: | `gradle_wrapper_enabled` | `false` | Use the project's Gradle wrapper (`gradlew`) instead of the bundled Gradle distribution. Enable this for projects with custom plugins or repositories. | | `gradle_java_home` | `null` | Path to the JDK used by Gradle. When unset, Gradle uses the bundled JRE. | | `use_system_java_home` | `false` | Use the system's `JAVA_HOME` environment variable for JDTLS itself. Enable this if your project requires a specific JDK vendor or version for Gradle's JDK checks. | +| `gradle_version` | `8.14.2` | Override the Gradle distribution version Serena downloads by default. | +| `vscode_java_version` | `1.42.0-561` | Override the bundled `vscode-java` runtime bundle version Serena downloads by default. | +| `intellicode_version` | `1.2.30` | Override the IntelliCode VSIX version Serena downloads by default. | + +Note: +- When overriding `vscode_java_version`, Serena still assumes that the downloaded runtime bundle keeps the same internal + directory layout and file names as the bundled default version. Example for a project with custom Gradle plugins and JDK requirements: @@ -343,21 +516,44 @@ ls_specific_settings: Serena uses [JetBrains' Kotlin Language Server](https://github.com/Kotlin/kotlin-lsp) for Kotlin support. -**Runtime Requirements:** +Supported settings: -- Java 21 or higher is required. If not found, Serena automatically downloads an appropriate JRE. -- The Kotlin Language Server is automatically downloaded from JetBrains' CDN. +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed download | Override the Kotlin Language Server executable path. | +| `kotlin_lsp_version` | `261.13587.0` | Override the Kotlin Language Server version Serena downloads when `ls_path` is not set. | +| `jvm_options` | `-Xmx2G` | Value assigned to `JAVA_TOOL_OPTIONS` for the Kotlin LS process. Set to `""` to disable JVM options entirely. | -**Configuration:** +Example: ```yaml ls_specific_settings: kotlin: - ls_path: "/path/to/kotlin-lsp.sh" # Override the Kotlin Language Server executable - kotlin_lsp_version: "261.13587.0" # Override the Kotlin Language Server version - jvm_options: "-Xmx4G -XX:+UseG1GC" # JVM options (default: -Xmx2G). Set to "" to disable. + kotlin_lsp_version: "261.13587.0" + jvm_options: "-Xmx4G -XX:+UseG1GC" ``` +#### Lean 4 + +Serena uses `lean --server` for Lean 4 support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | `lean` from PATH | Override the `lean` executable path. Serena does not manage Lean downloads. | + +#### Lua + +Serena uses `lua-language-server` for Lua support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `lua_language_server_version` | `3.15.0` | Override the bundled `lua-language-server` version Serena downloads when it cannot use an existing installation from PATH or common install locations. | + + #### Luau Serena uses [`luau-lsp`](https://github.com/JohnnyMorganz/luau-lsp) for Luau support. @@ -373,6 +569,7 @@ Serena uses [`luau-lsp`](https://github.com/JohnnyMorganz/luau-lsp) for Luau sup ls_specific_settings: luau: ls_path: "/path/to/luau-lsp" # Optional: override the language server executable + luau_lsp_version: "1.63.0" # Optional: override the bundled luau-lsp version platform: "roblox" # "roblox" (default) or "standard" roblox_security_level: "PluginSecurity" # Roblox only: None, PluginSecurity, LocalUserSecurity, RobloxScriptSecurity ``` @@ -381,6 +578,29 @@ Notes: - In `roblox` mode, Serena downloads Roblox definitions and Roblox API docs and passes them to `luau-lsp`. - In `standard` mode, Serena skips Roblox definitions and only downloads the standard Luau docs bundle. +#### Markdown + +Serena uses [Marksman](https://github.com/artempyanykh/marksman) for the `markdown` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed download | Override the `marksman` executable path. | +| `marksman_version` | `2024-12-18` | Override the Marksman release tag Serena downloads when `ls_path` is not set. | + +#### MATLAB + +Serena uses the official MathWorks MATLAB language server from the VS Code extension. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `matlab_path` | auto-detected | Path to the MATLAB installation. This overrides `MATLAB_PATH` and auto-detection, but not Serena's managed extension download. | +| `matlab_extension_version` | `1.3.9` | Override the MathWorks VS Code extension version Serena downloads. | + + #### Pascal (`pasls`) Serena uses [pasls](https://github.com/genericptr/pascal-language-server) (Pascal Language Server) for Pascal/Free Pascal support. @@ -405,6 +625,7 @@ Configure pasls via `ls_specific_settings.pascal` in `serena_config.yml`: | Setting | Description | | ---------------- | --------------------------------------------------------------------------- | +| `pasls_version` | Override the pinned pasls version Serena downloads by default | | `pp` | Path to FPC compiler driver (must be `fpc` or `fpc.exe`, not `ppc386.exe`) | | `fpcdir` | Path to FPC source directory | | `lazarusdir` | Path to Lazarus directory (required for LCL projects) | @@ -426,6 +647,183 @@ Notes: - Use the FPC compiler driver (`fpc`/`fpc.exe`), not backend compilers like `ppc386.exe`. - These settings are passed as environment variables to the pasls process. +#### PHP (`Intelephense`) + +Serena uses Intelephense for the `php` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed install | Override the `intelephense` executable path. | +| `intelephense_version` | `1.14.4` | Override the npm package version Serena installs when `ls_path` is not set. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | +| `ignore_vendor` | `true` | Ignore directories named `vendor` while indexing the project. | +| `maxFileSize` | unset | Forwarded as `intelephense.files.maxSize` in `initializationOptions`. | +| `maxMemory` | unset | Forwarded as `intelephense.maxMemory` in `initializationOptions`. | + +#### PHP (`Phpactor`) + +Serena uses the `php_phpactor` language key for Phpactor. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed download | Override the Phpactor PHAR path. | +| `phpactor_version` | `2025.12.21.1` | Override the Phpactor PHAR version Serena downloads when `ls_path` is not set. | +| `ignore_vendor` | `true` | Ignore directories named `vendor` while indexing the project. | + +#### PowerShell + +Serena uses PowerShell Editor Services for PowerShell support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `pses_version` | `4.4.0` | Override the PowerShell Editor Services version Serena downloads. Serena still requires `pwsh` to be available locally. | + +#### Python + +Serena uses Pyright for the `python` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | current Python executable | Override the Python interpreter Serena uses to run `-m pyright.langserver`. | + +Note: +- There is currently no separate `python_ty` language key in Serena's current SolidLSP implementation. + +#### Ruby + +Serena uses Shopify's `ruby-lsp` for Ruby support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ruby_lsp_version` | `0.26.8` | Override the `ruby-lsp` gem version Serena installs when no project-local or global `ruby-lsp` is already available. | + +#### Rust + +Serena uses `rust-analyzer` for Rust support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | auto-detected | Override the `rust-analyzer` executable path. Without `ls_path`, Serena prefers `rustup which rust-analyzer`, then `rustup component add rust-analyzer`, then PATH/common install locations. | + +#### Scala + +Serena uses Metals for Scala support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `metals_version` | `1.6.4` | Override the Metals version Serena bootstraps. | +| `client_name` | `Serena` | Client identifier sent to Metals. | +| `on_stale_lock` | `auto-clean` | How Serena handles stale Metals H2 database locks. Supported values: `auto-clean`, `warn`, `fail`. | +| `log_multi_instance_notice` | `true` | Log a notice when another Metals instance is detected. | + +#### Solidity + +Serena uses `@nomicfoundation/solidity-language-server` for Solidity support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed install | Override the Solidity language server executable path. | +| `solidity_language_server_version` | `0.8.4` | Override the npm package version Serena installs when `ls_path` is not set. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | + +#### SystemVerilog + +Serena uses `verible-verilog-ls` for SystemVerilog support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | system PATH or managed download | Override the `verible-verilog-ls` executable path. | +| `verible_version` | `v0.0-4051-g9fdb4057` | Override the Verible release Serena downloads when `ls_path` is not set and no system installation is found. | + +#### Terraform + +Serena uses `terraform-ls` for Terraform support. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `terraform_ls_version` | `0.36.5` | Override the `terraform-ls` version Serena downloads. Terraform itself must still be installed and available in PATH. | + +#### TOML + +Serena uses [Taplo](https://github.com/tamasfe/taplo) for the `toml` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed download | Override the `taplo` executable path. | +| `taplo_version` | `0.10.0` | Override the Taplo version Serena downloads when `ls_path` is not set. | + +#### TypeScript + +Serena uses `typescript-language-server` for the `typescript` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed install | Override the `typescript-language-server` executable path. | +| `typescript_version` | `5.9.3` | Override the bundled `typescript` npm package version Serena installs when `ls_path` is not set. | +| `typescript_language_server_version` | `5.1.3` | Override the bundled `typescript-language-server` npm package version Serena installs when `ls_path` is not set. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | + +#### TypeScript via `vtsls` + +The actual configuration key for vtsls is `typescript_vts`, not `vts`. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `vtsls_version` | `0.2.9` | Override the `@vtsls/language-server` npm package version Serena installs. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | + +#### Vue + +Serena uses `@vue/language-server` (Volar) for the `vue` language key, together with a companion TypeScript language server. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `vue_language_server_version` | `3.1.5` | Override the bundled `@vue/language-server` npm package version Serena installs. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. If unset on `vue`, Serena falls back to `ls_specific_settings.typescript.npm_registry`. | + +Notes: +- `typescript_version` and `typescript_language_server_version` are read from `ls_specific_settings.typescript`, not from `ls_specific_settings.vue`. + +#### YAML + +Serena uses `yaml-language-server` for the `yaml` language key. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed install | Override the `yaml-language-server` executable path. | +| `yaml_language_server_version` | `1.19.2` | Override the npm package version Serena installs when `ls_path` is not set. | +| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | + ### Custom Prompts All of Serena's prompts can be fully customized. diff --git a/docs/02-usage/070_security.md b/docs/02-usage/070_security.md index 12faabdf..f1366e17 100644 --- a/docs/02-usage/070_security.md +++ b/docs/02-usage/070_security.md @@ -1,12 +1,149 @@ # Security Considerations -As fundamental abilities for a coding agent, Serena contains tools for executing shell commands and modifying files. -Therefore, if the respective tool calls are not monitored or restricted (and execution takes place in a sensitive environment), -there is a risk of unintended consequences. +Security and protection against supply chain attacks are important to us, and we take this topic seriously. -Therefore, to reduce the risk of unintended consequences when using Serena, it is recommended to - * back up your work regularly (e.g. use a version control system like Git), - * monitor tool executions carefully (e.g. via your MCP client, provided that it supports it), - * consider enabling read-only mode for your project (set `read_only: True` in project.yml) if you only want to analyze code without modifying it, - * restrict the set of allowed tools via the [configuration](050_configuration), - * use a sandboxed environment for running Serena (e.g. by [using Docker](docker)). +Serena comes in two main variants with different security characteristics: + +- the **JetBrains-based variant**, which integrates with a running JetBrains IDE, and +- the **language-server-based variant** (the free variant), which can automatically acquire language-server dependencies on demand. + +## JetBrains Variant + +The JetBrains variant of Serena is safe by default. + +At runtime, Serena does **not** download additional components and does **not** start extra helper processes beyond the Serena MCP server itself. It talks to the already running JetBrains IDE and relies on the IDE's own indexing and language intelligence. + +This means that, from a supply-chain perspective, the JetBrains variant has a much smaller runtime attack surface than the language-server-based variant. + +## Language-Server Variant + +The language-server-based variant supports many languages, including languages whose language servers are not typically preinstalled on a machine. For convenience, Serena can therefore download or install certain language-server dependencies on demand. + +We treat this path as security-sensitive and have hardened it accordingly. + +### How Serena Secures Downloaded Language-Server Dependencies + +For language servers that download archives, binaries, VSIX packages, NuGet packages, or other release artifacts, Serena uses a hardened shared download path with the following protections: + +- **Pinned versions by default**: default downloads use exact versions instead of floating `latest` or nightly channels. +- **Integrity verification**: downloaded artifacts are checked against pinned SHA256 hashes stored in Serena's source code. +- **Host allowlists**: download URLs are restricted to the expected hosts for a given dependency. +- **Safe extraction**: archive extraction validates paths to prevent path traversal and zip-slip style attacks. +- **Managed install locations**: dependencies are installed into Serena-managed directories instead of into the project repository. + +In practice, this means that a downloaded artifact must match all of the following: + +- the expected version, +- the expected host, +- the expected SHA256 checksum, +- and the expected extraction layout. + +If any of these checks fail, Serena aborts the installation instead of continuing. + +### npm-Based Language Servers + +Some language servers are distributed primarily through npm. For those, Serena currently uses pinned package versions and installs them into Serena-managed directories. + +By default, Serena uses the **user's normal npm configuration**. We do **not** force a registry override unless one is explicitly configured. If needed, both the package version and the registry can be overridden through `ls_specific_settings`. + +For npm-based installs, Serena's current security posture is based on these rules: + +- **Exact package versions are pinned by default**. +- **The install location is isolated from the project** and lives in Serena-managed language-server directories. +- **The user's npm configuration is trusted by default**. +- **Repository and user configuration are assumed to be trusted**. + +This means Serena protects well against accidental version drift, but npm installs still rely on the npm ecosystem and package-manager execution model. In particular, Serena does **not** currently use lockfile-based `npm ci` installs for bundled language-server dependencies. + +### `uvx` and Python Dependency Pinning + +Some parts of Serena rely on `uv` / `uvx`. + +One important detail is that `uvx` ignores the lockfile when installing directly from a Git repository. Because of that, we pin Serena's Python dependencies exactly in `pyproject.toml` so that installations from Git still resolve to exact dependency versions rather than floating ranges. + +For the `ty` Python language server, Serena also uses an exact pinned version when invoking it through `uvx`. + +### Our Assumptions + +The current security model for Serena's language-server variant assumes: + +- the local machine is trusted, +- the checked-out repository is trusted, +- user configuration is trusted, +- package-manager configuration such as npm config is trusted unless explicitly overridden, +- and the main risk to defend against is compromised or unexpectedly changing upstream artifacts. + +Under these assumptions, the most important supply-chain protections are: + +- exact version pinning, +- hash verification, +- host restriction, +- and isolated Serena-managed installation directories. + +## Operational Recommendations + +As fundamental abilities for a coding agent, Serena contains tools for executing shell commands and modifying files. Therefore, if the respective tool calls are not monitored or restricted, and execution takes place in a sensitive environment, there is a risk of unintended consequences. + +To reduce that risk, we recommend that you: + +- back up your work regularly, for example by using Git, +- monitor tool executions carefully, if your MCP client supports this, +- consider enabling read-only mode for analysis-only sessions by setting `read_only: True` in `project.yml`, +- restrict the set of allowed tools via the [configuration](050_configuration), +- and use a sandboxed environment for running Serena, for example by [using Docker](docker). + +```{dropdown} What Serena Downloads by Default for Language Servers +:open: + +Only the language servers listed below download or install additional dependencies automatically by default when the required dependency is missing. Everything else either relies on a system-installed server or on tooling you install separately. + +### Release Artifacts, Archives, or VSIX Packages + +- **AL**: the pinned Microsoft AL VS Code extension (`ms-dynamics-smb.al`) from the VS Code Marketplace. +- **C/C++ (`clangd`)**: pinned `clangd` release archives on supported platforms. +- **C# (Roslyn LS)**: pinned Roslyn language-server NuGet package for the current platform. +- **Clojure**: pinned `clojure-lsp` release artifact. +- **Dart**: pinned Dart SDK archive that contains the language server. +- **Elixir (`expert`)**: pinned Expert release binary, if not already available locally. +- **Groovy**: pinned `vscode-java` runtime bundle used to provide Java for the Groovy LS setup. +- **HLSL / shader-language-server**: pinned GitHub release artifacts on supported prebuilt platforms. +- **Java (`eclipse.jdt.ls`)**: pinned Gradle distribution, pinned `vscode-java` extension bundle, and pinned IntelliCode VSIX. +- **Kotlin**: pinned Kotlin LSP archive. +- **Lua**: pinned `lua-language-server` release archive. +- **Luau**: pinned `luau-lsp` release archive. In Roblox or standard-doc modes it may also download Luau/Roblox docs and type-definition files. +- **Markdown (`marksman`)**: pinned Marksman release binary. +- **MATLAB**: the pinned MathWorks MATLAB VS Code extension from the VS Code Marketplace. +- **OmniSharp (legacy C# backend)**: pinned OmniSharp and Razor plugin archives. +- **Pascal**: pinned Pascal language-server release artifact. +- **PHP (`phpactor`)**: pinned `phpactor.phar`. +- **PowerShell**: pinned PowerShell Editor Services archive. +- **SystemVerilog (`verible`)**: pinned Verible release archive on supported platforms. +- **TOML (`taplo`)**: pinned Taplo release artifact. +- **Terraform**: pinned `terraform-ls` release archive. The Terraform CLI itself must still already be installed. + +### npm Package Installs + +- **Ansible**: `@ansible/ansible-language-server` +- **Bash**: `bash-language-server` +- **Elm**: `@elm-tooling/elm-language-server` +- **PHP (`intelephense`)**: `intelephense` +- **Solidity**: `@nomicfoundation/solidity-language-server` +- **TypeScript**: `typescript` and `typescript-language-server` +- **Vue**: `@vue/language-server`, plus `typescript` and `typescript-language-server` +- **VTSLS**: `@vtsls/language-server` +- **YAML**: `yaml-language-server` + +All of the above are installed with exact pinned package versions by default, into Serena-managed directories. + +### Other Package-Manager Based Installs + +- **F#**: installs pinned `fsautocomplete` via `dotnet tool install`. +- **Ruby (`ruby-lsp`)**: if not already available through Bundler or as a global executable, Serena installs a pinned `ruby-lsp` gem. +- **Python (`ty`)**: launched through `uvx` / `uv x` using an exact pinned `ty` version. +- **HLSL on macOS**: if no prebuilt binary is used, Serena builds `shader_language_server` from a pinned version using Cargo. + +### No Automatic Download by Serena + +- **Python (`pyright`)**: Serena uses the locally available Python environment and starts `pyright.langserver` from there. +- **Go (`gopls`)**, **Rust (`rust-analyzer`)**, and several other system-tool based integrations expect the language server to be available locally and do not download it automatically. +``` diff --git a/src/solidlsp/language_servers/al_language_server.py b/src/solidlsp/language_servers/al_language_server.py index e63b1ada..3e3c4584 100644 --- a/src/solidlsp/language_servers/al_language_server.py +++ b/src/solidlsp/language_servers/al_language_server.py @@ -1,4 +1,9 @@ -"""AL Language Server implementation for Microsoft Dynamics 365 Business Central.""" +"""AL Language Server implementation for Microsoft Dynamics 365 Business Central. + +You can pass the following entries in ``ls_specific_settings["al"]``: + - al_extension_version: Override the pinned AL VS Code extension version + downloaded by Serena (default: the bundled Serena version). +""" import logging import os @@ -7,23 +12,29 @@ import platform import re import stat import time -import zipfile from pathlib import Path -import requests from overrides import override from solidlsp import ls_types from solidlsp.language_servers.common import quote_windows_path from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation +from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo 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" +AL_EXTENSION_ALLOWED_HOSTS = ("marketplace.visualstudio.com",) + class ALLanguageServer(SolidLanguageServer): """ @@ -38,6 +49,7 @@ class ALLanguageServer(SolidLanguageServer): - Special initialization sequence required by AL Language Server - Custom AL-specific LSP commands (al/gotodefinition, al/setActiveWorkspace) - File opening requirement before symbol retrieval + - `al_extension_version` to override the bundled AL VS Code extension version """ # Regex pattern to match AL object names like: @@ -141,46 +153,14 @@ class ALLanguageServer(SolidLanguageServer): """ try: log.info(f"Downloading AL extension from {url}") - - # Create target directory for the extension os.makedirs(target_dir, exist_ok=True) - - # Download with proper headers to mimic VS Code marketplace client - # These headers are required for the marketplace to serve the VSIX file - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - "Accept": "application/octet-stream, application/vsix, */*", - } - - response = requests.get(url, headers=headers, stream=True, timeout=300) - response.raise_for_status() - - # Save to temporary VSIX file (will be deleted after extraction) - temp_file = os.path.join(target_dir, "al_extension_temp.vsix") - total_size = int(response.headers.get("content-length", 0)) - - log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...") - - with open(temp_file, "wb") as f: - downloaded = 0 - for chunk in response.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - downloaded += len(chunk) - if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0: # Log progress every 10MB - progress = (downloaded / total_size) * 100 - log.info(f"Download progress: {progress:.1f}%") - - log.info("Download complete, extracting...") - - # Extract VSIX file (VSIX files are just ZIP archives with a different extension) - # This will extract the extension folder containing the language server binaries - with zipfile.ZipFile(temp_file, "r") as zip_ref: - zip_ref.extractall(target_dir) - - # Clean up temp file - os.remove(temp_file) - + FileUtils.download_and_extract_archive_verified( + url, + target_dir, + "zip", + expected_sha256=AL_EXTENSION_SHA256 if url == AL_EXTENSION_URL else None, + allowed_hosts=AL_EXTENSION_ALLOWED_HOSTS, + ) log.info("AL extension extracted successfully") return True @@ -276,14 +256,16 @@ class ALLanguageServer(SolidLanguageServer): """ 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_url = ( + "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/" + f"vsextensions/al/{al_extension_version}/vspackage" + ) - # AL extension version - using latest stable version - AL_VERSION = "latest" - url = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/vsextensions/al/{AL_VERSION}/vspackage" + log.info(f"Downloading AL extension from: {al_extension_url}") - log.info(f"Downloading AL extension from: {url}") - - if cls._download_al_extension(url, al_extension_dir): + if cls._download_al_extension(al_extension_url, al_extension_dir): 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 7c2f88a1..2b3da993 100644 --- a/src/solidlsp/language_servers/ansible_language_server.py +++ b/src/solidlsp/language_servers/ansible_language_server.py @@ -12,7 +12,7 @@ from typing import Any, ClassVar from overrides import override -from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection +from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -162,13 +162,19 @@ 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") + npm_registry = self._custom_settings.get("npm_registry") deps = RuntimeDependencyCollection( [ RuntimeDependency( id="ansible-language-server", description="Ansible Language Server (@ansible/ansible-language-server)", - command="npm install --prefix ./ @ansible/ansible-language-server@1.2.3", + command=build_npm_install_command( + "@ansible/ansible-language-server", + ansible_language_server_version, + npm_registry, + ), platform_id="any", ), ] diff --git a/src/solidlsp/language_servers/bash_language_server.py b/src/solidlsp/language_servers/bash_language_server.py index b868cebb..c618884b 100644 --- a/src/solidlsp/language_servers/bash_language_server.py +++ b/src/solidlsp/language_servers/bash_language_server.py @@ -9,7 +9,7 @@ import pathlib import shutil import threading -from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection +from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command from solidlsp.ls import ( DocumentSymbols, LanguageServerDependencyProvider, @@ -58,13 +58,15 @@ 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") + npm_registry = self._custom_settings.get("npm_registry") deps = RuntimeDependencyCollection( [ RuntimeDependency( id="bash-language-server", description="bash-language-server package", - command="npm install --prefix ./ bash-language-server@5.6.0", + command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry), platform_id="any", ), ] diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index 37aa6d70..c5488810 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -14,12 +14,20 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) +CLANGD_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") + class ClangdLanguageServer(SolidLanguageServer): """ Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly. Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example. + + You can pass the following entries in ``ls_specific_settings["cpp"]``: + - compile_commands_dir: Directory where Serena writes its transformed + ``compile_commands.json`` if needed. + - clangd_version: Override the pinned Clangd version downloaded by Serena + (default: the bundled Serena version). """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): @@ -125,39 +133,50 @@ class ClangdLanguageServer(SolidLanguageServer): """ import shutil + clangd_version = self._custom_settings.get("clangd_version", "19.1.2") + default_version = clangd_version == "19.1.2" + deps = RuntimeDependencyCollection( [ RuntimeDependency( id="Clangd", description="Clangd for Linux (x64)", - url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", + url=f"https://github.com/clangd/clangd/releases/download/{clangd_version}/clangd-linux-{clangd_version}.zip", platform_id="linux-x64", archive_type="zip", - binary_name="clangd_19.1.2/bin/clangd", + binary_name=f"clangd_{clangd_version}/bin/clangd", + sha256="7c09614eff857d590e4502ef516f035ff94cfb8b795de14ece5afbc53a206caf" if default_version else None, + allowed_hosts=CLANGD_ALLOWED_HOSTS, ), RuntimeDependency( id="Clangd", description="Clangd for Windows (x64)", - url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", + url=f"https://github.com/clangd/clangd/releases/download/{clangd_version}/clangd-windows-{clangd_version}.zip", platform_id="win-x64", archive_type="zip", - binary_name="clangd_19.1.2/bin/clangd.exe", + binary_name=f"clangd_{clangd_version}/bin/clangd.exe", + sha256="5b6ceb0f85d63fa0c2c9aab31c29bebd41dc11da1f160ef21bc2fea93270a20d" if default_version else None, + allowed_hosts=CLANGD_ALLOWED_HOSTS, ), RuntimeDependency( id="Clangd", description="Clangd for macOS (x64)", - url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", + url=f"https://github.com/clangd/clangd/releases/download/{clangd_version}/clangd-mac-{clangd_version}.zip", platform_id="osx-x64", archive_type="zip", - binary_name="clangd_19.1.2/bin/clangd", + binary_name=f"clangd_{clangd_version}/bin/clangd", + sha256="d3b329b3f58602c57ca6501d255147af1bccad3691b1cb0c12c258fcd2da1be3" if default_version else None, + allowed_hosts=CLANGD_ALLOWED_HOSTS, ), RuntimeDependency( id="Clangd", description="Clangd for macOS (Arm64)", - url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", + url=f"https://github.com/clangd/clangd/releases/download/{clangd_version}/clangd-mac-{clangd_version}.zip", platform_id="osx-arm64", archive_type="zip", - binary_name="clangd_19.1.2/bin/clangd", + binary_name=f"clangd_{clangd_version}/bin/clangd", + sha256="d3b329b3f58602c57ca6501d255147af1bccad3691b1cb0c12c258fcd2da1be3" if default_version else None, + allowed_hosts=CLANGD_ALLOWED_HOSTS, ), ] ) diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index 505f8228..3fe17de1 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -19,6 +19,13 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) +CLOJURE_LSP_VERSION = "2026.02.20-16.08.58" +CLOJURE_LSP_ALLOWED_HOSTS = ( + "github.com", + "release-assets.githubusercontent.com", + "objects.githubusercontent.com", +) + def run_command(cmd: list, capture_output: bool = True) -> subprocess.CompletedProcess: return subprocess.run( @@ -42,49 +49,69 @@ def verify_clojure_cli() -> None: class ClojureLSP(SolidLanguageServer): """ - Provides a clojure-lsp specific instantiation of the LanguageServer class. Contains various configurations and settings specific to clojure. + Provides a clojure-lsp specific instantiation of the LanguageServer class. + + You can pass the following entries in ``ls_specific_settings["clojure"]``: + - clojure_lsp_version: Override the pinned clojure-lsp version downloaded + by Serena (default: the bundled Serena version). """ - clojure_lsp_releases = "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download" - runtime_dependencies = RuntimeDependencyCollection( - [ - RuntimeDependency( - id="clojure-lsp", - url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip", - platform_id="osx-arm64", - archive_type="zip", - binary_name="clojure-lsp", - ), - RuntimeDependency( - id="clojure-lsp", - url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-amd64.zip", - platform_id="osx-x64", - archive_type="zip", - binary_name="clojure-lsp", - ), - RuntimeDependency( - id="clojure-lsp", - url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip", - platform_id="linux-arm64", - archive_type="zip", - binary_name="clojure-lsp", - ), - RuntimeDependency( - id="clojure-lsp", - url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip", - platform_id="linux-x64", - archive_type="zip", - binary_name="clojure-lsp", - ), - RuntimeDependency( - id="clojure-lsp", - url=f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip", - platform_id="win-x64", - archive_type="zip", - binary_name="clojure-lsp.exe", - ), - ] - ) + CLOJURE_LSP_VERSION = CLOJURE_LSP_VERSION + CLOJURE_LSP_ALLOWED_HOSTS = CLOJURE_LSP_ALLOWED_HOSTS + + @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( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip", + platform_id="osx-arm64", + archive_type="zip", + binary_name="clojure-lsp", + sha256="a14d4db074f665378214e2dc888472e186c228dfa065c777b0534bfda5571669" if default_version else None, + allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, + ), + RuntimeDependency( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-amd64.zip", + platform_id="osx-x64", + archive_type="zip", + binary_name="clojure-lsp", + sha256="5507434c27104ab816e096d3336d8191641de8a65b57d76afb585d07167a3cf2" if default_version else None, + allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, + ), + RuntimeDependency( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip", + platform_id="linux-arm64", + archive_type="zip", + binary_name="clojure-lsp", + sha256="f8f09fa07dd4b6743b5c57270ccf1ee5cdbc5fca09dbca8b6a3b22705b5da4e1" if default_version else None, + allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, + ), + RuntimeDependency( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip", + platform_id="linux-x64", + archive_type="zip", + binary_name="clojure-lsp", + sha256="52e8bf4fd4cf171df0a3077c8bb5a3bf598d4c621e94b4876dab943a61267309" if default_version else None, + allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, + ), + RuntimeDependency( + id="clojure-lsp", + url=f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip", + platform_id="win-x64", + archive_type="zip", + binary_name="clojure-lsp.exe", + sha256="817b1271288817c954fb9e595278b1f25003827ce31f8785f253dc4ac911041f" if default_version else None, + allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS, + ), + ] + ) def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): """ @@ -109,7 +136,8 @@ 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() - deps = ClojureLSP.runtime_dependencies + clojure_lsp_version = self._custom_settings.get("clojure_lsp_version", ClojureLSP.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) diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index 504b0637..a651de70 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -21,6 +21,8 @@ class RuntimeDependency: id: str platform_id: str | None = None url: str | None = None + sha256: str | None = None + allowed_hosts: tuple[str, ...] | list[str] | None = None archive_type: str | None = None binary_name: str | None = None command: str | list[str] | None = None @@ -136,9 +138,30 @@ class RuntimeDependencyCollection: if dep.archive_type in ("gz", "binary") and dep.binary_name: dest = os.path.join(target_dir, dep.binary_name) - FileUtils.download_and_extract_archive(dep.url, dest, dep.archive_type) + FileUtils.download_and_extract_archive_verified( + dep.url, + dest, + dep.archive_type, + expected_sha256=dep.sha256, + allowed_hosts=dep.allowed_hosts, + ) else: - FileUtils.download_and_extract_archive(dep.url, target_dir, dep.archive_type or "zip") + FileUtils.download_and_extract_archive_verified( + dep.url, + target_dir, + dep.archive_type or "zip", + expected_sha256=dep.sha256, + allowed_hosts=dep.allowed_hosts, + ) + + +def build_npm_install_command(package_name: str, version: str, registry: str | None = None) -> list[str]: + """Build a pinned npm install command for a package in a Serena-managed install directory.""" + command = ["npm", "install", "--prefix", "./"] + if registry: + command.extend(["--registry", registry]) + command.append(f"{package_name}@{version}") + return command def quote_windows_path(path: str) -> str: diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index 28621bb2..f5b14e18 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -7,8 +7,8 @@ import os import platform import shutil import threading -import urllib.request from collections.abc import Iterable +from dataclasses import replace from pathlib import Path from typing import Any, cast @@ -19,85 +19,117 @@ from solidlsp.ls import DocumentSymbols, LanguageServerDependencyProvider, LSPFi from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_types import Hover, UnifiedSymbolInformation -from solidlsp.ls_utils import PathUtils +from solidlsp.ls_utils import FileUtils, PathUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult from solidlsp.settings import SolidLSPSettings -from solidlsp.util.zip import SafeZipExtractor from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) +NUGET_ALLOWED_HOSTS = ("www.nuget.org", "nuget.org", "globalcdn.nuget.org") +DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION = "5.5.0-2.26078.4" + _RUNTIME_DEPENDENCIES = [ RuntimeDependency( id="CSharpLanguageServer", description="Roslyn Language Server for Windows (x64)", package_name="roslyn-language-server.win-x64", - package_version="5.5.0-2.26078.4", - url="https://www.nuget.org/api/v2/package/roslyn-language-server.win-x64/5.5.0-2.26078.4", + package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION, + url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.win-x64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}", platform_id="win-x64", archive_type="nupkg", binary_name="Microsoft.CodeAnalysis.LanguageServer.dll", extract_path="tools/net10.0/win-x64", + sha256="7f3d4119e75305399e6faa81a68240b33c48b94ad523a904594abd00db95572a", + allowed_hosts=NUGET_ALLOWED_HOSTS, ), RuntimeDependency( id="CSharpLanguageServer", description="Roslyn Language Server for Windows (ARM64)", package_name="roslyn-language-server.win-arm64", - package_version="5.5.0-2.26078.4", - url="https://www.nuget.org/api/v2/package/roslyn-language-server.win-arm64/5.5.0-2.26078.4", + package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION, + url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.win-arm64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}", platform_id="win-arm64", archive_type="nupkg", binary_name="Microsoft.CodeAnalysis.LanguageServer.dll", extract_path="tools/net10.0/win-arm64", + sha256="0fe3381c4340a7494a5242c3d0c8be1af6ef0802de8b458f947cebca76fd26bc", + allowed_hosts=NUGET_ALLOWED_HOSTS, ), RuntimeDependency( id="CSharpLanguageServer", description="Roslyn Language Server for macOS (x64)", package_name="roslyn-language-server.osx-x64", - package_version="5.5.0-2.26078.4", - url="https://www.nuget.org/api/v2/package/roslyn-language-server.osx-x64/5.5.0-2.26078.4", + package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION, + url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.osx-x64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}", platform_id="osx-x64", archive_type="nupkg", binary_name="Microsoft.CodeAnalysis.LanguageServer.dll", extract_path="tools/net10.0/osx-x64", + sha256="c8de61a88c65150e12f561a2659f70b59d27a7465865136a1de950d2ef826c6d", + allowed_hosts=NUGET_ALLOWED_HOSTS, ), RuntimeDependency( id="CSharpLanguageServer", description="Roslyn Language Server for macOS (ARM64)", package_name="roslyn-language-server.osx-arm64", - package_version="5.5.0-2.26078.4", - url="https://www.nuget.org/api/v2/package/roslyn-language-server.osx-arm64/5.5.0-2.26078.4", + package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION, + url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.osx-arm64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}", platform_id="osx-arm64", archive_type="nupkg", binary_name="Microsoft.CodeAnalysis.LanguageServer.dll", extract_path="tools/net10.0/osx-arm64", + sha256="995207c14e01dafa71e84080a7eb1f045a697b0c3bb468077bb3809b69bdf456", + allowed_hosts=NUGET_ALLOWED_HOSTS, ), RuntimeDependency( id="CSharpLanguageServer", description="Roslyn Language Server for Linux (x64)", package_name="roslyn-language-server.linux-x64", - package_version="5.5.0-2.26078.4", - url="https://www.nuget.org/api/v2/package/roslyn-language-server.linux-x64/5.5.0-2.26078.4", + package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION, + url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.linux-x64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}", platform_id="linux-x64", archive_type="nupkg", binary_name="Microsoft.CodeAnalysis.LanguageServer.dll", extract_path="tools/net10.0/linux-x64", + sha256="1aad25de456d637a1eee993ca0d569a1b78d711744ccb36410a3a20250a48aa6", + allowed_hosts=NUGET_ALLOWED_HOSTS, ), RuntimeDependency( id="CSharpLanguageServer", description="Roslyn Language Server for Linux (ARM64)", package_name="roslyn-language-server.linux-arm64", - package_version="5.5.0-2.26078.4", - url="https://www.nuget.org/api/v2/package/roslyn-language-server.linux-arm64/5.5.0-2.26078.4", + package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION, + url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.linux-arm64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}", platform_id="linux-arm64", archive_type="nupkg", binary_name="Microsoft.CodeAnalysis.LanguageServer.dll", extract_path="tools/net10.0/linux-arm64", + sha256="a7dd49bbc0e25d0e2968ae31ec5f3c774373866db51f3500fcea0ac320e2bbc1", + allowed_hosts=NUGET_ALLOWED_HOSTS, ), ] +def _runtime_dependencies_for_version(version: str) -> list[RuntimeDependency]: + """Return the Roslyn LS runtime dependencies for the configured version.""" + default_version = version == DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION + result: list[RuntimeDependency] = [] + for dependency in _RUNTIME_DEPENDENCIES: + assert dependency.package_version is not None + assert dependency.url is not None + result.append( + replace( + dependency, + package_version=version, + url=dependency.url.replace(dependency.package_version, version), + sha256=dependency.sha256 if default_version else None, + ) + ) + return result + + def breadth_first_file_scan(root_dir: str) -> Iterable[str]: """ Perform a breadth-first scan of files in the given directory. @@ -151,6 +183,9 @@ class CSharpLanguageServer(SolidLanguageServer): This is a list of dicts, each containing at least the "id" key, and optionally "platform_id" to uniquely identify the dependency to override. + You can also set `csharp_language_server_version` in ``ls_specific_settings["csharp"]`` to override + the pinned Roslyn Language Server package version Serena downloads by default. + Example - Override Roslyn Language Server URL: ``` { @@ -347,9 +382,12 @@ class CSharpLanguageServer(SolidLanguageServer): filtered_overrides.append(dep_override) log.debug("Resolving runtime dependencies") + csharp_language_server_version = self._custom_settings.get( + "csharp_language_server_version", DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION + ) runtime_dependencies = RuntimeDependencyCollection( - _RUNTIME_DEPENDENCIES, + _runtime_dependencies_for_version(csharp_language_server_version), overrides=filtered_overrides, ) @@ -433,27 +471,19 @@ class CSharpLanguageServer(SolidLanguageServer): if url is None: raise SolidLSPException(f"No URL specified for package {package_name} version {package_version}") - # Create temporary directory for package download temp_dir = Path(self._ls_resources_dir) / "temp_downloads" temp_dir.mkdir(parents=True, exist_ok=True) try: log.debug(f"Downloading package from: {url}") - - # Download the .nupkg file - nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg" - urllib.request.urlretrieve(url, nupkg_file) - - # Extract the .nupkg file (it's just a zip file) package_extract_dir = temp_dir / f"{package_name}.{package_version}" - package_extract_dir.mkdir(exist_ok=True) - - # Use SafeZipExtractor to handle long paths and skip errors - extractor = SafeZipExtractor(archive_path=nupkg_file, extract_dir=package_extract_dir, verbose=False) - extractor.extract_all() - - # Clean up the nupkg file - nupkg_file.unlink() + FileUtils.download_and_extract_archive_verified( + url, + str(package_extract_dir), + "zip", + expected_sha256=dependency.sha256, + allowed_hosts=dependency.allowed_hosts, + ) log.info(f"Successfully downloaded and extracted {package_name} version {package_version} from NuGet.org") return package_extract_dir diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index 12dc3143..406c06e0 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -7,16 +7,22 @@ from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings -from ..ls_config import LanguageServerConfig +from ..ls_config import Language, LanguageServerConfig from ..lsp_protocol_handler.lsp_types import InitializeParams from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) +DART_ALLOWED_HOSTS = ("storage.googleapis.com",) + class DartLanguageServer(SolidLanguageServer): """ Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. + + You can pass the following entries in ``ls_specific_settings["dart"]``: + - dart_sdk_version: Override the pinned Dart SDK version downloaded by Serena + (default: the bundled Serena version). """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None: @@ -30,47 +36,59 @@ 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") deps = RuntimeDependencyCollection( [ RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for Linux (x64)", - url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip", + url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-linux-x64-release.zip", platform_id="linux-x64", archive_type="zip", binary_name="dart-sdk/bin/dart", + sha256="2813959e7d9650334015b927cc533f5beadfbf7fa48248beec471f8942a0ee71" if dart_sdk_version == "3.7.1" else None, + allowed_hosts=DART_ALLOWED_HOSTS, ), RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for Windows (x64)", - url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip", + url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-windows-x64-release.zip", 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, + allowed_hosts=DART_ALLOWED_HOSTS, ), RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for Windows (arm64)", - url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip", + url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-windows-arm64-release.zip", 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, + allowed_hosts=DART_ALLOWED_HOSTS, ), RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for macOS (x64)", - url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip", + url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-macos-x64-release.zip", platform_id="osx-x64", archive_type="zip", binary_name="dart-sdk/bin/dart", + sha256="a2765917b6ae49d1ac119553df9584989f9c441a46e8f18c129ba52489658d2e" if dart_sdk_version == "3.7.1" else None, + allowed_hosts=DART_ALLOWED_HOSTS, ), RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for macOS (arm64)", - url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip", + url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-macos-arm64-release.zip", platform_id="osx-arm64", archive_type="zip", binary_name="dart-sdk/bin/dart", + sha256="f57c25163092bac818f8ca6250a0d8b2c56344c6a075a1bd7c60da7ac28b32a4" if dart_sdk_version == "3.7.1" else None, + allowed_hosts=DART_ALLOWED_HOSTS, ), ] ) diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index c44bb791..b63a3fef 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -25,6 +25,23 @@ from solidlsp.settings import SolidLSPSettings 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 = { + "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" + @dataclasses.dataclass class RuntimeDependencyPaths: @@ -52,6 +69,9 @@ class EclipseJDTLS(SolidLanguageServer): - gradle_wrapper_enabled: Whether to use the project's Gradle wrapper (default: false) - gradle_java_home: Path to JDK for Gradle (default: null, uses bundled JRE) - use_system_java_home: Whether to use the system's JAVA_HOME for JDTLS itself (default: false) + - gradle_version: Override the pinned Gradle distribution version downloaded by Serena + - vscode_java_version: Override the pinned vscode-java runtime bundle version downloaded by Serena + - intellicode_version: Override the pinned IntelliCode VSIX version downloaded by Serena Example configuration in ~/.serena/serena_config.yml: ```yaml @@ -64,6 +84,9 @@ class EclipseJDTLS(SolidLanguageServer): gradle_wrapper_enabled: true # set to true for projects with custom plugins/repositories gradle_java_home: "/path/to/jdk" # set to override Gradle's JDK use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS + gradle_version: "8.14.2" + vscode_java_version: "1.42.0-561" + intellicode_version: "1.2.30" ``` """ @@ -115,33 +138,48 @@ class EclipseJDTLS(SolidLanguageServer): super().__init__(custom_settings, ls_resources_dir) self._solidlsp_settings = solidlsp_settings self._repository_root_path = repository_root_path - self.runtime_dependency_paths = self._setup_runtime_dependencies(ls_resources_dir) + self.runtime_dependency_paths = self._setup_runtime_dependencies(ls_resources_dir, custom_settings) - @classmethod - def _setup_runtime_dependencies(cls, ls_resources_dir: str) -> RuntimeDependencyPaths: + @staticmethod + def _setup_runtime_dependencies( + ls_resources_dir: str, custom_settings: SolidLSPSettings.CustomLSSettings + ) -> RuntimeDependencyPaths: """ Setup runtime dependencies for EclipseJDTLS and return the paths. """ 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_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}" + intellicode_version = custom_settings.get("intellicode_version", "1.2.30") + 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" - runtime_dependencies = { + runtime_dependencies: dict[str, dict[str, dict[str, object]]] = { "gradle": { "platform-agnostic": { - "url": "https://services.gradle.org/distributions/gradle-8.14.2-bin.zip", + "url": f"https://services.gradle.org/distributions/gradle-{gradle_version}-bin.zip", "archiveType": "zip", "relative_extraction_path": ".", + "sha256": GRADLE_SHA256 if default_gradle_version else None, + "allowed_hosts": GRADLE_ALLOWED_HOSTS, } }, "vscode-java": { "darwin-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "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, + "allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS, }, "osx-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "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, + "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", "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", @@ -149,9 +187,11 @@ class EclipseJDTLS(SolidLanguageServer): "jdtls_readonly_config_path": "extension/server/config_mac_arm", }, "osx-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "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, + "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", "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", @@ -159,9 +199,11 @@ class EclipseJDTLS(SolidLanguageServer): "jdtls_readonly_config_path": "extension/server/config_mac", }, "linux-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "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, + "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", "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", @@ -169,9 +211,11 @@ class EclipseJDTLS(SolidLanguageServer): "jdtls_readonly_config_path": "extension/server/config_linux_arm", }, "linux-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "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, + "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", "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", @@ -179,9 +223,11 @@ class EclipseJDTLS(SolidLanguageServer): "jdtls_readonly_config_path": "extension/server/config_linux", }, "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "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, + "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", "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", @@ -191,10 +237,12 @@ class EclipseJDTLS(SolidLanguageServer): }, "intellicode": { "platform-agnostic": { - "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", - "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage", + "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, + "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", } @@ -204,27 +252,30 @@ class EclipseJDTLS(SolidLanguageServer): gradle_path = str( PurePath( ls_resources_dir, - "gradle-8.14.2", + f"gradle-{gradle_version}", ) ) if not os.path.exists(gradle_path): - FileUtils.download_and_extract_archive( - runtime_dependencies["gradle"]["platform-agnostic"]["url"], + gradle_dependency = runtime_dependencies["gradle"]["platform-agnostic"] + FileUtils.download_and_extract_archive_verified( + cast(str, gradle_dependency["url"]), str(PurePath(gradle_path).parent), - runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"], + cast(str, gradle_dependency["archiveType"]), + expected_sha256=cast(str | None, gradle_dependency["sha256"]), + allowed_hosts=cast(tuple[str, ...], gradle_dependency["allowed_hosts"]), ) assert os.path.exists(gradle_path) dependency = runtime_dependencies["vscode-java"][platformId.value] - vscode_java_path = str(PurePath(ls_resources_dir, dependency["relative_extraction_path"])) + vscode_java_path = str(PurePath(ls_resources_dir, cast(str, dependency["relative_extraction_path"]))) os.makedirs(vscode_java_path, exist_ok=True) - jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) - jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) - lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"])) - jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"])) - jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"])) + jre_home_path = str(PurePath(vscode_java_path, cast(str, dependency["jre_home_path"]))) + jre_path = str(PurePath(vscode_java_path, cast(str, dependency["jre_path"]))) + lombok_jar_path = str(PurePath(vscode_java_path, cast(str, dependency["lombok_jar_path"]))) + jdtls_launcher_jar_path = str(PurePath(vscode_java_path, cast(str, dependency["jdtls_launcher_jar_path"]))) + jdtls_readonly_config_path = str(PurePath(vscode_java_path, cast(str, dependency["jdtls_readonly_config_path"]))) if not all( [ os.path.exists(vscode_java_path), @@ -235,7 +286,13 @@ class EclipseJDTLS(SolidLanguageServer): os.path.exists(jdtls_readonly_config_path), ] ): - FileUtils.download_and_extract_archive(dependency["url"], vscode_java_path, dependency["archiveType"]) + FileUtils.download_and_extract_archive_verified( + cast(str, dependency["url"]), + vscode_java_path, + cast(str, dependency["archiveType"]), + expected_sha256=cast(str | None, dependency["sha256"]), + allowed_hosts=cast(tuple[str, ...], dependency["allowed_hosts"]), + ) os.chmod(jre_path, 0o755) @@ -247,10 +304,10 @@ class EclipseJDTLS(SolidLanguageServer): assert os.path.exists(jdtls_readonly_config_path) dependency = runtime_dependencies["intellicode"]["platform-agnostic"] - intellicode_directory_path = str(PurePath(ls_resources_dir, dependency["relative_extraction_path"])) + intellicode_directory_path = str(PurePath(ls_resources_dir, cast(str, dependency["relative_extraction_path"]))) os.makedirs(intellicode_directory_path, exist_ok=True) - intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"])) - intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"])) + intellicode_jar_path = str(PurePath(intellicode_directory_path, cast(str, dependency["intellicode_jar_path"]))) + intellisense_members_path = str(PurePath(intellicode_directory_path, cast(str, dependency["intellisense_members_path"]))) if not all( [ os.path.exists(intellicode_directory_path), @@ -258,7 +315,13 @@ class EclipseJDTLS(SolidLanguageServer): os.path.exists(intellisense_members_path), ] ): - FileUtils.download_and_extract_archive(dependency["url"], intellicode_directory_path, dependency["archiveType"]) + FileUtils.download_and_extract_archive_verified( + cast(str, dependency["url"]), + intellicode_directory_path, + cast(str, dependency["archiveType"]), + expected_sha256=cast(str | None, dependency["sha256"]), + allowed_hosts=cast(tuple[str, ...], dependency["allowed_hosts"]), + ) assert os.path.exists(intellicode_directory_path) assert os.path.exists(intellicode_jar_path) diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py index 60f5157d..d200c899 100644 --- a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -9,7 +9,7 @@ from typing import Any, cast from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -19,10 +19,21 @@ from ..common import RuntimeDependency log = logging.getLogger(__name__) +EXPERT_VERSION = "v0.1.0-rc.6" +EXPERT_ALLOWED_HOSTS = ( + "github.com", + "release-assets.githubusercontent.com", + "objects.githubusercontent.com", +) + class ElixirTools(SolidLanguageServer): """ Provides Elixir specific instantiation of the LanguageServer class using Expert, the official Elixir language server. + + You can pass the following entries in ``ls_specific_settings["elixir"]``: + - expert_version: Override the pinned Expert version downloaded by Serena + (default: the bundled Serena version). """ @override @@ -66,6 +77,8 @@ class ElixirTools(SolidLanguageServer): Setup runtime dependencies for Expert. Downloads the Expert binary for the current platform and returns the path to the executable. """ + elixir_settings = solidlsp_settings.get_ls_specific_settings(Language.ELIXIR) + expert_version = elixir_settings.get("expert_version", EXPERT_VERSION) # Check if Elixir is available first elixir_version = cls._get_elixir_version() if not elixir_version: @@ -91,63 +104,62 @@ class ElixirTools(SolidLanguageServer): PlatformId.OSX_x64, PlatformId.OSX_arm64, PlatformId.WIN_x64, - PlatformId.WIN_arm64, ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Expert at the moment" expert_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "expert") - EXPERT_VERSION = "nightly" - # Define runtime dependencies inline runtime_deps = { PlatformId.LINUX_x64: RuntimeDependency( id="expert_linux_amd64", platform_id="linux-x64", - url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_amd64", + url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_linux_amd64", archive_type="binary", binary_name="expert_linux_amd64", extract_path="expert", + sha256="643a492ff972246668b0ca356a84c3d0a0f5feeae0ab5dc1b9a126876ed460e4" if expert_version == EXPERT_VERSION else None, + allowed_hosts=EXPERT_ALLOWED_HOSTS, ), PlatformId.LINUX_arm64: RuntimeDependency( id="expert_linux_arm64", platform_id="linux-arm64", - url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_arm64", + url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_linux_arm64", archive_type="binary", binary_name="expert_linux_arm64", extract_path="expert", + sha256="d8b830bdaa8991d7ebf255dacbb3674f3ea335c87d0bfba4b7f907ded4a8f014" if expert_version == EXPERT_VERSION else None, + allowed_hosts=EXPERT_ALLOWED_HOSTS, ), PlatformId.OSX_x64: RuntimeDependency( id="expert_darwin_amd64", platform_id="osx-x64", - url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_amd64", + url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_darwin_amd64", archive_type="binary", binary_name="expert_darwin_amd64", extract_path="expert", + sha256="964f316f1633090b33aab392b6b85fb778c5fb3c0db862671424458da34b1d4d" if expert_version == EXPERT_VERSION else None, + allowed_hosts=EXPERT_ALLOWED_HOSTS, ), PlatformId.OSX_arm64: RuntimeDependency( id="expert_darwin_arm64", platform_id="osx-arm64", - url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_arm64", + url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_darwin_arm64", archive_type="binary", binary_name="expert_darwin_arm64", extract_path="expert", + sha256="5fb5be151baedd635d99835cf3f9986afc9af6ae7b07bd001a1962f4298e45da" if expert_version == EXPERT_VERSION else None, + allowed_hosts=EXPERT_ALLOWED_HOSTS, ), PlatformId.WIN_x64: RuntimeDependency( id="expert_windows_amd64", platform_id="win-x64", - url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_amd64.exe", + url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_windows_amd64.exe", archive_type="binary", binary_name="expert_windows_amd64.exe", extract_path="expert.exe", - ), - PlatformId.WIN_arm64: RuntimeDependency( - id="expert_windows_arm64", - platform_id="win-arm64", - url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_arm64.exe", - archive_type="binary", - binary_name="expert_windows_arm64.exe", - extract_path="expert.exe", + sha256="babee77d2653679021600b99c68d984d4463290cb221e0fc0d1093b3afdeb3b0" if expert_version == EXPERT_VERSION else None, + allowed_hosts=EXPERT_ALLOWED_HOSTS, ), } @@ -161,7 +173,12 @@ class ElixirTools(SolidLanguageServer): if not os.path.exists(executable_path): log.info(f"Downloading Expert binary from {dependency.url}") assert dependency.url is not None - FileUtils.download_file(dependency.url, binary_path) + FileUtils.download_file_verified( + dependency.url, + binary_path, + expected_sha256=dependency.sha256, + allowed_hosts=dependency.allowed_hosts, + ) # Make the binary executable on Unix-like systems if not platform_id.value.startswith("win"): @@ -173,6 +190,10 @@ class ElixirTools(SolidLanguageServer): os.remove(executable_path) os.symlink(os.path.basename(binary_path), executable_path) + # normalizing the executable name on Windows + if binary_path != executable_path and platform_id.value.startswith("win"): + shutil.copy2(binary_path, executable_path) + assert os.path.exists(executable_path), f"Expert executable not found at {executable_path}" log.info(f"Expert binary ready at: {executable_path}") diff --git a/src/solidlsp/language_servers/elm_language_server.py b/src/solidlsp/language_servers/elm_language_server.py index e4598942..e1960c32 100644 --- a/src/solidlsp/language_servers/elm_language_server.py +++ b/src/solidlsp/language_servers/elm_language_server.py @@ -12,12 +12,12 @@ from overrides import override from sensai.util.logging import LogTime from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings -from .common import RuntimeDependency, RuntimeDependencyCollection +from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command log = logging.getLogger(__name__) @@ -76,13 +76,16 @@ class ElmLanguageServer(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." + elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM) + elm_language_server_version = elm_config.get("elm_language_server_version", "2.8.0") + npm_registry = elm_config.get("npm_registry") deps = RuntimeDependencyCollection( [ RuntimeDependency( id="elm-language-server", description="@elm-tooling/elm-language-server package", - command=["npm", "install", "--prefix", "./", "@elm-tooling/elm-language-server@2.8.0"], + command=build_npm_install_command("@elm-tooling/elm-language-server", elm_language_server_version, npm_registry), platform_id="any", ), ] diff --git a/src/solidlsp/language_servers/fsharp_language_server.py b/src/solidlsp/language_servers/fsharp_language_server.py index 66e472b5..79970bae 100644 --- a/src/solidlsp/language_servers/fsharp_language_server.py +++ b/src/solidlsp/language_servers/fsharp_language_server.py @@ -14,7 +14,7 @@ from overrides import override from serena.util.dotnet import DotNETUtil from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_exceptions import SolidLSPException from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -22,11 +22,17 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +FSAUTOCOMPLETE_VERSION = "0.83.0" + class FSharpLanguageServer(SolidLanguageServer): """ Provides F# specific instantiation of the LanguageServer class using Ionide LSP (FsAutoComplete). Contains various configurations and settings specific to F# development. + + You can pass the following entries in ``ls_specific_settings["fsharp"]``: + - fsautocomplete_version: Override the pinned FsAutoComplete version + installed by Serena (default: the bundled Serena version). """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): @@ -62,6 +68,8 @@ 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) dotnet_exe = DotNETUtil("8.0", allow_higher_version=True).get_dotnet_path_or_raise() RuntimeDependencyCollection( @@ -69,7 +77,7 @@ class FSharpLanguageServer(SolidLanguageServer): RuntimeDependency( id="fsautocomplete", description="FsAutoComplete (Ionide F# Language Server)", - command="dotnet tool install --tool-path ./ fsautocomplete", + command=f"dotnet tool install --tool-path ./ fsautocomplete --version {fsautocomplete_version}", platform_id="any", ), ] @@ -94,7 +102,7 @@ class FSharpLanguageServer(SolidLanguageServer): import subprocess result = subprocess.run( - [dotnet_exe, "tool", "install", "--tool-path", fsharp_ls_dir, "fsautocomplete"], + [dotnet_exe, "tool", "install", "--tool-path", fsharp_ls_dir, "fsautocomplete", "--version", fsautocomplete_version], cwd=fsharp_ls_dir, capture_output=True, text=True, diff --git a/src/solidlsp/language_servers/groovy_language_server.py b/src/solidlsp/language_servers/groovy_language_server.py index 81a4b4b1..66bcacc7 100644 --- a/src/solidlsp/language_servers/groovy_language_server.py +++ b/src/solidlsp/language_servers/groovy_language_server.py @@ -17,6 +17,15 @@ 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 = { + "win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3", + "linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9", + "linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1", + "osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f", + "osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933", +} + @dataclasses.dataclass class GroovyRuntimeDependencyPaths: @@ -34,6 +43,13 @@ class GroovyLanguageServer(SolidLanguageServer): """ Provides Groovy specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Groovy. + + You can pass the following entries in ``ls_specific_settings["groovy"]``: + - ls_jar_path: Path to the Groovy Language Server JAR. + - ls_java_home_path: Optional Java home to use instead of Serena's managed JRE. + - ls_jar_options: Additional JVM/JAR options passed to the Groovy LS. + - vscode_java_version: Override the pinned vscode-java runtime bundle version + downloaded by Serena when it manages Java itself (default: the bundled Serena version). """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): @@ -76,6 +92,9 @@ class GroovyLanguageServer(SolidLanguageServer): Setup runtime dependencies for Groovy Language Server and return paths. """ 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_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}" # Verify platform support assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), ( @@ -105,39 +124,52 @@ class GroovyLanguageServer(SolidLanguageServer): runtime_dependencies = { "java": { "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-win32-x64-{vscode_java_version}.vsix", "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, }, "linux-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "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, }, "linux-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "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, }, "osx-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "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, }, "osx-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "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, }, }, } java_dependency = runtime_dependencies["java"][platform_id.value] + java_home_relative_path = java_dependency["java_home_path"] + java_relative_path = java_dependency["java_path"] + java_download_url = java_dependency["url"] + java_archive_type = java_dependency["archiveType"] + assert java_home_relative_path is not None + assert java_relative_path is not None + assert java_download_url is not None + assert java_archive_type is not None static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "groovy_language_server") os.makedirs(static_dir, exist_ok=True) @@ -145,12 +177,18 @@ class GroovyLanguageServer(SolidLanguageServer): java_dir = os.path.join(static_dir, "java") os.makedirs(java_dir, exist_ok=True) - java_home_path = os.path.join(java_dir, java_dependency["java_home_path"]) - java_path = os.path.join(java_dir, java_dependency["java_path"]) + java_home_path = os.path.join(java_dir, java_home_relative_path) + java_path = os.path.join(java_dir, java_relative_path) if not os.path.exists(java_path): log.info(f"Downloading Java for {platform_id.value}...") - FileUtils.download_and_extract_archive(java_dependency["url"], java_dir, java_dependency["archiveType"]) + FileUtils.download_and_extract_archive_verified( + java_download_url, + java_dir, + java_archive_type, + expected_sha256=java_dependency["sha256"], + allowed_hosts=VSCODE_JAVA_ALLOWED_HOSTS, + ) if not platform_id.value.startswith("win-"): os.chmod(java_path, 0o755) diff --git a/src/solidlsp/language_servers/hlsl_language_server.py b/src/solidlsp/language_servers/hlsl_language_server.py index 5ac09c26..0a3203eb 100644 --- a/src/solidlsp/language_servers/hlsl_language_server.py +++ b/src/solidlsp/language_servers/hlsl_language_server.py @@ -24,12 +24,22 @@ log = logging.getLogger(__name__) # GitHub release version to download when not installed locally _DEFAULT_VERSION = "1.3.0" _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 = { + "shader-language-server-x86_64-pc-windows-msvc.zip": "a945b000c296cdeebb9ee2d4452cec2a0f26544dd076bb08bfdcade2278296a6", + "shader-language-server-x86_64-unknown-linux-gnu.zip": "8c0a7b36f51cc58593762db3592ae13e21ca3cb982b2526cfaaf7c82e92ca089", + "shader-language-server-aarch64-pc-windows-msvc.zip": "cdbd7b41e71cf6040d5cdb7e211ba4b76671a404ee0f7add281d72d3ab8dfa65", +} class HlslLanguageServer(SolidLanguageServer): """ Shader language server using shader-language-server. Supports .hlsl, .hlsli, .fx, .fxh, .cginc, .compute, .shader, .glsl, .vert, .frag, .geom, .tesc, .tese, .comp, .wgsl files. + + You can pass the following entries in ``ls_specific_settings["hlsl"]``: + - version: Override the pinned shader-language-server version downloaded + or built by Serena (default: the bundled Serena version). """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None: @@ -53,7 +63,7 @@ class HlslLanguageServer(SolidLanguageServer): base_url = f"{_GITHUB_RELEASE_BASE}/{tag}" # macOS has no pre-built binaries; build from source via cargo install - cargo_install_cmd = f"cargo install shader_language_server --version {version} --root ." + cargo_install_cmd = ["cargo", "install", "shader_language_server", "--version", version, "--root", "."] deps = RuntimeDependencyCollection( [ @@ -64,6 +74,10 @@ 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, + allowed_hosts=_HLSL_ALLOWED_HOSTS, ), RuntimeDependency( id="shader-language-server", @@ -72,6 +86,10 @@ 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, + allowed_hosts=_HLSL_ALLOWED_HOSTS, ), RuntimeDependency( id="shader-language-server", @@ -80,6 +98,10 @@ 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, + allowed_hosts=_HLSL_ALLOWED_HOSTS, ), RuntimeDependency( id="shader-language-server", diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index e5953c9d..0c77af6d 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -17,7 +17,7 @@ from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams from solidlsp.settings import SolidLSPSettings from ..lsp_protocol_handler import lsp_types -from .common import RuntimeDependency, RuntimeDependencyCollection +from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command log = logging.getLogger(__name__) @@ -59,6 +59,8 @@ 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") + 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") @@ -69,7 +71,7 @@ class Intelephense(SolidLanguageServer): [ RuntimeDependency( id="intelephense", - command="npm install --prefix ./ intelephense@1.14.4", + command=build_npm_install_command("intelephense", intelephense_version, npm_registry), platform_id="any", ) ] diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py index e8a04fc5..bd2d3035 100644 --- a/src/solidlsp/language_servers/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -43,6 +43,14 @@ 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 = { + "win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad", + "linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec", + "linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651", + "mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3", + "mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c", +} # Platform-specific Kotlin LSP download suffixes PLATFORM_KOTLIN_SUFFIX = { @@ -111,8 +119,17 @@ class KotlinLanguageServer(SolidLanguageServer): 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] log.info("Downloading Kotlin Language Server...") - FileUtils.download_and_extract_archive(kotlin_url, static_dir, "zip") + FileUtils.download_and_extract_archive_verified( + kotlin_url, + static_dir, + "zip", + expected_sha256=expected_sha256, + allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS, + ) if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): os.chmod( diff --git a/src/solidlsp/language_servers/lua_ls.py b/src/solidlsp/language_servers/lua_ls.py index 8f6731d4..f8a37b3f 100644 --- a/src/solidlsp/language_servers/lua_ls.py +++ b/src/solidlsp/language_servers/lua_ls.py @@ -1,5 +1,9 @@ """ Provides Lua specific instantiation of the LanguageServer class using lua-language-server. + +You can pass the following entries in ``ls_specific_settings["lua"]``: + - lua_language_server_version: Override the pinned lua-language-server version + downloaded by Serena (default: the bundled Serena version). """ import logging @@ -7,21 +11,29 @@ import os import pathlib import platform import shutil -import tarfile -import zipfile from pathlib import Path -import requests from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo 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 = { + "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", +} + class LuaLanguageServer(SolidLanguageServer): """ @@ -37,7 +49,7 @@ class LuaLanguageServer(SolidLanguageServer): return super().is_ignored_dirname(dirname) or dirname in [".luarocks", "lua_modules", "node_modules", "build", "dist", ".cache"] @staticmethod - def _get_lua_ls_path() -> str | None: + def _get_lua_ls_path(solidlsp_settings: SolidLSPSettings | None = None) -> str | None: """Get the path to lua-language-server executable.""" # First check if it's in PATH lua_ls = shutil.which("lua-language-server") @@ -48,17 +60,24 @@ class LuaLanguageServer(SolidLanguageServer): home = Path.home() possible_paths = [ home / ".local" / "bin" / "lua-language-server", - home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server", Path("/usr/local/bin/lua-language-server"), Path("/opt/lua-language-server/bin/lua-language-server"), ] + if solidlsp_settings is not None: + ls_resource_dir = Path(LuaLanguageServer.ls_resources_dir(solidlsp_settings)) / "lua" + possible_paths.extend( + [ + ls_resource_dir / "bin" / "lua-language-server", + ls_resource_dir / "bin" / "lua-language-server.exe", + ] + ) + # Add Windows-specific paths if platform.system() == "Windows": possible_paths.extend( [ home / "AppData" / "Local" / "lua-language-server" / "bin" / "lua-language-server.exe", - home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server.exe", ] ) @@ -69,11 +88,12 @@ class LuaLanguageServer(SolidLanguageServer): return None @staticmethod - def _download_lua_ls() -> str: + 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) system = platform.system() machine = platform.machine().lower() - lua_ls_version = "3.15.0" # Map platform and architecture to download URL if system == "Linux": @@ -100,31 +120,18 @@ class LuaLanguageServer(SolidLanguageServer): download_url = f"https://github.com/LuaLS/lua-language-server/releases/download/{lua_ls_version}/{download_name}" - # Create installation directory - install_dir = Path.home() / ".serena" / "language_servers" / "lua" + install_dir = Path(LuaLanguageServer.ls_resources_dir(solidlsp_settings)) / "lua" install_dir.mkdir(parents=True, exist_ok=True) - # Download the file - print(f"Downloading lua-language-server from {download_url}...") - response = requests.get(download_url, stream=True) - response.raise_for_status() - - # Save and extract - download_path = install_dir / download_name - with open(download_path, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - - print(f"Extracting lua-language-server to {install_dir}...") - if download_name.endswith(".tar.gz"): - with tarfile.open(download_path, "r:gz") as tar: - tar.extractall(install_dir) - elif download_name.endswith(".zip"): - with zipfile.ZipFile(download_path, "r") as zip_ref: - zip_ref.extractall(install_dir) - - # Clean up download file - download_path.unlink() + log.info("Downloading lua-language-server from %s", download_url) + archive_type = "gztar" if download_name.endswith(".tar.gz") else "zip" + FileUtils.download_and_extract_archive_verified( + 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, + allowed_hosts=LUA_LS_ALLOWED_HOSTS, + ) # Make executable on Unix systems if system != "Windows": @@ -140,22 +147,22 @@ class LuaLanguageServer(SolidLanguageServer): raise RuntimeError("Failed to find lua-language-server executable after extraction") @staticmethod - def _setup_runtime_dependency() -> str: + def _setup_runtime_dependency(solidlsp_settings: SolidLSPSettings) -> str: """ Check if required Lua runtime dependencies are available. Downloads lua-language-server if not present. """ - lua_ls_path = LuaLanguageServer._get_lua_ls_path() + lua_ls_path = LuaLanguageServer._get_lua_ls_path(solidlsp_settings) if not lua_ls_path: - print("lua-language-server not found. Downloading...") - lua_ls_path = LuaLanguageServer._download_lua_ls() - print(f"lua-language-server installed at: {lua_ls_path}") + log.info("lua-language-server not found. Downloading...") + lua_ls_path = LuaLanguageServer._download_lua_ls(solidlsp_settings) + log.info("lua-language-server installed at: %s", lua_ls_path) return lua_ls_path def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): - lua_ls_path = self._setup_runtime_dependency() + lua_ls_path = self._setup_runtime_dependency(solidlsp_settings) super().__init__( config, repository_root_path, ProcessLaunchInfo(cmd=lua_ls_path, cwd=repository_root_path), "lua", solidlsp_settings diff --git a/src/solidlsp/language_servers/luau_lsp.py b/src/solidlsp/language_servers/luau_lsp.py index 9aaf254a..4150374d 100644 --- a/src/solidlsp/language_servers/luau_lsp.py +++ b/src/solidlsp/language_servers/luau_lsp.py @@ -10,6 +10,8 @@ Requirements: or it will be automatically downloaded from GitHub releases. Advanced settings via ls_specific_settings["luau"]: + - luau_lsp_version: Override the pinned luau-lsp version downloaded by Serena + (default: the bundled Serena version) - platform: "roblox" (default) or "standard" - roblox_security_level: "None", "PluginSecurity" (default), "LocalUserSecurity", or "RobloxScriptSecurity" @@ -23,14 +25,13 @@ import pathlib import platform import shutil import threading -import zipfile from pathlib import Path -import requests from overrides import override from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.settings import SolidLSPSettings @@ -38,6 +39,13 @@ 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 = { + "luau-lsp-linux-x86_64.zip": "e4b633ad9a2c15437f60f9e721263f79aa0da606867d8458f0e159a325bf2db8", + "luau-lsp-linux-arm64.zip": "355be010f337a6772df6255c92e1fb28a59d194abe5c570453f4186472244355", + "luau-lsp-macos.zip": "01c1d6dd5fee27295b2968915dabb08c192192c46d9fe9c97bf31a130c96b8cb", + "luau-lsp-win64.zip": "eea596d47dc1c94a61ba1b78e6472bb4445bc3309780751515e6ab0a0abba57d", +} # Luau built-in docs CDN LUAU_DOCS_URL = "https://luau-lsp.pages.dev/api-docs/luau-en-us.json" @@ -51,6 +59,7 @@ SUPPORTED_ROBLOX_SECURITY_LEVELS = { "LocalUserSecurity", "RobloxScriptSecurity", } +LUAU_DOCS_ALLOWED_HOSTS = ("luau-lsp.pages.dev",) class LuauLanguageServer(SolidLanguageServer): @@ -96,24 +105,18 @@ class LuauLanguageServer(SolidLanguageServer): 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}" - download_path = install_dir / asset_name + download_url = f"https://github.com/JohnnyMorganz/luau-lsp/releases/download/{luau_lsp_version}/{asset_name}" - log.info("Downloading luau-lsp %s from %s", LUAU_LSP_VERSION, download_url) - with requests.get(download_url, stream=True, timeout=60) as response: - response.raise_for_status() - with open(download_path, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - - log.info("Extracting luau-lsp to %s", install_dir) - with zipfile.ZipFile(download_path, "r") as zip_ref: - zip_ref.extractall(install_dir) - - if download_path.exists(): - download_path.unlink() + log.info("Downloading luau-lsp %s from %s", luau_lsp_version, download_url) + FileUtils.download_and_extract_archive_verified( + 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, + allowed_hosts=LUAU_LSP_ALLOWED_HOSTS, + ) binary_path = self._find_existing_binary(install_dir) if binary_path is None: @@ -164,9 +167,7 @@ class LuauLanguageServer(SolidLanguageServer): try: log.info("Downloading %s from %s", description, url) - response = requests.get(url, timeout=30) - response.raise_for_status() - path.write_bytes(response.content) + FileUtils.download_file_verified(url, str(path), allowed_hosts=LUAU_DOCS_ALLOWED_HOSTS) return str(path) except Exception as exc: log.warning("Failed to download %s: %s", description, exc) diff --git a/src/solidlsp/language_servers/marksman.py b/src/solidlsp/language_servers/marksman.py index c5d039e3..affe2dcb 100644 --- a/src/solidlsp/language_servers/marksman.py +++ b/src/solidlsp/language_servers/marksman.py @@ -26,57 +26,79 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) +MARKSMAN_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") + class Marksman(SolidLanguageServer): """ Provides Markdown specific instantiation of the LanguageServer class using marksman. + + You can pass the following entries in ``ls_specific_settings["markdown"]``: + - marksman_version: Override the pinned Marksman release tag downloaded by + Serena (default: the bundled Serena version). """ class DependencyProvider(LanguageServerDependencyProviderSinglePath): - marksman_releases = "https://github.com/artempyanykh/marksman/releases/download/2024-12-18" - runtime_dependencies = RuntimeDependencyCollection( - [ - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman-linux-x64", - platform_id="linux-x64", - archive_type="binary", - binary_name="marksman", - ), - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman-linux-arm64", - platform_id="linux-arm64", - archive_type="binary", - binary_name="marksman", - ), - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman-macos", - platform_id="osx-x64", - archive_type="binary", - binary_name="marksman", - ), - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman-macos", - platform_id="osx-arm64", - archive_type="binary", - binary_name="marksman", - ), - RuntimeDependency( - id="marksman", - url=f"{marksman_releases}/marksman.exe", - platform_id="win-x64", - archive_type="binary", - binary_name="marksman.exe", - ), - ] - ) + 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( + id="marksman", + url=f"{marksman_releases}/marksman-linux-x64", + platform_id="linux-x64", + archive_type="binary", + binary_name="marksman", + sha256="b9cb666c643dfd9b699811fdfc445ed4c56be65c1d878c21d46847f0d7b0e475" if default_version else None, + allowed_hosts=MARKSMAN_ALLOWED_HOSTS, + ), + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman-linux-arm64", + platform_id="linux-arm64", + archive_type="binary", + binary_name="marksman", + sha256="b8d6972a56f3f9b7bbbf7c77ef8998e3b66fa82fb03c01398e224144486c9e73" if default_version else None, + allowed_hosts=MARKSMAN_ALLOWED_HOSTS, + ), + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman-macos", + platform_id="osx-x64", + archive_type="binary", + binary_name="marksman", + sha256="7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83" if default_version else None, + allowed_hosts=MARKSMAN_ALLOWED_HOSTS, + ), + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman-macos", + platform_id="osx-arm64", + archive_type="binary", + binary_name="marksman", + sha256="7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83" if default_version else None, + allowed_hosts=MARKSMAN_ALLOWED_HOSTS, + ), + RuntimeDependency( + id="marksman", + url=f"{marksman_releases}/marksman.exe", + platform_id="win-x64", + archive_type="binary", + binary_name="marksman.exe", + sha256="39de9df039c8b0d627ac5918a9d8792ad20fc49e2461d1f5c906975c016799ec" if default_version else None, + allowed_hosts=MARKSMAN_ALLOWED_HOSTS, + ), + ] + ) def _get_or_install_core_dependency(self) -> str: """Setup runtime dependencies for marksman and return the command to start the server.""" - deps = self.runtime_dependencies + marksman_version = self._custom_settings.get("marksman_version", self.DEFAULT_MARKSMAN_VERSION) + deps = self._runtime_dependencies(marksman_version) dependency = deps.get_single_dep_for_current_platform() marksman_ls_dir = self._ls_resources_dir diff --git a/src/solidlsp/language_servers/matlab_language_server.py b/src/solidlsp/language_servers/matlab_language_server.py index 1b9ef439..d31c2224 100644 --- a/src/solidlsp/language_servers/matlab_language_server.py +++ b/src/solidlsp/language_servers/matlab_language_server.py @@ -37,13 +37,11 @@ import pathlib import platform import shutil import threading -import zipfile from typing import Any, cast -import requests - from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation from solidlsp.settings import SolidLSPSettings @@ -52,10 +50,10 @@ log = logging.getLogger(__name__) # Environment variable for MATLAB installation path MATLAB_PATH_ENV_VAR = "MATLAB_PATH" -# VS Code Marketplace URL for MATLAB extension -MATLAB_EXTENSION_URL = ( - "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/vsextensions/language-matlab/latest/vspackage" -) +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",) class MatlabLanguageServer(SolidLanguageServer): @@ -72,6 +70,8 @@ class MatlabLanguageServer(SolidLanguageServer): You can pass the following entries in ls_specific_settings["matlab"]: - matlab_path: Path to MATLAB installation (overrides MATLAB_PATH env var) + - matlab_extension_version: Override the pinned MathWorks VS Code extension + version downloaded by Serena (default: the bundled Serena version) """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): @@ -119,44 +119,14 @@ class MatlabLanguageServer(SolidLanguageServer): """ try: log.info(f"Downloading MATLAB extension from {url}") - - # Create target directory for the extension os.makedirs(target_dir, exist_ok=True) - - # Download with proper headers to mimic VS Code marketplace client - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - "Accept": "application/octet-stream, application/vsix, */*", - } - - response = requests.get(url, headers=headers, stream=True, timeout=300) - response.raise_for_status() - - # Save to temporary VSIX file - temp_file = os.path.join(target_dir, "matlab_extension_temp.vsix") - total_size = int(response.headers.get("content-length", 0)) - - log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...") - - with open(temp_file, "wb") as f: - downloaded = 0 - for chunk in response.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - downloaded += len(chunk) - if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0: - progress = (downloaded / total_size) * 100 - log.info(f"Download progress: {progress:.1f}%") - - log.info("Download complete, extracting...") - - # Extract VSIX file (VSIX files are ZIP archives) - with zipfile.ZipFile(temp_file, "r") as zip_ref: - zip_ref.extractall(target_dir) - - # Clean up temp file - os.remove(temp_file) - + FileUtils.download_and_extract_archive_verified( + url, + target_dir, + "zip", + expected_sha256=MATLAB_EXTENSION_SHA256 if url == MATLAB_EXTENSION_URL else None, + allowed_hosts=MATLAB_EXTENSION_ALLOWED_HOSTS, + ) log.info("MATLAB extension extracted successfully") return True @@ -213,10 +183,15 @@ class MatlabLanguageServer(SolidLanguageServer): """ 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_url = ( + "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/" + f"vsextensions/language-matlab/{matlab_extension_version}/vspackage" + ) - log.info(f"Downloading MATLAB extension from: {MATLAB_EXTENSION_URL}") + 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): 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 c1d60834..ddd21698 100644 --- a/src/solidlsp/language_servers/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp.py @@ -12,7 +12,7 @@ from collections.abc import Iterable from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -21,6 +21,10 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +OMNISHARP_ALLOWED_HOSTS = ("roslynomnisharp.blob.core.windows.net", "download.visualstudio.microsoft.com") +DEFAULT_OMNISHARP_VERSION = "1.39.10" +DEFAULT_RAZOR_OMNISHARP_VERSION = "7.0.0-preview.23363.1" + def breadth_first_file_scan(root: str) -> Iterable[str]: """ @@ -58,6 +62,10 @@ def find_least_depth_sln_file(root_dir: str) -> str | None: class OmniSharp(SolidLanguageServer): """ Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. + + You can pass the following entries in ``ls_specific_settings["csharp_omnisharp"]``: + - omnisharp_version: Override the pinned OmniSharp version downloaded by Serena. + - razor_omnisharp_version: Override the pinned Razor plugin version downloaded by Serena. """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): @@ -150,6 +158,22 @@ class OmniSharp(SolidLanguageServer): with open(os.path.join(os.path.dirname(__file__), "omnisharp", "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] + omnisharp_settings = solidlsp_settings.get_ls_specific_settings(Language.CSHARP_OMNISHARP) + omnisharp_version = omnisharp_settings.get("omnisharp_version", DEFAULT_OMNISHARP_VERSION) + razor_omnisharp_version = omnisharp_settings.get("razor_omnisharp_version", DEFAULT_RAZOR_OMNISHARP_VERSION) + for dependency in d["runtimeDependencies"]: + if dependency["id"] == "OmniSharp": + dependency["url"] = dependency["url"].replace(DEFAULT_OMNISHARP_VERSION, omnisharp_version) + if "installPath" in dependency: + 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: + 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: + dependency["integrity"] = None assert platform_id in [ PlatformId.LINUX_x64, @@ -186,7 +210,13 @@ class OmniSharp(SolidLanguageServer): omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp") if not os.path.exists(omnisharp_ls_dir): os.makedirs(omnisharp_ls_dir) - FileUtils.download_and_extract_archive(runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip") + FileUtils.download_and_extract_archive_verified( + runtime_dependencies["OmniSharp"]["url"], + omnisharp_ls_dir, + "zip", + expected_sha256=runtime_dependencies["OmniSharp"].get("integrity"), + allowed_hosts=OMNISHARP_ALLOWED_HOSTS, + ) omnisharp_executable_path = os.path.join(omnisharp_ls_dir, runtime_dependencies["OmniSharp"]["binaryName"]) assert os.path.exists(omnisharp_executable_path) os.chmod(omnisharp_executable_path, 0o755) @@ -194,7 +224,13 @@ class OmniSharp(SolidLanguageServer): razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp") if not os.path.exists(razor_omnisharp_ls_dir): os.makedirs(razor_omnisharp_ls_dir) - FileUtils.download_and_extract_archive(runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip") + FileUtils.download_and_extract_archive_verified( + runtime_dependencies["RazorOmnisharp"]["url"], + razor_omnisharp_ls_dir, + "zip", + expected_sha256=runtime_dependencies["RazorOmnisharp"].get("integrity"), + allowed_hosts=OMNISHARP_ALLOWED_HOSTS, + ) razor_omnisharp_dll_path = os.path.join(razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"]) assert os.path.exists(razor_omnisharp_dll_path) diff --git a/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json b/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json index 7dfa1fd4..7f884b93 100644 --- a/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json +++ b/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json @@ -310,7 +310,8 @@ "x86_64" ], "platformId": "win-x64", - "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll", + "integrity": "8D2DC5484018A2390A2366D05CADDD2BE21F619122DACD1BA1A87815B98D7361" }, { "id": "RazorOmnisharp", @@ -324,7 +325,8 @@ "x86" ], "platformId": "win-x86", - "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll", + "integrity": "94C365B9D4B8D17612AF3574A3ABFB8CA75727866A8AF892EEEBE8F7559729A7" }, { "id": "RazorOmnisharp", @@ -338,7 +340,8 @@ "arm64" ], "platformId": "win-arm64", - "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll", + "integrity": "2186E17AB20621C13A5B04F7053BDC744A9938AF6F9FA6259208F09F55999946" }, { "id": "RazorOmnisharp", @@ -355,7 +358,8 @@ "./rzls" ], "platformId": "linux-x64", - "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll", + "integrity": "4B08C47D70AA96BE40AEDE22CDFCD909EB9073F1E1992E2B5B99D9D7A47F3276" }, { "id": "RazorOmnisharp", @@ -371,7 +375,8 @@ "binaries": [ "./rzls" ], - "platformId": "linux-arm64" + "platformId": "linux-arm64", + "integrity": "A73FA7D0AC5E4DAC9FF15B9B675D90E30DFB45E40A66ECC70E22433EE028791C" }, { "id": "RazorOmnisharp", @@ -387,7 +392,8 @@ "binaries": [ "./rzls" ], - "platformId": "linux-musl-x64" + "platformId": "linux-musl-x64", + "integrity": "7B0F8B1FEAC7726A5AC8D2C19F19B3530615F9226F75BB40D74EB0D8757CD11C" }, { "id": "RazorOmnisharp", @@ -403,7 +409,8 @@ "binaries": [ "./rzls" ], - "platformId": "linux-musl-arm64" + "platformId": "linux-musl-arm64", + "integrity": "8BC53D60D53B7FC9E40144D862B0748BCB8768C7C18C8E6900D675DED14ABD08" }, { "id": "RazorOmnisharp", @@ -419,7 +426,8 @@ "binaries": [ "./rzls" ], - "platformId": "osx-x64" + "platformId": "osx-x64", + "integrity": "7678022CBAC975A6D45E01A2985B88307E88F110578F3C04DB9D1C545BC69BDB" }, { "id": "RazorOmnisharp", @@ -435,7 +443,8 @@ "binaries": [ "./rzls" ], - "platformId": "osx-arm64" + "platformId": "osx-arm64", + "integrity": "0A68EE21F5B8DDFDFC8D5E86E519C54D29B3A4D582F4D4284545C40A741AAFE6" } ] -} \ No newline at end of file +} diff --git a/src/solidlsp/language_servers/pascal_server.py b/src/solidlsp/language_servers/pascal_server.py index d7b9c91f..9de2a926 100644 --- a/src/solidlsp/language_servers/pascal_server.py +++ b/src/solidlsp/language_servers/pascal_server.py @@ -4,21 +4,24 @@ Contains various configurations and settings specific to Pascal and Free Pascal. pasls installation strategy: 1. Use existing pasls from PATH -2. Download prebuilt binary from GitHub releases (auto-updated) +2. Download a pinned prebuilt binary from GitHub releases Supported platforms for binary download: - linux-x64, linux-arm64 - osx-x64, osx-arm64 - win-x64 -Auto-update features: -- Checks for updates every 24 hours via GitHub API +Integrity features: - SHA256 checksum verification before installation - Atomic update with rollback on failure - Windows file locking detection You can pass the following entries in ls_specific_settings["pascal"]: +Version management: +- pasls_version: Override the pinned pasls version downloaded by Serena + (default: the bundled Serena version). + Environment variables (recommended for CodeTools configuration): - pp: Path to FPC compiler driver, must be "fpc.exe" (e.g., "D:/laz32/fpc/bin/i386-win32/fpc.exe"). Do NOT use backend compilers like ppc386.exe or ppcx64.exe - CodeTools queries fpc.exe for @@ -59,23 +62,27 @@ import zipfile from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, quote_windows_path from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +PASLS_VERSION = "v0.2.0" + class PascalLanguageServer(SolidLanguageServer): """ Provides Pascal specific instantiation of the LanguageServer class using pasls. Contains various configurations and settings specific to Free Pascal and Lazarus. + Supports overriding the bundled pasls version via ``pasls_version``. """ # URL configuration - PASLS_RELEASES_URL = "https://github.com/zen010101/pascal-language-server/releases/latest/download" - PASLS_API_URL = "https://api.github.com/repos/zen010101/pascal-language-server/releases/latest" + PASLS_VERSION = PASLS_VERSION + PASLS_RELEASES_URL = f"https://github.com/zen010101/pascal-language-server/releases/download/{PASLS_VERSION}" + PASLS_API_URL = f"https://api.github.com/repos/zen010101/pascal-language-server/releases/tags/{PASLS_VERSION}" # Update check interval (seconds) UPDATE_CHECK_INTERVAL = 86400 # 24 hours @@ -523,21 +530,18 @@ class PascalLanguageServer(SolidLanguageServer): return False # 3. Verify SHA256 checksum (critical security step, before extraction) - if checksums: - expected_sha256 = checksums.get(archive_filename) - if expected_sha256: - log.info(f"Verifying SHA256 checksum for {archive_filename}...") - if not cls._verify_checksum(archive_path, expected_sha256): - log.error(f"SHA256 checksum verification FAILED for {archive_filename}") - log.error("Aborting installation due to checksum mismatch - possible security issue!") - try: - os.remove(archive_path) - except OSError: - pass - return False - log.info("SHA256 checksum verified successfully") - else: - log.warning(f"No checksum found for {archive_filename} in checksums file") + expected_sha256 = checksums.get(archive_filename) if checksums else dep.sha256 + if expected_sha256: + log.info(f"Verifying SHA256 checksum for {archive_filename}...") + if not cls._verify_checksum(archive_path, expected_sha256): + log.error(f"SHA256 checksum verification FAILED for {archive_filename}") + log.error("Aborting installation due to checksum mismatch - possible security issue!") + try: + os.remove(archive_path) + except OSError: + pass + return False + log.info("SHA256 checksum verified successfully") else: log.warning("No checksums available - skipping verification (not recommended for production)") @@ -607,12 +611,18 @@ class PascalLanguageServer(SolidLanguageServer): def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str: """ Setup runtime dependencies for Pascal Language Server (pasls). - Automatically checks for updates every 24 hours with security verification. + Downloads the pinned Serena-supported pasls release with checksum verification. Returns: str: The command to start the pasls server """ + pascal_settings = solidlsp_settings.get_ls_specific_settings(Language.PASCAL) + pasls_version = pascal_settings.get("pasls_version", PASLS_VERSION) + cls.PASLS_VERSION = pasls_version + cls.PASLS_RELEASES_URL = f"https://github.com/zen010101/pascal-language-server/releases/download/{pasls_version}" + cls.PASLS_API_URL = f"https://api.github.com/repos/zen010101/pascal-language-server/releases/tags/{pasls_version}" + # Check if pasls is already in PATH pasls_in_path = shutil.which("pasls") if pasls_in_path: @@ -637,6 +647,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="linux-x64", archive_type="gztar", binary_name="pasls", + sha256="517259395b0a385a5e848cf48b967645a984be3dd456118bc08771283a822a5b", ), RuntimeDependency( id="PascalLanguageServer", @@ -645,6 +656,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="linux-arm64", archive_type="gztar", binary_name="pasls", + sha256="cb4986941cfdcf9cb74ece6bbb53a443390a908e880108a37f4ccf82b2d6c502", ), RuntimeDependency( id="PascalLanguageServer", @@ -653,6 +665,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="osx-x64", archive_type="zip", binary_name="pasls", + sha256="0abfcd98f63f77dba74094339a40d4407b69317c1c77b13a26b9b7dbdfd885f1", ), RuntimeDependency( id="PascalLanguageServer", @@ -661,6 +674,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="osx-arm64", archive_type="zip", binary_name="pasls", + sha256="d4c2411e406af96ceae12b11e77fdb0c684ca15a68bfd8b4f9c6fe1fbdf515a7", ), RuntimeDependency( id="PascalLanguageServer", @@ -669,6 +683,7 @@ class PascalLanguageServer(SolidLanguageServer): platform_id="win-x64", archive_type="zip", binary_name="pasls.exe", + sha256="1493c31552e6f90a59800b2d44669e01fc4551d3647f3cea5dd105a9f6bc73e5", ), ] ) diff --git a/src/solidlsp/language_servers/phpactor.py b/src/solidlsp/language_servers/phpactor.py index d10a038b..80805c0d 100644 --- a/src/solidlsp/language_servers/phpactor.py +++ b/src/solidlsp/language_servers/phpactor.py @@ -22,6 +22,8 @@ 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") class PhpactorServer(SolidLanguageServer): @@ -33,6 +35,8 @@ class PhpactorServer(SolidLanguageServer): You can pass the following entries in ls_specific_settings["php_phpactor"]: - ignore_vendor: whether to ignore directories named "vendor" (default: true) + - phpactor_version: Override the pinned Phpactor PHAR version downloaded by + Serena (default: the bundled Serena version) """ @override @@ -44,6 +48,8 @@ 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_phar_url = f"https://github.com/phpactor/phpactor/releases/download/{phpactor_version}/phpactor.phar" # Verify PHP is installed php_path = shutil.which("php") assert php_path is not None, ( @@ -65,8 +71,14 @@ class PhpactorServer(SolidLanguageServer): phpactor_phar_path = os.path.join(self._ls_resources_dir, "phpactor.phar") if not os.path.exists(phpactor_phar_path): os.makedirs(self._ls_resources_dir, exist_ok=True) - log.info(f"Downloading phpactor PHAR from {PHPACTOR_PHAR_URL}") - FileUtils.download_and_extract_archive(PHPACTOR_PHAR_URL, phpactor_phar_path, "binary") + 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, + allowed_hosts=PHPACTOR_ALLOWED_HOSTS, + ) assert os.path.exists(phpactor_phar_path), f"phpactor PHAR not found at {phpactor_phar_path}, download may have failed." diff --git a/src/solidlsp/language_servers/powershell_language_server.py b/src/solidlsp/language_servers/powershell_language_server.py index ee095ff3..828babd9 100644 --- a/src/solidlsp/language_servers/powershell_language_server.py +++ b/src/solidlsp/language_servers/powershell_language_server.py @@ -1,6 +1,10 @@ """ Provides PowerShell specific instantiation of the LanguageServer class using PowerShell Editor Services. Contains various configurations and settings specific to PowerShell scripting. + +You can pass the following entries in ``ls_specific_settings["powershell"]``: + - pses_version: Override the pinned PowerShell Editor Services version + downloaded by Serena (default: the bundled Serena version). """ import logging @@ -10,14 +14,13 @@ import platform import shutil import tempfile import threading -import zipfile from pathlib import Path -import requests from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings @@ -26,6 +29,8 @@ log = logging.getLogger(__name__) # PowerShell Editor Services version to download PSES_VERSION = "4.4.0" +PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc" +PSES_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") class PowerShellLanguageServer(SolidLanguageServer): @@ -98,31 +103,24 @@ class PowerShellLanguageServer(SolidLanguageServer): @classmethod 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) download_url = ( - f"https://github.com/PowerShell/PowerShellEditorServices/releases/download/v{PSES_VERSION}/PowerShellEditorServices.zip" + 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" install_dir.mkdir(parents=True, exist_ok=True) - # Download the file log.info(f"Downloading PowerShell Editor Services from {download_url}...") - response = requests.get(download_url, stream=True, timeout=120) - response.raise_for_status() - - # Save the zip file - zip_path = install_dir / "PowerShellEditorServices.zip" - with open(zip_path, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - - log.info(f"Extracting PowerShell Editor Services to {install_dir}...") - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(install_dir) - - # Clean up zip file - zip_path.unlink() + FileUtils.download_and_extract_archive_verified( + download_url, + str(install_dir), + "zip", + expected_sha256=PSES_SHA256 if pses_version == PSES_VERSION else None, + allowed_hosts=PSES_ALLOWED_HOSTS, + ) start_script = install_dir / "PowerShellEditorServices" / "Start-EditorServices.ps1" if not start_script.exists(): diff --git a/src/solidlsp/language_servers/ruby_lsp.py b/src/solidlsp/language_servers/ruby_lsp.py index ee39d250..4a3d41b8 100644 --- a/src/solidlsp/language_servers/ruby_lsp.py +++ b/src/solidlsp/language_servers/ruby_lsp.py @@ -1,6 +1,11 @@ """ Ruby LSP Language Server implementation using Shopify's ruby-lsp. Provides modern Ruby language server capabilities with improved performance. + +You can pass the following entries in ``ls_specific_settings["ruby"]``: + - ruby_lsp_version: Override the pinned ruby-lsp gem version installed by + Serena when no project-local or global ruby-lsp is already available + (default: the bundled Serena version). """ import json @@ -14,18 +19,21 @@ import threading from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +RUBY_LSP_VERSION = "0.26.8" + class RubyLsp(SolidLanguageServer): """ Provides Ruby specific instantiation of the LanguageServer class using ruby-lsp. Contains various configurations and settings specific to Ruby with modern LSP features. + Supports overriding the bundled gem version via ``ruby_lsp_version``. """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): @@ -33,7 +41,7 @@ class RubyLsp(SolidLanguageServer): Creates a RubyLsp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - ruby_lsp_executable = self._setup_runtime_dependencies(config, repository_root_path) + ruby_lsp_executable = self._setup_runtime_dependencies(config, repository_root_path, solidlsp_settings) super().__init__( config, repository_root_path, ProcessLaunchInfo(cmd=ruby_lsp_executable, cwd=repository_root_path), "ruby", solidlsp_settings ) @@ -92,11 +100,15 @@ class RubyLsp(SolidLanguageServer): return shutil.which(executable_name) @staticmethod - def _setup_runtime_dependencies(config: LanguageServerConfig, repository_root_path: str) -> list[str]: + def _setup_runtime_dependencies( + config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ) -> list[str]: """ Setup runtime dependencies for ruby-lsp and return the command list to start the server. - Installation strategy: Bundler project > global ruby-lsp > gem install ruby-lsp + Installation strategy: Bundler project > global ruby-lsp > gem install ruby-lsp at the pinned version """ + ls_specific_settings = solidlsp_settings.get_ls_specific_settings(Language.RUBY) + ruby_lsp_version = ls_specific_settings.get("ruby_lsp_version", RUBY_LSP_VERSION) # Detect rbenv-managed Ruby environment # When .ruby-version exists, it indicates the project uses rbenv for version management. # rbenv automatically reads .ruby-version to determine which Ruby version to use. @@ -215,7 +227,12 @@ class RubyLsp(SolidLanguageServer): # Try to install ruby-lsp globally log.info("ruby-lsp not found, attempting to install globally...") try: - subprocess.run(["gem", "install", "ruby-lsp"], check=True, capture_output=True, cwd=repository_root_path) + subprocess.run( + ["gem", "install", "ruby-lsp", "-v", ruby_lsp_version], + check=True, + capture_output=True, + cwd=repository_root_path, + ) log.info("Successfully installed ruby-lsp globally") # Find the newly installed ruby-lsp executable ruby_lsp_path = RubyLsp._find_executable_with_extensions("ruby-lsp") @@ -226,9 +243,11 @@ class RubyLsp(SolidLanguageServer): raise RuntimeError( f"Failed to install ruby-lsp globally: {error_msg}\n" "For Bundler projects, please add 'gem \"ruby-lsp\"' to your Gemfile and run 'bundle install'.\n" - "Alternatively, install globally: gem install ruby-lsp" + f"Alternatively, install globally: gem install ruby-lsp -v {ruby_lsp_version}" ) from e - raise RuntimeError(f"Failed to install ruby-lsp: {error_msg}\nPlease try installing manually: gem install ruby-lsp") from e + raise RuntimeError( + f"Failed to install ruby-lsp: {error_msg}\nPlease try installing manually: gem install ruby-lsp -v {ruby_lsp_version}" + ) from e @staticmethod def _detect_rails_project(repository_root_path: str) -> bool: diff --git a/src/solidlsp/language_servers/solidity_language_server.py b/src/solidlsp/language_servers/solidity_language_server.py index 70c60161..223a6683 100644 --- a/src/solidlsp/language_servers/solidity_language_server.py +++ b/src/solidlsp/language_servers/solidity_language_server.py @@ -12,7 +12,7 @@ import threading from time import sleep from typing import Any -from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection +from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -69,13 +69,19 @@ 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") + npm_registry = self._custom_settings.get("npm_registry") deps = RuntimeDependencyCollection( [ RuntimeDependency( id="solidity-language-server", description="Nomic Foundation Solidity Language Server", - command="npm install --prefix ./ @nomicfoundation/solidity-language-server@0.8.4", + command=build_npm_install_command( + "@nomicfoundation/solidity-language-server", + solidity_language_server_version, + npm_registry, + ), platform_id="any", ), ] diff --git a/src/solidlsp/language_servers/systemverilog_server.py b/src/solidlsp/language_servers/systemverilog_server.py index 8262acc6..bd1782c6 100644 --- a/src/solidlsp/language_servers/systemverilog_server.py +++ b/src/solidlsp/language_servers/systemverilog_server.py @@ -18,11 +18,17 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) +VERIBLE_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") + class SystemVerilogLanguageServer(SolidLanguageServer): """ SystemVerilog language server using verible-verilog-ls. Supports .sv, .svh, .v, .vh files. + + You can pass the following entries in ``ls_specific_settings["systemverilog"]``: + - verible_version: Override the pinned Verible release version downloaded + by Serena (default: the bundled Serena version). """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None: @@ -67,6 +73,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer): platform_id="linux-x64", archive_type="gztar", binary_name=f"verible-{verible_version}/bin/verible-verilog-ls", + sha256="f52e5920ef63f70620a6086e09dea8bd778147cd7a9ff827bb7de5d6316b1754", + allowed_hosts=VERIBLE_ALLOWED_HOSTS, ), RuntimeDependency( id="verible-ls", @@ -75,6 +83,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer): platform_id="linux-arm64", archive_type="gztar", binary_name=f"verible-{verible_version}/bin/verible-verilog-ls", + sha256="30dd9c6f6e0f4840d6ba0c9e81ea2774a50b5a1a523a855245f9a9b4beb6b58b", + allowed_hosts=VERIBLE_ALLOWED_HOSTS, ), RuntimeDependency( id="verible-ls", @@ -83,6 +93,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer): platform_id="osx-x64", archive_type="gztar", binary_name=f"verible-{verible_version}/bin/verible-verilog-ls", + sha256="9ef92e9ad345285dd593763e10ca61c8532fcf47bbb6cf4448f9a9423882d662", + allowed_hosts=VERIBLE_ALLOWED_HOSTS, ), RuntimeDependency( id="verible-ls", @@ -91,6 +103,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer): platform_id="osx-arm64", archive_type="gztar", binary_name=f"verible-{verible_version}/bin/verible-verilog-ls", + sha256="9ef92e9ad345285dd593763e10ca61c8532fcf47bbb6cf4448f9a9423882d662", + allowed_hosts=VERIBLE_ALLOWED_HOSTS, ), RuntimeDependency( id="verible-ls", @@ -99,6 +113,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer): platform_id="win-x64", archive_type="zip", binary_name=f"verible-{verible_version}/bin/verible-verilog-ls.exe", + sha256="729aa244036da4a4f87bc026d33555456fc7f7be79778d983ebe9c893f4a0ca3", + allowed_hosts=VERIBLE_ALLOWED_HOSTS, ), ] ) diff --git a/src/solidlsp/language_servers/taplo_server.py b/src/solidlsp/language_servers/taplo_server.py index 213af874..a832e13e 100644 --- a/src/solidlsp/language_servers/taplo_server.py +++ b/src/solidlsp/language_servers/taplo_server.py @@ -1,25 +1,22 @@ """ Provides TOML specific instantiation of the LanguageServer class using Taplo. Contains various configurations and settings specific to TOML files. + +You can pass the following entries in ``ls_specific_settings["toml"]``: + - taplo_version: Override the pinned Taplo version downloaded by Serena + (default: the bundled Serena version). """ -import gzip -import hashlib import logging import os import platform import shutil -import socket import stat -import urllib.request from typing import Any -# Download timeout in seconds (prevents indefinite hangs) -DOWNLOAD_TIMEOUT_SECONDS = 120 - from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_utils import PathUtils +from solidlsp.ls_utils import FileUtils, PathUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.settings import SolidLSPSettings @@ -28,6 +25,7 @@ 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 @@ -43,17 +41,7 @@ TAPLO_SHA256_CHECKSUMS: dict[str, str] = { } -def _verify_sha256(file_path: str, expected_hash: str) -> bool: - """Verify SHA256 checksum of a downloaded file.""" - sha256_hash = hashlib.sha256() - with open(file_path, "rb") as f: - for chunk in iter(lambda: f.read(8192), b""): - sha256_hash.update(chunk) - actual_hash = sha256_hash.hexdigest() - return actual_hash.lower() == expected_hash.lower() - - -def _get_taplo_download_url() -> tuple[str, str]: +def _get_taplo_download_url(version: str = TAPLO_VERSION) -> tuple[str, str]: """ Get the appropriate Taplo download URL for the current platform. @@ -88,7 +76,7 @@ def _get_taplo_download_url() -> tuple[str, str]: filename = f"taplo-linux-{arch}.gz" executable = "taplo" - return f"{TAPLO_DOWNLOAD_BASE}/{filename}", executable + return f"https://github.com/tamasfe/taplo/releases/download/{version}/{filename}", executable class TaploServer(SolidLanguageServer): @@ -144,7 +132,8 @@ class TaploServer(SolidLanguageServer): taplo_dir = os.path.join(self._ls_resources_dir, "taplo") os.makedirs(taplo_dir, exist_ok=True) - _, executable_name = _get_taplo_download_url() + 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) if os.path.exists(taplo_executable) and os.access(taplo_executable, os.X_OK): @@ -152,8 +141,8 @@ class TaploServer(SolidLanguageServer): return taplo_executable # Download and install Taplo - log.info(f"Taplo not found. Downloading version {TAPLO_VERSION}...") - self._download_taplo(taplo_dir, taplo_executable) + log.info(f"Taplo not found. Downloading version {taplo_version}...") + self._download_taplo(taplo_dir, taplo_executable, taplo_version) if not os.path.exists(taplo_executable): raise FileNotFoundError( @@ -167,64 +156,30 @@ class TaploServer(SolidLanguageServer): return [core_path, "lsp", "stdio"] @classmethod - def _download_taplo(cls, install_dir: str, executable_path: str) -> None: - """Download and extract Taplo binary with SHA256 verification.""" - # TODO: consider using existing download utilities in SolidLSP instead of the custom logic here - download_url, _ = _get_taplo_download_url() + def _download_taplo(cls, install_dir: str, executable_path: str, version: str = 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: + raise RuntimeError(f"No SHA256 checksum configured for Taplo archive: {archive_filename}") try: log.info(f"Downloading Taplo from: {download_url}") - archive_path = os.path.join(install_dir, archive_filename) - - # Download the archive with timeout to prevent indefinite hangs - old_timeout = socket.getdefaulttimeout() - try: - socket.setdefaulttimeout(DOWNLOAD_TIMEOUT_SECONDS) - urllib.request.urlretrieve(download_url, archive_path) - finally: - socket.setdefaulttimeout(old_timeout) - - # Verify SHA256 checksum - expected_hash = TAPLO_SHA256_CHECKSUMS.get(archive_filename) - if expected_hash: - if not _verify_sha256(archive_path, expected_hash): - os.remove(archive_path) - raise RuntimeError( - f"SHA256 checksum verification failed for {archive_filename}. " - "The downloaded file may be corrupted or tampered with. " - "Try installing manually: cargo install taplo-cli --locked" - ) - log.info(f"SHA256 checksum verified for {archive_filename}") - else: - log.warning( - f"No SHA256 checksum available for {archive_filename}. " - "Skipping verification - consider installing manually: cargo install taplo-cli --locked" - ) - - # Extract based on format - if archive_path.endswith(".gz") and not archive_path.endswith(".tar.gz"): - # Single file gzip - with gzip.open(archive_path, "rb") as f_in: - with open(executable_path, "wb") as f_out: - f_out.write(f_in.read()) - elif archive_path.endswith(".zip"): - import zipfile - - with zipfile.ZipFile(archive_path, "r") as zip_ref: - # Security: Validate paths to prevent zip slip vulnerability - for member in zip_ref.namelist(): - member_path = os.path.normpath(os.path.join(install_dir, member)) - if not member_path.startswith(os.path.normpath(install_dir)): - raise RuntimeError(f"Zip slip detected: {member} attempts to escape install directory") - zip_ref.extractall(install_dir) + archive_type = "zip" if archive_filename.endswith(".zip") else "gz" + target_path = install_dir if archive_type == "zip" else executable_path + FileUtils.download_and_extract_archive_verified( + download_url, + target_path, + archive_type, + expected_sha256=expected_hash, + allowed_hosts=TAPLO_ALLOWED_HOSTS, + ) # Make executable on Unix systems if os.name != "nt": os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - # Clean up archive - os.remove(archive_path) log.info(f"Taplo installed successfully at: {executable_path}") except Exception as e: diff --git a/src/solidlsp/language_servers/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls.py index 333c1c70..ec26126d 100644 --- a/src/solidlsp/language_servers/terraform_ls.py +++ b/src/solidlsp/language_servers/terraform_ls.py @@ -6,7 +6,7 @@ from typing import cast from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import PathUtils, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -16,10 +16,16 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) +TERRAFORM_LS_ALLOWED_HOSTS = ("releases.hashicorp.com",) + class TerraformLS(SolidLanguageServer): """ Provides Terraform specific instantiation of the LanguageServer class using terraform-ls. + + You can pass the following entries in ``ls_specific_settings["terraform"]``: + - terraform_ls_version: Override the pinned terraform-ls version downloaded + by Serena (default: the bundled Serena version). """ @override @@ -92,48 +98,60 @@ class TerraformLS(SolidLanguageServer): Downloads and installs terraform-ls if not already present. """ 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") platform_id = PlatformUtils.get_platform_id() deps = RuntimeDependencyCollection( [ RuntimeDependency( id="TerraformLS", description="terraform-ls for macOS (ARM64)", - url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip", + url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_darwin_arm64.zip", platform_id="osx-arm64", archive_type="zip", binary_name="terraform-ls", + sha256="fee8743aa71fe2d8b0b9b91283b844cfa57d58457306a62e53a8f38d143cec8c" if terraform_ls_version == "0.36.5" else None, + allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), RuntimeDependency( id="TerraformLS", description="terraform-ls for macOS (x64)", - url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip", + url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_darwin_amd64.zip", platform_id="osx-x64", archive_type="zip", binary_name="terraform-ls", + sha256="17c5c480f8eec7e528292565f1c05d5097a41edf7ef8ee2a9f3a18d288a1415a" if terraform_ls_version == "0.36.5" else None, + allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), RuntimeDependency( id="TerraformLS", description="terraform-ls for Linux (ARM64)", - url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_arm64.zip", + url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_linux_arm64.zip", platform_id="linux-arm64", archive_type="zip", binary_name="terraform-ls", + sha256="724f45029f32d02d88b1952c7d1526c59fc8cd5dae49e31b9fed676a83f6cae7" if terraform_ls_version == "0.36.5" else None, + allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), RuntimeDependency( id="TerraformLS", description="terraform-ls for Linux (x64)", - url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip", + url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_linux_amd64.zip", platform_id="linux-x64", archive_type="zip", binary_name="terraform-ls", + sha256="37e645cc54fd03e863157e2a3e773e7a5ff1d6cb3d045e4c20860cac1f550a44" if terraform_ls_version == "0.36.5" else None, + allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), RuntimeDependency( id="TerraformLS", description="terraform-ls for Windows (x64)", - url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip", + url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_windows_amd64.zip", platform_id="win-x64", archive_type="zip", binary_name="terraform-ls.exe", + sha256="a9223462cac9e1c0e6ba33043fbf9fb4483609b6970b5681a6306b04366698ec" if terraform_ls_version == "0.36.5" else None, + allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS, ), ] ) diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index 4c428986..5b7ec4fe 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -19,7 +19,7 @@ from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.settings import SolidLSPSettings -from .common import RuntimeDependency, RuntimeDependencyCollection +from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command log = logging.getLogger(__name__) @@ -149,19 +149,20 @@ class TypeScriptLanguageServer(SolidLanguageServer): 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") + npm_registry = language_specific_config.get("npm_registry") deps = RuntimeDependencyCollection( [ RuntimeDependency( id="typescript", description="typescript package", - command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"], + command=build_npm_install_command("typescript", typescript_version, npm_registry), platform_id="any", ), RuntimeDependency( id="typescript-language-server", description="typescript-language-server package", - command=["npm", "install", "--prefix", "./", f"typescript-language-server@{typescript_language_server_version}"], + command=build_npm_install_command("typescript-language-server", typescript_language_server_version, npm_registry), platform_id="any", ), ] diff --git a/src/solidlsp/language_servers/vts_language_server.py b/src/solidlsp/language_servers/vts_language_server.py index 90fc18ff..dc6db249 100644 --- a/src/solidlsp/language_servers/vts_language_server.py +++ b/src/solidlsp/language_servers/vts_language_server.py @@ -14,13 +14,13 @@ from typing import cast from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings -from .common import RuntimeDependency, RuntimeDependencyCollection +from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command log = logging.getLogger(__name__) @@ -72,13 +72,16 @@ class VtsLanguageServer(SolidLanguageServer): PlatformId.WIN_arm64, ] 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") + npm_registry = vts_config.get("npm_registry") deps = RuntimeDependencyCollection( [ RuntimeDependency( id="vtsls", description="vtsls language server package", - command="npm install --prefix ./ @vtsls/language-server@0.2.9", + command=build_npm_install_command("@vtsls/language-server", vtsls_version, npm_registry), platform_id="any", ), ] diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py index 1314bec1..e810226c 100644 --- a/src/solidlsp/language_servers/vue_language_server.py +++ b/src/solidlsp/language_servers/vue_language_server.py @@ -15,7 +15,7 @@ from typing import Any from overrides import override from solidlsp import ls_types -from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection +from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command from solidlsp.language_servers.typescript_language_server import ( TypeScriptLanguageServer, prefer_non_node_modules_definition, @@ -407,31 +407,26 @@ class VueLanguageServer(SolidLanguageServer): typescript_language_server_version = typescript_config.get("typescript_language_server_version", "5.1.3") vue_config = solidlsp_settings.get_ls_specific_settings(Language.VUE) vue_language_server_version = vue_config.get("vue_language_server_version", "3.1.5") + npm_registry = vue_config.get("npm_registry", typescript_config.get("npm_registry")) deps = RuntimeDependencyCollection( [ RuntimeDependency( id="vue-language-server", description="Vue language server package (Volar)", - command=["npm", "install", "--prefix", "./", f"@vue/language-server@{vue_language_server_version}"], + command=build_npm_install_command("@vue/language-server", vue_language_server_version, npm_registry), platform_id="any", ), RuntimeDependency( id="typescript", description="TypeScript (required for tsdk)", - command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"], + command=build_npm_install_command("typescript", typescript_version, npm_registry), platform_id="any", ), RuntimeDependency( id="typescript-language-server", description="TypeScript language server (for Vue LS 3.x tsserver forwarding)", - command=[ - "npm", - "install", - "--prefix", - "./", - f"typescript-language-server@{typescript_language_server_version}", - ], + command=build_npm_install_command("typescript-language-server", typescript_language_server_version, npm_registry), platform_id="any", ), ] diff --git a/src/solidlsp/language_servers/yaml_language_server.py b/src/solidlsp/language_servers/yaml_language_server.py index d78b80e2..8a471921 100644 --- a/src/solidlsp/language_servers/yaml_language_server.py +++ b/src/solidlsp/language_servers/yaml_language_server.py @@ -9,7 +9,7 @@ import pathlib import shutil from typing import Any -from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection +from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -66,13 +66,15 @@ 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") + npm_registry = self._custom_settings.get("npm_registry") deps = RuntimeDependencyCollection( [ RuntimeDependency( id="yaml-language-server", description="yaml-language-server package (Red Hat)", - command="npm install --prefix ./ yaml-language-server@1.19.2", + command=build_npm_install_command("yaml-language-server", yaml_language_server_version, npm_registry), platform_id="any", ), ] diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index ae0363fd..1dad11f3 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -3,15 +3,19 @@ This file contains various utility functions like I/O operations, handling paths """ import gzip +import hashlib import logging import os import platform import shutil import subprocess +import tarfile import uuid import zipfile from enum import Enum from pathlib import Path, PurePath +from typing import Literal, cast +from urllib.parse import urlparse import charset_normalizer import requests @@ -204,58 +208,106 @@ class FileUtils: """ Downloads the file from the given URL to the given {target_path} """ - os.makedirs(os.path.dirname(target_path), exist_ok=True) + FileUtils.download_file_verified(url, target_path) + + @staticmethod + def download_file_verified( + url: str, + target_path: str, + expected_sha256: str | None = None, + allowed_hosts: tuple[str, ...] | list[str] | None = None, + ) -> None: + """ + Downloads a file from ``url`` to ``target_path`` with optional integrity and host validation. + """ + # validating the requested host + FileUtils._validate_download_host(url, allowed_hosts) + + # streaming the download into a temporary file + target_directory = os.path.dirname(target_path) or "." + os.makedirs(target_directory, exist_ok=True) + temp_file_path = str(PurePath(target_directory, f".{Path(target_path).name}.{uuid.uuid4().hex}.download")) + response: requests.Response | None = None try: response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: log.error(f"Error downloading file '{url}': {response.status_code} {response.text}") raise SolidLSPException("Error downloading file.") - with open(target_path, "wb") as f: - shutil.copyfileobj(response.raw, f) + + FileUtils._validate_download_host(response.url, allowed_hosts) + + with open(temp_file_path, "wb") as output_file: + for chunk in response.iter_content(chunk_size=1024 * 1024): + if chunk: + output_file.write(chunk) + + FileUtils._verify_sha256_if_configured(temp_file_path, expected_sha256) + + os.replace(temp_file_path, target_path) except Exception as exc: log.error(f"Error downloading file '{url}': {exc}") raise SolidLSPException("Error downloading file.") from None + finally: + if response is not None: + response.close() + if os.path.exists(temp_file_path): + Path.unlink(Path(temp_file_path)) @staticmethod def download_and_extract_archive(url: str, target_path: str, archive_type: str) -> None: """ Downloads the archive from the given URL having format {archive_type} and extracts it to the given {target_path} """ + FileUtils.download_and_extract_archive_verified(url, target_path, archive_type) + + @staticmethod + def download_and_extract_archive_verified( + url: str, + target_path: str, + archive_type: str, + expected_sha256: str | None = None, + allowed_hosts: tuple[str, ...] | list[str] | None = None, + ) -> None: + """ + Downloads an archive from ``url`` and extracts it safely into ``target_path``. + """ try: - tmp_files = [] + # preparing the temporary download location + tmp_files: list[str] = [] tmp_file_name = str(PurePath(os.path.expanduser("~"), "solidlsp_tmp", uuid.uuid4().hex)) - tmp_files.append(tmp_file_name) os.makedirs(os.path.dirname(tmp_file_name), exist_ok=True) - FileUtils.download_file(url, tmp_file_name) + + # downloading the archive with optional verification + FileUtils.download_file_verified(url, tmp_file_name, expected_sha256=expected_sha256, allowed_hosts=allowed_hosts) + tmp_files.append(tmp_file_name) + + # extracting the archive according to its format if archive_type in ["tar", "gztar", "bztar", "xztar"]: os.makedirs(target_path, exist_ok=True) - shutil.unpack_archive(tmp_file_name, target_path, archive_type) + FileUtils._extract_tar_archive(tmp_file_name, target_path, archive_type) elif archive_type == "zip": os.makedirs(target_path, exist_ok=True) - with zipfile.ZipFile(tmp_file_name, "r") as zip_ref: - for zip_info in zip_ref.infolist(): - extracted_path = zip_ref.extract(zip_info, target_path) - ZIP_SYSTEM_UNIX = 3 # zip file created on Unix system - if zip_info.create_system != ZIP_SYSTEM_UNIX: - continue - # extractall() does not preserve permissions - # see. https://github.com/python/cpython/issues/59999 - attrs = (zip_info.external_attr >> 16) & 0o777 - if attrs: - os.chmod(extracted_path, attrs) + FileUtils._extract_zip_archive(tmp_file_name, target_path) elif archive_type == "zip.gz": os.makedirs(target_path, exist_ok=True) tmp_file_name_ungzipped = tmp_file_name + ".zip" tmp_files.append(tmp_file_name_ungzipped) with gzip.open(tmp_file_name, "rb") as f_in, open(tmp_file_name_ungzipped, "wb") as f_out: shutil.copyfileobj(f_in, f_out) - shutil.unpack_archive(tmp_file_name_ungzipped, target_path, "zip") + FileUtils._extract_zip_archive(tmp_file_name_ungzipped, target_path) elif archive_type == "gz": - with gzip.open(tmp_file_name, "rb") as f_in, open(target_path, "wb") as f_out: + target_directory = os.path.dirname(target_path) or "." + os.makedirs(target_directory, exist_ok=True) + temp_output_path = str(PurePath(target_directory, f".{Path(target_path).name}.{uuid.uuid4().hex}.extract")) + tmp_files.append(temp_output_path) + with gzip.open(tmp_file_name, "rb") as f_in, open(temp_output_path, "wb") as f_out: shutil.copyfileobj(f_in, f_out) + os.replace(temp_output_path, target_path) elif archive_type == "binary": - # For single binary files, just move to target without extraction + target_directory = os.path.dirname(target_path) or "." + os.makedirs(target_directory, exist_ok=True) shutil.move(tmp_file_name, target_path) + tmp_files.remove(tmp_file_name) else: log.error(f"Unknown archive type '{archive_type}' for extraction") raise SolidLSPException(f"Unknown archive type '{archive_type}'") @@ -267,6 +319,102 @@ class FileUtils: if os.path.exists(tmp_file_name): Path.unlink(Path(tmp_file_name)) + @staticmethod + def calculate_sha256(file_path: str) -> str: + """ + Calculates the SHA256 checksum of a file. + """ + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as input_file: + for chunk in iter(lambda: input_file.read(8192), b""): + sha256_hash.update(chunk) + return sha256_hash.hexdigest() + + @staticmethod + def _verify_sha256_if_configured(file_path: str, expected_sha256: str | None) -> None: + """ + Verifies the SHA256 checksum of a file when an expected value is provided. + """ + if expected_sha256 is None: + return + + actual_sha256 = FileUtils.calculate_sha256(file_path) + if actual_sha256.lower() != expected_sha256.lower(): + raise SolidLSPException(f"Checksum verification failed for '{file_path}': expected {expected_sha256}, got {actual_sha256}") + + @staticmethod + def _validate_download_host(url: str, allowed_hosts: tuple[str, ...] | list[str] | None) -> None: + """ + Validates that a download URL resolves to one of the configured hosts. + """ + if not allowed_hosts: + return + + hostname = urlparse(url).hostname + normalized_allowed_hosts = {host.lower() for host in allowed_hosts} + if hostname is None or hostname.lower() not in normalized_allowed_hosts: + raise SolidLSPException( + f"Refusing to download from host '{hostname or ''}'; allowed hosts: {sorted(normalized_allowed_hosts)}" + ) + + @staticmethod + def _validate_extraction_path(member_name: str, target_path: str) -> str: + """ + Validates that an archive member stays within the extraction root and returns its destination path. + """ + normalized_parts = Path(member_name).parts + if any(part == ".." for part in normalized_parts): + raise SolidLSPException(f"Unsafe archive member '{member_name}': path traversal is not allowed") + + absolute_target_path = os.path.abspath(target_path) + absolute_member_path = os.path.abspath(os.path.join(target_path, member_name)) + if not (absolute_member_path.startswith(absolute_target_path + os.sep) or absolute_member_path == absolute_target_path): + raise SolidLSPException(f"Unsafe archive member '{member_name}': path escapes extraction directory") + + return absolute_member_path + + @staticmethod + def _extract_zip_archive(archive_path: str, target_path: str) -> None: + """ + Extracts a ZIP archive safely while preserving Unix permissions when available. + """ + with zipfile.ZipFile(archive_path, "r") as zip_ref: + for zip_info in zip_ref.infolist(): + extracted_path = FileUtils._validate_extraction_path(zip_info.filename, target_path) + + if zip_info.is_dir(): + os.makedirs(extracted_path, exist_ok=True) + continue + + os.makedirs(os.path.dirname(extracted_path), exist_ok=True) + with zip_ref.open(zip_info, "r") as source_file, open(extracted_path, "wb") as output_file: + shutil.copyfileobj(source_file, output_file) + + ZIP_SYSTEM_UNIX = 3 + if zip_info.create_system == ZIP_SYSTEM_UNIX: + attrs = (zip_info.external_attr >> 16) & 0o777 + if attrs: + os.chmod(extracted_path, attrs) + + @staticmethod + def _extract_tar_archive(archive_path: str, target_path: str, archive_type: str) -> None: + """ + Extracts a tar archive safely into the target directory. + """ + archive_mode_by_type = { + "tar": "r:", + "gztar": "r:gz", + "bztar": "r:bz2", + "xztar": "r:xz", + } + tar_mode = cast(Literal["r:", "r:gz", "r:bz2", "r:xz"], archive_mode_by_type[archive_type]) + + with tarfile.open(archive_path, tar_mode) as tar_ref: + for tar_member in tar_ref.getmembers(): + FileUtils._validate_extraction_path(tar_member.name, target_path) + + tar_ref.extractall(target_path) + class PlatformId(str, Enum): WIN_x86 = "win-x86" diff --git a/test/solidlsp/csharp/test_csharp_nuget_download.py b/test/solidlsp/csharp/test_csharp_nuget_download.py index 09b7eaed..2319766d 100644 --- a/test/solidlsp/csharp/test_csharp_nuget_download.py +++ b/test/solidlsp/csharp/test_csharp_nuget_download.py @@ -1,6 +1,7 @@ """Tests for C# language server NuGet package download from NuGet.org.""" import tempfile +from pathlib import Path from unittest.mock import patch import pytest @@ -15,7 +16,7 @@ class TestNuGetOrgDownload: """Test downloading Roslyn language server packages from NuGet.org.""" def test_download_nuget_package_uses_direct_url(self): - """Test that _download_nuget_package uses the URL from RuntimeDependency directly.""" + """Test that _download_nuget_package uses the URL and checksum from RuntimeDependency directly.""" with tempfile.TemporaryDirectory() as temp_dir: # Create a RuntimeDependency with a NuGet.org URL test_dependency = RuntimeDependency( @@ -41,21 +42,38 @@ class TestNuGetOrgDownload: repository_root_path="/fake/repo", ) - # Mock urllib.request.urlretrieve to capture the URL being used - with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve") as mock_retrieve: - with patch("solidlsp.language_servers.csharp_language_server.SafeZipExtractor"): - try: - dependency_provider._download_nuget_package(test_dependency) - except Exception: - # Expected to fail since we're mocking, but we want to check the URL - pass + captured_calls: list[tuple[str, str, str, str | None, tuple[str, ...] | list[str] | None]] = [] - # Verify that urlretrieve was called with the NuGet.org URL - assert mock_retrieve.called, "urlretrieve should be called" - called_url = mock_retrieve.call_args[0][0] - assert called_url == test_dependency.url, f"Should use URL from RuntimeDependency: {test_dependency.url}" - assert "nuget.org" in called_url, "Should use NuGet.org URL" - assert "azure" not in called_url.lower(), "Should not use Azure feed" + def fake_download_and_extract( + url: str, + target_path: str, + archive_type: str, + expected_sha256: str | None = None, + allowed_hosts: tuple[str, ...] | list[str] | None = None, + ) -> None: + captured_calls.append((url, target_path, archive_type, expected_sha256, allowed_hosts)) + Path(target_path).mkdir(parents=True, exist_ok=True) + + with patch( + "solidlsp.language_servers.csharp_language_server.FileUtils.download_and_extract_archive_verified", + side_effect=fake_download_and_extract, + ): + package_dir = dependency_provider._download_nuget_package(test_dependency) + + assert package_dir == Path(temp_dir) / "temp_downloads" / "roslyn-language-server.linux-x64.5.5.0-2.26078.4" + assert captured_calls == [ + ( + test_dependency.url, + str(package_dir), + "zip", + test_dependency.sha256, + test_dependency.allowed_hosts, + ) + ] + called_url = captured_calls[0][0] + assert called_url == test_dependency.url, f"Should use URL from RuntimeDependency: {test_dependency.url}" + assert "nuget.org" in called_url, "Should use NuGet.org URL" + assert "azure" not in called_url.lower(), "Should not use Azure feed" def test_runtime_dependencies_use_nuget_org_urls(self): """Test that _RUNTIME_DEPENDENCIES are configured with NuGet.org URLs.""" @@ -106,13 +124,7 @@ class TestNuGetOrgDownload: ) # Mock urllib.request.urlopen to track if Azure feed is accessed - with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlopen") as mock_urlopen: - with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve"): - with patch("solidlsp.language_servers.csharp_language_server.SafeZipExtractor"): - try: - dependency_provider._download_nuget_package(test_dependency) - except Exception: - pass - - # Verify that urlopen was NOT called (no service index lookup) - assert not mock_urlopen.called, "Should not call urlopen for Azure service index lookup" + with patch( + "solidlsp.language_servers.csharp_language_server.FileUtils.download_and_extract_archive_verified", + ): + dependency_provider._download_nuget_package(test_dependency) diff --git a/test/solidlsp/luau/test_luau_dependency_provider.py b/test/solidlsp/luau/test_luau_dependency_provider.py index 35ad9de3..d274364b 100644 --- a/test/solidlsp/luau/test_luau_dependency_provider.py +++ b/test/solidlsp/luau/test_luau_dependency_provider.py @@ -1,7 +1,5 @@ """Tests for the Luau language server dependency provider.""" -import io -import zipfile from pathlib import Path from unittest.mock import patch @@ -21,23 +19,6 @@ def _make_provider( ) -class _FakeResponse: - def __init__(self, content: bytes) -> None: - self.content = content - - def raise_for_status(self) -> None: - return - - def iter_content(self, chunk_size: int = 8192): - yield self.content - - def __enter__(self) -> "_FakeResponse": - return self - - def __exit__(self, exc_type, exc, tb) -> None: - return None - - @pytest.mark.luau class TestLuauDependencyProvider: def test_create_launch_command_uses_ls_path_override_and_adds_assets(self, tmp_path: Path) -> None: @@ -110,13 +91,24 @@ class TestLuauDependencyProvider: def test_download_luau_lsp_extracts_binary_into_ls_resources_dir(self, tmp_path: Path) -> None: provider = _make_provider(tmp_path) - archive = io.BytesIO() - with zipfile.ZipFile(archive, "w") as zip_file: - zip_file.writestr("nested/luau-lsp", "#!/bin/sh\n") + def fake_extract( + url: str, + target_path: str, + archive_type: str, + expected_sha256: str | None = None, + allowed_hosts: tuple[str, ...] | list[str] | None = None, + ) -> None: + del url, archive_type, expected_sha256, allowed_hosts + nested_dir = Path(target_path) / "nested" + nested_dir.mkdir(parents=True, exist_ok=True) + (nested_dir / "luau-lsp").write_text("#!/bin/sh\n", encoding="utf-8") with patch("solidlsp.language_servers.luau_lsp.platform.system", return_value="Linux"): with patch("solidlsp.language_servers.luau_lsp.platform.machine", return_value="aarch64"): - with patch("solidlsp.language_servers.luau_lsp.requests.get", return_value=_FakeResponse(archive.getvalue())): + with patch( + "solidlsp.language_servers.luau_lsp.FileUtils.download_and_extract_archive_verified", + side_effect=fake_extract, + ): binary_path = provider._download_luau_lsp() resolved_binary = Path(binary_path) @@ -127,10 +119,16 @@ class TestLuauDependencyProvider: def test_download_roblox_support_files_writes_into_ls_resources_dir(self, tmp_path: Path) -> None: provider = _make_provider(tmp_path) - with patch( - "solidlsp.language_servers.luau_lsp.requests.get", - side_effect=[_FakeResponse(b"types"), _FakeResponse(b"docs")], - ): + def fake_download( + url: str, target_path: str, expected_sha256: str | None = None, allowed_hosts: tuple[str, ...] | list[str] | None = None + ) -> None: + del expected_sha256, allowed_hosts + if "type-definitions" in url: + Path(target_path).write_bytes(b"types") + else: + Path(target_path).write_bytes(b"docs") + + with patch("solidlsp.language_servers.luau_lsp.FileUtils.download_file_verified", side_effect=fake_download): definitions_path, docs_path = provider._download_roblox_support_files("LocalUserSecurity") assert definitions_path == str(tmp_path / "globalTypes.LocalUserSecurity.d.luau") @@ -141,7 +139,10 @@ class TestLuauDependencyProvider: def test_download_standard_docs_writes_into_ls_resources_dir(self, tmp_path: Path) -> None: provider = _make_provider(tmp_path, {"platform": "standard"}) - with patch("solidlsp.language_servers.luau_lsp.requests.get", return_value=_FakeResponse(b"docs")): + with patch( + "solidlsp.language_servers.luau_lsp.FileUtils.download_file_verified", + side_effect=lambda url, target_path, expected_sha256=None, allowed_hosts=None: Path(target_path).write_bytes(b"docs"), + ): docs_path = provider._download_standard_docs() assert docs_path == str(tmp_path / "luau-en-us.json") diff --git a/test/solidlsp/util/test_ls_utils.py b/test/solidlsp/util/test_ls_utils.py new file mode 100644 index 00000000..cad603c3 --- /dev/null +++ b/test/solidlsp/util/test_ls_utils.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path +from unittest.mock import patch + +from solidlsp.ls_utils import FileUtils + + +class _FakeResponse: + def __init__(self, payload: bytes, final_url: str) -> None: + self.status_code = 200 + self.headers = {"content-encoding": "gzip"} + self.url = final_url + self._payload = payload + + def iter_content(self, chunk_size: int = 1): + for offset in range(0, len(self._payload), chunk_size): + yield self._payload[offset : offset + chunk_size] + + def close(self) -> None: + return None + + +def test_download_file_verified_writes_decoded_response_body(tmp_path: Path) -> None: + """Gzip-encoded transfer bodies should be written as decoded payload bytes.""" + payload = b"PK\x03\x04zip-content" + target_path = tmp_path / "downloaded.vsix" + final_url = "https://marketplace.visualstudio.com/example.vsix" + + with patch( + "solidlsp.ls_utils.requests.get", + return_value=_FakeResponse(payload, final_url), + ): + FileUtils.download_file_verified( + "https://marketplace.visualstudio.com/example.vsix", + str(target_path), + expected_sha256=hashlib.sha256(payload).hexdigest(), + allowed_hosts=("marketplace.visualstudio.com",), + ) + + assert target_path.read_bytes() == payload