mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 03:13:51 +00:00
The repository is licensed per component. SolidLSP (src/solidlsp,
test/solidlsp, test/resources) remains MIT-licensed and independently
reusable; the Serena application (src/serena, src/interprompt, scripts,
test/serena, docs) is licensed under GPL-3.0-or-later starting with the v2
licensing transition. The change is not retroactive: all releases and
commits up to v1.7.0 / 74c38a65 (tag mit-final) remain available under MIT.
Since MIT is GPL-compatible, a distribution combining both (such as the
serena-agent package) is as a whole subject to GPL-3.0-or-later, while the
SolidLSP files themselves stay MIT and can be extracted and used separately
under MIT terms. The distribution metadata therefore declares
GPL-3.0-or-later, with both license texts shipped alongside it.
Serena originally began under the GPL (v2) and was switched to MIT in
May 2025 following community requests. We consider that change a mistake;
the substantial changes in v2 make this the appropriate time to revert it.
We want the best version of Serena to remain free.
Changes:
* LICENSE is now the licensing overview; canonical license texts live in
LICENSES/ (MIT.txt is the previous LICENSE verbatim, GPL-3.0-or-later.txt
is the unmodified FSF text)
* pyproject.toml declares the PEP 639 license expression
"GPL-3.0-or-later" and bundles LICENSE and LICENSES/* as license files;
the deprecated MIT classifier is dropped and flake.nix declares gpl3Plus;
README has per-component license badges and a License section
* SPDX-License-Identifier headers in all Python sources under src/ and
scripts/, added by the new idempotent scripts/add_spdx_headers.py, which
gen_prompt_factory.py also uses to keep the header on the generated
module; existing third-party notices are preserved
* CLA.md: Contributor License Agreement (contributor retains copyright;
grants a perpetual, irrevocable license including relicensing under any
terms, incl. proprietary/commercial; patent grant; authority
representations), to be enforced repository-wide via cla-assistant.io
* CONTRIBUTING.md, PR template and a new docs page explain the licensing
boundary and the CLA workflow
299 lines
14 KiB
Python
299 lines
14 KiB
Python
"""
|
|
Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
|
|
Contains various configurations and settings specific to Bash scripting.
|
|
"""
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import threading
|
|
|
|
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.ls_utils import FileUtils, PlatformId, PlatformUtils
|
|
from solidlsp.settings import SolidLSPSettings
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
# ShellCheck binary release info; download directly from upstream GitHub releases rather than
|
|
# relying on the `shellcheck` npm wrapper (which lazily downloads on first invocation and
|
|
# turned the bash LS install into a fragile, network-dependent step).
|
|
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
|
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
|
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
|
INITIAL_BASH_LANGUAGE_SERVER_VERSION = "5.6.0"
|
|
DEFAULT_BASH_LANGUAGE_SERVER_VERSION = "5.6.0"
|
|
|
|
# ShellCheck binary path already encodes _SHELLCHECK_VERSION (see _shellcheck_binary_path),
|
|
# so version bumps trigger reinstall correctly without further intervention.
|
|
_SHELLCHECK_VERSION = "0.10.0"
|
|
_SHELLCHECK_RELEASE_BASE = f"https://github.com/koalaman/shellcheck/releases/download/v{_SHELLCHECK_VERSION}"
|
|
_SHELLCHECK_ALLOWED_HOSTS = (
|
|
"github.com",
|
|
"objects.githubusercontent.com",
|
|
"release-assets.githubusercontent.com",
|
|
)
|
|
# Per-platform archive metadata: tar.xz on POSIX (extracts to shellcheck-v<ver>/shellcheck),
|
|
# zip on Windows (extracts to shellcheck.exe at archive root).
|
|
_SHELLCHECK_DEPENDENCIES: dict[PlatformId, dict[str, str]] = {
|
|
PlatformId.LINUX_x64: {
|
|
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.linux.x86_64.tar.xz",
|
|
"sha256": "6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87",
|
|
},
|
|
PlatformId.LINUX_arm64: {
|
|
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.linux.aarch64.tar.xz",
|
|
"sha256": "324a7e89de8fa2aed0d0c28f3dab59cf84c6d74264022c00c22af665ed1a09bb",
|
|
},
|
|
PlatformId.OSX_x64: {
|
|
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.darwin.x86_64.tar.xz",
|
|
"sha256": "ef27684f23279d112d8ad84e0823642e43f838993bbb8c0963db9b58a90464c2",
|
|
},
|
|
PlatformId.OSX_arm64: {
|
|
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.darwin.aarch64.tar.xz",
|
|
"sha256": "bbd2f14826328eee7679da7221f2bc3afb011f6a928b848c80c321f6046ddf81",
|
|
},
|
|
PlatformId.WIN_x64: {
|
|
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.zip",
|
|
"sha256": "eb6cd53a54ea97a56540e9d296ce7e2fa68715aa507ff23574646c1e12b2e143",
|
|
},
|
|
}
|
|
|
|
|
|
def _shellcheck_install_dir(bash_ls_dir: str) -> str:
|
|
return os.path.join(bash_ls_dir, "shellcheck")
|
|
|
|
|
|
def _shellcheck_binary_path(bash_ls_dir: str) -> str:
|
|
"""
|
|
Returns the path to the extracted ShellCheck binary. POSIX archives extract under
|
|
``shellcheck-v<ver>/shellcheck``; the Windows zip drops ``shellcheck.exe`` at archive root.
|
|
"""
|
|
install_dir = _shellcheck_install_dir(bash_ls_dir)
|
|
if os.name == "nt":
|
|
return os.path.join(install_dir, "shellcheck.exe")
|
|
return os.path.join(install_dir, f"shellcheck-v{_SHELLCHECK_VERSION}", "shellcheck")
|
|
|
|
|
|
class BashLanguageServer(SolidLanguageServer):
|
|
"""
|
|
Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
|
|
Contains various configurations and settings specific to Bash scripting.
|
|
"""
|
|
|
|
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
|
"""
|
|
Creates a BashLanguageServer instance. This class is not meant to be instantiated directly.
|
|
Use LanguageServer.create() instead.
|
|
"""
|
|
super().__init__(
|
|
config,
|
|
repository_root_path,
|
|
None,
|
|
"bash",
|
|
solidlsp_settings,
|
|
)
|
|
self.server_ready = threading.Event()
|
|
self.initialize_searcher_command_available = threading.Event()
|
|
|
|
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
|
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
|
|
|
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
|
def _get_or_install_core_dependency(self) -> str:
|
|
"""
|
|
Setup runtime dependencies for Bash Language Server and return the command to start the server.
|
|
"""
|
|
# verify node + npm are available for the bash-language-server install
|
|
is_node_installed = shutil.which("node") is not None
|
|
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", DEFAULT_BASH_LANGUAGE_SERVER_VERSION)
|
|
npm_registry = self._custom_settings.get("npm_registry")
|
|
|
|
bash_ls_dir = self._resolve_bash_ls_dir(bash_language_server_version)
|
|
managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin")
|
|
bash_executable_path = os.path.join(managed_bin_dir, "bash-language-server")
|
|
if os.name == "nt":
|
|
bash_executable_path += ".cmd"
|
|
|
|
# install bash-language-server via npm
|
|
if not os.path.exists(bash_executable_path):
|
|
bls_deps = RuntimeDependencyCollection(
|
|
[
|
|
RuntimeDependency(
|
|
id="bash-language-server",
|
|
description="bash-language-server package",
|
|
command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry),
|
|
platform_id="any",
|
|
),
|
|
]
|
|
)
|
|
log.info("Installing bash-language-server...")
|
|
bls_deps.install(bash_ls_dir)
|
|
|
|
# install ShellCheck binary directly from upstream releases for the current platform
|
|
self._install_shellcheck_if_missing(bash_ls_dir)
|
|
|
|
if not os.path.exists(bash_executable_path):
|
|
raise FileNotFoundError(
|
|
f"bash-language-server executable not found at {bash_executable_path}, something went wrong with the installation."
|
|
)
|
|
|
|
return bash_executable_path
|
|
|
|
@staticmethod
|
|
def _install_shellcheck_if_missing(bash_ls_dir: str) -> None:
|
|
"""
|
|
Downloads and extracts the platform-appropriate ShellCheck release into
|
|
``${bash_ls_dir}/shellcheck`` if the binary is not already present.
|
|
"""
|
|
binary_path = _shellcheck_binary_path(bash_ls_dir)
|
|
if os.path.exists(binary_path):
|
|
return
|
|
|
|
install_dir = _shellcheck_install_dir(bash_ls_dir)
|
|
os.makedirs(install_dir, exist_ok=True)
|
|
|
|
release = _SHELLCHECK_DEPENDENCIES.get(PlatformUtils.get_platform_id())
|
|
if release is None:
|
|
raise RuntimeError(f"ShellCheck has no upstream binary release for platform {PlatformUtils.get_platform_id().value}")
|
|
|
|
archive_type = "zip" if os.name == "nt" else "xztar"
|
|
log.info(f"Downloading ShellCheck v{_SHELLCHECK_VERSION} for {PlatformUtils.get_platform_id().value}")
|
|
FileUtils.download_and_extract_archive_verified(
|
|
release["url"],
|
|
install_dir,
|
|
archive_type,
|
|
expected_sha256=release["sha256"],
|
|
allowed_hosts=_SHELLCHECK_ALLOWED_HOSTS,
|
|
)
|
|
|
|
if not os.path.exists(binary_path):
|
|
raise FileNotFoundError(f"ShellCheck binary not found at {binary_path} after extraction; archive layout may have changed.")
|
|
|
|
# ensure the binary is executable on POSIX (zip extraction does not preserve perms)
|
|
if os.name != "nt":
|
|
current = os.stat(binary_path).st_mode
|
|
os.chmod(binary_path, current | 0o111)
|
|
|
|
def create_launch_command_env(self) -> dict[str, str]:
|
|
bash_language_server_version = self._custom_settings.get("bash_language_server_version", DEFAULT_BASH_LANGUAGE_SERVER_VERSION)
|
|
bash_ls_dir = self._resolve_bash_ls_dir(bash_language_server_version)
|
|
managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin")
|
|
return {
|
|
"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", ""),
|
|
"SHELLCHECK_PATH": _shellcheck_binary_path(bash_ls_dir),
|
|
}
|
|
|
|
def _resolve_bash_ls_dir(self, bash_language_server_version: str) -> str:
|
|
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
|
ls_dirname = (
|
|
"bash-lsp"
|
|
if bash_language_server_version == INITIAL_BASH_LANGUAGE_SERVER_VERSION
|
|
else f"bash-lsp-{bash_language_server_version}"
|
|
)
|
|
return os.path.join(self._ls_resources_dir, ls_dirname)
|
|
|
|
def _create_launch_command(self, core_path: str) -> list[str]:
|
|
return [core_path, "start"]
|
|
|
|
def _create_base_initialize_params(self) -> dict:
|
|
"""
|
|
Returns the initialize params for the Bash Language Server.
|
|
"""
|
|
initialize_params = {
|
|
"locale": "en",
|
|
"capabilities": {
|
|
"textDocument": {
|
|
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
|
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
|
|
"definition": {"dynamicRegistration": True},
|
|
"references": {"dynamicRegistration": True},
|
|
"documentSymbol": {
|
|
"dynamicRegistration": True,
|
|
"hierarchicalDocumentSymbolSupport": True,
|
|
"symbolKind": {"valueSet": list(range(1, 27))},
|
|
},
|
|
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
|
"signatureHelp": {"dynamicRegistration": True},
|
|
"codeAction": {"dynamicRegistration": True},
|
|
},
|
|
"workspace": {
|
|
"workspaceFolders": True,
|
|
"didChangeConfiguration": {"dynamicRegistration": True},
|
|
"symbol": {"dynamicRegistration": True},
|
|
},
|
|
},
|
|
}
|
|
return initialize_params
|
|
|
|
def _start_server(self) -> None:
|
|
"""
|
|
Starts the Bash Language Server, waits for the server to be ready and yields the LanguageServer instance.
|
|
"""
|
|
|
|
def register_capability_handler(params: dict) -> None:
|
|
assert "registrations" in params
|
|
for registration in params["registrations"]:
|
|
if registration["method"] == "workspace/executeCommand":
|
|
self.initialize_searcher_command_available.set()
|
|
return
|
|
|
|
def execute_client_command_handler(params: dict) -> list:
|
|
return []
|
|
|
|
def do_nothing(params: dict) -> None:
|
|
return
|
|
|
|
def window_log_message(msg: dict) -> None:
|
|
log.info(f"LSP: window/logMessage: {msg}")
|
|
# Check for bash-language-server ready signals
|
|
message_text = msg.get("message", "")
|
|
if "Analyzing" in message_text or "analysis complete" in message_text.lower():
|
|
log.info("Bash language server analysis signals detected")
|
|
self.server_ready.set()
|
|
|
|
self.server.on_request("client/registerCapability", register_capability_handler)
|
|
self.server.on_notification("window/logMessage", window_log_message)
|
|
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
|
self.server.on_notification("$/progress", do_nothing)
|
|
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
|
|
|
log.info("Starting Bash server process")
|
|
self.server.start()
|
|
initialize_params = self._create_initialize_params()
|
|
|
|
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
|
|
init_response = self.server.send.initialize(initialize_params)
|
|
log.debug(f"Received initialize response from bash server: {init_response}")
|
|
|
|
# Enhanced capability checks for bash-language-server 5.6.0
|
|
assert init_response["capabilities"]["textDocumentSync"] in [1, 2] # Full or Incremental
|
|
assert "completionProvider" in init_response["capabilities"]
|
|
|
|
# Verify document symbol support is available
|
|
if "documentSymbolProvider" in init_response["capabilities"]:
|
|
log.info("Bash server supports document symbols")
|
|
else:
|
|
log.warning("Warning: Bash server does not report document symbol support")
|
|
|
|
self.server.notify.initialized({})
|
|
|
|
# Wait for server readiness with timeout
|
|
log.info("Waiting for Bash language server to be ready...")
|
|
if not self.server_ready.wait(timeout=3.0):
|
|
# Fallback: assume server is ready after timeout
|
|
# This is common. bash-language-server doesn't always send explicit ready signals. Log as info
|
|
log.info("Timeout waiting for bash server ready signal, proceeding anyway")
|
|
self.server_ready.set()
|
|
else:
|
|
log.info("Bash server initialization complete")
|