refactor: use rustup-managed rust-analyzer instead of bundling (#495)

rust-analyzer now requires rustup to be installed

## Motivation

The previous implementation bundled rust-analyzer binaries that quickly
became outdated and required manual updates to Serena's codebase. This
caused version mismatches between the bundled rust-analyzer and users'
actual Rust toolchain versions.

## Changes

- Remove RuntimeDependency-based binary bundling for rust-analyzer
- Use rustup-managed rust-analyzer from the user's toolchain
- Add automatic installation of rust-analyzer component if missing
- Install rustup with minimal profile in Docker images

## Rationale

Rust development universally relies on rustup for toolchain management.
Unlike some other languages where system packages or bundled binaries
make sense, Rust developers already have rustup installed and expect
tools to use their toolchain's versions.

This approach ensures:
- rust-analyzer always matches the user's Rust version
- No outdated binaries shipped with Serena
- Automatic updates via `rustup update`
- Consistency with Rust ecosystem expectations

The LSP API of rust-analyzer is stable enough that version differences
are not a concern. If users don't have rustup, they'll get a clear error
message - this is acceptable since you cannot meaningfully develop Rust
without rustup.

## Docker Support

Added rustup installation to Docker images to ensure Rust support works
in containerized environments. Uses minimal profile to reduce image size
while still providing full rust-analyzer functionality.
This commit is contained in:
Vladimir authored and GitHub committed 2025-08-20 17:03:16 +02:00
1 parent 6ebff74f9d
commit 807ff1f56e
5 files changed
+72 -73

No files matched your search

+4
View File
@@ -1,6 +1,10 @@
# latest
Status of the `main` branch. Changes prior to the next official version change will appear here.
## Breaking Changes
* **Rust support now requires rustup**: The rust-analyzer is no longer bundled with Serena. Instead, it uses the rust-analyzer from your Rust toolchain managed by rustup. This ensures compatibility with your Rust version and eliminates outdated bundled binaries. If you don't have rustup installed, you'll need to install it from https://rustup.rs/
# 0.1.4
## Summary
+2 -2
View File
@@ -13,7 +13,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
**Test Markers:**
Available pytest markers for selective testing:
- `python`, `go`, `java`, `rust`, `typescript`, `php`, `csharp`, `elixir`, `terraform`, `clojure`
- `python`, `go`, `java`, `rust`, `typescript`, `php`, `csharp`, `elixir`, `terraform`, `clojure`, `swift`, `bash`, `ruby`
- `snapshot` - for symbolic editing operation tests
**Project Management:**
@@ -100,7 +100,7 @@ Configuration is loaded from (in order of precedence):
- **Symbol-based editing** - Uses LSP for precise code manipulation
- **Caching strategy** - Reduces language server overhead
- **Error recovery** - Automatic language server restart on crashes
- **Multi-language support** - 13+ languages with LSP integration
- **Multi-language support** - 16+ languages with LSP integration
- **MCP protocol** - Exposes tools to AI agents via Model Context Protocol
- **Async operation** - Non-blocking language server interactions
+9
View File
@@ -41,6 +41,15 @@ ENV PATH="${PATH}:/root/.local/bin"
# Install the latest version of uv
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
# Install Rust and rustup for rust-analyzer support (minimal profile)
ENV RUSTUP_HOME=/usr/local/rustup
ENV CARGO_HOME=/usr/local/cargo
ENV PATH="${CARGO_HOME}/bin:${PATH}"
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
--default-toolchain stable \
--profile minimal \
&& rustup component add rust-analyzer
# Set the working directory
WORKDIR /workspaces/serena
+1 -1
View File
@@ -102,7 +102,7 @@ With Serena, we provide
* TypeScript/Javascript
* PHP (uses Intelephense LSP; set `INTELEPHENSE_LICENSE_KEY` environment variable for premium features)
* Go (requires installation of gopls)
* Rust
* Rust (requires [rustup](https://rustup.rs/) - uses rust-analyzer from your toolchain)
* C#
* Ruby
* Swift
+56 -70
View File
@@ -5,6 +5,8 @@ Provides Rust specific instantiation of the LanguageServer class. Contains vario
import logging
import os
import pathlib
import shutil
import subprocess
import threading
from overrides import override
@@ -16,21 +18,72 @@ 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
class RustAnalyzer(SolidLanguageServer):
"""
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
"""
@staticmethod
def _get_rustup_version():
"""Get installed rustup version or None if not found."""
try:
result = subprocess.run(["rustup", "--version"], capture_output=True, text=True, check=False)
if result.returncode == 0:
return result.stdout.strip()
except FileNotFoundError:
return None
return None
@staticmethod
def _get_rust_analyzer_path():
"""Get rust-analyzer path via rustup or system PATH."""
# First try rustup
try:
result = subprocess.run(["rustup", "which", "rust-analyzer"], capture_output=True, text=True, check=False)
if result.returncode == 0:
return result.stdout.strip()
except FileNotFoundError:
pass
# Fallback to system PATH
return shutil.which("rust-analyzer")
@staticmethod
def _ensure_rust_analyzer_installed():
"""Ensure rust-analyzer is available, install via rustup if needed."""
path = RustAnalyzer._get_rust_analyzer_path()
if path:
return path
# Check if rustup is available
if not RustAnalyzer._get_rustup_version():
raise RuntimeError(
"Neither rust-analyzer nor rustup is installed.\n"
"Please install Rust via https://rustup.rs/ or install rust-analyzer separately."
)
# Try to install rust-analyzer component
result = subprocess.run(["rustup", "component", "add", "rust-analyzer"], check=False, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"Failed to install rust-analyzer via rustup: {result.stderr}")
# Try again after installation
path = RustAnalyzer._get_rust_analyzer_path()
if not path:
raise RuntimeError("rust-analyzer installation succeeded but binary not found in PATH")
return path
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
rustanalyzer_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
rustanalyzer_executable_path = self._ensure_rust_analyzer_installed()
logger.log(f"Using rust-analyzer at: {rustanalyzer_executable_path}", logging.INFO)
super().__init__(
config,
logger,
@@ -48,73 +101,6 @@ class RustAnalyzer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["target"]
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Setup runtime dependencies for rust_analyzer and return the command to start the server.
"""
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="RustAnalyzer",
description="RustAnalyzer for macOS (x64)",
url="https://github.com/rust-lang/rust-analyzer/releases/download/2025-08-04/rust-analyzer-x86_64-apple-darwin.gz",
platform_id="osx-x64",
archive_type="gz",
binary_name="rust_analyzer",
),
RuntimeDependency(
id="RustAnalyzer",
description="RustAnalyzer for macOS (arm64)",
url="https://github.com/rust-lang/rust-analyzer/releases/download/2025-08-04/rust-analyzer-aarch64-apple-darwin.gz",
platform_id="osx-arm64",
archive_type="gz",
binary_name="rust_analyzer",
),
RuntimeDependency(
id="RustAnalyzer",
description="RustAnalyzer for Linux (x64)",
url="https://github.com/rust-lang/rust-analyzer/releases/download/2025-08-04/rust-analyzer-x86_64-unknown-linux-gnu.gz",
platform_id="linux-x64",
archive_type="gz",
binary_name="rust_analyzer",
),
RuntimeDependency(
id="RustAnalyzer",
description="RustAnalyzer for Linux (arm64)",
url="https://github.com/rust-lang/rust-analyzer/releases/download/2025-08-04/rust-analyzer-aarch64-unknown-linux-gnu.gz",
platform_id="linux-arm64",
archive_type="gz",
binary_name="rust_analyzer",
),
RuntimeDependency(
id="RustAnalyzer",
description="RustAnalyzer for Windows (x64)",
url="https://github.com/rust-lang/rust-analyzer/releases/download/2025-08-04/rust-analyzer-x86_64-pc-windows-msvc.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="rust-analyzer.exe",
),
]
)
# assert platform_id.value in [
# "linux-x64",
# "win-x64",
# ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment"
rustanalyzer_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RustAnalyzer")
rustanalyzer_executable_path = deps.binary_path(rustanalyzer_ls_dir)
if not os.path.exists(rustanalyzer_ls_dir):
os.makedirs(rustanalyzer_ls_dir)
deps.install(logger, rustanalyzer_ls_dir)
assert os.path.exists(rustanalyzer_executable_path)
os.chmod(rustanalyzer_executable_path, 0o755)
return rustanalyzer_executable_path
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""