diff --git a/npm/scripts/postinstall.js b/npm/scripts/postinstall.js index ff1ad06..da4a2f3 100644 --- a/npm/scripts/postinstall.js +++ b/npm/scripts/postinstall.js @@ -1,6 +1,8 @@ const { spawnSync } = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); +const https = require("node:https"); +const os = require("node:os"); const packageRoot = path.resolve(__dirname, "..", ".."); const venvDir = path.join(packageRoot, ".smart-search-python"); @@ -16,7 +18,7 @@ function run(command, args, options = {}) { if (result.error) { return { ok: false, error: result.error }; } - return { ok: result.status === 0, status: result.status, stdout: result.stdout || "" }; + return { ok: result.status === 0, status: result.status, stdout: result.stdout || "", stderr: result.stderr || "" }; } function pythonCandidates() { @@ -54,34 +56,192 @@ function venvPython() { : path.join(venvDir, "bin", "python"); } -const python = findPython(); -if (!python) { - console.error("smart-search requires Python 3.10 or newer."); - console.error("Install Python, then run: npm install -g @konbakuyomu/smart-search@latest"); - process.exit(1); +/** + * Download a URL to a temporary file and return the file path. + */ +function downloadToTempFile(url) { + return new Promise((resolve, reject) => { + const tmpFile = path.join(os.tmpdir(), `smart-search-get-pip-${Date.now()}.py`); + const file = fs.createWriteStream(tmpFile); + https.get(url, (response) => { + if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { + file.close(); + fs.unlinkSync(tmpFile); + downloadToTempFile(response.headers.location).then(resolve).catch(reject); + return; + } + if (response.statusCode !== 200) { + file.close(); + fs.unlinkSync(tmpFile); + reject(new Error(`HTTP ${response.statusCode} downloading ${url}`)); + return; + } + response.pipe(file); + file.on("finish", () => { + file.close(); + resolve(tmpFile); + }); + }).on("error", (err) => { + file.close(); + if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); + reject(err); + }); + }); } -if (!fs.existsSync(venvPython())) { - console.log("Creating smart-search Python runtime..."); - const created = run(python.command, [...python.args, "-m", "venv", venvDir]); - if (!created.ok) { - console.error("Failed to create the smart-search Python virtual environment."); - process.exit(created.status || 1); +/** + * Strategy 1: python3 -m venv (works on most systems) + */ +function tryVenv(python) { + console.log(" Trying: python3 -m venv ..."); + const result = run(python.command, [...python.args, "-m", "venv", venvDir], { stdio: "pipe" }); + if (result.ok) return true; + + const stderr = result.stderr || result.stdout || ""; + if (stderr.includes("ensurepip") || stderr.includes("pip") || stderr.includes("venv")) { + console.log(" venv creation failed (likely missing ensurepip/python3-venv)."); + } else { + console.log(" venv creation failed."); } + return false; } -const py = venvPython(); +/** + * Strategy 2: install virtualenv via pip --user, then use it + */ +function tryVirtualenv(python) { + console.log(" Trying: virtualenv fallback ..."); -console.log("Installing smart-search Python package..."); -const install = run(py, [ - "-m", - "pip", - "install", - "--disable-pip-version-check", - packageRoot -]); + // Check if virtualenv is already available + let hasVirtualenv = run(python.command, [...python.args, "-m", "virtualenv", "--version"], { stdio: "pipe" }); + + if (!hasVirtualenv.ok) { + console.log(" virtualenv not found, installing via pip --user ..."); + const installVenv = run(python.command, [...python.args, "-m", "pip", "install", "--user", "virtualenv"], { stdio: "pipe" }); + if (!installVenv.ok) { + console.log(" Failed to install virtualenv."); + return false; + } + } -if (!install.ok) { - console.error("Failed to install the bundled smart-search Python package."); - process.exit(install.status || 1); + const result = run(python.command, [...python.args, "-m", "virtualenv", venvDir], { stdio: "pipe" }); + if (result.ok) return true; + + console.log(" virtualenv failed to create the environment."); + return false; } + +/** + * Strategy 3: python3 -m venv --without-pip, then bootstrap pip + */ +async function tryVenvWithoutPipAsync(python) { + console.log(" Trying: venv --without-pip + pip bootstrap ..."); + + // Remove any partial venv from earlier attempts + if (fs.existsSync(venvDir)) { + fs.rmSync(venvDir, { recursive: true, force: true }); + } + + const result = run(python.command, [...python.args, "-m", "venv", "--without-pip", venvDir], { stdio: "pipe" }); + if (!result.ok) { + console.log(" venv --without-pip also failed."); + return false; + } + + const py = venvPython(); + + // Try ensurepip.bootstrap() first + console.log(" Bootstrapping pip via ensurepip ..."); + const bootstrap = run(py, ["-m", "ensurepip", "--upgrade"], { stdio: "pipe" }); + if (bootstrap.ok) return true; + + // Fall back to get-pip.py + console.log(" ensurepip unavailable, downloading get-pip.py ..."); + let tmpFile; + try { + tmpFile = await downloadToTempFile("https://bootstrap.pypa.io/get-pip.py"); + } catch (err) { + console.log(` Failed to download get-pip.py: ${err.message}`); + return false; + } + + try { + const getPipResult = run(py, [tmpFile], { stdio: "pipe" }); + if (getPipResult.ok) return true; + console.log(" get-pip.py failed to install pip."); + return false; + } finally { + if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); + } +} + +/** + * Create a virtual environment using the best available method. + * Tries: venv -> virtualenv -> venv --without-pip + pip bootstrap + */ +async function createVenv(python) { + if (tryVenv(python)) return true; + + // Clean up failed attempt + if (fs.existsSync(venvDir)) { + fs.rmSync(venvDir, { recursive: true, force: true }); + } + + if (tryVirtualenv(python)) return true; + + // Clean up failed attempt + if (fs.existsSync(venvDir)) { + fs.rmSync(venvDir, { recursive: true, force: true }); + } + + if (await tryVenvWithoutPipAsync(python)) return true; + + return false; +} + +// --- Main --- + +async function main() { + const python = findPython(); + if (!python) { + console.error("smart-search requires Python 3.10 or newer."); + console.error("Install Python, then run: npm install -g @konbakuyomu/smart-search@latest"); + process.exit(1); + } + + if (!fs.existsSync(venvPython())) { + console.log("Creating smart-search Python runtime..."); + const created = await createVenv(python); + if (!created) { + console.error(""); + console.error("Failed to create the smart-search Python virtual environment."); + console.error(""); + console.error("On Debian/Ubuntu, install the required package:"); + console.error(" sudo apt install python3-venv"); + console.error(""); + console.error("Then retry: npm install -g @konbakuyomu/smart-search@latest"); + process.exit(1); + } + } + + const py = venvPython(); + + console.log("Installing smart-search Python package..."); + const install = run(py, [ + "-m", + "pip", + "install", + "--disable-pip-version-check", + packageRoot + ]); + + if (!install.ok) { + console.error("Failed to install the bundled smart-search Python package."); + process.exit(install.status || 1); + } +} + +main().catch((err) => { + console.error("Unexpected error during postinstall:", err.message); + process.exit(1); +});