diff --git a/lib/find-python.js b/lib/find-python.js index af269de2fc..47a9aa6dd4 100644 --- a/lib/find-python.js +++ b/lib/find-python.js @@ -1,6 +1,5 @@ 'use strict' -const path = require('path') const log = require('npmlog') const semver = require('semver') const cp = require('child_process') @@ -8,6 +7,23 @@ const extend = require('util')._extend // eslint-disable-line const win = process.platform === 'win32' const logWithPrefix = require('./util').logWithPrefix +const systemDrive = process.env.SystemDrive || 'C:' +const username = process.env.USERNAME || process.env.USER || require('os').userInfo().username +const localAppData = process.env.LOCALAPPDATA || `${systemDrive}\\${username}\\AppData\\Local` +const programFiles = process.env.ProgramW6432 || process.env.ProgramFiles || `${systemDrive}\\Program Files` +const programFilesX86 = process.env['ProgramFiles(x86)'] || `${programFiles} (x86)` + +const winDefaultLocationsArray = [] +for (const majorMinor of ['39', '38', '37', '36']) { + winDefaultLocationsArray.push( + `${localAppData}\\Programs\\Python\\Python${majorMinor}\\python.exe`, + `${programFiles}\\Python${majorMinor}\\python.exe`, + `${localAppData}\\Programs\\Python\\Python${majorMinor}-32\\python.exe`, + `${programFiles}\\Python${majorMinor}-32\\python.exe`, + `${programFilesX86}\\Python${majorMinor}-32\\python.exe` + ) +} + function PythonFinder (configPython, callback) { this.callback = callback this.configPython = configPython @@ -18,17 +34,14 @@ PythonFinder.prototype = { log: logWithPrefix(log, 'find Python'), argsExecutable: ['-c', 'import sys; print(sys.executable);'], argsVersion: ['-c', 'import sys; print("%s.%s.%s" % sys.version_info[:3]);'], - semverRange: '2.7.x || >=3.5.0', + semverRange: '>=3.6.0', // These can be overridden for testing: execFile: cp.execFile, env: process.env, win: win, pyLauncher: 'py.exe', - winDefaultLocations: [ - path.join(process.env.SystemDrive || 'C:', 'Python37', 'python.exe'), - path.join(process.env.SystemDrive || 'C:', 'Python27', 'python.exe') - ], + winDefaultLocations: winDefaultLocationsArray, // Logs a message at verbose level, but also saves it to be displayed later // at error level if an error occurs. This should help diagnose the problem. @@ -96,11 +109,6 @@ PythonFinder.prototype = { before: () => { this.addLog('checking if "python" can be used') }, check: this.checkCommand, arg: 'python' - }, - { - before: () => { this.addLog('checking if "python2" can be used') }, - check: this.checkCommand, - arg: 'python2' } ] @@ -119,7 +127,7 @@ PythonFinder.prototype = { checks.push({ before: () => { this.addLog( - 'checking if the py launcher can be used to find Python') + 'checking if the py launcher can be used to find Python 3') }, check: this.checkPyLauncher }) @@ -188,10 +196,15 @@ PythonFinder.prototype = { // Distributions of Python on Windows by default install with the "py.exe" // Python launcher which is more likely to exist than the Python executable // being in the $PATH. + // Because the Python launcher supports Python 2 and Python 3, we should + // explicitly request a Python 3 version. This is done by supplying "-3" as + // the first command line argument. Since "py.exe -3" would be an invalid + // executable for "execFile", we have to use the launcher to figure out + // where the actual "python.exe" executable is located. checkPyLauncher: function checkPyLauncher (errorCallback) { this.log.verbose( - `- executing "${this.pyLauncher}" to get Python executable path`) - this.run(this.pyLauncher, this.argsExecutable, false, + `- executing "${this.pyLauncher}" to get Python 3 executable path`) + this.run(this.pyLauncher, ['-3', ...this.argsExecutable], false, function (err, execPath) { // Possible outcomes: same as checkCommand if (err) { diff --git a/test/test-find-python.js b/test/test-find-python.js index 6be887f7eb..67d0b2664f 100644 --- a/test/test-find-python.js +++ b/test/test-find-python.js @@ -16,13 +16,8 @@ test('find python', function (t) { t.strictEqual(err, null) var proc = execFile(found, ['-V'], function (err, stdout, stderr) { t.strictEqual(err, null) - if (/Python 2/.test(stderr)) { - t.strictEqual(stdout, '') - t.ok(/Python 2/.test(stderr)) - } else { - t.ok(/Python 3/.test(stdout)) - t.strictEqual(stderr, '') - } + t.ok(/Python 3/.test(stdout)) + t.strictEqual(stderr, '') }) proc.stdout.setEncoding('utf-8') proc.stderr.setEncoding('utf-8') @@ -66,7 +61,7 @@ test('find python - python', function (t) { poison(f, 'execFile') t.strictEqual(program, '/path/python') t.ok(/sys\.version_info/.test(args[1])) - cb(null, '2.7.15') + cb(null, '3.9.1') } t.strictEqual(program, process.platform === 'win32' ? '"python"' : 'python') @@ -146,13 +141,14 @@ test('find python - no python2, no python, unix', function (t) { }) test('find python - no python, use python launcher', function (t) { - t.plan(3) + t.plan(4) var f = new TestPythonFinder(null, done) f.win = true f.execFile = function (program, args, opts, cb) { if (program === 'py.exe') { + t.notEqual(args.indexOf('-3'), -1) t.notEqual(args.indexOf('-c'), -1) return cb(null, 'Z:\\snake.exe') } @@ -162,7 +158,7 @@ test('find python - no python, use python launcher', function (t) { cb(new Error('not found')) } else if (/sys\.version_info/.test(args[args.length - 1])) { if (program === 'Z:\\snake.exe') { - cb(null, '2.7.14') + cb(null, '3.9.0') } else { t.fail() } @@ -181,9 +177,9 @@ test('find python - no python, use python launcher', function (t) { test('find python - no python, no python launcher, good guess', function (t) { t.plan(2) - var re = /C:[\\/]Python37[\\/]python[.]exe/ var f = new TestPythonFinder(null, done) f.win = true + const expectedProgram = f.winDefaultLocations[0] f.execFile = function (program, args, opts, cb) { if (program === 'py.exe') { @@ -191,7 +187,7 @@ test('find python - no python, no python launcher, good guess', function (t) { } if (/sys\.executable/.test(args[args.length - 1])) { cb(new Error('not found')) - } else if (re.test(program) && + } else if (program === expectedProgram && /sys\.version_info/.test(args[args.length - 1])) { cb(null, '3.7.3') } else { @@ -202,7 +198,7 @@ test('find python - no python, no python launcher, good guess', function (t) { function done (err, python) { t.strictEqual(err, null) - t.ok(re.test(python)) + t.ok(python === expectedProgram) } })