mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 10:16:00 +00:00
fix(solidity): isolate Hardhat state on macOS (#1910)
* fix(solidity): isolate Hardhat state on macOS --------- Co-authored-by: Yu-0312 <2.66498311e+08+Yu-0312@users.noreply.github.com>
This commit is contained in:
1 parent
14f1a3ce11
commit
6f86ae308e
5 files changed
+154
-1
No files matched your search
@@ -28,6 +28,9 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
Serena's own tools to close the gap (#1852)
|
||||
|
||||
* Language Servers:
|
||||
- Fix: the managed Solidity language server could report no diagnostics on macOS when Hardhat could not write
|
||||
its global state under ``~/Library``; Serena now gives the child process an isolated home-directory view
|
||||
via ``solidity_state_dir`` without changing the parent process's ``HOME`` (#1817)
|
||||
- Add Fatou support as an alternative Julia language server (`julia_fatou`)
|
||||
- Fix: Nextflow's `_flush_deferred_workspace_scan` marked the workspace scan flushed even when both
|
||||
of its `completion` probes failed, permanently skipping the flush (and silencing retries) for the
|
||||
|
||||
@@ -1164,8 +1164,19 @@ Supported settings:
|
||||
|---|---|---|
|
||||
| `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. |
|
||||
| `solidity_state_dir` | `<ls_resources_dir>/solidity-state` on macOS | Writable state root for the managed Solidity language server on macOS. Serena uses a child-process-only home-directory override so Hardhat does not write to `~/Library`; `HOME` in the Serena process is unchanged. |
|
||||
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
|
||||
|
||||
On macOS, if the default Solid-LSP resources directory is not writable, configure an alternative path:
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
solidity:
|
||||
solidity_state_dir: /path/to/writable/solidity-state
|
||||
```
|
||||
|
||||
This setting is ignored on Linux and Windows, where the existing launch environment is unchanged.
|
||||
|
||||
#### SystemVerilog
|
||||
|
||||
Serena uses `verible-verilog-ls` for SystemVerilog support.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
const os = require("node:os");
|
||||
const stateRoot = process.env.SERENA_SOLIDITY_STATE_DIR;
|
||||
|
||||
if (stateRoot) {
|
||||
// env-paths uses os.homedir() on Darwin and does not honor XDG_* variables.
|
||||
// Replace only this child process's view of the home directory; Serena's
|
||||
// parent environment and the user's HOME remain unchanged.
|
||||
os.homedir = () => stateRoot;
|
||||
}
|
||||
@@ -38,6 +38,12 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
the Nomic Foundation Solidity Language Server (@nomicfoundation/solidity-language-server).
|
||||
Supports go-to-definition, find references, document symbols, hover, and diagnostics.
|
||||
Requires Node.js and npm to be installed.
|
||||
|
||||
The ``solidity_state_dir`` setting can be used on macOS to choose the writable
|
||||
state root for the managed Solidity language server. By default, Serena uses
|
||||
``<ls_resources_dir>/solidity-state``. This is needed because Hardhat's
|
||||
``env-paths`` dependency resolves its Darwin paths from ``os.homedir()`` and
|
||||
ignores the XDG directory variables.
|
||||
"""
|
||||
|
||||
_VALIDATION_COMPLETION_TIMEOUT = 60.0
|
||||
@@ -84,6 +90,8 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
_HOMEDIR_PRELOAD = os.path.join(os.path.dirname(__file__), "solidity_homedir_preload.cjs")
|
||||
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
"""
|
||||
Install @nomicfoundation/solidity-language-server via npm and return the
|
||||
@@ -174,7 +182,36 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
forge_version = self._custom_settings.get("forge_version", DEFAULT_FORGE_VERSION)
|
||||
solidity_ls_dir = self._resolve_solidity_ls_dir(solidity_language_server_version, forge_version)
|
||||
managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin")
|
||||
return {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")}
|
||||
launch_env = {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")}
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
state_dir = self._get_solidity_state_dir()
|
||||
os.makedirs(state_dir, exist_ok=True)
|
||||
launch_env["SERENA_SOLIDITY_STATE_DIR"] = state_dir
|
||||
preload_arg = f"--require {self._quote_node_option_argument(self._HOMEDIR_PRELOAD)}"
|
||||
existing_node_options = os.environ.get("NODE_OPTIONS", "").strip()
|
||||
launch_env["NODE_OPTIONS"] = f"{existing_node_options} {preload_arg}".strip()
|
||||
|
||||
return launch_env
|
||||
|
||||
@staticmethod
|
||||
def _quote_node_option_argument(value: str) -> str:
|
||||
"""Quote a value for Node's NODE_OPTIONS parser.
|
||||
|
||||
Node parses NODE_OPTIONS independently from the shell. POSIX single-quote escaping
|
||||
produced by ``shlex.quote`` is therefore not understood by Node. Use double quotes,
|
||||
escaping the two characters that have special meaning inside Node's double-quoted
|
||||
option values.
|
||||
"""
|
||||
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
|
||||
def _get_solidity_state_dir(self) -> str:
|
||||
configured_state_dir = self._custom_settings.get("solidity_state_dir", os.path.join(self._ls_resources_dir, "solidity-state"))
|
||||
if not isinstance(configured_state_dir, str):
|
||||
raise TypeError("solidity_state_dir must be a path string")
|
||||
if not configured_state_dir:
|
||||
raise ValueError("solidity_state_dir must not be empty")
|
||||
return os.path.abspath(os.path.expanduser(configured_state_dir))
|
||||
|
||||
def _resolve_solidity_ls_dir(self, solidity_language_server_version: str, forge_version: str) -> str:
|
||||
# legacy unversioned dir reserved for INITIAL pair; any other combination goes into a versioned subdir
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.solidity_language_server import SolidityLanguageServer
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider(tmp_path: Path) -> SolidityLanguageServer.DependencyProvider:
|
||||
return SolidityLanguageServer.DependencyProvider(
|
||||
SolidLSPSettings.CustomLSSettings({}),
|
||||
str(tmp_path / "language-server-resources"),
|
||||
)
|
||||
|
||||
|
||||
def test_darwin_uses_isolated_state_dir_and_preserves_node_options(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
state_dir = tmp_path / "solidity state"
|
||||
existing_node_options = "--max-old-space-size=2048 --trace-warnings"
|
||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
||||
monkeypatch.setenv("NODE_OPTIONS", existing_node_options)
|
||||
|
||||
provider = SolidityLanguageServer.DependencyProvider(
|
||||
SolidLSPSettings.CustomLSSettings({"solidity_state_dir": str(state_dir)}),
|
||||
str(tmp_path / "language-server-resources"),
|
||||
)
|
||||
|
||||
launch_env = provider.create_launch_command_env()
|
||||
|
||||
assert launch_env["SERENA_SOLIDITY_STATE_DIR"] == str(state_dir)
|
||||
assert state_dir.is_dir()
|
||||
assert launch_env["NODE_OPTIONS"] == (
|
||||
f"{existing_node_options} --require {provider._quote_node_option_argument(provider._HOMEDIR_PRELOAD)}"
|
||||
)
|
||||
assert "HOME" not in launch_env
|
||||
assert os.environ["NODE_OPTIONS"] == existing_node_options
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="Node.js is required for this test")
|
||||
def test_darwin_node_options_loads_preload_path_with_spaces(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
||||
monkeypatch.delenv("NODE_OPTIONS", raising=False)
|
||||
|
||||
preload_path = tmp_path / "solidity state" / "preload.cjs"
|
||||
preload_path.parent.mkdir()
|
||||
preload_path.write_text("console.log('PRELOAD_LOADED_OK')", encoding="utf-8")
|
||||
provider = SolidityLanguageServer.DependencyProvider(
|
||||
SolidLSPSettings.CustomLSSettings({"solidity_state_dir": str(tmp_path / "state dir")}),
|
||||
str(tmp_path / "language-server-resources"),
|
||||
)
|
||||
monkeypatch.setattr(provider, "_HOMEDIR_PRELOAD", str(preload_path))
|
||||
|
||||
launch_env = provider.create_launch_command_env()
|
||||
result = subprocess.run(
|
||||
["node", "-e", "console.log('NODE_STARTED_OK')"],
|
||||
env={**os.environ, **launch_env},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "PRELOAD_LOADED_OK" in result.stdout
|
||||
assert "NODE_STARTED_OK" in result.stdout
|
||||
|
||||
|
||||
def test_darwin_defaults_state_dir_under_language_server_resources(
|
||||
provider: SolidityLanguageServer.DependencyProvider, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
||||
|
||||
launch_env = provider.create_launch_command_env()
|
||||
|
||||
assert launch_env["SERENA_SOLIDITY_STATE_DIR"] == str(Path(provider._ls_resources_dir) / "solidity-state")
|
||||
assert Path(launch_env["SERENA_SOLIDITY_STATE_DIR"]).is_dir()
|
||||
|
||||
|
||||
def test_non_darwin_does_not_add_state_isolation(
|
||||
monkeypatch: pytest.MonkeyPatch, provider: SolidityLanguageServer.DependencyProvider
|
||||
) -> None:
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
|
||||
launch_env = provider.create_launch_command_env()
|
||||
|
||||
assert "SERENA_SOLIDITY_STATE_DIR" not in launch_env
|
||||
assert "NODE_OPTIONS" not in launch_env
|
||||
Reference in new issue
Block a user