From 637ab7b7eeb0151c6d530d3cfece9ce4788b8eeb Mon Sep 17 00:00:00 2001 From: sxh Date: Wed, 23 Sep 2026 21:43:28 +0800 Subject: [PATCH] Fix AL language server lookup in newer extension layouts (#2087) The AL adapter built a single executable path with a platform-specific subdirectory (`bin/win32/...` on Windows) and raised "executable not found" when it was absent. Compare the two VSIX packages from the marketplace with the URL the adapter itself uses: 18.0.2242655 (Serena's pinned version): bin/{win32,linux,darwin}/ 18.0.2732683 (the build reported in #2069): bin/, no platform subdirectories decision(al): probe both layouts instead of making the path configurable, since the extension build Serena downloads and the one the user has in VS Code differ by design. constraint(al): keep the platform subdirectory first so existing behaviour is unchanged for the pinned version; only a missing executable falls through. learned(al): neither VSIX declares a targetPlatform, yet the newer package contains no unix or darwin binaries at all; why Microsoft changed the packaging is upstream of this fix. The error message now lists every candidate it tried, so the next layout change upstream is identifiable from the failure alone rather than by reading the adapter. Fixes #2069 Co-authored-by: sxh313 --- CHANGELOG.md | 6 ++ .../language_servers/al_language_server.py | 40 ++++++--- test/solidlsp/al/test_al_executable_layout.py | 85 +++++++++++++++++++ 3 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 test/solidlsp/al/test_al_executable_layout.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 03941763..9280692c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -192,6 +192,12 @@ Status of the `main` branch. Changes prior to the next official version change w symbols, which was applied outside the caches; the processing of language servers that post-process symbols (e.g. Go, Nix, Fortran, F#, Vue) was therefore repeated on every request or, if it mutated symbols in place, re-applied to already processed cached results + - Fix: the AL language server executable was only searched for in a platform-specific subdirectory of + the extension's `bin` directory (`bin/win32/...` on Windows). Some AL extension builds (e.g. + 18.0.2732683, as opposed to the 18.0.2242655 that Serena pins) have no such subdirectories and + place the executable directly in `bin`, so activating an AL project failed with "AL Language + Server executable not found" for users whose VS Code extension was on such a build. Both layouts + are now probed, the platform subdirectory first (#2069) CLI: - Fix `project index-file` command not using only the relevant language server to index the given file (#1965) diff --git a/src/solidlsp/language_servers/al_language_server.py b/src/solidlsp/language_servers/al_language_server.py index 021cacf1..3e1f0190 100644 --- a/src/solidlsp/language_servers/al_language_server.py +++ b/src/solidlsp/language_servers/al_language_server.py @@ -36,6 +36,9 @@ INITIAL_AL_EXTENSION_SHA256 = "3971995e61a59dc4fcce4a65053072a67991ed624a16635c4 DEFAULT_AL_EXTENSION_VERSION = "18.0.2242655" DEFAULT_AL_EXTENSION_SHA256 = "3971995e61a59dc4fcce4a65053072a67991ed624a16635c4f2911f12564b2b9" +# Base name of the language server executable within the extension; on Windows it is suffixed with ".exe" +AL_HOST_EXECUTABLE_NAME = "Microsoft.Dynamics.Nav.EditorServices.Host" + def _al_extension_sha(version: str) -> str | None: if version == INITIAL_AL_EXTENSION_VERSION: @@ -188,10 +191,8 @@ class ALLanguageServer(SolidLanguageServer): 3. Configures executable permissions on Unix systems 4. Returns the properly formatted command string - The AL Language Server executable is located in different paths based on the platform: - - Windows: bin/win32/Microsoft.Dynamics.Nav.EditorServices.Host.exe - - Linux: bin/linux/Microsoft.Dynamics.Nav.EditorServices.Host - - macOS: bin/darwin/Microsoft.Dynamics.Nav.EditorServices.Host + The executable lives in the extension's `bin` directory, whose internal layout depends on + the extension version; see `_get_executable_path_candidates`. """ system = platform.system() @@ -209,11 +210,12 @@ class ALLanguageServer(SolidLanguageServer): "3. Ensure internet connection for automatic download" ) - # Build executable path based on platform - executable_path = cls._get_executable_path(extension_path, system) + # Build the executable path, tolerating the layouts of the different extension versions + candidates = cls._get_executable_path_candidates(extension_path, system) + executable_path = next((path for path in candidates if os.path.isfile(path)), None) - if not os.path.exists(executable_path): - raise RuntimeError(f"AL Language Server executable not found at: {executable_path}") + if executable_path is None: + raise RuntimeError("AL Language Server executable not found. Looked for:\n" + "\n".join(f" - {path}" for path in candidates)) # Prepare and return the executable command return cls._prepare_executable(executable_path, system) @@ -289,26 +291,36 @@ class ALLanguageServer(SolidLanguageServer): return None @classmethod - def _get_executable_path(cls, extension_path: str, system: str) -> str: + def _get_executable_path_candidates(cls, extension_path: str, system: str) -> list[str]: """ - Build platform-specific executable path. + Build the candidate paths of the language server executable for the given platform. + + The AL extension has shipped the executable in two layouts: in the builds up to at least + 18.0.2242655 it lies in a platform-specific subdirectory of `bin` (`bin/win32/...exe` on + Windows), whereas in build 18.0.2732683 the platform subdirectories are gone and it lies + directly in `bin`. Both are queried because the build that Serena downloads and the build + that the user has installed in VS Code need not be the same. Args: extension_path: Path to AL extension directory system: Operating system name Returns: - Full path to executable + Candidate paths to the executable, the platform subdirectory layout first """ if system == "Windows": - return os.path.join(extension_path, "bin", "win32", "Microsoft.Dynamics.Nav.EditorServices.Host.exe") + platform_dir, executable_name = "win32", AL_HOST_EXECUTABLE_NAME + ".exe" elif system == "Linux": - return os.path.join(extension_path, "bin", "linux", "Microsoft.Dynamics.Nav.EditorServices.Host") + platform_dir, executable_name = "linux", AL_HOST_EXECUTABLE_NAME elif system == "Darwin": - return os.path.join(extension_path, "bin", "darwin", "Microsoft.Dynamics.Nav.EditorServices.Host") + platform_dir, executable_name = "darwin", AL_HOST_EXECUTABLE_NAME else: raise RuntimeError(f"Unsupported platform: {system}") + return [ + os.path.join(extension_path, "bin", platform_dir, executable_name), + os.path.join(extension_path, "bin", executable_name), + ] @classmethod def _prepare_executable(cls, executable_path: str, system: str) -> str: diff --git a/test/solidlsp/al/test_al_executable_layout.py b/test/solidlsp/al/test_al_executable_layout.py new file mode 100644 index 00000000..999bafa1 --- /dev/null +++ b/test/solidlsp/al/test_al_executable_layout.py @@ -0,0 +1,85 @@ +"""Tests for locating the AL language server executable within the VS Code extension. + +The AL extension ships the executable in two different directory layouts depending on its build, +so both must be resolved without downloading the extension (see #2069). +""" + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from solidlsp.language_servers.al_language_server import ALLanguageServer +from solidlsp.ls_config import LanguageServerConfig, LanguageServerId +from solidlsp.settings import SolidLSPSettings + +pytestmark = pytest.mark.al + +# the name the AL extension ships in its `bin` directory, independent of Serena's constant +HOST_EXECUTABLE_NAME = "Microsoft.Dynamics.Nav.EditorServices.Host" + +_PLATFORM_SUBDIR = {"Windows": "win32", "Linux": "linux", "Darwin": "darwin"} + + +def _executable_name(system: str) -> str: + return HOST_EXECUTABLE_NAME + (".exe" if system == "Windows" else "") + + +def _create_host(extension_root: Path, system: str, *, layout: str) -> Path: + """Create a stub executable in either the versioned platform subdirectory or the flat `bin` + directory of the given extension root, returning its path. + """ + subdir = _PLATFORM_SUBDIR[system] if layout == "platform-subdir" else "" + host_path = extension_root.joinpath("bin", subdir, _executable_name(system)) + host_path.parent.mkdir(parents=True, exist_ok=True) + host_path.write_bytes(b"stub") + return host_path + + +def _setup(extension_root: Path, system: str) -> str: + with ( + patch("solidlsp.language_servers.al_language_server.platform.system", return_value=system), + patch.object(ALLanguageServer, "_find_al_extension", return_value=str(extension_root)), + ): + config = LanguageServerConfig(ls_id=LanguageServerId.AL) + return ALLanguageServer._setup_runtime_dependencies(config, SolidLSPSettings()) + + +@pytest.mark.parametrize("system", sorted(_PLATFORM_SUBDIR)) +@pytest.mark.parametrize("layout", ["platform-subdir", "flat"]) +def test_executable_is_resolved_in_either_extension_layout(tmp_path: Path, system: str, layout: str) -> None: + """Regression test for #2069: builds >= 18.0.2732683 place the executable directly in `bin`, + while the layout up to 18.0.2242655 keeps it in a platform subdirectory. + """ + host_path = _create_host(tmp_path, system, layout=layout) + + command = _setup(tmp_path, system) + + assert str(host_path) in command + + +@pytest.mark.parametrize("system", sorted(_PLATFORM_SUBDIR)) +def test_platform_subdir_layout_takes_precedence_when_both_are_present(tmp_path: Path, system: str) -> None: + flat_host = _create_host(tmp_path, system, layout="flat") + versioned_host = _create_host(tmp_path, system, layout="platform-subdir") + + command = _setup(tmp_path, system) + + assert str(versioned_host) in command + assert str(flat_host) not in command + + +def test_missing_executable_reports_every_candidate_layout(tmp_path: Path) -> None: + with pytest.raises(RuntimeError, match="AL Language Server executable not found") as exc_info: + _setup(tmp_path, "Windows") + + message = str(exc_info.value) + assert str(tmp_path / "bin" / "win32" / f"{HOST_EXECUTABLE_NAME}.exe") in message + assert str(tmp_path / "bin" / f"{HOST_EXECUTABLE_NAME}.exe") in message + + +def test_unsupported_platform_is_rejected(tmp_path: Path) -> None: + _create_host(tmp_path, "Windows", layout="flat") + + with pytest.raises(RuntimeError, match="Unsupported platform: FreeBSD"): + _setup(tmp_path, "FreeBSD")