@flun/nodejs-mobile-gyp 1.0.2 → 1.1.1
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/CHANGELOG.md +104 -56
- package/README.md +362 -256
- package/gyp/pylib/gyp/common.py +5 -2
- package/gyp/pylib/gyp/generator/make.py +18 -3
- package/lib/build.js +95 -9
- package/lib/configure.js +10 -6
- package/lib/find-python.js +96 -74
- package/package.json +1 -1
package/gyp/pylib/gyp/common.py
CHANGED
|
@@ -153,7 +153,9 @@ def RelativePath(path, relative_to, follow_path_symlink=True):
|
|
|
153
153
|
os.path.splitdrive(path)[0].lower()
|
|
154
154
|
!= os.path.splitdrive(relative_to)[0].lower()
|
|
155
155
|
):
|
|
156
|
-
|
|
156
|
+
# Makefile / cmake / ninja 等生成器要求 POSIX 路径,统一用正斜杠。
|
|
157
|
+
# MSBuild 也接受正斜杠,故对 msvs 无影响。
|
|
158
|
+
return path.replace(os.sep, "/")
|
|
157
159
|
|
|
158
160
|
# Split the paths into components.
|
|
159
161
|
path_split = path.split(os.path.sep)
|
|
@@ -173,7 +175,8 @@ def RelativePath(path, relative_to, follow_path_symlink=True):
|
|
|
173
175
|
return ""
|
|
174
176
|
|
|
175
177
|
# Turn it back into a string and we're done.
|
|
176
|
-
|
|
178
|
+
# Makefile / cmake / ninja 等生成器要求 POSIX 路径,统一用正斜杠。
|
|
179
|
+
return os.path.join(*relative_split).replace(os.sep, "/")
|
|
177
180
|
|
|
178
181
|
|
|
179
182
|
@memoize
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import os
|
|
26
26
|
import re
|
|
27
|
+
import sys
|
|
27
28
|
import subprocess
|
|
28
29
|
import gyp
|
|
29
30
|
import gyp.common
|
|
@@ -410,6 +411,7 @@ unreplace_spaces = $(subst """
|
|
|
410
411
|
+ SPACE_REPLACEMENT
|
|
411
412
|
+ """,$(space),$1)
|
|
412
413
|
dirx = $(call unreplace_spaces,$(dir $(call replace_spaces,$1)))
|
|
414
|
+
slashpath = $(subst \\,/,$(1))
|
|
413
415
|
|
|
414
416
|
# Flags to make gcc output dependency info. Note that you need to be
|
|
415
417
|
# careful here to use the flags that ccache and distcc can understand.
|
|
@@ -547,7 +549,7 @@ endef
|
|
|
547
549
|
define do_cmd
|
|
548
550
|
$(if $(or $(command_changed),$(prereq_changed)),
|
|
549
551
|
@$(call exact_echo, $($(quiet)cmd_$(1)))
|
|
550
|
-
@mkdir -p "$(call dirx,$@)" "$(dir $(depfile))"
|
|
552
|
+
@mkdir -p "$(call slashpath,$(call dirx,$@))" "$(call slashpath,$(dir $(depfile)))"
|
|
551
553
|
$(if $(findstring flock,$(word %(flock_index)d,$(cmd_$1))),
|
|
552
554
|
@$(cmd_$(1))
|
|
553
555
|
@echo " $(quiet_cmd_$(1)): Finished",
|
|
@@ -2318,7 +2320,9 @@ $(obj).$(TOOLSET)/$(TARGET)/%%.o: $(obj)/%%%s FORCE_DO_CMD
|
|
|
2318
2320
|
# path too aggressively if it features '..'. However it's still
|
|
2319
2321
|
# important to strip trailing slashes.
|
|
2320
2322
|
return path.rstrip("/")
|
|
2321
|
-
|
|
2323
|
+
# Makefile 要求 POSIX 路径;Windows 上 normpath 会把 "/" 变成 "\",
|
|
2324
|
+
# 导致编译产物路径(.o / .d.raw)带反斜杠,sh 与 sed 解析失败。
|
|
2325
|
+
return os.path.normpath(os.path.join(self.path, path)).replace(os.sep, "/")
|
|
2322
2326
|
|
|
2323
2327
|
def ExpandInputRoot(self, template, expansion, dirname):
|
|
2324
2328
|
if "%(INPUT_ROOT)s" not in template and "%(INPUT_DIRNAME)s" not in template:
|
|
@@ -2361,6 +2365,17 @@ def WriteAutoRegenerationRule(params, root_makefile, makefile_name, build_files)
|
|
|
2361
2365
|
if not gyp_binary.startswith(os.sep):
|
|
2362
2366
|
gyp_binary = os.path.join(".", gyp_binary)
|
|
2363
2367
|
|
|
2368
|
+
# Windows 下 GNU make(NDK 自带)对深层相对路径的解析有缺陷,会把
|
|
2369
|
+
# 形如 $(srcdir)/../../.. 的外部 gypi 判为 "No rule to make target"。
|
|
2370
|
+
# 这里跳过所有来自 node_modules 的依赖——它们是只读的外部 gypi,
|
|
2371
|
+
# 内容不会在编译过程中变化;如确需重新生成 Makefile,重跑 configure 即可。
|
|
2372
|
+
# 本地的 binding.gyp 与 build/config.gypi 仍会保留,足以触发自动重新生成。
|
|
2373
|
+
deps_list = []
|
|
2374
|
+
for bf in build_files:
|
|
2375
|
+
if sys.platform == "win32" and "node_modules" in bf.replace("\\", "/"):
|
|
2376
|
+
continue
|
|
2377
|
+
deps_list.append(SourceifyAndQuoteSpaces(bf))
|
|
2378
|
+
|
|
2364
2379
|
root_makefile.write(
|
|
2365
2380
|
"quiet_cmd_regen_makefile = ACTION Regenerating $@\n"
|
|
2366
2381
|
"cmd_regen_makefile = cd $(srcdir); %(cmd)s\n"
|
|
@@ -2368,7 +2383,7 @@ def WriteAutoRegenerationRule(params, root_makefile, makefile_name, build_files)
|
|
|
2368
2383
|
"\t$(call do_cmd,regen_makefile)\n\n"
|
|
2369
2384
|
% {
|
|
2370
2385
|
"makefile_name": makefile_name,
|
|
2371
|
-
"deps": " ".join(
|
|
2386
|
+
"deps": " ".join(deps_list),
|
|
2372
2387
|
"cmd": gyp.common.EncodePOSIXShellList(
|
|
2373
2388
|
[gyp_binary, "-fmake"] + gyp.RegenerateFlags(options) + build_files_args
|
|
2374
2389
|
),
|
package/lib/build.js
CHANGED
|
@@ -4,8 +4,75 @@ import { glob } from 'glob';
|
|
|
4
4
|
import * as log from './log.js';
|
|
5
5
|
import which from 'which';
|
|
6
6
|
import os from 'os';
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
8
|
+
import fsSync from 'fs';
|
|
9
|
+
|
|
10
|
+
const fs = gracefulFs.promises,
|
|
11
|
+
_hostIsWindows = process.platform === 'win32',
|
|
12
|
+
// Android 交叉编译即使宿主是 Windows,也必须走 make(make-android 生成器)
|
|
13
|
+
_isAndroidCrossBuild = process.env.npm_config_platform === 'android' || process.env.npm_config_format === 'make-android',
|
|
14
|
+
// win 在此文件中语义为"使用 MSBuild":仅 Windows 宿主且非 Android 交叉编译
|
|
15
|
+
win = _hostIsWindows && !_isAndroidCrossBuild,
|
|
16
|
+
/**
|
|
17
|
+
* Windows 上 GNU make 需要 sh.exe 来执行 Makefile 里的 Unix 命令
|
|
18
|
+
* (printf / xargs / rm 等)。优先复用 Git for Windows 自带的 usr/bin,
|
|
19
|
+
* 避免要求用户额外安装 MSYS2。通过 "where git" 反推,不硬编码路径。
|
|
20
|
+
*/
|
|
21
|
+
findWindowsShellDir = () => {
|
|
22
|
+
if (process.platform !== 'win32') return null;
|
|
23
|
+
const tryShDir = (dir) => {
|
|
24
|
+
try { return fsSync.existsSync(path.join(dir, 'sh.exe')) ? dir : null } catch { return null }
|
|
25
|
+
};
|
|
26
|
+
const toShDir = (gitRoot) => tryShDir(path.join(gitRoot, 'usr', 'bin'));
|
|
27
|
+
// 1) 注册表:Git for Windows 安装时必写,与 PATH 无关(最可靠)
|
|
28
|
+
const reg = process.env.SystemRoot ? path.join(process.env.SystemRoot, 'System32', 'reg.exe') : 'reg.exe';
|
|
29
|
+
for (const hive of ['HKLM', 'HKCU']) {
|
|
30
|
+
for (const key of ['SOFTWARE\\GitForWindows', 'SOFTWARE\\WOW6432Node\\GitForWindows']) {
|
|
31
|
+
try {
|
|
32
|
+
const r = spawnSync(reg, ['query', hive + '\\' + key, '/v', 'InstallPath'], { encoding: 'utf8' });
|
|
33
|
+
if (r.status === 0) {
|
|
34
|
+
const m = (r.stdout || '').match(/REG_SZ\s+(.+)/);
|
|
35
|
+
if (m) {
|
|
36
|
+
const dir = toShDir(m[1].trim());
|
|
37
|
+
if (dir) return dir;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch { }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 2) which.sync(PATH 中能找到 git 时可用)
|
|
44
|
+
try {
|
|
45
|
+
const gitPath = which.sync('git');
|
|
46
|
+
if (gitPath) {
|
|
47
|
+
const dir = path.resolve(path.dirname(gitPath), '..', 'usr', 'bin');
|
|
48
|
+
const r = tryShDir(dir);
|
|
49
|
+
if (r) return r;
|
|
50
|
+
}
|
|
51
|
+
} catch { }
|
|
52
|
+
// 3) PATH 中找 git.exe,再反推 ../usr/bin
|
|
53
|
+
for (const pd of (process.env.PATH || '').split(';')) {
|
|
54
|
+
if (!pd) continue;
|
|
55
|
+
try {
|
|
56
|
+
if (fsSync.existsSync(path.join(pd, 'git.exe'))) {
|
|
57
|
+
const dir = path.resolve(pd, '..', 'usr', 'bin');
|
|
58
|
+
const r = tryShDir(dir);
|
|
59
|
+
if (r) return r;
|
|
60
|
+
}
|
|
61
|
+
} catch { }
|
|
62
|
+
}
|
|
63
|
+
// 4) 全盘符常见位置兜底
|
|
64
|
+
const suffixes = [
|
|
65
|
+
'Program Files\\Git', 'Program Files (x86)\\Git', 'Git', 'msys64',
|
|
66
|
+
];
|
|
67
|
+
for (const drv of ['C:', 'D:', 'E:', 'F:']) {
|
|
68
|
+
for (const sfx of suffixes) {
|
|
69
|
+
const dir = tryShDir(drv + '\\' + sfx);
|
|
70
|
+
if (dir) return dir;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
},
|
|
7
75
|
|
|
8
|
-
const fs = gracefulFs.promises, win = process.platform === 'win32',
|
|
9
76
|
/**
|
|
10
77
|
* 调用 make/msbuild 构建原生模块
|
|
11
78
|
*/
|
|
@@ -63,16 +130,35 @@ const fs = gracefulFs.promises, win = process.platform === 'win32',
|
|
|
63
130
|
if (!win) {
|
|
64
131
|
// 把构建期依赖的符号链接(如 Python)加入 PATH
|
|
65
132
|
buildBinsDir = path.resolve('build', 'node_gyp_bins');
|
|
66
|
-
|
|
133
|
+
if (_hostIsWindows) {
|
|
134
|
+
const shellDir = findWindowsShellDir();
|
|
135
|
+
if (shellDir) {
|
|
136
|
+
process.env.PATH = `${shellDir};${process.env.PATH}`;
|
|
137
|
+
log.verbose('bin shell', `prepended ${shellDir} to PATH for make`);
|
|
138
|
+
} else {
|
|
139
|
+
log.warn('bin shell', 'no sh.exe found; GNU make 的 Unix 命令可能失败。建议安装 Git for Windows');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
process.env.PATH = _hostIsWindows
|
|
143
|
+
? `${buildBinsDir};${process.env.PATH}`
|
|
144
|
+
: `${buildBinsDir}:${process.env.PATH}`;
|
|
67
145
|
await fs.mkdir(buildBinsDir, { recursive: true });
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
146
|
+
|
|
147
|
+
if (_hostIsWindows) {
|
|
148
|
+
// Windows 无 python3 名称,且创建符号链接需要管理员权限;改用 .cmd 桥接
|
|
149
|
+
const shimPath = path.join(buildBinsDir, 'python3.cmd');
|
|
150
|
+
await fs.writeFile(shimPath, `@echo off\r\n"${python}" %*\r\n`);
|
|
151
|
+
log.verbose('bin shims', `created python3.cmd shim to "${python}" and added to PATH`);
|
|
152
|
+
} else {
|
|
153
|
+
const symlinkDestination = path.join(buildBinsDir, 'python3');
|
|
154
|
+
try {
|
|
155
|
+
await fs.unlink(symlinkDestination);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err.code !== 'ENOENT') throw err;
|
|
158
|
+
}
|
|
159
|
+
await fs.symlink(python, symlinkDestination);
|
|
160
|
+
log.verbose('bin symlinks', `created symlink to "${python}" in "${buildBinsDir}" and added to PATH`);
|
|
73
161
|
}
|
|
74
|
-
await fs.symlink(python, symlinkDestination);
|
|
75
|
-
log.verbose('bin symlinks', `created symlink to "${python}" in "${buildBinsDir}" and added to PATH`);
|
|
76
162
|
}
|
|
77
163
|
|
|
78
164
|
const proc = gyp.spawn(command, argv);
|
package/lib/configure.js
CHANGED
|
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'url';
|
|
|
13
13
|
const fs = gracefulFs.promises, win = process.platform === 'win32', __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
|
|
15
15
|
// 条件加载:仅在 Windows 上需要
|
|
16
|
-
let findVisualStudio = null, nodeDir;
|
|
16
|
+
let findVisualStudio = null, nodeDir, vsInfo = null;
|
|
17
17
|
if (win) {
|
|
18
18
|
const { VisualStudioFinder } = await import('./find-visualstudio.js');
|
|
19
19
|
findVisualStudio = VisualStudioFinder.findVisualStudio;
|
|
@@ -127,7 +127,12 @@ const configure = async (gyp, argv) => {
|
|
|
127
127
|
argv.push('-Dnode_engine=' + (gyp.opts.node_engine || process.jsEngine || 'v8'));
|
|
128
128
|
argv.push('--depth=.'), argv.push('--no-parallel');
|
|
129
129
|
argv.push('--generator-output', outputDir); // 告诉 gyp 把 Makefile/Solution 文件写到 output_dir
|
|
130
|
-
|
|
130
|
+
// 使用 find-visualstudio 探测到的实际 toolset(v140/v141/v142/v143/v145...),
|
|
131
|
+
// 不再硬编码,以兼容 VS2013+ 各版本
|
|
132
|
+
if (win) {
|
|
133
|
+
if (vsInfo?.toolset) argv.push('-Gmsvs_toolset=' + vsInfo.toolset);
|
|
134
|
+
else log.warn('gyp', '未能探测到 VS toolset,将使用 gyp 默认值');
|
|
135
|
+
}
|
|
131
136
|
|
|
132
137
|
argv.push('-Goutput_dir=.'); // 告诉 make 把输出写到同一目录
|
|
133
138
|
argv.unshift('binding.gyp'); // 强制使用 binding.gyp 文件
|
|
@@ -160,7 +165,7 @@ const configure = async (gyp, argv) => {
|
|
|
160
165
|
|
|
161
166
|
return findConfigs();
|
|
162
167
|
},
|
|
163
|
-
createConfigFile = async
|
|
168
|
+
createConfigFile = async () => {
|
|
164
169
|
if (win) {
|
|
165
170
|
process.env.GYP_MSVS_VERSION = Math.min(vsInfo.versionYear, 2015);
|
|
166
171
|
process.env.GYP_MSVS_OVERRIDE_PATH = vsInfo.path;
|
|
@@ -174,8 +179,8 @@ const configure = async (gyp, argv) => {
|
|
|
174
179
|
|
|
175
180
|
const isNew = await fs.mkdir(buildDir, { recursive: true });
|
|
176
181
|
log.verbose('build dir', '"build" dir needed to be created?', isNew ? 'Yes' : 'No');
|
|
177
|
-
|
|
178
|
-
return createConfigFile(
|
|
182
|
+
vsInfo = win ? await findVisualStudio(release.semver, gyp.opts['msvs-version']) : null;
|
|
183
|
+
return createConfigFile();
|
|
179
184
|
},
|
|
180
185
|
getNodeDir = async () => {
|
|
181
186
|
// 此时 python 已确定
|
|
@@ -196,7 +201,6 @@ const configure = async (gyp, argv) => {
|
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
if (!release.semver) throw new Error('版本号无效: ' + release.version); // 无法用 semver 解析版本字符串
|
|
199
|
-
|
|
200
204
|
// 如果指定了 tarball 选项,总是删除并重装 headers 到 devdir,
|
|
201
205
|
// 否则只在不存在时安装
|
|
202
206
|
gyp.opts.ensure = !gyp.opts.tarball, await gyp.commands.install([release.version]);
|
package/lib/find-python.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import * as log from './log.js';
|
|
2
2
|
import semver from 'semver';
|
|
3
3
|
import { execFile } from './util.js';
|
|
4
4
|
import os from 'os';
|
|
5
|
+
import fs from 'fs';
|
|
5
6
|
|
|
6
7
|
const win = process.platform === 'win32',
|
|
7
8
|
/**
|
|
@@ -10,29 +11,39 @@ const win = process.platform === 'win32',
|
|
|
10
11
|
getOsUserInfo = () => {
|
|
11
12
|
try { return os.userInfo().username } catch { }
|
|
12
13
|
},
|
|
13
|
-
systemDrive = process.env.SystemDrive || 'C:',
|
|
14
|
+
systemDrive = process.env.SystemDrive || 'C:',
|
|
15
|
+
username = process.env.USERNAME || process.env.USER || getOsUserInfo(),
|
|
14
16
|
localAppData = process.env.LOCALAPPDATA || `${systemDrive}\\${username}\\AppData\\Local`,
|
|
15
17
|
foundLocalAppData = process.env.LOCALAPPDATA || username,
|
|
16
18
|
programFiles = process.env.ProgramW6432 || process.env.ProgramFiles || `${systemDrive}\\Program Files`,
|
|
17
|
-
programFilesX86 = process.env['ProgramFiles(x86)'] || `${programFiles} (x86)`,
|
|
19
|
+
programFilesX86 = process.env['ProgramFiles(x86)'] || `${programFiles} (x86)`,
|
|
20
|
+
systemRoot = process.env.SystemRoot || `${systemDrive}\\Windows`,
|
|
21
|
+
/**
|
|
22
|
+
* 动态扫描 Windows 上所有已安装的 Python,不硬编码版本号。
|
|
23
|
+
* 官方安装器目录命名:3.14 -> Python314,3.9 -> Python39,32 位加 -32 后缀。
|
|
24
|
+
*/
|
|
25
|
+
scanWindowsPythonLocations = () => {
|
|
26
|
+
const results = [], bases = [foundLocalAppData ? `${localAppData}\\Programs\\Python` : null, `${programFiles}\\Python`,
|
|
27
|
+
`${programFilesX86}\\Python`].filter(Boolean), seen = new Set();
|
|
18
28
|
|
|
19
|
-
for (const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
for (const base of bases) {
|
|
30
|
+
if (seen.has(base)) continue;
|
|
31
|
+
seen.add(base);
|
|
32
|
+
let entries;
|
|
33
|
+
try { entries = fs.readdirSync(base); } catch { continue; }
|
|
34
|
+
for (const name of entries) {
|
|
35
|
+
if (!/^Python\d+/i.test(name)) continue;
|
|
36
|
+
const exe = `${base}\\${name}\\python.exe`;
|
|
37
|
+
try { if (fs.statSync(exe).isFile()) results.push(exe); } catch { }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* py.exe 启动器候选:先试 PATH 中的,再试常见绝对路径,
|
|
44
|
+
* 避免 Gradle / CI 环境下 PATH 不含 C:\Windows 的情况。
|
|
45
|
+
*/
|
|
46
|
+
pyLauncherCandidates = win ? ['py.exe', `${systemRoot}\\py.exe`, `${systemRoot}\\System32\\py.exe`] : [];
|
|
36
47
|
|
|
37
48
|
/**
|
|
38
49
|
* Python 查找器:按顺序尝试多种途径定位可用的 Python 3.6+
|
|
@@ -45,12 +56,11 @@ class PythonFinder {
|
|
|
45
56
|
argsVersion = ['-c', 'import sys; print("%s.%s.%s" % sys.version_info[:3]);'];
|
|
46
57
|
semverRange = '>=3.6.0';
|
|
47
58
|
|
|
48
|
-
// 以下可在测试时覆盖:
|
|
49
59
|
execFile = execFile;
|
|
50
60
|
env = process.env;
|
|
51
61
|
win = win;
|
|
52
62
|
pyLauncher = 'py.exe';
|
|
53
|
-
winDefaultLocations =
|
|
63
|
+
winDefaultLocations = win ? scanWindowsPythonLocations() : [];
|
|
54
64
|
|
|
55
65
|
constructor(configPython) {
|
|
56
66
|
this.configPython = configPython, this.errorLog = [];
|
|
@@ -71,7 +81,7 @@ class PythonFinder {
|
|
|
71
81
|
this.addLog('checking Python explicitly set from NODE_GYP_FORCE_PYTHON');
|
|
72
82
|
this.addLog('- process.env.NODE_GYP_FORCE_PYTHON is ' + `"${this.env.NODE_GYP_FORCE_PYTHON}"`);
|
|
73
83
|
}, check: () => this.checkCommand(this.env.NODE_GYP_FORCE_PYTHON)
|
|
74
|
-
}]
|
|
84
|
+
}];
|
|
75
85
|
}
|
|
76
86
|
const checks = [
|
|
77
87
|
{
|
|
@@ -80,8 +90,8 @@ class PythonFinder {
|
|
|
80
90
|
this.addLog('Python is not set from command line or npm configuration');
|
|
81
91
|
return SKIP;
|
|
82
92
|
}
|
|
83
|
-
this.addLog('checking Python explicitly set from command line or ' + 'npm configuration')
|
|
84
|
-
this.addLog('- "--python=" or "npm config get python" is ' + `"${this.configPython}"`)
|
|
93
|
+
this.addLog('checking Python explicitly set from command line or ' + 'npm configuration');
|
|
94
|
+
this.addLog('- "--python=" or "npm config get python" is ' + `"${this.configPython}"`);
|
|
85
95
|
}, check: () => this.checkCommand(this.configPython)
|
|
86
96
|
},
|
|
87
97
|
{
|
|
@@ -98,23 +108,28 @@ class PythonFinder {
|
|
|
98
108
|
];
|
|
99
109
|
|
|
100
110
|
if (this.win) {
|
|
111
|
+
for (const launcher of pyLauncherCandidates) {
|
|
112
|
+
checks.push({
|
|
113
|
+
before: () => this.addLog(`checking if "${launcher}" can be used to find Python 3`),
|
|
114
|
+
check: () => this.checkPyLauncher(launcher)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
101
117
|
checks.push({
|
|
102
|
-
before: () =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
})
|
|
118
|
+
before: () => this.addLog('enumerating installed Pythons via "py -0p"'),
|
|
119
|
+
check: () => this.checkPyList()
|
|
120
|
+
});
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
checks.push(...[
|
|
109
124
|
{
|
|
110
|
-
before: () => { this.addLog('checking if "python3" can be used') },
|
|
125
|
+
before: () => { this.addLog('checking if "python3" can be used'); },
|
|
111
126
|
check: () => this.checkCommand('python3')
|
|
112
127
|
},
|
|
113
128
|
{
|
|
114
|
-
before: () => { this.addLog('checking if "python" can be used') },
|
|
129
|
+
before: () => { this.addLog('checking if "python" can be used'); },
|
|
115
130
|
check: () => this.checkCommand('python')
|
|
116
131
|
}
|
|
117
|
-
])
|
|
132
|
+
]);
|
|
118
133
|
|
|
119
134
|
if (this.win) {
|
|
120
135
|
for (let i = 0; i < this.winDefaultLocations.length; ++i) {
|
|
@@ -122,7 +137,7 @@ class PythonFinder {
|
|
|
122
137
|
checks.push({
|
|
123
138
|
before: () => this.addLog(`checking if Python is ${location}`),
|
|
124
139
|
check: () => this.checkExecPath(location)
|
|
125
|
-
})
|
|
140
|
+
});
|
|
126
141
|
}
|
|
127
142
|
}
|
|
128
143
|
|
|
@@ -130,7 +145,7 @@ class PythonFinder {
|
|
|
130
145
|
})();
|
|
131
146
|
|
|
132
147
|
for (const check of toCheck) {
|
|
133
|
-
const before = check.before()
|
|
148
|
+
const before = check.before();
|
|
134
149
|
if (before === SKIP) continue;
|
|
135
150
|
if (before === FAIL) return this.fail();
|
|
136
151
|
try {
|
|
@@ -143,52 +158,64 @@ class PythonFinder {
|
|
|
143
158
|
return this.fail();
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
// 检查命令是否为可用的 Python
|
|
147
|
-
// Windows 下需在 CMD shell 中运行以支持 BAT/CMD 启动器。
|
|
161
|
+
// 检查命令是否为可用的 Python。
|
|
148
162
|
async checkCommand(command) {
|
|
149
|
-
let exec = command, args = this.argsExecutable, shell = false;
|
|
150
|
-
// 参数需手动加引号
|
|
151
|
-
if (this.win) exec = `"${exec}"`, args = args.map(a => `"${a}"`), shell = true;
|
|
152
|
-
|
|
153
163
|
this.log.verbose(`- executing "${command}" to get executable path`);
|
|
154
|
-
// 可能结果:
|
|
155
|
-
// - 报错:不在 PATH、不可执行或执行失败
|
|
156
|
-
// - 乱码:后续版本检查会失败
|
|
157
|
-
// - 可执行文件的绝对路径
|
|
158
164
|
try {
|
|
159
|
-
const execPath = await this.run(
|
|
165
|
+
const execPath = await this.run(command, this.argsExecutable, false);
|
|
160
166
|
this.addLog(`- executable path is "${execPath}"`);
|
|
161
|
-
return this.checkExecPath(execPath)
|
|
167
|
+
return this.checkExecPath(execPath);
|
|
162
168
|
} catch (err) {
|
|
163
169
|
this.addLog(`- "${command}" is not in PATH or produced an error`);
|
|
164
170
|
throw err;
|
|
165
171
|
}
|
|
166
172
|
}
|
|
167
173
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// 由于 "py.exe -3" 对 execFile 来说不是合法可执行文件,需要用启动器找出真正的 python.exe 路径。
|
|
172
|
-
async checkPyLauncher() {
|
|
173
|
-
this.log.verbose(`- executing "${this.pyLauncher}" to get Python 3 executable path`)
|
|
174
|
-
// 可能结果:与 checkCommand 相同
|
|
174
|
+
// 检查指定 py 启动器是否能找到 Python 3。
|
|
175
|
+
async checkPyLauncher(launcher = this.pyLauncher) {
|
|
176
|
+
this.log.verbose(`- executing "${launcher}" to get Python 3 executable path`);
|
|
175
177
|
try {
|
|
176
|
-
const execPath = await this.run(
|
|
178
|
+
const execPath = await this.run(launcher, ['-3', ...this.argsExecutable], false);
|
|
177
179
|
this.addLog(`- executable path is "${execPath}"`);
|
|
178
180
|
return this.checkExecPath(execPath);
|
|
179
181
|
} catch (err) {
|
|
180
|
-
this.addLog(`- "${
|
|
182
|
+
this.addLog(`- "${launcher}" is not in PATH or produced an error`);
|
|
181
183
|
throw err;
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
//
|
|
187
|
+
// 通过 "py -0p" 枚举所有已注册的 Python,覆盖非标准安装位置。
|
|
188
|
+
async checkPyList() {
|
|
189
|
+
this.log.verbose('- executing "py -0p" to list registered Pythons');
|
|
190
|
+
let lastErr;
|
|
191
|
+
for (const launcher of pyLauncherCandidates) {
|
|
192
|
+
try {
|
|
193
|
+
const stdout = await this.run(launcher, ['-0p'], false);
|
|
194
|
+
const paths = [];
|
|
195
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
196
|
+
const m = line.match(/([A-Za-z]:\\[^\s]+)/);
|
|
197
|
+
if (m && /python\.exe$/i.test(m[1])) paths.push(m[1]);
|
|
198
|
+
}
|
|
199
|
+
this.addLog(`- "py -0p" (via ${launcher}) found ${paths.length} path(s)`);
|
|
200
|
+
for (const p of paths) {
|
|
201
|
+
try {
|
|
202
|
+
return await this.checkExecPath(p);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
lastErr = err;
|
|
205
|
+
this.log.silly('checkPyList: skipping %s: %s', p, err.message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
lastErr = err;
|
|
210
|
+
this.addLog(`- "py -0p" via "${launcher}" failed`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
throw lastErr || new Error('py -0p returned no usable Python');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 检查某个 Python 可执行文件版本是否可用。
|
|
186
217
|
async checkExecPath(execPath) {
|
|
187
|
-
this.log.verbose(`- executing "${execPath}" to get version`)
|
|
188
|
-
// 可能结果:
|
|
189
|
-
// - 报错:可执行文件无法运行(多半上一步输出了乱码)
|
|
190
|
-
// - 乱码:上一步不知为何输出了可执行文件路径,验证版本时会失败
|
|
191
|
-
// - Python 可执行文件的版本号
|
|
218
|
+
this.log.verbose(`- executing "${execPath}" to get version`);
|
|
192
219
|
try {
|
|
193
220
|
const version = await this.run(execPath, this.argsVersion, false);
|
|
194
221
|
this.addLog(`- version is "${version}"`);
|
|
@@ -215,24 +242,20 @@ class PythonFinder {
|
|
|
215
242
|
}
|
|
216
243
|
}
|
|
217
244
|
|
|
218
|
-
//
|
|
245
|
+
// 执行外部命令。execFile 包装不 reject,通过 [err, stdout, stderr] 返回结果,
|
|
246
|
+
// 必须显式检查 err,否则失败会被当成"成功但空输出",掩盖真实原因。
|
|
219
247
|
async run(exec, args, shell) {
|
|
220
248
|
const env = Object.assign({}, this.env), opts = { env, shell };
|
|
221
|
-
|
|
222
249
|
env.TERM = 'dumb';
|
|
223
250
|
this.log.silly('execFile: exec = %j', exec);
|
|
224
251
|
this.log.silly('execFile: args = %j', args);
|
|
225
252
|
this.log.silly('execFile: opts = %j', opts);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} catch (err) {
|
|
233
|
-
this.log.silly('execFile: threw:\n%s', err.stack);
|
|
234
|
-
throw err;
|
|
235
|
-
}
|
|
253
|
+
const [err, stdout, stderr] = await this.execFile(exec, args, opts);
|
|
254
|
+
this.log.silly('execFile result: err = %j', (err && err.stack) || err);
|
|
255
|
+
this.log.silly('execFile result: stdout = %j', stdout);
|
|
256
|
+
this.log.silly('execFile result: stderr = %j', stderr);
|
|
257
|
+
if (err) throw err;
|
|
258
|
+
return stdout.trim();
|
|
236
259
|
}
|
|
237
260
|
|
|
238
261
|
succeed(execPath, version) {
|
|
@@ -243,7 +266,6 @@ class PythonFinder {
|
|
|
243
266
|
fail() {
|
|
244
267
|
const errorLog = this.errorLog.join('\n'),
|
|
245
268
|
pathExample = this.win ? 'C:\\Path\\To\\python.exe' : '/path/to/pythonexecutable',
|
|
246
|
-
// Windows 80 列控制台,用到 X 标记列之前(含日志前缀共 79 字符,可用 58 字符)
|
|
247
269
|
info = [
|
|
248
270
|
'**********************************************************',
|
|
249
271
|
'You need to install the latest version of Python.',
|