Skip to content

Commit d42b08d

Browse files
committed
feat: expose HOST_ARCH and support running node on arm64 windows
1 parent cc32947 commit d42b08d

9 files changed

Lines changed: 72 additions & 36 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# caches
2+
.DS_Store
23
__pycache__
34
.mypy_cache
45
*.pickle

lsp_utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ._util import download_file
77
from ._util import extract_archive
88
from .api_wrapper_interface import ApiWrapperInterface
9+
from .constants import HOST_ARCH
910
from .constants import SETTINGS_FILENAME
1011
from .generic_client_handler import GenericClientHandler
1112
from .helpers import rmtree_ex
@@ -20,6 +21,7 @@
2021
from .uv_venv_manager import UvVenvManager
2122

2223
__all__ = [
24+
'HOST_ARCH',
2325
'SETTINGS_FILENAME',
2426
'ApiWrapperInterface',
2527
'ClientHandler',

lsp_utils/_util/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
from .download_file import download_file
44
from .download_file import extract_archive
5+
from .host_arch import get_host_arch
56
from .logging import logger
67

78
__all__ = [
89
'download_file',
910
'extract_archive',
11+
'get_host_arch',
1012
'logger',
1113
]

lsp_utils/_util/download_file.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ def download_file(url: str, target_path: Path) -> None:
2121
copyfileobj(response, out_file)
2222

2323

24-
def extract_archive(archive_file: Path, target_directory: Path) -> Path:
24+
def extract_archive(archive_file: Path, target_directory: Path) -> Path | None:
2525
"""
2626
Extract all files from an archive.
2727
2828
:param archive_file: Path to the archive file to extract.
2929
:param target_directory: Directory where files will be extracted.
30-
:return: Path to the extracted directory. If the archive contains a single
31-
root directory, the returned path will include that directory.
30+
:return: Path to the top-level directory within extracted files or `None`.
31+
If the archive contains all files in a single root directory then returns path to that directory.
32+
If there is no single parent then returns `None` (means that files are directly under `target_directory`).
3233
"""
3334
archive_name = archive_file.name
3435
if archive_name.endswith('.zip'):
@@ -42,17 +43,15 @@ def extract_archive(archive_file: Path, target_directory: Path) -> Path:
4243
raise Exception(msg)
4344

4445

45-
def extract_files_from_archive(archive: ZipFile | tarfile.TarFile, target_directory: Path) -> Path:
46+
def extract_files_from_archive(archive: ZipFile | tarfile.TarFile, target_directory: Path) -> Path | None:
4647
names = archive.namelist() if isinstance(archive, ZipFile) else archive.getnames()
4748
bad_members = [x for x in names if x.startswith(('/', '..'))]
4849
if bad_members:
4950
msg = f'archive appears to be malicious, bad filenames: {bad_members}'
5051
raise Exception(msg)
5152
topdir_name = get_top_level_directory(names)
5253
archive.extractall(str(target_directory)) # noqa: S202
53-
if topdir_name:
54-
return target_directory / topdir_name
55-
return target_directory
54+
return target_directory / topdir_name if topdir_name else None
5655

5756

5857
def get_top_level_directory(names: list[str]) -> str | None:

lsp_utils/_util/host_arch.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
from enum import IntEnum
4+
from typing import cast
5+
from typing import Literal
6+
import ctypes
7+
import sublime
8+
9+
Architecture = Literal["x32", "x64", "arm64"]
10+
11+
12+
class ImageFileMachine(IntEnum):
13+
IMAGE_FILE_MACHINE_AMD64 = 0x8664
14+
IMAGE_FILE_MACHINE_ARM64 = 0xAA64
15+
IMAGE_FILE_MACHINE_I386 = 0x014C
16+
IMAGE_FILE_MACHINE_UNKNOWN = 0x0000
17+
18+
19+
MACHINE_NAMES: dict[ImageFileMachine, Architecture] = {
20+
ImageFileMachine.IMAGE_FILE_MACHINE_AMD64: "x64",
21+
ImageFileMachine.IMAGE_FILE_MACHINE_ARM64: "arm64",
22+
ImageFileMachine.IMAGE_FILE_MACHINE_I386: "x32",
23+
ImageFileMachine.IMAGE_FILE_MACHINE_UNKNOWN: "x64",
24+
}
25+
26+
27+
def get_host_arch() -> Architecture:
28+
if sublime.platform() == "windows":
29+
kernel32 = ctypes.windll.kernel32
30+
c_ushort_p = ctypes.POINTER(ctypes.c_ushort)
31+
kernel32.IsWow64Process2.argtypes = (ctypes.c_void_p, c_ushort_p, c_ushort_p)
32+
process_machine = ctypes.c_ushort(0)
33+
native_machine = ctypes.c_ushort(0)
34+
success = kernel32.IsWow64Process2(
35+
kernel32.GetCurrentProcess(), ctypes.byref(process_machine), ctypes.byref(native_machine))
36+
if success:
37+
return MACHINE_NAMES[cast("ImageFileMachine", native_machine.value)]
38+
return sublime.arch()

lsp_utils/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
from __future__ import annotations
22

3+
from ._util import get_host_arch
4+
5+
HOST_ARCH = get_host_arch()
36
SETTINGS_FILENAME = 'lsp_utils.sublime-settings'

lsp_utils/node_runtime.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,20 @@
33
from ._util import download_file
44
from ._util import extract_archive
55
from ._util import logger
6+
from .constants import HOST_ARCH
67
from .constants import SETTINGS_FILENAME
78
from .helpers import rmtree_ex
89
from .helpers import run_command_sync
910
from .helpers import SemanticVersion
1011
from .helpers import version_to_string
1112
from .third_party.semantic_version import NpmSpec # pyright: ignore[reportPrivateLocalImportUsage]
1213
from .third_party.semantic_version import Version # pyright: ignore[reportPrivateLocalImportUsage]
13-
from contextlib import contextmanager
1414
from LSP.plugin.core.logging import debug
1515
from pathlib import Path
1616
from sublime_lib import ActivityIndicator
1717
from typing import Any
1818
from typing import cast
1919
from typing import final
20-
from typing import Generator
2120
from typing_extensions import override
2221
import os
2322
import shutil
@@ -131,6 +130,7 @@ def _resolve_node_runtime(
131130
log_lines.append(f' * {ex}')
132131
if not resolved_runtime:
133132
log_lines.append('--- lsp_utils Node.js resolving end ---')
133+
logger.debug('\n'.join(log_lines))
134134
msg = 'Failed resolving Node.js Runtime. Please check in the console for more details.'
135135
raise Exception(msg)
136136
return resolved_runtime
@@ -280,7 +280,6 @@ def install_node(self) -> None:
280280
with ActivityIndicator(sublime.active_window(), '[LSP] Setting up local Node.js'):
281281
install_node = NodeInstaller(self._base_dir, self._node_version)
282282
install_node.run()
283-
self._resolve_paths()
284283
self._install_in_progress_marker_file.unlink()
285284
self._resolve_paths()
286285

@@ -339,8 +338,8 @@ def run(self) -> None:
339338

340339
def _node_archive(self) -> tuple[str, str]:
341340
platform = sublime.platform()
342-
arch = sublime.arch()
343-
if platform == 'windows' and arch == 'x64':
341+
arch = HOST_ARCH
342+
if platform == 'windows':
344343
node_os = 'win'
345344
archive = 'zip'
346345
elif platform == 'linux':
@@ -357,8 +356,10 @@ def _node_archive(self) -> tuple[str, str]:
357356
return filename, dist_url
358357

359358
def _install_node(self, archive_path: Path) -> None:
360-
install_directory = extract_archive(archive_path, self._base_dir)
361-
install_directory.rename(install_directory.parent / 'node')
359+
temporary_target_path = self._base_dir / 'node-temp'
360+
extracted_path = extract_archive(archive_path, temporary_target_path) or temporary_target_path
361+
extracted_path.rename(self._base_dir / 'node')
362+
rmtree_ex(temporary_target_path, ignore_errors=True)
362363
archive_path.unlink()
363364

364365

@@ -462,7 +463,7 @@ def run(self) -> None:
462463

463464
def _node_archive(self) -> tuple[str, str]:
464465
platform = sublime.platform()
465-
arch = sublime.arch()
466+
arch = HOST_ARCH
466467
if platform == 'windows':
467468
platform_code = 'win32'
468469
elif platform == 'linux':
@@ -488,14 +489,3 @@ def _install(self, archive_path: Path) -> None:
488489
raise Exception(msg)
489490
finally:
490491
archive_path.unlink()
491-
492-
493-
@contextmanager
494-
def chdir(new_dir: str) -> Generator[None, None, None]:
495-
"""Context Manager for changing the working directory."""
496-
cur_dir = Path.cwd()
497-
os.chdir(new_dir)
498-
try:
499-
yield
500-
finally:
501-
os.chdir(cur_dir)

lsp_utils/uv_runner.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ def __init__(self, storage_path: Path) -> None:
6868
with TemporaryDirectory(dir=target_directory) as tempdir:
6969
archive_path = Path(tempdir, filename)
7070
download_file(url, archive_path)
71-
source_directory = extract_archive(archive_path, Path(tempdir))
72-
source_directory.joinpath(UV_BINARY).replace(target_uv_path)
71+
temporary_directory_path = Path(tempdir)
72+
extracted_path = extract_archive(archive_path, temporary_directory_path) or temporary_directory_path
73+
extracted_path.joinpath(UV_BINARY).replace(target_uv_path)
7374
target_uv_path.chmod(0o744)
7475
target_version_path.write_text(UV_TAG)
7576
self._uv = str(target_uv_path)

pyproject.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,6 @@ skip-magic-trailing-comma = false
7878
# Automatically detect the appropriate line ending.
7979
line-ending = "auto"
8080

81-
[tool.ruff.lint.isort]
82-
case-sensitive = false
83-
force-single-line = true
84-
from-first = true
85-
no-sections = true
86-
order-by-type = false
87-
required-imports = ["from __future__ import annotations"]
88-
8981
[tool.ruff.lint]
9082
# Enable preview rules.
9183
preview = true
@@ -118,6 +110,14 @@ ignore = [
118110
"TRY002", # https://docs.astral.sh/ruff/rules/raise-vanilla-class/
119111
]
120112

113+
[tool.ruff.lint.isort]
114+
case-sensitive = false
115+
force-single-line = true
116+
from-first = true
117+
no-sections = true
118+
order-by-type = false
119+
required-imports = ["from __future__ import annotations"]
120+
121121
[tool.tox]
122122
env_list = ["py3"]
123123
skipsdist = true

0 commit comments

Comments
 (0)