Security enhancement: pinned all runtime deps, verify checksums

Extended documentation of ls_specific_settings and of our security approach
This commit is contained in:
Michael Panchenko committed 2026-03-27 13:00:09 +01:00
1 parent c6250aeeab
commit f00a992e5d
40 files changed
+1724 -614

No files matched your search

+415 -17
View File
@@ -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.
+146 -9
View File
@@ -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.
```
@@ -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")
@@ -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",
),
]
@@ -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",
),
]
@@ -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,
),
]
)
+70 -42
View File
@@ -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)
+25 -2
View File
@@ -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:
@@ -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
@@ -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,
),
]
)
+91 -28
View File
@@ -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)
@@ -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}")
@@ -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",
),
]
@@ -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,
@@ -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)
@@ -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",
@@ -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",
)
]
@@ -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(
+45 -38
View File
@@ -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
+22 -21
View File
@@ -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)
+63 -41
View File
@@ -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
@@ -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")
+39 -3
View File
@@ -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)
@@ -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"
}
]
}
}
+37 -22
View File
@@ -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",
),
]
)
+14 -2
View File
@@ -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."
@@ -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():
+26 -7
View File
@@ -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:
@@ -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",
),
]
@@ -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,
),
]
)
+27 -72
View File
@@ -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:
+24 -6
View File
@@ -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,
),
]
)
@@ -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",
),
]
@@ -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",
),
]
@@ -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",
),
]
@@ -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",
),
]
+169 -21
View File
@@ -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 '<unknown>'}'; 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"
@@ -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)
@@ -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")
+42
View File
@@ -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