nodepyx 1.0.0
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.
- package/LICENSE +22 -0
- package/README.md +399 -0
- package/binding.gyp +73 -0
- package/dist/core/PyCallable.d.ts +65 -0
- package/dist/core/PyCallable.d.ts.map +1 -0
- package/dist/core/PyCallable.js +109 -0
- package/dist/core/PyCallable.js.map +1 -0
- package/dist/core/PyContext.d.ts +76 -0
- package/dist/core/PyContext.d.ts.map +1 -0
- package/dist/core/PyContext.js +228 -0
- package/dist/core/PyContext.js.map +1 -0
- package/dist/core/PyIterator.d.ts +84 -0
- package/dist/core/PyIterator.d.ts.map +1 -0
- package/dist/core/PyIterator.js +243 -0
- package/dist/core/PyIterator.js.map +1 -0
- package/dist/core/PyModule.d.ts +55 -0
- package/dist/core/PyModule.d.ts.map +1 -0
- package/dist/core/PyModule.js +172 -0
- package/dist/core/PyModule.js.map +1 -0
- package/dist/core/PyProxy.d.ts +65 -0
- package/dist/core/PyProxy.d.ts.map +1 -0
- package/dist/core/PyProxy.js +483 -0
- package/dist/core/PyProxy.js.map +1 -0
- package/dist/core/PyRuntime.d.ts +105 -0
- package/dist/core/PyRuntime.d.ts.map +1 -0
- package/dist/core/PyRuntime.js +438 -0
- package/dist/core/PyRuntime.js.map +1 -0
- package/dist/env/CondaManager.d.ts +118 -0
- package/dist/env/CondaManager.d.ts.map +1 -0
- package/dist/env/CondaManager.js +401 -0
- package/dist/env/CondaManager.js.map +1 -0
- package/dist/env/PackageInstaller.d.ts +233 -0
- package/dist/env/PackageInstaller.d.ts.map +1 -0
- package/dist/env/PackageInstaller.js +609 -0
- package/dist/env/PackageInstaller.js.map +1 -0
- package/dist/env/PythonDetector.d.ts +103 -0
- package/dist/env/PythonDetector.d.ts.map +1 -0
- package/dist/env/PythonDetector.js +381 -0
- package/dist/env/PythonDetector.js.map +1 -0
- package/dist/env/VenvManager.d.ts +117 -0
- package/dist/env/VenvManager.d.ts.map +1 -0
- package/dist/env/VenvManager.js +331 -0
- package/dist/env/VenvManager.js.map +1 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/Plugin.interface.d.ts +41 -0
- package/dist/plugins/Plugin.interface.d.ts.map +1 -0
- package/dist/plugins/Plugin.interface.js +12 -0
- package/dist/plugins/Plugin.interface.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +26 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +174 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.js +41 -0
- package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.js +57 -0
- package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.js +50 -0
- package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/serialization/DataFrameBridge.d.ts +141 -0
- package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
- package/dist/serialization/DataFrameBridge.js +355 -0
- package/dist/serialization/DataFrameBridge.js.map +1 -0
- package/dist/serialization/MsgPackSerializer.d.ts +45 -0
- package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
- package/dist/serialization/MsgPackSerializer.js +242 -0
- package/dist/serialization/MsgPackSerializer.js.map +1 -0
- package/dist/serialization/NumpyBridge.d.ts +96 -0
- package/dist/serialization/NumpyBridge.d.ts.map +1 -0
- package/dist/serialization/NumpyBridge.js +323 -0
- package/dist/serialization/NumpyBridge.js.map +1 -0
- package/dist/serialization/Serializer.d.ts +78 -0
- package/dist/serialization/Serializer.d.ts.map +1 -0
- package/dist/serialization/Serializer.js +281 -0
- package/dist/serialization/Serializer.js.map +1 -0
- package/dist/types/PythonTypeMapper.d.ts +87 -0
- package/dist/types/PythonTypeMapper.d.ts.map +1 -0
- package/dist/types/PythonTypeMapper.js +449 -0
- package/dist/types/PythonTypeMapper.js.map +1 -0
- package/dist/types/StubCache.d.ts +109 -0
- package/dist/types/StubCache.d.ts.map +1 -0
- package/dist/types/StubCache.js +333 -0
- package/dist/types/StubCache.js.map +1 -0
- package/dist/types/TypeGenerator.d.ts +139 -0
- package/dist/types/TypeGenerator.d.ts.map +1 -0
- package/dist/types/TypeGenerator.js +372 -0
- package/dist/types/TypeGenerator.js.map +1 -0
- package/dist/types/addon.d.ts +114 -0
- package/dist/types/addon.d.ts.map +1 -0
- package/dist/types/addon.js +32 -0
- package/dist/types/addon.js.map +1 -0
- package/dist/types/config.d.ts +175 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/python.d.ts +235 -0
- package/dist/types/python.d.ts.map +1 -0
- package/dist/types/python.js +7 -0
- package/dist/types/python.js.map +1 -0
- package/dist/utils/ErrorTranslator.d.ts +83 -0
- package/dist/utils/ErrorTranslator.d.ts.map +1 -0
- package/dist/utils/ErrorTranslator.js +210 -0
- package/dist/utils/ErrorTranslator.js.map +1 -0
- package/dist/utils/Logger.d.ts +27 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +115 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/MemoryMonitor.d.ts +44 -0
- package/dist/utils/MemoryMonitor.d.ts.map +1 -0
- package/dist/utils/MemoryMonitor.js +143 -0
- package/dist/utils/MemoryMonitor.js.map +1 -0
- package/package.json +177 -0
- package/python/error_handler.py +433 -0
- package/python/nodepyx_runtime.py +575 -0
- package/python/serializer.py +379 -0
- package/python/type_inspector.py +288 -0
- package/scripts/build-native.js +68 -0
- package/scripts/download-prebuilds.js +99 -0
- package/scripts/generate-stubs.js +405 -0
- package/scripts/install.js +260 -0
- package/src/core/PyCallable.ts +137 -0
- package/src/core/PyContext.ts +296 -0
- package/src/core/PyIterator.ts +294 -0
- package/src/core/PyModule.ts +194 -0
- package/src/core/PyProxy.ts +605 -0
- package/src/core/PyRuntime.ts +504 -0
- package/src/env/CondaManager.ts +451 -0
- package/src/env/PackageInstaller.ts +738 -0
- package/src/env/PythonDetector.ts +414 -0
- package/src/env/VenvManager.ts +396 -0
- package/src/index.ts +425 -0
- package/src/native/gil_guard.cpp +26 -0
- package/src/native/gil_guard.h +175 -0
- package/src/native/nodepyx_addon.cpp +886 -0
- package/src/native/python_bridge.cpp +790 -0
- package/src/native/python_bridge.h +257 -0
- package/src/native/thread_pool.cpp +336 -0
- package/src/native/thread_pool.h +175 -0
- package/src/native/type_converter.cpp +901 -0
- package/src/native/type_converter.h +272 -0
- package/src/nextjs/PyProvider.tsx +123 -0
- package/src/nextjs/index.ts +21 -0
- package/src/nextjs/usePython.ts +106 -0
- package/src/nextjs/withnodepyx.ts +88 -0
- package/src/plugins/Plugin.interface.ts +51 -0
- package/src/plugins/PluginManager.ts +155 -0
- package/src/plugins/builtin/NumpyPlugin.ts +36 -0
- package/src/plugins/builtin/PandasPlugin.ts +49 -0
- package/src/plugins/builtin/TorchPlugin.ts +56 -0
- package/src/plugins/index.ts +7 -0
- package/src/serialization/DataFrameBridge.ts +398 -0
- package/src/serialization/MsgPackSerializer.ts +220 -0
- package/src/serialization/NumpyBridge.ts +332 -0
- package/src/serialization/Serializer.ts +320 -0
- package/src/types/PythonTypeMapper.ts +495 -0
- package/src/types/StubCache.ts +340 -0
- package/src/types/TypeGenerator.ts +491 -0
- package/src/types/addon.ts +170 -0
- package/src/types/config.ts +226 -0
- package/src/types/index.ts +55 -0
- package/src/types/python.ts +309 -0
- package/src/types/stubs/numpy.d.ts +441 -0
- package/src/types/stubs/pandas.d.ts +575 -0
- package/src/types/stubs/sklearn.d.ts +728 -0
- package/src/types/stubs/torch.d.ts +694 -0
- package/src/utils/ErrorTranslator.ts +220 -0
- package/src/utils/Logger.ts +119 -0
- package/src/utils/MemoryMonitor.ts +175 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — PythonDetector
|
|
3
|
+
* Locates a usable Python interpreter on the host system.
|
|
4
|
+
*
|
|
5
|
+
* Detection order:
|
|
6
|
+
* 1. nodepyx_PYTHON environment variable
|
|
7
|
+
* 2. Explicit pythonPath from nodepyxConfig
|
|
8
|
+
* 3. Virtualenv / Conda path from config
|
|
9
|
+
* 4. python3 / python3.x on PATH
|
|
10
|
+
* 5. python on PATH
|
|
11
|
+
* 6. Common installation locations (Windows/macOS/Linux)
|
|
12
|
+
*
|
|
13
|
+
* Validates:
|
|
14
|
+
* - Python version ≥ 3.8
|
|
15
|
+
* - Not a stub (pyenv shim without real installation)
|
|
16
|
+
* - Has required modules available (when requested)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execSync, spawnSync } from 'child_process';
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { Logger } from '../utils/Logger';
|
|
23
|
+
|
|
24
|
+
const logger = new Logger('PythonDetector');
|
|
25
|
+
|
|
26
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface PythonInfo {
|
|
29
|
+
/** Absolute path to Python executable */
|
|
30
|
+
executable: string;
|
|
31
|
+
/** Python version string (e.g. '3.11.4') */
|
|
32
|
+
version: string;
|
|
33
|
+
/** Major.minor as numbers */
|
|
34
|
+
major: number;
|
|
35
|
+
minor: number;
|
|
36
|
+
patch: number;
|
|
37
|
+
/** Absolute path to Python prefix (sys.prefix) */
|
|
38
|
+
prefix: string;
|
|
39
|
+
/** Whether this is inside a virtualenv */
|
|
40
|
+
isVirtualenv: boolean;
|
|
41
|
+
/** Whether this is inside a Conda environment */
|
|
42
|
+
isConda: boolean;
|
|
43
|
+
/** conda environment name (if applicable) */
|
|
44
|
+
condaEnv?: string;
|
|
45
|
+
/** Platform: 'linux' | 'darwin' | 'win32' */
|
|
46
|
+
platform: string;
|
|
47
|
+
/** Architecture: 'x64' | 'arm64' */
|
|
48
|
+
arch: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DetectorOptions {
|
|
52
|
+
/** Minimum Python version required. Default: [3, 8] */
|
|
53
|
+
minVersion?: [number, number];
|
|
54
|
+
/** Explicit path to check first */
|
|
55
|
+
pythonPath?: string;
|
|
56
|
+
/** Timeout for subprocess calls (ms). Default: 10000 */
|
|
57
|
+
timeout?: number;
|
|
58
|
+
/** Skip PATH scanning and only check explicit path. Default: false */
|
|
59
|
+
explicitOnly?: boolean;
|
|
60
|
+
/** Modules that must be importable. Default: [] */
|
|
61
|
+
requiredModules?: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Platform-specific common locations ─────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const COMMON_LOCATIONS: Readonly<Record<NodeJS.Platform, string[]>> = {
|
|
67
|
+
linux: [
|
|
68
|
+
'/usr/bin/python3',
|
|
69
|
+
'/usr/local/bin/python3',
|
|
70
|
+
'/opt/conda/bin/python3',
|
|
71
|
+
'/usr/bin/python3.12',
|
|
72
|
+
'/usr/bin/python3.11',
|
|
73
|
+
'/usr/bin/python3.10',
|
|
74
|
+
'/usr/bin/python3.9',
|
|
75
|
+
'/usr/bin/python3.8',
|
|
76
|
+
'/usr/bin/python',
|
|
77
|
+
],
|
|
78
|
+
darwin: [
|
|
79
|
+
'/usr/local/bin/python3',
|
|
80
|
+
'/opt/homebrew/bin/python3',
|
|
81
|
+
'/opt/homebrew/bin/python3.12',
|
|
82
|
+
'/opt/homebrew/bin/python3.11',
|
|
83
|
+
'/opt/homebrew/bin/python3.10',
|
|
84
|
+
'/Library/Frameworks/Python.framework/Versions/3.12/bin/python3',
|
|
85
|
+
'/Library/Frameworks/Python.framework/Versions/3.11/bin/python3',
|
|
86
|
+
'/Library/Frameworks/Python.framework/Versions/3.10/bin/python3',
|
|
87
|
+
'/usr/bin/python3',
|
|
88
|
+
'/usr/bin/python',
|
|
89
|
+
],
|
|
90
|
+
win32: [
|
|
91
|
+
'C:\\Python312\\python.exe',
|
|
92
|
+
'C:\\Python311\\python.exe',
|
|
93
|
+
'C:\\Python310\\python.exe',
|
|
94
|
+
'C:\\Python39\\python.exe',
|
|
95
|
+
'C:\\Python38\\python.exe',
|
|
96
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Python\\Python312\\python.exe`,
|
|
97
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Python\\Python311\\python.exe`,
|
|
98
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Python\\Python310\\python.exe`,
|
|
99
|
+
`${process.env.APPDATA}\\Python\\Python312\\python.exe`,
|
|
100
|
+
],
|
|
101
|
+
aix: [],
|
|
102
|
+
android: [],
|
|
103
|
+
cygwin: [],
|
|
104
|
+
freebsd: [],
|
|
105
|
+
haiku: [],
|
|
106
|
+
netbsd: [],
|
|
107
|
+
openbsd: [],
|
|
108
|
+
sunos: [],
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
111
|
+
// Python info extraction script
|
|
112
|
+
const PROBE_SCRIPT = `
|
|
113
|
+
import sys, os, json
|
|
114
|
+
prefix = sys.prefix
|
|
115
|
+
is_venv = (
|
|
116
|
+
hasattr(sys, 'real_prefix') or
|
|
117
|
+
(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
|
|
118
|
+
)
|
|
119
|
+
is_conda = 'conda' in sys.version or os.path.exists(os.path.join(prefix, 'conda-meta'))
|
|
120
|
+
conda_env = os.environ.get('CONDA_DEFAULT_ENV')
|
|
121
|
+
info = {
|
|
122
|
+
'version': '.'.join(map(str, sys.version_info[:3])),
|
|
123
|
+
'major': sys.version_info.major,
|
|
124
|
+
'minor': sys.version_info.minor,
|
|
125
|
+
'patch': sys.version_info.micro,
|
|
126
|
+
'prefix': prefix,
|
|
127
|
+
'is_venv': is_venv,
|
|
128
|
+
'is_conda': is_conda,
|
|
129
|
+
'conda_env': conda_env,
|
|
130
|
+
'platform': sys.platform,
|
|
131
|
+
'arch': __import__('platform').machine(),
|
|
132
|
+
}
|
|
133
|
+
print(json.dumps(info))
|
|
134
|
+
`.trim();
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* PythonDetector — locates a usable Python interpreter.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const detector = new PythonDetector({ minVersion: [3, 9] });
|
|
142
|
+
* const info = await detector.detect();
|
|
143
|
+
* console.log(info.executable); // /usr/bin/python3
|
|
144
|
+
* console.log(info.version); // 3.11.4
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export class PythonDetector {
|
|
148
|
+
private readonly _opts: Required<DetectorOptions>;
|
|
149
|
+
|
|
150
|
+
constructor(options: DetectorOptions = {}) {
|
|
151
|
+
this._opts = {
|
|
152
|
+
minVersion: options.minVersion ?? [3, 8],
|
|
153
|
+
pythonPath: options.pythonPath ?? '',
|
|
154
|
+
timeout: options.timeout ?? 10_000,
|
|
155
|
+
explicitOnly: options.explicitOnly ?? false,
|
|
156
|
+
requiredModules: options.requiredModules ?? [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Detect and validate a Python interpreter.
|
|
164
|
+
* Returns the first suitable interpreter found.
|
|
165
|
+
* Throws if no suitable interpreter is found.
|
|
166
|
+
*/
|
|
167
|
+
async detect(): Promise<PythonInfo> {
|
|
168
|
+
const candidates = this._buildCandidates();
|
|
169
|
+
|
|
170
|
+
for (const candidate of candidates) {
|
|
171
|
+
const info = this._probe(candidate);
|
|
172
|
+
if (!info) {continue;}
|
|
173
|
+
|
|
174
|
+
if (!this._meetsVersionRequirement(info)) {
|
|
175
|
+
logger.debug(`PythonDetector: ${candidate} — version ${info.version} < ${this._opts.minVersion.join('.')}`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this._opts.requiredModules.length > 0) {
|
|
180
|
+
const missing = this._checkModules(info.executable, this._opts.requiredModules);
|
|
181
|
+
if (missing.length > 0) {
|
|
182
|
+
logger.debug(`PythonDetector: ${candidate} — missing modules: ${missing.join(', ')}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
logger.info(`PythonDetector: using ${info.executable} (${info.version})`);
|
|
188
|
+
return info;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw new PythonNotFoundError(
|
|
192
|
+
`Could not find a suitable Python interpreter (>= ${this._opts.minVersion.join('.')}).` +
|
|
193
|
+
'\nSolutions:' +
|
|
194
|
+
'\n 1. Install Python 3.8+: https://python.org' +
|
|
195
|
+
'\n 2. Set nodepyx_PYTHON env var to the Python executable path' +
|
|
196
|
+
'\n 3. Pass pythonPath in the nodepyx config' +
|
|
197
|
+
'\n 4. Create a virtualenv: nodepyx.init({ virtualenv: { path: ".venv", packages: [] } })',
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Probe a specific Python executable and return its info.
|
|
203
|
+
* Returns null if the executable is invalid or doesn't exist.
|
|
204
|
+
*/
|
|
205
|
+
probe(executablePath: string): PythonInfo | null {
|
|
206
|
+
return this._probe(executablePath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if a Python executable meets the minimum version requirement.
|
|
211
|
+
*/
|
|
212
|
+
isCompatible(info: PythonInfo): boolean {
|
|
213
|
+
return this._meetsVersionRequirement(info);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check which required modules are missing for a given Python.
|
|
218
|
+
* Returns an empty array if all modules are available.
|
|
219
|
+
*/
|
|
220
|
+
checkModules(executable: string, modules: string[]): string[] {
|
|
221
|
+
return this._checkModules(executable, modules);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* List all Python executables found in common locations and PATH.
|
|
226
|
+
*/
|
|
227
|
+
async listAll(): Promise<PythonInfo[]> {
|
|
228
|
+
const results: PythonInfo[] = [];
|
|
229
|
+
const candidates = this._buildCandidates();
|
|
230
|
+
|
|
231
|
+
for (const candidate of candidates) {
|
|
232
|
+
const info = this._probe(candidate);
|
|
233
|
+
if (info) {results.push(info);}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Private ─────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
private _buildCandidates(): string[] {
|
|
242
|
+
const candidates: string[] = [];
|
|
243
|
+
|
|
244
|
+
// 1. nodepyx_PYTHON env var (highest priority)
|
|
245
|
+
if (process.env.nodepyx_PYTHON) {
|
|
246
|
+
candidates.push(process.env.nodepyx_PYTHON);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 2. Explicit config path
|
|
250
|
+
if (this._opts.pythonPath) {
|
|
251
|
+
candidates.push(this._opts.pythonPath);
|
|
252
|
+
// Also try bin/python inside a venv/conda root
|
|
253
|
+
const binDir = process.platform === 'win32' ? 'Scripts' : 'bin';
|
|
254
|
+
candidates.push(path.join(this._opts.pythonPath, binDir, 'python3'));
|
|
255
|
+
candidates.push(path.join(this._opts.pythonPath, binDir, 'python'));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (this._opts.explicitOnly) {return candidates;}
|
|
259
|
+
|
|
260
|
+
// 3. PATH candidates
|
|
261
|
+
const pathCandidates = [
|
|
262
|
+
'python3.12', 'python3.11', 'python3.10', 'python3.9', 'python3.8',
|
|
263
|
+
'python3', 'python',
|
|
264
|
+
];
|
|
265
|
+
for (const name of pathCandidates) {
|
|
266
|
+
const found = this._which(name);
|
|
267
|
+
if (found) {candidates.push(found);}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 4. Common installation locations
|
|
271
|
+
const platformKey = process.platform as NodeJS.Platform;
|
|
272
|
+
for (const loc of COMMON_LOCATIONS[platformKey] ?? []) {
|
|
273
|
+
if (loc) {candidates.push(loc);}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Deduplicate while preserving order
|
|
277
|
+
return [...new Set(candidates)];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private _probe(executable: string): PythonInfo | null {
|
|
281
|
+
if (!executable) {return null;}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// Resolve Windows executable extension
|
|
285
|
+
const exec = this._resolveExecutable(executable);
|
|
286
|
+
if (!exec) {return null;}
|
|
287
|
+
|
|
288
|
+
const result = spawnSync(exec, ['-c', PROBE_SCRIPT], {
|
|
289
|
+
encoding: 'utf8',
|
|
290
|
+
timeout: this._opts.timeout,
|
|
291
|
+
maxBuffer: 1024 * 1024,
|
|
292
|
+
env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' },
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (result.status !== 0 || result.error || !result.stdout.trim()) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const raw = JSON.parse(result.stdout.trim()) as {
|
|
300
|
+
version: string;
|
|
301
|
+
major: number;
|
|
302
|
+
minor: number;
|
|
303
|
+
patch: number;
|
|
304
|
+
prefix: string;
|
|
305
|
+
is_venv: boolean;
|
|
306
|
+
is_conda: boolean;
|
|
307
|
+
conda_env: string | null;
|
|
308
|
+
platform: string;
|
|
309
|
+
arch: string;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
executable: exec,
|
|
314
|
+
version: raw.version,
|
|
315
|
+
major: raw.major,
|
|
316
|
+
minor: raw.minor,
|
|
317
|
+
patch: raw.patch,
|
|
318
|
+
prefix: raw.prefix,
|
|
319
|
+
isVirtualenv: raw.is_venv,
|
|
320
|
+
isConda: raw.is_conda,
|
|
321
|
+
condaEnv: raw.conda_env ?? undefined,
|
|
322
|
+
platform: raw.platform,
|
|
323
|
+
arch: raw.arch,
|
|
324
|
+
};
|
|
325
|
+
} catch (err) {
|
|
326
|
+
logger.debug(`PythonDetector._probe(${executable}) failed`, err);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private _resolveExecutable(exec: string): string | null {
|
|
332
|
+
// Absolute path — check existence
|
|
333
|
+
if (path.isAbsolute(exec)) {
|
|
334
|
+
if (fs.existsSync(exec)) {return exec;}
|
|
335
|
+
// Try with .exe on Windows
|
|
336
|
+
if (process.platform === 'win32' && !exec.endsWith('.exe')) {
|
|
337
|
+
const withExt = exec + '.exe';
|
|
338
|
+
if (fs.existsSync(withExt)) {return withExt;}
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Relative name — resolve via which
|
|
344
|
+
return this._which(exec);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private _which(name: string): string | null {
|
|
348
|
+
try {
|
|
349
|
+
const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`;
|
|
350
|
+
const result = execSync(cmd, {
|
|
351
|
+
encoding: 'utf8',
|
|
352
|
+
timeout: 3000,
|
|
353
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
354
|
+
}).trim();
|
|
355
|
+
|
|
356
|
+
// `where` returns multiple lines; take the first
|
|
357
|
+
const first = result.split(/\r?\n/)[0]?.trim();
|
|
358
|
+
return first || null;
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private _meetsVersionRequirement(info: PythonInfo): boolean {
|
|
365
|
+
const [minMajor, minMinor] = this._opts.minVersion;
|
|
366
|
+
if (info.major !== minMajor!) {return info.major > minMajor!;}
|
|
367
|
+
return info.minor >= minMinor!;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private _checkModules(executable: string, modules: string[]): string[] {
|
|
371
|
+
if (modules.length === 0) {return [];}
|
|
372
|
+
|
|
373
|
+
const script = modules
|
|
374
|
+
.map(m => `try:\n import ${m}\nexcept ImportError:\n print("MISSING:${m}")`)
|
|
375
|
+
.join('\n');
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const result = spawnSync(executable, ['-c', script], {
|
|
379
|
+
encoding: 'utf8',
|
|
380
|
+
timeout: this._opts.timeout,
|
|
381
|
+
maxBuffer: 1024 * 256,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const missing: string[] = [];
|
|
385
|
+
for (const line of (result.stdout ?? '').split('\n')) {
|
|
386
|
+
const match = line.match(/^MISSING:(.+)$/);
|
|
387
|
+
if (match) {missing.push(match[1]!);}
|
|
388
|
+
}
|
|
389
|
+
return missing;
|
|
390
|
+
} catch {
|
|
391
|
+
return modules; // assume all missing if we can't check
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─── Error types ─────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
export class PythonNotFoundError extends Error {
|
|
399
|
+
constructor(message: string) {
|
|
400
|
+
super(message);
|
|
401
|
+
this.name = 'PythonNotFoundError';
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export class PythonVersionError extends Error {
|
|
406
|
+
constructor(found: string, required: string) {
|
|
407
|
+
super(
|
|
408
|
+
`Python ${found} is below the minimum required version ${required}. ` +
|
|
409
|
+
`Please upgrade Python.`,
|
|
410
|
+
);
|
|
411
|
+
this.name = 'PythonVersionError';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|