From e8a570b83a77622506afb84eac4846c5177e2441 Mon Sep 17 00:00:00 2001 From: Jason Buchanan Date: Fri, 22 Mar 2024 21:05:46 -0600 Subject: [PATCH] fix: give mac developers feedback when `yarn` install fails because of an incorrect python dependency On macOS, this script checks if Python 3.10 is installed and accessible to node-gyp. I ran into a problem trying to `yarn` install, with a system Python version of `3.12.2`, but ran into the error `ModuleNotFoundError: No module named 'distutils'`. Since node-gyp relies on `distutils`, which is removed in Python `3.12`, you need to use a Python version that still includes `distutils`. --- .setup/error-with-remedy.mjs | 12 +++++ .setup/format.mjs | 23 +++++++++ .setup/log.mjs | 21 ++++++++ .setup/macos/python-path.mjs | 13 +++++ .setup/macos/validate-executable.mjs | 27 ++++++++++ .setup/macos/validate-setup.mjs | 53 ++++++++++++++++++++ .setup/yarn-preinstall-system-validation.mjs | 20 ++++++++ package.json | 1 + 8 files changed, 170 insertions(+) create mode 100644 .setup/error-with-remedy.mjs create mode 100644 .setup/format.mjs create mode 100644 .setup/log.mjs create mode 100644 .setup/macos/python-path.mjs create mode 100644 .setup/macos/validate-executable.mjs create mode 100644 .setup/macos/validate-setup.mjs create mode 100644 .setup/yarn-preinstall-system-validation.mjs diff --git a/.setup/error-with-remedy.mjs b/.setup/error-with-remedy.mjs new file mode 100644 index 0000000..856d685 --- /dev/null +++ b/.setup/error-with-remedy.mjs @@ -0,0 +1,12 @@ +export class ErrorWithRemedy extends Error { + constructor(errorMessage, remedyMessage) { + super(errorMessage); + this.name = this.constructor.name; + this.remedy = remedyMessage; + + // Maintaining proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} diff --git a/.setup/format.mjs b/.setup/format.mjs new file mode 100644 index 0000000..9dda37f --- /dev/null +++ b/.setup/format.mjs @@ -0,0 +1,23 @@ +export function formatRed(message) { + return `\x1b[31m${message}\x1b[0m`; +} + +export function formatGray(message) { + return `\x1b[37m${message}\x1b[0m`; +} + +export function formatItalics(message) { + return `\x1b[3m${message}\x1b[0m`; +} + +export function formatBold(message) { + return `\x1b[1m${message}\x1b[0m`; +} + +export function formatError(message) { + return formatBold(formatRed(message)); +} + +export function formatExample(message) { + return formatGray(formatItalics(message)); +} \ No newline at end of file diff --git a/.setup/log.mjs b/.setup/log.mjs new file mode 100644 index 0000000..549e253 --- /dev/null +++ b/.setup/log.mjs @@ -0,0 +1,21 @@ +import { formatBold, formatError, formatGray } from './format.mjs'; + +export function logInfo(message) { + console.log(formatGray(message)); +} + +/** + * Log an error to the console with an error `message` and optional `remedy` + * @param error in the format { message: string, remedy?: string } + * @returns void + */ +export function logError(error) { + console.error(); + console.error(formatError(error?.message || error)); + if (error?.remedy) { + console.error(); + console.error(formatBold('Suggested remedy:')); + console.error(error.remedy); + } + console.error(); +} diff --git a/.setup/macos/python-path.mjs b/.setup/macos/python-path.mjs new file mode 100644 index 0000000..50d8462 --- /dev/null +++ b/.setup/macos/python-path.mjs @@ -0,0 +1,13 @@ +import { logInfo } from '../log.mjs'; +import { execSync } from 'child_process'; + +export function getPythonPath() { + const nodeGypPythonPath = process.env.NODE_GYP_FORCE_PYTHON; + if (nodeGypPythonPath) { + logInfo(`NODE_GYP_FORCE_PYTHON=${nodeGypPythonPath}`); + return nodeGypPythonPath; + } + logInfo(`defaulting to system's python3`); + return execSync(`which python3`).toString().trim(); +} + diff --git a/.setup/macos/validate-executable.mjs b/.setup/macos/validate-executable.mjs new file mode 100644 index 0000000..e9ac3a2 --- /dev/null +++ b/.setup/macos/validate-executable.mjs @@ -0,0 +1,27 @@ +import fs from 'fs'; + +export function validateExecutable(path) { + existsOnSystem(path) + isNotADirectory(path) + isExecutable(path) +} + +function existsOnSystem(path) { + if (!fs.existsSync(path)) { + throw new Error(`Path ${path} does not exist`); + } +} + +function isNotADirectory(path) { + if (fs.statSync(path).isDirectory()) { + throw new Error(`${path} is a directory. Please provide the path to an executable.`); + } +} + +function isExecutable(path) { + try { + fs.accessSync(path, fs.constants.X_OK); + } catch (err) { + throw new Error(`${path} is not executable`); + } +} diff --git a/.setup/macos/validate-setup.mjs b/.setup/macos/validate-setup.mjs new file mode 100644 index 0000000..f2f2511 --- /dev/null +++ b/.setup/macos/validate-setup.mjs @@ -0,0 +1,53 @@ +import { execSync } from 'child_process'; +import { ErrorWithRemedy } from '../error-with-remedy.mjs'; +import { formatExample } from '../format.mjs'; +import { getPythonPath } from './python-path.mjs'; +import { logInfo } from '../log.mjs'; +import { validateExecutable } from './validate-executable.mjs'; + +/** + * On macOS, this script checks if Python 3.10 is installed and accessible to node-gyp. + * + * I ran into a problem trying to `yarn` install, with a system Python version of `3.12.2`, + * but ran into the error `ModuleNotFoundError: No module named 'distutils'`. + * Since node-gyp relies on `distutils`, which is removed in Python `3.12`, + * you need to use a Python version that still includes `distutils`. + */ +export function validateMacSetup() { + logInfo('Installing on macOS'); + const pythonPath = getPythonPath(); + validateExecutable(pythonPath); + + let error; + try { + const pythonVersionOutput = execSync(`${pythonPath} --version`).toString().trim(); + logInfo(`${pythonPath} == (${pythonVersionOutput})`); + + const pythonVersion = pythonVersionOutput.split(' ')[1].trim(); + const majorVersion = parseInt(pythonVersion.split('.')[0]); + const minorVersion = parseInt(pythonVersion.split('.')[1]); + const noCompatiblePythonVersionFound = !(majorVersion === 3 && (minorVersion >= 10 && minorVersion < 12)); + + if (noCompatiblePythonVersionFound) { + error = `Incompatible Python version ${pythonVersion} found. Python 3.10 is required.`; + } + + } catch (caughtError) { + error = `Python 3.10 was not found with error: ${caughtError?.message || caughtError}`; + } + if (error) { + const checkForPythonInstall = 'Check for versions of python installed on your system. For example, if you use brew:'; + const displayBrewPythonVersionsExample = formatExample('brew list --versions | grep python'); + + const pleaseInstallPython = 'If python 3.10 was not found, install it. For example:'; + const installPythonExample = formatExample('brew install python@3.10'); + + const configureNodeGypPython = 'Ensure you have an environment variable for NODE_GYP_FORCE_PYTHON pointing to your python 3.10 path.\n For example, assuming you installed python@3.10 with brew:'; + const exportNodeGypPythonEnvVariable = formatExample('export NODE_GYP_FORCE_PYTHON=$(brew --prefix python@3.10)/bin/python3.10'); + + throw new ErrorWithRemedy(error, ` STEP 1: ${checkForPythonInstall} ${displayBrewPythonVersionsExample} + \n STEP 2: ${pleaseInstallPython} ${installPythonExample} + \n STEP 3: ${configureNodeGypPython} ${exportNodeGypPythonEnvVariable}` + ); + } +} \ No newline at end of file diff --git a/.setup/yarn-preinstall-system-validation.mjs b/.setup/yarn-preinstall-system-validation.mjs new file mode 100644 index 0000000..d57013f --- /dev/null +++ b/.setup/yarn-preinstall-system-validation.mjs @@ -0,0 +1,20 @@ +import * as os from 'os'; +import { validateMacSetup } from './macos/validate-setup.mjs'; +import { logError } from './log.mjs'; + +/** + * Validate the system setup at the beginning of `yarn` install. + */ +try { + const platform = os.platform(); + switch (platform) { + case 'darwin': + validateMacSetup(); + break; + default: + console.log(`No setup validation required for platform ${platform}`); + } +} catch(setupError) { + logError(setupError); + process.exit(1); +} diff --git a/package.json b/package.json index 2c31f50..7070015 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "Morpheus is private, sovereign, AI", "main": ".webpack/main", "scripts": { + "preinstall": "node .setup/yarn-preinstall-system-validation.mjs", "start": "cross-env NODE_ENV=development DEBUG=electron-packager electron-forge start", "package": "set DEBUG=electron-packager && electron-forge package", "make": "electron-forge make",