@flun/nodejs-mobile-gyp 1.0.2 → 1.1.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.
@@ -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
- return path
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
- return os.path.join(*relative_split)
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
- return os.path.normpath(os.path.join(self.path, path))
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(SourceifyAndQuoteSpaces(bf) for bf in build_files),
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
- process.env.PATH = `${buildBinsDir}:${process.env.PATH}`;
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
- const symlinkDestination = path.join(buildBinsDir, 'python3');
69
- try {
70
- await fs.unlink(symlinkDestination);
71
- } catch (err) {
72
- if (err.code !== 'ENOENT') throw err;
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
- if (win) argv.push('-Gmsvs_toolset=v145');
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 vsInfo => {
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
- const vsInfo = win ? await findVisualStudio(release.semver, gyp.opts['msvs-version']) : null;
178
- return createConfigFile(vsInfo);
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]);
@@ -1,7 +1,8 @@
1
- import * as log from './log.js';
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:', username = process.env.USERNAME || process.env.USER || getOsUserInfo(),
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)`, winDefaultLocationsArray = [];
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 majorMinor of ['311', '310', '39', '38']) {
20
- if (foundLocalAppData) {
21
- winDefaultLocationsArray.push(
22
- `${localAppData}\\Programs\\Python\\Python${majorMinor}\\python.exe`,
23
- `${programFiles}\\Python${majorMinor}\\python.exe`,
24
- `${localAppData}\\Programs\\Python\\Python${majorMinor}-32\\python.exe`,
25
- `${programFiles}\\Python${majorMinor}-32\\python.exe`,
26
- `${programFilesX86}\\Python${majorMinor}-32\\python.exe`
27
- );
28
- } else {
29
- winDefaultLocationsArray.push(
30
- `${programFiles}\\Python${majorMinor}\\python.exe`,
31
- `${programFiles}\\Python${majorMinor}-32\\python.exe`,
32
- `${programFilesX86}\\Python${majorMinor}-32\\python.exe`
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 = winDefaultLocationsArray;
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
- this.addLog('checking if the py launcher can be used to find Python 3');
104
- }, check: () => this.checkPyLauncher()
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(exec, args, shell);
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
- // 检查 py 启动器是否能找到可用的 Python。成功则返回结果。
169
- // Windows 上的 Python 发行版默认安装 "py.exe" 启动器,比 python.exe 出现在 PATH 更常见。
170
- // 因为 py 启动器同时支持 Python 2 和 3,这里显式请求 Python 3:用 "-3" 作为首个参数。
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(this.pyLauncher, ['-3', ...this.argsExecutable], false);
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(`- "${this.pyLauncher}" is not in PATH or produced an error`);
182
+ this.addLog(`- "${launcher}" is not in PATH or produced an error`);
181
183
  throw err;
182
184
  }
183
185
  }
184
186
 
185
- // 检查某个 Python 可执行文件版本是否可用。成功则返回结果。
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
- // 运行可执行文件或 shell 命令,并去除输出两端空白
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
- try {
227
- const [err, stdout, stderr] = await this.execFile(exec, args, opts)
228
- this.log.silly('execFile result: err = %j', (err && err.stack) || err);
229
- this.log.silly('execFile result: stdout = %j', stdout);
230
- this.log.silly('execFile result: stderr = %j', stderr);
231
- return stdout.trim();
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.',
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bindings",
12
12
  "gyp"
13
13
  ],
14
- "version": "1.0.2",
14
+ "version": "1.1.0",
15
15
  "installVersion": 11,
16
16
  "author": "flun <open@flun.top>",
17
17
  "bin": {