dbx-tools-node-runtime 0.9.54__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbx_tools/node_runtime/__init__.py +31 -0
- dbx_tools/node_runtime/__main__.py +47 -0
- dbx_tools/node_runtime/bootstrap.py +362 -0
- dbx_tools/node_runtime/py.typed +1 -0
- dbx_tools/node_runtime/runtime.js +11362 -0
- dbx_tools/node_runtime/runtime.py +226 -0
- dbx_tools_node_runtime-0.9.54.dist-info/METADATA +101 -0
- dbx_tools_node_runtime-0.9.54.dist-info/RECORD +11 -0
- dbx_tools_node_runtime-0.9.54.dist-info/WHEEL +4 -0
- dbx_tools_node_runtime-0.9.54.dist-info/entry_points.txt +3 -0
- dbx_tools_node_runtime-0.9.54.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .bootstrap import (
|
|
2
|
+
NodeEnvironment,
|
|
3
|
+
ensure_pythonmonkey,
|
|
4
|
+
node_environment,
|
|
5
|
+
prepare_node_environment,
|
|
6
|
+
runtime_lock_directory,
|
|
7
|
+
)
|
|
8
|
+
from .runtime import (
|
|
9
|
+
MISSING,
|
|
10
|
+
RUNTIME_ABI_VERSION,
|
|
11
|
+
NodeObject,
|
|
12
|
+
RuntimeBundle,
|
|
13
|
+
get_builtin,
|
|
14
|
+
get_runtime,
|
|
15
|
+
load_bundle,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"MISSING",
|
|
20
|
+
"RUNTIME_ABI_VERSION",
|
|
21
|
+
"NodeEnvironment",
|
|
22
|
+
"NodeObject",
|
|
23
|
+
"RuntimeBundle",
|
|
24
|
+
"ensure_pythonmonkey",
|
|
25
|
+
"get_builtin",
|
|
26
|
+
"get_runtime",
|
|
27
|
+
"load_bundle",
|
|
28
|
+
"node_environment",
|
|
29
|
+
"prepare_node_environment",
|
|
30
|
+
"runtime_lock_directory",
|
|
31
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .bootstrap import ensure_pythonmonkey
|
|
8
|
+
|
|
9
|
+
"""Command-line entrypoint for preparing PythonMonkey and executing CommonJS files."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run_javascript(runtime: Any, path: Path) -> Any:
|
|
13
|
+
resolved = path.expanduser().resolve()
|
|
14
|
+
if not resolved.is_file():
|
|
15
|
+
raise FileNotFoundError(f"JavaScript file does not exist: {resolved}")
|
|
16
|
+
return runtime.require(str(resolved))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> None:
|
|
20
|
+
"""Install or verify PythonMonkey and optionally execute JavaScript."""
|
|
21
|
+
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
description="Install the locked PythonMonkey runtime and optionally run JavaScript."
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"javascript",
|
|
27
|
+
nargs="?",
|
|
28
|
+
type=Path,
|
|
29
|
+
help="CommonJS JavaScript file to execute with PythonMonkey require.",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--lock-directory",
|
|
33
|
+
type=Path,
|
|
34
|
+
help="Override the directory used for cross-process installation locks.",
|
|
35
|
+
)
|
|
36
|
+
arguments = parser.parse_args()
|
|
37
|
+
runtime = ensure_pythonmonkey(arguments.lock_directory)
|
|
38
|
+
if arguments.javascript is None:
|
|
39
|
+
print(Path(runtime.__file__).resolve())
|
|
40
|
+
return
|
|
41
|
+
result = _run_javascript(runtime, arguments.javascript)
|
|
42
|
+
if result is not None:
|
|
43
|
+
print(result)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import importlib
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import shutil
|
|
10
|
+
import stat
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from threading import RLock
|
|
19
|
+
from types import ModuleType
|
|
20
|
+
|
|
21
|
+
"""Install and load the locked PythonMonkey runtime in managed Python environments."""
|
|
22
|
+
|
|
23
|
+
_LOCK_DIRECTORY_ENVIRONMENT = "DBX_TOOLS_NODE_RUNTIME_LOCK_DIRECTORY"
|
|
24
|
+
_NODEJS_WHEEL_REQUIREMENT = "nodejs-wheel>=22.20,<23"
|
|
25
|
+
_PYTHONMONKEY_VERSION = "1.3.2"
|
|
26
|
+
_PROCESS_LOCK = RLock()
|
|
27
|
+
_PROCESS_RUNTIME: ModuleType | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class NodeEnvironment:
|
|
32
|
+
"""Resolved Node.js files and executable launcher directory."""
|
|
33
|
+
|
|
34
|
+
package_root: Path
|
|
35
|
+
launcher_directory: Path
|
|
36
|
+
node: Path
|
|
37
|
+
npm_cli: Path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def runtime_lock_directory() -> Path:
|
|
41
|
+
"""Return the directory used only for cross-process installation locks."""
|
|
42
|
+
|
|
43
|
+
configured = os.environ.get(_LOCK_DIRECTORY_ENVIRONMENT)
|
|
44
|
+
if configured:
|
|
45
|
+
return Path(configured).expanduser().resolve()
|
|
46
|
+
if sys.platform == "win32":
|
|
47
|
+
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
48
|
+
elif sys.platform == "darwin":
|
|
49
|
+
base = Path.home() / "Library" / "Caches"
|
|
50
|
+
else:
|
|
51
|
+
base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
52
|
+
return base / "dbx-tools" / "node-runtime" / "locks"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _environment_key() -> str:
|
|
56
|
+
identity = f"{Path(sys.prefix).resolve()}\0{Path(sys.executable).resolve()}"
|
|
57
|
+
return hashlib.sha256(identity.encode()).hexdigest()[:16]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _lock_path(name: str, directory: str | Path | None = None) -> Path:
|
|
61
|
+
root = (
|
|
62
|
+
Path(directory).expanduser().resolve()
|
|
63
|
+
if directory is not None
|
|
64
|
+
else runtime_lock_directory()
|
|
65
|
+
)
|
|
66
|
+
return root / f"{_environment_key()}-{name}.lock"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _distribution_version(name: str) -> str | None:
|
|
70
|
+
try:
|
|
71
|
+
return importlib.metadata.version(name)
|
|
72
|
+
except importlib.metadata.PackageNotFoundError:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _environment_pythonmonkey() -> ModuleType | None:
|
|
77
|
+
loaded = sys.modules.get("pythonmonkey")
|
|
78
|
+
if loaded is not None:
|
|
79
|
+
version = getattr(loaded, "__version__", None)
|
|
80
|
+
if version != _PYTHONMONKEY_VERSION:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
f"PythonMonkey {version or 'with an unknown version'} is already loaded; "
|
|
83
|
+
f"dbx-tools-node-runtime requires {_PYTHONMONKEY_VERSION}"
|
|
84
|
+
)
|
|
85
|
+
importlib.import_module("pythonmonkey.require")
|
|
86
|
+
return loaded
|
|
87
|
+
if _distribution_version("pythonmonkey") != _PYTHONMONKEY_VERSION:
|
|
88
|
+
return None
|
|
89
|
+
module = importlib.import_module("pythonmonkey")
|
|
90
|
+
importlib.import_module("pythonmonkey.require")
|
|
91
|
+
return module
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@contextmanager
|
|
95
|
+
def _file_lock(path: Path) -> Iterator[None]:
|
|
96
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
handle = path.open("a+b")
|
|
98
|
+
try:
|
|
99
|
+
if os.name == "nt":
|
|
100
|
+
import msvcrt
|
|
101
|
+
|
|
102
|
+
if path.stat().st_size == 0:
|
|
103
|
+
handle.write(b"0")
|
|
104
|
+
handle.flush()
|
|
105
|
+
handle.seek(0)
|
|
106
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
107
|
+
else:
|
|
108
|
+
import fcntl
|
|
109
|
+
|
|
110
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
111
|
+
yield
|
|
112
|
+
finally:
|
|
113
|
+
if os.name == "nt":
|
|
114
|
+
import msvcrt
|
|
115
|
+
|
|
116
|
+
handle.seek(0)
|
|
117
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
118
|
+
else:
|
|
119
|
+
import fcntl
|
|
120
|
+
|
|
121
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
122
|
+
handle.close()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _nodejs_wheel_root() -> Path | None:
|
|
126
|
+
importlib.invalidate_caches()
|
|
127
|
+
spec = importlib.util.find_spec("nodejs_wheel")
|
|
128
|
+
if spec is None or spec.origin is None:
|
|
129
|
+
return None
|
|
130
|
+
return Path(spec.origin).resolve().parent
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _write_posix_launcher(path: Path, command: Sequence[Path | str]) -> None:
|
|
134
|
+
arguments = " ".join(shlex.quote(str(argument)) for argument in command)
|
|
135
|
+
path.write_text(f'#!/bin/sh\nexec {arguments} "$@"\n', encoding="utf-8")
|
|
136
|
+
path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _write_windows_launcher(path: Path, command: Sequence[Path | str]) -> None:
|
|
140
|
+
arguments = " ".join(f'"{argument}"' for argument in command)
|
|
141
|
+
path.write_text(f"@echo off\r\n{arguments} %*\r\n", encoding="utf-8")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def prepare_node_environment(
|
|
145
|
+
directory: str | Path | None = None,
|
|
146
|
+
*,
|
|
147
|
+
package_root: str | Path | None = None,
|
|
148
|
+
) -> NodeEnvironment:
|
|
149
|
+
"""Create direct Node and npm launchers backed by `nodejs-wheel` files."""
|
|
150
|
+
|
|
151
|
+
resolved_root = (
|
|
152
|
+
Path(package_root).resolve() if package_root is not None else _nodejs_wheel_root()
|
|
153
|
+
)
|
|
154
|
+
if resolved_root is None:
|
|
155
|
+
raise RuntimeError(
|
|
156
|
+
"nodejs-wheel is unavailable; call ensure_pythonmonkey() to install the locked runtime"
|
|
157
|
+
)
|
|
158
|
+
node = resolved_root / "bin" / ("node.exe" if os.name == "nt" else "node")
|
|
159
|
+
npm_cli = resolved_root / "lib" / "node_modules" / "npm" / "bin" / "npm-cli.js"
|
|
160
|
+
for required in (node, npm_cli):
|
|
161
|
+
if not required.is_file():
|
|
162
|
+
raise RuntimeError(f"nodejs-wheel is missing required file: {required}")
|
|
163
|
+
|
|
164
|
+
launcher_directory = (
|
|
165
|
+
Path(directory).resolve()
|
|
166
|
+
if directory is not None
|
|
167
|
+
else Path(tempfile.mkdtemp(prefix="dbx-tools-node-launchers-"))
|
|
168
|
+
)
|
|
169
|
+
launcher_directory.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
if os.name == "nt":
|
|
171
|
+
_write_windows_launcher(launcher_directory / "node.cmd", [node])
|
|
172
|
+
_write_windows_launcher(launcher_directory / "npm.cmd", [node, npm_cli])
|
|
173
|
+
else:
|
|
174
|
+
_write_posix_launcher(launcher_directory / "node", [node])
|
|
175
|
+
_write_posix_launcher(launcher_directory / "npm", [node, npm_cli])
|
|
176
|
+
return NodeEnvironment(
|
|
177
|
+
package_root=resolved_root,
|
|
178
|
+
launcher_directory=launcher_directory,
|
|
179
|
+
node=node,
|
|
180
|
+
npm_cli=npm_cli,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@contextmanager
|
|
185
|
+
def node_environment(
|
|
186
|
+
directory: str | Path | None = None,
|
|
187
|
+
*,
|
|
188
|
+
package_root: str | Path | None = None,
|
|
189
|
+
) -> Iterator[NodeEnvironment]:
|
|
190
|
+
"""Temporarily prepend direct Node and npm launchers to `PATH`."""
|
|
191
|
+
|
|
192
|
+
temporary = directory is None
|
|
193
|
+
environment = prepare_node_environment(directory, package_root=package_root)
|
|
194
|
+
previous = os.environ.get("PATH")
|
|
195
|
+
os.environ["PATH"] = os.pathsep.join(
|
|
196
|
+
value for value in (str(environment.launcher_directory), previous) if value
|
|
197
|
+
)
|
|
198
|
+
try:
|
|
199
|
+
yield environment
|
|
200
|
+
finally:
|
|
201
|
+
if previous is None:
|
|
202
|
+
os.environ.pop("PATH", None)
|
|
203
|
+
else:
|
|
204
|
+
os.environ["PATH"] = previous
|
|
205
|
+
if temporary:
|
|
206
|
+
shutil.rmtree(environment.launcher_directory, ignore_errors=True)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _pip_install(
|
|
210
|
+
requirement: str,
|
|
211
|
+
*,
|
|
212
|
+
environment: Mapping[str, str] | None = None,
|
|
213
|
+
only_binary: Sequence[str] = (),
|
|
214
|
+
) -> None:
|
|
215
|
+
_ensure_pip()
|
|
216
|
+
command = [
|
|
217
|
+
sys.executable,
|
|
218
|
+
"-m",
|
|
219
|
+
"pip",
|
|
220
|
+
"install",
|
|
221
|
+
"--disable-pip-version-check",
|
|
222
|
+
"--no-input",
|
|
223
|
+
]
|
|
224
|
+
if only_binary:
|
|
225
|
+
command.extend(["--only-binary", ",".join(only_binary)])
|
|
226
|
+
command.append(requirement)
|
|
227
|
+
result = subprocess.run(
|
|
228
|
+
command,
|
|
229
|
+
check=False,
|
|
230
|
+
env=dict(environment) if environment is not None else None,
|
|
231
|
+
stdout=subprocess.PIPE,
|
|
232
|
+
stderr=subprocess.STDOUT,
|
|
233
|
+
text=True,
|
|
234
|
+
)
|
|
235
|
+
if result.returncode != 0:
|
|
236
|
+
raise RuntimeError(
|
|
237
|
+
f"Could not install locked runtime requirement {requirement} "
|
|
238
|
+
f"into {sys.prefix}:\n{result.stdout}"
|
|
239
|
+
)
|
|
240
|
+
importlib.invalidate_caches()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _ensure_pip() -> None:
|
|
244
|
+
"""Install CPython's bundled pip when the active environment omits it."""
|
|
245
|
+
if importlib.util.find_spec("pip") is not None:
|
|
246
|
+
return
|
|
247
|
+
command = [sys.executable, "-m", "ensurepip", "--upgrade"]
|
|
248
|
+
result = subprocess.run(
|
|
249
|
+
command,
|
|
250
|
+
check=False,
|
|
251
|
+
stdout=subprocess.PIPE,
|
|
252
|
+
stderr=subprocess.STDOUT,
|
|
253
|
+
text=True,
|
|
254
|
+
)
|
|
255
|
+
importlib.invalidate_caches()
|
|
256
|
+
if result.returncode != 0 or importlib.util.find_spec("pip") is None:
|
|
257
|
+
raise RuntimeError(
|
|
258
|
+
f"Could not install pip into {sys.prefix} with ensurepip:\n{result.stdout}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _npm_available() -> bool:
|
|
263
|
+
return shutil.which("npm") is not None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _ensure_nodejs_wheel(lock_directory: str | Path | None = None) -> Path | None:
|
|
267
|
+
if _npm_available():
|
|
268
|
+
return None
|
|
269
|
+
package_root = _nodejs_wheel_root()
|
|
270
|
+
if package_root is not None:
|
|
271
|
+
return package_root
|
|
272
|
+
|
|
273
|
+
with _file_lock(_lock_path("nodejs-wheel", lock_directory)):
|
|
274
|
+
if _npm_available():
|
|
275
|
+
return None
|
|
276
|
+
package_root = _nodejs_wheel_root()
|
|
277
|
+
if package_root is None:
|
|
278
|
+
_pip_install(
|
|
279
|
+
_NODEJS_WHEEL_REQUIREMENT,
|
|
280
|
+
only_binary=("nodejs-wheel", "nodejs-wheel-binaries"),
|
|
281
|
+
)
|
|
282
|
+
package_root = _nodejs_wheel_root()
|
|
283
|
+
if package_root is None:
|
|
284
|
+
raise RuntimeError(
|
|
285
|
+
"The locked nodejs-wheel installation did not provide its package files"
|
|
286
|
+
)
|
|
287
|
+
return package_root
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@contextmanager
|
|
291
|
+
def _pythonmonkey_install_environment(
|
|
292
|
+
lock_directory: str | Path | None = None,
|
|
293
|
+
) -> Iterator[Mapping[str, str] | None]:
|
|
294
|
+
package_root = _ensure_nodejs_wheel(lock_directory)
|
|
295
|
+
if package_root is None:
|
|
296
|
+
yield None
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
launcher_directory = Path(tempfile.mkdtemp(prefix="dbx-tools-node-launchers-"))
|
|
300
|
+
try:
|
|
301
|
+
launchers = prepare_node_environment(
|
|
302
|
+
launcher_directory,
|
|
303
|
+
package_root=package_root,
|
|
304
|
+
)
|
|
305
|
+
environment = dict(os.environ)
|
|
306
|
+
environment["PATH"] = os.pathsep.join(
|
|
307
|
+
value for value in (str(launchers.launcher_directory), environment.get("PATH")) if value
|
|
308
|
+
)
|
|
309
|
+
yield environment
|
|
310
|
+
finally:
|
|
311
|
+
shutil.rmtree(launcher_directory, ignore_errors=True)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _verify_runtime() -> None:
|
|
315
|
+
result = subprocess.run(
|
|
316
|
+
[
|
|
317
|
+
sys.executable,
|
|
318
|
+
"-c",
|
|
319
|
+
(
|
|
320
|
+
"import pythonmonkey as pm; import pythonmonkey.require; "
|
|
321
|
+
"assert pm.eval('1 + 2') == 3; assert callable(pm.require)"
|
|
322
|
+
),
|
|
323
|
+
],
|
|
324
|
+
check=False,
|
|
325
|
+
stdout=subprocess.PIPE,
|
|
326
|
+
stderr=subprocess.STDOUT,
|
|
327
|
+
text=True,
|
|
328
|
+
)
|
|
329
|
+
if result.returncode != 0:
|
|
330
|
+
raise RuntimeError(f"Installed PythonMonkey runtime failed verification:\n{result.stdout}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def ensure_pythonmonkey(lock_directory: str | Path | None = None) -> ModuleType:
|
|
334
|
+
"""Return PythonMonkey, installing its locked version into the active environment."""
|
|
335
|
+
|
|
336
|
+
global _PROCESS_RUNTIME
|
|
337
|
+
with _PROCESS_LOCK:
|
|
338
|
+
if _PROCESS_RUNTIME is not None:
|
|
339
|
+
return _PROCESS_RUNTIME
|
|
340
|
+
environment_runtime = _environment_pythonmonkey()
|
|
341
|
+
if environment_runtime is not None:
|
|
342
|
+
_PROCESS_RUNTIME = environment_runtime
|
|
343
|
+
return environment_runtime
|
|
344
|
+
|
|
345
|
+
with _file_lock(_lock_path("pythonmonkey", lock_directory)):
|
|
346
|
+
environment_runtime = _environment_pythonmonkey()
|
|
347
|
+
if environment_runtime is None:
|
|
348
|
+
with _pythonmonkey_install_environment(lock_directory) as environment:
|
|
349
|
+
_pip_install(
|
|
350
|
+
f"pythonmonkey=={_PYTHONMONKEY_VERSION}",
|
|
351
|
+
environment=environment,
|
|
352
|
+
only_binary=("pythonmonkey",),
|
|
353
|
+
)
|
|
354
|
+
_verify_runtime()
|
|
355
|
+
environment_runtime = _environment_pythonmonkey()
|
|
356
|
+
if environment_runtime is None:
|
|
357
|
+
raise RuntimeError(
|
|
358
|
+
"The locked PythonMonkey installation is unavailable in the active "
|
|
359
|
+
f"Python environment {sys.prefix}"
|
|
360
|
+
)
|
|
361
|
+
_PROCESS_RUNTIME = environment_runtime
|
|
362
|
+
return environment_runtime
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|