feat: add runtime dependency setup and AL extension download for AL Language Server

This commit is contained in:
Torben Leth committed 2025-09-03 09:02:13 +02:00
1 parent 9ae42e2e1d
commit 40a84fb22d
2 files changed
+131 -5

No files matched your search

+5
View File
@@ -278,5 +278,10 @@ markers = [
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = '.git*,*.svg,*.lock,*.min.*'
check-hidden = true
[dependency-groups]
dev = [
"types-requests>=2.32.4.20250809",
]
# ignore-regex = ''
# ignore-words-list = ''
@@ -4,8 +4,11 @@ import logging
import os
import platform
import time
import zipfile
from pathlib import Path
import requests
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
@@ -35,8 +38,8 @@ class ALLanguageServer(SolidLanguageServer):
solidlsp_settings: Solid LSP settings
"""
# Get the language server command
cmd = self._get_language_server_command(logger)
# Setup runtime dependencies and get the language server command
cmd = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(
config,
@@ -47,7 +50,124 @@ class ALLanguageServer(SolidLanguageServer):
solidlsp_settings,
)
def _get_language_server_command(self, logger: LanguageServerLogger) -> str:
@classmethod
def _download_al_extension(cls, logger: LanguageServerLogger, url: str, target_dir: str) -> bool:
"""
Download and extract the AL extension from VS Code marketplace.
Returns:
True if successful, False otherwise
"""
try:
logger.log(f"Downloading AL extension from {url}", logging.INFO)
# Create target directory
os.makedirs(target_dir, exist_ok=True)
# Download with proper headers
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 file
temp_file = os.path.join(target_dir, "al_extension_temp.vsix")
total_size = int(response.headers.get("content-length", 0))
logger.log(f"Downloading {total_size / 1024 / 1024:.1f} MB...", logging.INFO)
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
logger.log(f"Download progress: {progress:.1f}%", logging.INFO)
logger.log("Download complete, extracting...", logging.INFO)
# Extract VSIX (which is a ZIP file)
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(target_dir)
# Clean up temp file
os.remove(temp_file)
logger.log("AL extension extracted successfully", logging.INFO)
return True
except Exception as e:
logger.log(f"Error downloading/extracting AL extension: {e}", logging.ERROR)
return False
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Setup runtime dependencies for AL Language Server and return the command to start the server.
"""
# Directory where the AL extension will be installed
al_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "al-extension")
# Determine platform-specific executable path within the extension
system = platform.system()
if system == "Windows":
executable_relative_path = os.path.join("extension", "bin", "win32", "Microsoft.Dynamics.Nav.EditorServices.Host.exe")
elif system == "Linux":
executable_relative_path = os.path.join("extension", "bin", "linux", "Microsoft.Dynamics.Nav.EditorServices.Host")
elif system == "Darwin":
executable_relative_path = os.path.join("extension", "bin", "darwin", "Microsoft.Dynamics.Nav.EditorServices.Host")
else:
raise RuntimeError(f"Unsupported platform: {system}")
executable_path = os.path.join(al_extension_dir, executable_relative_path)
# Check if the extension is already installed
if not os.path.exists(executable_path):
logger.log(f"AL Language Server not found at {executable_path}. Downloading from VS Code marketplace...", logging.INFO)
# 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"
# Try to download the extension
if cls._download_al_extension(logger, url, al_extension_dir):
logger.log("AL Extension downloaded successfully.", logging.INFO)
else:
logger.log("Failed to download AL extension, falling back to manual search.", logging.WARNING)
# Fall back to the old method of finding the extension
return cls._get_language_server_command_fallback(logger)
# Verify executable exists after download
if not os.path.exists(executable_path):
logger.log("AL Language Server executable still not found after download. Falling back to manual search.", logging.WARNING)
return cls._get_language_server_command_fallback(logger)
# Make sure executable has proper permissions on Unix-like systems
if system in ["Linux", "Darwin"]:
import stat
st = os.stat(executable_path)
os.chmod(executable_path, st.st_mode | stat.S_IEXEC)
logger.log(f"Using AL Language Server executable: {executable_path}", level=5)
# The AL Language Server uses stdio for LSP communication (no --stdio flag needed)
# On Windows, we need to properly quote the path if it contains spaces
if system == "Windows" and " " in executable_path:
return f'"{executable_path}"'
else:
return executable_path
@classmethod
def _get_language_server_command_fallback(cls, logger: LanguageServerLogger) -> str:
"""
Get the command to start the AL language server.
@@ -78,7 +198,7 @@ class ALLanguageServer(SolidLanguageServer):
logger.log(f"Found AL extension in current directory: {al_extension_path}", level=5)
else:
# Try to find in common VS Code extension locations
al_extension_path = self._find_al_extension_in_vscode(logger)
al_extension_path = cls._find_al_extension_in_vscode(logger)
if not al_extension_path:
raise RuntimeError(
@@ -121,7 +241,8 @@ class ALLanguageServer(SolidLanguageServer):
else:
return executable
def _find_al_extension_in_vscode(self, logger: LanguageServerLogger) -> str | None:
@classmethod
def _find_al_extension_in_vscode(cls, logger: LanguageServerLogger) -> str | None:
"""
Try to find AL extension in common VS Code extension locations.