11cb0ef41Sopenharmony_ci'use strict'
21cb0ef41Sopenharmony_ci
31cb0ef41Sopenharmony_ciconst log = require('./log')
41cb0ef41Sopenharmony_ciconst semver = require('semver')
51cb0ef41Sopenharmony_ciconst { execFile } = require('./util')
61cb0ef41Sopenharmony_ciconst win = process.platform === 'win32'
71cb0ef41Sopenharmony_ci
81cb0ef41Sopenharmony_cifunction getOsUserInfo () {
91cb0ef41Sopenharmony_ci  try {
101cb0ef41Sopenharmony_ci    return require('os').userInfo().username
111cb0ef41Sopenharmony_ci  } catch {}
121cb0ef41Sopenharmony_ci}
131cb0ef41Sopenharmony_ci
141cb0ef41Sopenharmony_ciconst systemDrive = process.env.SystemDrive || 'C:'
151cb0ef41Sopenharmony_ciconst username = process.env.USERNAME || process.env.USER || getOsUserInfo()
161cb0ef41Sopenharmony_ciconst localAppData = process.env.LOCALAPPDATA || `${systemDrive}\\${username}\\AppData\\Local`
171cb0ef41Sopenharmony_ciconst foundLocalAppData = process.env.LOCALAPPDATA || username
181cb0ef41Sopenharmony_ciconst programFiles = process.env.ProgramW6432 || process.env.ProgramFiles || `${systemDrive}\\Program Files`
191cb0ef41Sopenharmony_ciconst programFilesX86 = process.env['ProgramFiles(x86)'] || `${programFiles} (x86)`
201cb0ef41Sopenharmony_ci
211cb0ef41Sopenharmony_ciconst winDefaultLocationsArray = []
221cb0ef41Sopenharmony_cifor (const majorMinor of ['311', '310', '39', '38']) {
231cb0ef41Sopenharmony_ci  if (foundLocalAppData) {
241cb0ef41Sopenharmony_ci    winDefaultLocationsArray.push(
251cb0ef41Sopenharmony_ci      `${localAppData}\\Programs\\Python\\Python${majorMinor}\\python.exe`,
261cb0ef41Sopenharmony_ci      `${programFiles}\\Python${majorMinor}\\python.exe`,
271cb0ef41Sopenharmony_ci      `${localAppData}\\Programs\\Python\\Python${majorMinor}-32\\python.exe`,
281cb0ef41Sopenharmony_ci      `${programFiles}\\Python${majorMinor}-32\\python.exe`,
291cb0ef41Sopenharmony_ci      `${programFilesX86}\\Python${majorMinor}-32\\python.exe`
301cb0ef41Sopenharmony_ci    )
311cb0ef41Sopenharmony_ci  } else {
321cb0ef41Sopenharmony_ci    winDefaultLocationsArray.push(
331cb0ef41Sopenharmony_ci      `${programFiles}\\Python${majorMinor}\\python.exe`,
341cb0ef41Sopenharmony_ci      `${programFiles}\\Python${majorMinor}-32\\python.exe`,
351cb0ef41Sopenharmony_ci      `${programFilesX86}\\Python${majorMinor}-32\\python.exe`
361cb0ef41Sopenharmony_ci    )
371cb0ef41Sopenharmony_ci  }
381cb0ef41Sopenharmony_ci}
391cb0ef41Sopenharmony_ci
401cb0ef41Sopenharmony_ciclass PythonFinder {
411cb0ef41Sopenharmony_ci  static findPython = (...args) => new PythonFinder(...args).findPython()
421cb0ef41Sopenharmony_ci
431cb0ef41Sopenharmony_ci  log = log.withPrefix('find Python')
441cb0ef41Sopenharmony_ci  argsExecutable = ['-c', 'import sys; print(sys.executable);']
451cb0ef41Sopenharmony_ci  argsVersion = ['-c', 'import sys; print("%s.%s.%s" % sys.version_info[:3]);']
461cb0ef41Sopenharmony_ci  semverRange = '>=3.6.0'
471cb0ef41Sopenharmony_ci
481cb0ef41Sopenharmony_ci  // These can be overridden for testing:
491cb0ef41Sopenharmony_ci  execFile = execFile
501cb0ef41Sopenharmony_ci  env = process.env
511cb0ef41Sopenharmony_ci  win = win
521cb0ef41Sopenharmony_ci  pyLauncher = 'py.exe'
531cb0ef41Sopenharmony_ci  winDefaultLocations = winDefaultLocationsArray
541cb0ef41Sopenharmony_ci
551cb0ef41Sopenharmony_ci  constructor (configPython) {
561cb0ef41Sopenharmony_ci    this.configPython = configPython
571cb0ef41Sopenharmony_ci    this.errorLog = []
581cb0ef41Sopenharmony_ci  }
591cb0ef41Sopenharmony_ci
601cb0ef41Sopenharmony_ci  // Logs a message at verbose level, but also saves it to be displayed later
611cb0ef41Sopenharmony_ci  // at error level if an error occurs. This should help diagnose the problem.
621cb0ef41Sopenharmony_ci  addLog (message) {
631cb0ef41Sopenharmony_ci    this.log.verbose(message)
641cb0ef41Sopenharmony_ci    this.errorLog.push(message)
651cb0ef41Sopenharmony_ci  }
661cb0ef41Sopenharmony_ci
671cb0ef41Sopenharmony_ci  // Find Python by trying a sequence of possibilities.
681cb0ef41Sopenharmony_ci  // Ignore errors, keep trying until Python is found.
691cb0ef41Sopenharmony_ci  async findPython () {
701cb0ef41Sopenharmony_ci    const SKIP = 0
711cb0ef41Sopenharmony_ci    const FAIL = 1
721cb0ef41Sopenharmony_ci    const toCheck = (() => {
731cb0ef41Sopenharmony_ci      if (this.env.NODE_GYP_FORCE_PYTHON) {
741cb0ef41Sopenharmony_ci        return [{
751cb0ef41Sopenharmony_ci          before: () => {
761cb0ef41Sopenharmony_ci            this.addLog(
771cb0ef41Sopenharmony_ci              'checking Python explicitly set from NODE_GYP_FORCE_PYTHON')
781cb0ef41Sopenharmony_ci            this.addLog('- process.env.NODE_GYP_FORCE_PYTHON is ' +
791cb0ef41Sopenharmony_ci              `"${this.env.NODE_GYP_FORCE_PYTHON}"`)
801cb0ef41Sopenharmony_ci          },
811cb0ef41Sopenharmony_ci          check: () => this.checkCommand(this.env.NODE_GYP_FORCE_PYTHON)
821cb0ef41Sopenharmony_ci        }]
831cb0ef41Sopenharmony_ci      }
841cb0ef41Sopenharmony_ci
851cb0ef41Sopenharmony_ci      const checks = [
861cb0ef41Sopenharmony_ci        {
871cb0ef41Sopenharmony_ci          before: () => {
881cb0ef41Sopenharmony_ci            if (!this.configPython) {
891cb0ef41Sopenharmony_ci              this.addLog(
901cb0ef41Sopenharmony_ci                'Python is not set from command line or npm configuration')
911cb0ef41Sopenharmony_ci              return SKIP
921cb0ef41Sopenharmony_ci            }
931cb0ef41Sopenharmony_ci            this.addLog('checking Python explicitly set from command line or ' +
941cb0ef41Sopenharmony_ci              'npm configuration')
951cb0ef41Sopenharmony_ci            this.addLog('- "--python=" or "npm config get python" is ' +
961cb0ef41Sopenharmony_ci              `"${this.configPython}"`)
971cb0ef41Sopenharmony_ci          },
981cb0ef41Sopenharmony_ci          check: () => this.checkCommand(this.configPython)
991cb0ef41Sopenharmony_ci        },
1001cb0ef41Sopenharmony_ci        {
1011cb0ef41Sopenharmony_ci          before: () => {
1021cb0ef41Sopenharmony_ci            if (!this.env.PYTHON) {
1031cb0ef41Sopenharmony_ci              this.addLog('Python is not set from environment variable ' +
1041cb0ef41Sopenharmony_ci                'PYTHON')
1051cb0ef41Sopenharmony_ci              return SKIP
1061cb0ef41Sopenharmony_ci            }
1071cb0ef41Sopenharmony_ci            this.addLog('checking Python explicitly set from environment ' +
1081cb0ef41Sopenharmony_ci              'variable PYTHON')
1091cb0ef41Sopenharmony_ci            this.addLog(`- process.env.PYTHON is "${this.env.PYTHON}"`)
1101cb0ef41Sopenharmony_ci          },
1111cb0ef41Sopenharmony_ci          check: () => this.checkCommand(this.env.PYTHON)
1121cb0ef41Sopenharmony_ci        }
1131cb0ef41Sopenharmony_ci      ]
1141cb0ef41Sopenharmony_ci
1151cb0ef41Sopenharmony_ci      if (this.win) {
1161cb0ef41Sopenharmony_ci        checks.push({
1171cb0ef41Sopenharmony_ci          before: () => {
1181cb0ef41Sopenharmony_ci            this.addLog(
1191cb0ef41Sopenharmony_ci              'checking if the py launcher can be used to find Python 3')
1201cb0ef41Sopenharmony_ci          },
1211cb0ef41Sopenharmony_ci          check: () => this.checkPyLauncher()
1221cb0ef41Sopenharmony_ci        })
1231cb0ef41Sopenharmony_ci      }
1241cb0ef41Sopenharmony_ci
1251cb0ef41Sopenharmony_ci      checks.push(...[
1261cb0ef41Sopenharmony_ci        {
1271cb0ef41Sopenharmony_ci          before: () => { this.addLog('checking if "python3" can be used') },
1281cb0ef41Sopenharmony_ci          check: () => this.checkCommand('python3')
1291cb0ef41Sopenharmony_ci        },
1301cb0ef41Sopenharmony_ci        {
1311cb0ef41Sopenharmony_ci          before: () => { this.addLog('checking if "python" can be used') },
1321cb0ef41Sopenharmony_ci          check: () => this.checkCommand('python')
1331cb0ef41Sopenharmony_ci        }
1341cb0ef41Sopenharmony_ci      ])
1351cb0ef41Sopenharmony_ci
1361cb0ef41Sopenharmony_ci      if (this.win) {
1371cb0ef41Sopenharmony_ci        for (let i = 0; i < this.winDefaultLocations.length; ++i) {
1381cb0ef41Sopenharmony_ci          const location = this.winDefaultLocations[i]
1391cb0ef41Sopenharmony_ci          checks.push({
1401cb0ef41Sopenharmony_ci            before: () => this.addLog(`checking if Python is ${location}`),
1411cb0ef41Sopenharmony_ci            check: () => this.checkExecPath(location)
1421cb0ef41Sopenharmony_ci          })
1431cb0ef41Sopenharmony_ci        }
1441cb0ef41Sopenharmony_ci      }
1451cb0ef41Sopenharmony_ci
1461cb0ef41Sopenharmony_ci      return checks
1471cb0ef41Sopenharmony_ci    })()
1481cb0ef41Sopenharmony_ci
1491cb0ef41Sopenharmony_ci    for (const check of toCheck) {
1501cb0ef41Sopenharmony_ci      const before = check.before()
1511cb0ef41Sopenharmony_ci      if (before === SKIP) {
1521cb0ef41Sopenharmony_ci        continue
1531cb0ef41Sopenharmony_ci      }
1541cb0ef41Sopenharmony_ci      if (before === FAIL) {
1551cb0ef41Sopenharmony_ci        return this.fail()
1561cb0ef41Sopenharmony_ci      }
1571cb0ef41Sopenharmony_ci      try {
1581cb0ef41Sopenharmony_ci        return await check.check()
1591cb0ef41Sopenharmony_ci      } catch (err) {
1601cb0ef41Sopenharmony_ci        this.log.silly('runChecks: err = %j', (err && err.stack) || err)
1611cb0ef41Sopenharmony_ci      }
1621cb0ef41Sopenharmony_ci    }
1631cb0ef41Sopenharmony_ci
1641cb0ef41Sopenharmony_ci    return this.fail()
1651cb0ef41Sopenharmony_ci  }
1661cb0ef41Sopenharmony_ci
1671cb0ef41Sopenharmony_ci  // Check if command is a valid Python to use.
1681cb0ef41Sopenharmony_ci  // Will exit the Python finder on success.
1691cb0ef41Sopenharmony_ci  // If on Windows, run in a CMD shell to support BAT/CMD launchers.
1701cb0ef41Sopenharmony_ci  async checkCommand (command) {
1711cb0ef41Sopenharmony_ci    let exec = command
1721cb0ef41Sopenharmony_ci    let args = this.argsExecutable
1731cb0ef41Sopenharmony_ci    let shell = false
1741cb0ef41Sopenharmony_ci    if (this.win) {
1751cb0ef41Sopenharmony_ci      // Arguments have to be manually quoted
1761cb0ef41Sopenharmony_ci      exec = `"${exec}"`
1771cb0ef41Sopenharmony_ci      args = args.map(a => `"${a}"`)
1781cb0ef41Sopenharmony_ci      shell = true
1791cb0ef41Sopenharmony_ci    }
1801cb0ef41Sopenharmony_ci
1811cb0ef41Sopenharmony_ci    this.log.verbose(`- executing "${command}" to get executable path`)
1821cb0ef41Sopenharmony_ci    // Possible outcomes:
1831cb0ef41Sopenharmony_ci    // - Error: not in PATH, not executable or execution fails
1841cb0ef41Sopenharmony_ci    // - Gibberish: the next command to check version will fail
1851cb0ef41Sopenharmony_ci    // - Absolute path to executable
1861cb0ef41Sopenharmony_ci    try {
1871cb0ef41Sopenharmony_ci      const execPath = await this.run(exec, args, shell)
1881cb0ef41Sopenharmony_ci      this.addLog(`- executable path is "${execPath}"`)
1891cb0ef41Sopenharmony_ci      return this.checkExecPath(execPath)
1901cb0ef41Sopenharmony_ci    } catch (err) {
1911cb0ef41Sopenharmony_ci      this.addLog(`- "${command}" is not in PATH or produced an error`)
1921cb0ef41Sopenharmony_ci      throw err
1931cb0ef41Sopenharmony_ci    }
1941cb0ef41Sopenharmony_ci  }
1951cb0ef41Sopenharmony_ci
1961cb0ef41Sopenharmony_ci  // Check if the py launcher can find a valid Python to use.
1971cb0ef41Sopenharmony_ci  // Will exit the Python finder on success.
1981cb0ef41Sopenharmony_ci  // Distributions of Python on Windows by default install with the "py.exe"
1991cb0ef41Sopenharmony_ci  // Python launcher which is more likely to exist than the Python executable
2001cb0ef41Sopenharmony_ci  // being in the $PATH.
2011cb0ef41Sopenharmony_ci  // Because the Python launcher supports Python 2 and Python 3, we should
2021cb0ef41Sopenharmony_ci  // explicitly request a Python 3 version. This is done by supplying "-3" as
2031cb0ef41Sopenharmony_ci  // the first command line argument. Since "py.exe -3" would be an invalid
2041cb0ef41Sopenharmony_ci  // executable for "execFile", we have to use the launcher to figure out
2051cb0ef41Sopenharmony_ci  // where the actual "python.exe" executable is located.
2061cb0ef41Sopenharmony_ci  async checkPyLauncher () {
2071cb0ef41Sopenharmony_ci    this.log.verbose(`- executing "${this.pyLauncher}" to get Python 3 executable path`)
2081cb0ef41Sopenharmony_ci    // Possible outcomes: same as checkCommand
2091cb0ef41Sopenharmony_ci    try {
2101cb0ef41Sopenharmony_ci      const execPath = await this.run(this.pyLauncher, ['-3', ...this.argsExecutable], false)
2111cb0ef41Sopenharmony_ci      this.addLog(`- executable path is "${execPath}"`)
2121cb0ef41Sopenharmony_ci      return this.checkExecPath(execPath)
2131cb0ef41Sopenharmony_ci    } catch (err) {
2141cb0ef41Sopenharmony_ci      this.addLog(`- "${this.pyLauncher}" is not in PATH or produced an error`)
2151cb0ef41Sopenharmony_ci      throw err
2161cb0ef41Sopenharmony_ci    }
2171cb0ef41Sopenharmony_ci  }
2181cb0ef41Sopenharmony_ci
2191cb0ef41Sopenharmony_ci  // Check if a Python executable is the correct version to use.
2201cb0ef41Sopenharmony_ci  // Will exit the Python finder on success.
2211cb0ef41Sopenharmony_ci  async checkExecPath (execPath) {
2221cb0ef41Sopenharmony_ci    this.log.verbose(`- executing "${execPath}" to get version`)
2231cb0ef41Sopenharmony_ci    // Possible outcomes:
2241cb0ef41Sopenharmony_ci    // - Error: executable can not be run (likely meaning the command wasn't
2251cb0ef41Sopenharmony_ci    //   a Python executable and the previous command produced gibberish)
2261cb0ef41Sopenharmony_ci    // - Gibberish: somehow the last command produced an executable path,
2271cb0ef41Sopenharmony_ci    //   this will fail when verifying the version
2281cb0ef41Sopenharmony_ci    // - Version of the Python executable
2291cb0ef41Sopenharmony_ci    try {
2301cb0ef41Sopenharmony_ci      const version = await this.run(execPath, this.argsVersion, false)
2311cb0ef41Sopenharmony_ci      this.addLog(`- version is "${version}"`)
2321cb0ef41Sopenharmony_ci
2331cb0ef41Sopenharmony_ci      const range = new semver.Range(this.semverRange)
2341cb0ef41Sopenharmony_ci      let valid = false
2351cb0ef41Sopenharmony_ci      try {
2361cb0ef41Sopenharmony_ci        valid = range.test(version)
2371cb0ef41Sopenharmony_ci      } catch (err) {
2381cb0ef41Sopenharmony_ci        this.log.silly('range.test() threw:\n%s', err.stack)
2391cb0ef41Sopenharmony_ci        this.addLog(`- "${execPath}" does not have a valid version`)
2401cb0ef41Sopenharmony_ci        this.addLog('- is it a Python executable?')
2411cb0ef41Sopenharmony_ci        throw err
2421cb0ef41Sopenharmony_ci      }
2431cb0ef41Sopenharmony_ci      if (!valid) {
2441cb0ef41Sopenharmony_ci        this.addLog(`- version is ${version} - should be ${this.semverRange}`)
2451cb0ef41Sopenharmony_ci        this.addLog('- THIS VERSION OF PYTHON IS NOT SUPPORTED')
2461cb0ef41Sopenharmony_ci        throw new Error(`Found unsupported Python version ${version}`)
2471cb0ef41Sopenharmony_ci      }
2481cb0ef41Sopenharmony_ci      return this.succeed(execPath, version)
2491cb0ef41Sopenharmony_ci    } catch (err) {
2501cb0ef41Sopenharmony_ci      this.addLog(`- "${execPath}" could not be run`)
2511cb0ef41Sopenharmony_ci      throw err
2521cb0ef41Sopenharmony_ci    }
2531cb0ef41Sopenharmony_ci  }
2541cb0ef41Sopenharmony_ci
2551cb0ef41Sopenharmony_ci  // Run an executable or shell command, trimming the output.
2561cb0ef41Sopenharmony_ci  async run (exec, args, shell) {
2571cb0ef41Sopenharmony_ci    const env = Object.assign({}, this.env)
2581cb0ef41Sopenharmony_ci    env.TERM = 'dumb'
2591cb0ef41Sopenharmony_ci    const opts = { env, shell }
2601cb0ef41Sopenharmony_ci
2611cb0ef41Sopenharmony_ci    this.log.silly('execFile: exec = %j', exec)
2621cb0ef41Sopenharmony_ci    this.log.silly('execFile: args = %j', args)
2631cb0ef41Sopenharmony_ci    this.log.silly('execFile: opts = %j', opts)
2641cb0ef41Sopenharmony_ci    try {
2651cb0ef41Sopenharmony_ci      const [err, stdout, stderr] = await this.execFile(exec, args, opts)
2661cb0ef41Sopenharmony_ci      this.log.silly('execFile result: err = %j', (err && err.stack) || err)
2671cb0ef41Sopenharmony_ci      this.log.silly('execFile result: stdout = %j', stdout)
2681cb0ef41Sopenharmony_ci      this.log.silly('execFile result: stderr = %j', stderr)
2691cb0ef41Sopenharmony_ci      return stdout.trim()
2701cb0ef41Sopenharmony_ci    } catch (err) {
2711cb0ef41Sopenharmony_ci      this.log.silly('execFile: threw:\n%s', err.stack)
2721cb0ef41Sopenharmony_ci      throw err
2731cb0ef41Sopenharmony_ci    }
2741cb0ef41Sopenharmony_ci  }
2751cb0ef41Sopenharmony_ci
2761cb0ef41Sopenharmony_ci  succeed (execPath, version) {
2771cb0ef41Sopenharmony_ci    this.log.info(`using Python version ${version} found at "${execPath}"`)
2781cb0ef41Sopenharmony_ci    return execPath
2791cb0ef41Sopenharmony_ci  }
2801cb0ef41Sopenharmony_ci
2811cb0ef41Sopenharmony_ci  fail () {
2821cb0ef41Sopenharmony_ci    const errorLog = this.errorLog.join('\n')
2831cb0ef41Sopenharmony_ci
2841cb0ef41Sopenharmony_ci    const pathExample = this.win
2851cb0ef41Sopenharmony_ci      ? 'C:\\Path\\To\\python.exe'
2861cb0ef41Sopenharmony_ci      : '/path/to/pythonexecutable'
2871cb0ef41Sopenharmony_ci    // For Windows 80 col console, use up to the column before the one marked
2881cb0ef41Sopenharmony_ci    // with X (total 79 chars including logger prefix, 58 chars usable here):
2891cb0ef41Sopenharmony_ci    //                                                           X
2901cb0ef41Sopenharmony_ci    const info = [
2911cb0ef41Sopenharmony_ci      '**********************************************************',
2921cb0ef41Sopenharmony_ci      'You need to install the latest version of Python.',
2931cb0ef41Sopenharmony_ci      'Node-gyp should be able to find and use Python. If not,',
2941cb0ef41Sopenharmony_ci      'you can try one of the following options:',
2951cb0ef41Sopenharmony_ci      `- Use the switch --python="${pathExample}"`,
2961cb0ef41Sopenharmony_ci      '  (accepted by both node-gyp and npm)',
2971cb0ef41Sopenharmony_ci      '- Set the environment variable PYTHON',
2981cb0ef41Sopenharmony_ci      '- Set the npm configuration variable python:',
2991cb0ef41Sopenharmony_ci      `  npm config set python "${pathExample}"`,
3001cb0ef41Sopenharmony_ci      'For more information consult the documentation at:',
3011cb0ef41Sopenharmony_ci      'https://github.com/nodejs/node-gyp#installation',
3021cb0ef41Sopenharmony_ci      '**********************************************************'
3031cb0ef41Sopenharmony_ci    ].join('\n')
3041cb0ef41Sopenharmony_ci
3051cb0ef41Sopenharmony_ci    this.log.error(`\n${errorLog}\n\n${info}\n`)
3061cb0ef41Sopenharmony_ci    throw new Error('Could not find any Python installation to use')
3071cb0ef41Sopenharmony_ci  }
3081cb0ef41Sopenharmony_ci}
3091cb0ef41Sopenharmony_ci
3101cb0ef41Sopenharmony_cimodule.exports = PythonFinder
311