From 881ed9a29d0231c87c975379c605fdd8c07fe3b6 Mon Sep 17 00:00:00 2001 From: kaihere14 Date: Tue, 21 Jul 2026 15:05:43 +0530 Subject: [PATCH 1/3] feat: add unified dev/prod launcher scripts for PictoPy - Introduced for Node.js-based launching, supporting both development and production modes, with Windows compatibility. - Added for bash-based launching, maintaining existing functionality while enhancing user guidance for command requirements and directory checks. - Updated to improve error message formatting for better readability. --- scripts/run.js | 292 +++++++++++++++++++++++++++++++++++++++++++++++++ scripts/run.sh | 189 ++++++++++++++++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 scripts/run.js create mode 100755 scripts/run.sh diff --git a/scripts/run.js b/scripts/run.js new file mode 100644 index 000000000..23424b3a8 --- /dev/null +++ b/scripts/run.js @@ -0,0 +1,292 @@ +// Unified dev/prod launcher for PictoPy: backend, sync-microservice, and frontend. +// Node.js port of run.sh with identical behavior, for native Windows (cmd/PowerShell) +// support without requiring Git Bash or WSL. +// +// On Linux, macOS, Git Bash, or WSL you can also use `bash scripts/run.sh`. +// +// Usage: +// node scripts/run.js -> dev mode (default) +// node scripts/run.js --prod -> production mode + +import { spawn, spawnSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --- Colors (matches scripts/run.sh / scripts/setup.sh conventions) --- +const RED = '\x1b[0;31m'; +const GREEN = '\x1b[0;32m'; +const YELLOW = '\x1b[0;33m'; +const NC = '\x1b[0m'; + +const MODE = process.argv[2] === '--prod' ? 'prod' : 'dev'; + +const ROOT_DIR = path.resolve(__dirname, '..'); +const BACKEND_DIR = path.join(ROOT_DIR, 'backend'); +const SYNC_DIR = path.join(ROOT_DIR, 'sync-microservice'); +const FRONTEND_DIR = path.join(ROOT_DIR, 'frontend'); + +// --- OS detection (same approach as scripts/run.sh and scripts/setup.js) --- +let OS_TYPE; +switch (process.platform) { + case 'linux': + OS_TYPE = 'linux'; + break; + case 'darwin': + OS_TYPE = 'macos'; + break; + case 'win32': + OS_TYPE = 'windows'; + break; + default: + OS_TYPE = 'unknown'; + console.log(`${YELLOW}Warning: unrecognized platform '${process.platform}'. Assuming Unix-like behavior.${NC}`); +} +const IS_WINDOWS = OS_TYPE === 'windows'; + +const SETUP_HINT = IS_WINDOWS + ? "Run 'npm run setup' from the repo root (this launches scripts/setup.ps1 on Windows)." + : "Run 'npm run setup' from the repo root to install dependencies."; + +// --- Preflight: fail fast with guidance instead of a raw "command not found" --- +function commandExists(cmd) { + const pathEnv = process.env.PATH || process.env.Path || ''; + const dirs = pathEnv.split(path.delimiter).filter(Boolean); + const exts = IS_WINDOWS ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';') : ['']; + for (const dir of dirs) { + for (const ext of exts) { + const candidate = path.join(dir, cmd + ext); + try { + fs.accessSync(candidate, fs.constants.X_OK); + return candidate; + } catch { + // keep searching + } + } + } + return null; +} + +function requireCmd(cmd, guidance) { + if (!commandExists(cmd)) { + console.error(`${RED}Error: required command '${cmd}' not found.${NC}`); + console.error(`${YELLOW}${guidance}${NC}`); + process.exit(1); + } +} + +function requireDir(dir, label) { + if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { + console.error(`${RED}Error: ${label} directory not found at ${dir}${NC}`); + console.error(`${YELLOW}Make sure you're running this from a full PictoPy checkout.${NC}`); + process.exit(1); + } +} + +requireDir(BACKEND_DIR, 'backend'); +requireDir(SYNC_DIR, 'sync-microservice'); +requireDir(FRONTEND_DIR, 'frontend'); + +requireCmd('node', `Node.js not found. Install it from https://nodejs.org, or ${SETUP_HINT}`); +requireCmd('npm', `npm not found (usually bundled with Node.js). Install Node.js from https://nodejs.org, or ${SETUP_HINT}`); +requireCmd('cargo', `Rust/Cargo not found (required by 'npm run tauri dev'). Install it from https://rustup.rs, or ${SETUP_HINT}`); + +// --- venv resolution helper --- +// Tries each candidate venv folder name in order, using the correct +// bin dir per OS (bin/ on Unix, Scripts/ on Windows). Instead of "sourcing" +// activate (there's no such thing for a child process spawned from Node), +// the resolved bin dir is prepended to PATH for that child. +function resolveVenv(baseDir, candidates) { + for (const name of candidates) { + const venvDir = path.join(baseDir, name); + const binDir = path.join(venvDir, IS_WINDOWS ? 'Scripts' : 'bin'); + if (fs.existsSync(path.join(binDir, 'activate'))) { + return { venvDir, binDir }; + } + } + console.error(`${RED}Could not find a venv in ${baseDir} (looked for: ${candidates.join(', ')})${NC}`); + console.error(`${YELLOW}${SETUP_HINT}${NC}`); + return null; +} + +// Verifies a command exists inside the resolved venv's bin dir, with +// guidance to reinstall dependencies rather than a bare "command not found". +function requireVenvCmd(binDir, venvDir, cmd) { + const resolved = IS_WINDOWS + ? [`${cmd}.exe`, `${cmd}.cmd`].map((f) => path.join(binDir, f)).find((p) => fs.existsSync(p)) + : (() => { + const p = path.join(binDir, cmd); + try { + fs.accessSync(p, fs.constants.X_OK); + return p; + } catch { + return null; + } + })(); + + if (!resolved) { + console.error(`${RED}'${cmd}' not found in venv at ${venvDir}.${NC}`); + console.error(`${YELLOW}The venv exists but looks incomplete. Try:${NC}`); + const activateHint = IS_WINDOWS + ? `${venvDir}\\Scripts\\activate && pip install -r requirements.txt` + : `source "${venvDir}/bin/activate" && pip install -r requirements.txt`; + console.error(`${YELLOW} ${activateHint}${NC}`); + console.error(`${YELLOW}...or rerun: ${SETUP_HINT}${NC}`); + } + return resolved; +} + +// --- process orchestration --- +const children = []; +let aliveCount = 0; +let shuttingDown = false; + +function prefixStream(stream, prefix, out) { + let buffer = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => { + buffer += chunk; + const lines = buffer.split('\n'); + buffer = lines.pop(); + for (const line of lines) { + out.write(`[${prefix}] ${line}\n`); + } + }); + stream.on('end', () => { + if (buffer.length > 0) { + out.write(`[${prefix}] ${buffer}\n`); + buffer = ''; + } + }); +} + +function spawnService(prefix, command, args, options) { + const child = spawn(command, args, { + stdio: ['ignore', 'pipe', 'pipe'], + ...options, + }); + + prefixStream(child.stdout, prefix, process.stdout); + prefixStream(child.stderr, prefix, process.stdout); // merged like run.sh's 2>&1 + + child.on('error', (err) => { + process.stdout.write(`[${prefix}] Failed to start: ${err.message}\n`); + }); + + aliveCount++; + child.on('exit', () => { + aliveCount--; + if (aliveCount === 0 && !shuttingDown) { + process.exit(0); + } + }); + + children.push(child); + return child; +} + +function killChild(child) { + if (!child.pid || child.exitCode !== null || child.signalCode !== null) return; + if (IS_WINDOWS) { + spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F']); + } else { + try { + process.kill(-child.pid, 'SIGTERM'); + } catch { + try { + child.kill('SIGTERM'); + } catch { + // process already gone + } + } + } +} + +function cleanup() { + if (shuttingDown) return; + shuttingDown = true; + console.log(''); + console.log('Shutting down all services...'); + for (const child of children) { + killChild(child); + } + process.exit(0); +} +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +function venvEnv(binDir) { + return { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH || process.env.Path || ''}` }; +} + +function startBackend() { + console.log(`[BACKEND] Starting... ${BACKEND_DIR}`); + const venv = resolveVenv(BACKEND_DIR, ['.env', 'venv']); + if (!venv) process.exit(1); + const uvicorn = requireVenvCmd(venv.binDir, venv.venvDir, 'uvicorn'); + if (!uvicorn) process.exit(1); + + const env = venvEnv(venv.binDir); + let args; + if (MODE === 'prod') { + console.log('[BACKEND] Starting in production mode on port 52123...'); + args = ['main:app', '--host', '0.0.0.0', '--port', '52123', '--workers', process.env.WORKERS || '1']; + } else { + console.log('[BACKEND] Starting in dev mode on port 52123...'); + args = ['main:app', '--host', '0.0.0.0', '--port', '52123', '--reload']; + } + spawnService('BACKEND', uvicorn, args, { + cwd: BACKEND_DIR, + env, + detached: !IS_WINDOWS, + }); +} + +function startSync() { + const venv = resolveVenv(SYNC_DIR, ['.sync-env', 'venv']); + if (!venv) process.exit(1); + console.log('[SYNC] Starting sync-microservice on port 52124...'); + + const env = venvEnv(venv.binDir); + if (MODE === 'prod') { + const uvicorn = requireVenvCmd(venv.binDir, venv.venvDir, 'uvicorn'); + if (!uvicorn) process.exit(1); + spawnService('SYNC', uvicorn, ['main:app', '--host', '0.0.0.0', '--port', '52124'], { + cwd: SYNC_DIR, + env, + detached: !IS_WINDOWS, + }); + } else { + const fastapi = requireVenvCmd(venv.binDir, venv.venvDir, 'fastapi'); + if (!fastapi) process.exit(1); + spawnService('SYNC', fastapi, ['dev', '--port', '52124'], { + cwd: SYNC_DIR, + env, + detached: !IS_WINDOWS, + }); + } +} + +function startFrontend() { + const nodeModules = path.join(FRONTEND_DIR, 'node_modules'); + if (!fs.existsSync(nodeModules)) { + console.error(`${RED}[FRONTEND] node_modules not found.${NC}`); + console.error(`${YELLOW}[FRONTEND] ${SETUP_HINT}${NC}`); + process.exit(1); + } + console.log('[FRONTEND] Starting Tauri dev...'); + spawnService('FRONTEND', 'npm', ['run', 'tauri', 'dev'], { + cwd: FRONTEND_DIR, + env: process.env, + shell: IS_WINDOWS, // npm ships as npm.cmd on Windows, which needs shell resolution + detached: !IS_WINDOWS, + }); +} + +console.log(`Starting PictoPy (mode: ${MODE}, OS: ${OS_TYPE})...`); +startBackend(); +startSync(); +startFrontend(); diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 000000000..1b9c86a3b --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,189 @@ +#!/bin/bash + +# Unified dev/prod launcher for PictoPy: backend, sync-microservice, and frontend +# +# Requires bash (Linux, macOS, Git Bash, or WSL on Windows). If you're on +# Windows without Git Bash/WSL, use `node scripts/run.js` instead. +# +# Usage: +# ./run.sh -> dev mode (default) +# ./run.sh --test -> dev mode (kept as an alias for backward compatibility) +# ./run.sh --prod -> production mode + +set -e + +# --- Colors (matches scripts/setup.sh conventions) --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +MODE="dev" +if [[ "$1" == "--prod" ]]; then + MODE="prod" +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BACKEND_DIR="$ROOT_DIR/backend" +SYNC_DIR="$ROOT_DIR/sync-microservice" +FRONTEND_DIR="$ROOT_DIR/frontend" + +# --- OS detection (same approach as scripts/setup.sh and scripts/setup.js) --- +case "$(uname -s)" in + Linux*) OS_TYPE="linux" ;; + Darwin*) OS_TYPE="macos" ;; + MINGW*|MSYS*|CYGWIN*) OS_TYPE="windows" ;; + *) + OS_TYPE="unknown" + echo -e "${YELLOW}Warning: unrecognized OS '$(uname -s)'. Assuming Unix-like behavior.${NC}" + ;; +esac + +SETUP_HINT="Run 'npm run setup' from the repo root to install dependencies." +if [[ "$OS_TYPE" == "windows" ]]; then + SETUP_HINT="Run 'npm run setup' from the repo root (this launches scripts/setup.ps1 on Windows)." +fi + +# --- Preflight: fail fast with guidance instead of a raw "command not found" --- +require_cmd() { + local cmd="$1" + local guidance="$2" + if ! command -v "$cmd" &> /dev/null; then + echo -e "${RED}Error: required command '$cmd' not found.${NC}" + echo -e "${YELLOW}${guidance}${NC}" + exit 1 + fi +} + +require_dir() { + local dir="$1" + local label="$2" + if [[ ! -d "$dir" ]]; then + echo -e "${RED}Error: $label directory not found at $dir${NC}" + echo -e "${YELLOW}Make sure you're running this from a full PictoPy checkout.${NC}" + exit 1 + fi +} + +require_dir "$BACKEND_DIR" "backend" +require_dir "$SYNC_DIR" "sync-microservice" +require_dir "$FRONTEND_DIR" "frontend" + +require_cmd "node" "Node.js not found. Install it from https://nodejs.org, or $SETUP_HINT" +require_cmd "npm" "npm not found (usually bundled with Node.js). Install Node.js from https://nodejs.org, or $SETUP_HINT" +require_cmd "cargo" "Rust/Cargo not found (required by 'npm run tauri dev'). Install it from https://rustup.rs, or $SETUP_HINT" + +# --- venv activation helper --- +# Tries each candidate venv folder name in order, using the correct +# activate path per OS (bin/activate on Unix, Scripts/activate on Windows). +activate_venv() { + local base_dir="$1" + shift + local candidates=("$@") + + for name in "${candidates[@]}"; do + local venv_dir="$base_dir/$name" + if [[ "$OS_TYPE" == "windows" ]]; then + if [[ -f "$venv_dir/Scripts/activate" ]]; then + source "$venv_dir/Scripts/activate" + return 0 + fi + else + if [[ -f "$venv_dir/bin/activate" ]]; then + source "$venv_dir/bin/activate" + return 0 + fi + fi + done + + echo -e "${RED}Could not find a venv in $base_dir (looked for: ${candidates[*]})${NC}" + echo -e "${YELLOW}${SETUP_HINT}${NC}" + return 1 +} + +# Verifies a command exists inside the *currently activated* venv, with +# guidance to reinstall dependencies rather than a bare "command not found". +require_venv_cmd() { + local cmd="$1" + local venv_dir="$2" + if ! command -v "$cmd" &> /dev/null; then + echo -e "${RED}'$cmd' not found in venv at $venv_dir.${NC}" + echo -e "${YELLOW}The venv exists but looks incomplete. Try:${NC}" + if [[ "$OS_TYPE" == "windows" ]]; then + echo -e "${YELLOW} source \"$venv_dir/Scripts/activate\" && pip install -r requirements.txt${NC}" + else + echo -e "${YELLOW} source \"$venv_dir/bin/activate\" && pip install -r requirements.txt${NC}" + fi + echo -e "${YELLOW}...or rerun: $SETUP_HINT${NC}" + return 1 + fi + return 0 +} + +PIDS=() + +cleanup() { + echo "" + echo "Shutting down all services..." + for pid in "${PIDS[@]}"; do + kill "$pid" 2>/dev/null + done + wait 2>/dev/null + exit 0 +} +trap cleanup INT TERM + +start_backend() { + ( + echo "[BACKEND] Starting... ${BACKEND_DIR}" + cd "$BACKEND_DIR" + activate_venv "$BACKEND_DIR" ".env" "venv" || exit 1 + require_venv_cmd "uvicorn" "$BACKEND_DIR/.env" || exit 1 + if [[ "$MODE" == "prod" ]]; then + echo "[BACKEND] Starting in production mode on port 52123..." + uvicorn main:app --host 0.0.0.0 --port 52123 --workers "${WORKERS:-1}" + else + echo "[BACKEND] Starting in dev mode on port 52123..." + uvicorn main:app --host 0.0.0.0 --port 52123 --reload + fi + ) 2>&1 | sed -u 's/^/[BACKEND] /' & + PIDS+=($!) +} + +start_sync() { + ( + cd "$SYNC_DIR" + activate_venv "$SYNC_DIR" ".sync-env" "venv" || exit 1 + echo "[SYNC] Starting sync-microservice on port 52124..." + if [[ "$MODE" == "prod" ]]; then + require_venv_cmd "uvicorn" "$SYNC_DIR/.sync-env" || exit 1 + uvicorn main:app --host 0.0.0.0 --port 52124 + else + require_venv_cmd "fastapi" "$SYNC_DIR/.sync-env" || exit 1 + fastapi dev --port 52124 + fi + ) 2>&1 | sed -u 's/^/[SYNC] /' & + PIDS+=($!) +} + +start_frontend() { + ( + cd "$FRONTEND_DIR" + if [[ ! -d "node_modules" ]]; then + echo -e "${RED}[FRONTEND] node_modules not found.${NC}" + echo -e "${YELLOW}[FRONTEND] ${SETUP_HINT}${NC}" + exit 1 + fi + echo "[FRONTEND] Starting Tauri dev..." + npm run tauri dev + ) 2>&1 | sed -u 's/^/[FRONTEND] /' & + PIDS+=($!) +} + +echo "Starting PictoPy (mode: $MODE, OS: $OS_TYPE)..." +start_backend +start_sync +start_frontend + +wait From 64d972c9246e4872e0c2e6f6813170e36eada747 Mon Sep 17 00:00:00 2001 From: Arman Thakur Date: Tue, 21 Jul 2026 20:54:19 +0530 Subject: [PATCH 2/3] refactor: streamline virtual environment handling in run scripts - Removed legacy command verification and activation logic for virtual environments in both JavaScript and shell scripts. - Introduced a more reliable method for resolving and using Python executables directly, enhancing compatibility across platforms. - Updated dependency installation process to ensure services start only with complete environments, improving error handling and user guidance. --- scripts/run.js | 107 +++++++++++++++++++++++++++++-------------------- scripts/run.sh | 85 +++++++++++++++++++++------------------ 2 files changed, 109 insertions(+), 83 deletions(-) diff --git a/scripts/run.js b/scripts/run.js index 23424b3a8..6fee9a622 100644 --- a/scripts/run.js +++ b/scripts/run.js @@ -112,33 +112,6 @@ function resolveVenv(baseDir, candidates) { return null; } -// Verifies a command exists inside the resolved venv's bin dir, with -// guidance to reinstall dependencies rather than a bare "command not found". -function requireVenvCmd(binDir, venvDir, cmd) { - const resolved = IS_WINDOWS - ? [`${cmd}.exe`, `${cmd}.cmd`].map((f) => path.join(binDir, f)).find((p) => fs.existsSync(p)) - : (() => { - const p = path.join(binDir, cmd); - try { - fs.accessSync(p, fs.constants.X_OK); - return p; - } catch { - return null; - } - })(); - - if (!resolved) { - console.error(`${RED}'${cmd}' not found in venv at ${venvDir}.${NC}`); - console.error(`${YELLOW}The venv exists but looks incomplete. Try:${NC}`); - const activateHint = IS_WINDOWS - ? `${venvDir}\\Scripts\\activate && pip install -r requirements.txt` - : `source "${venvDir}/bin/activate" && pip install -r requirements.txt`; - console.error(`${YELLOW} ${activateHint}${NC}`); - console.error(`${YELLOW}...or rerun: ${SETUP_HINT}${NC}`); - } - return resolved; -} - // --- process orchestration --- const children = []; let aliveCount = 0; @@ -219,50 +192,96 @@ process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); function venvEnv(binDir) { - return { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH || process.env.Path || ''}` }; + return { + ...process.env, + PATH: `${binDir}${path.delimiter}${process.env.PATH || process.env.Path || ''}`, + // Node pipes child stdio, so Python sees a non-tty and fully buffers + // stdout instead of line-buffering it. Force unbuffered output so + // uvicorn/fastapi logs show up live instead of only on exit. + PYTHONUNBUFFERED: '1', + // Without a real console attached, rich (used by `fastapi dev`'s banner) + // falls back to the legacy Windows console writer, which encodes with + // cp1252 and crashes on the emoji in its own startup banner. Forcing + // UTF-8 mode avoids that. + PYTHONUTF8: '1', + }; +} + +// pip/uvicorn/fastapi console-script .exe launchers generated inside a venv +// have turned out to be unreliable on this Windows setup (some fail +// instantly with no output, e.g. "Fatal error in launcher: Unable to find +// an appended archive"). Invoking everything as `python -m ` +// sidesteps those launcher stubs entirely and has proven reliable. +function venvPython(venv) { + const python = path.join(venv.binDir, IS_WINDOWS ? 'python.exe' : 'python'); + if (!fs.existsSync(python)) { + console.error(`${RED}'python' not found in venv at ${venv.venvDir}.${NC}`); + process.exit(1); + } + return python; +} + +// Installs a service's Python dependencies before starting it, matching +// `source /activate && pip install -r requirements.txt` from the +// project's README. Blocking by design: the server must not start against +// a venv with missing/outdated packages. +function pipInstall(prefix, serviceDir, venv, python) { + console.log(`[${prefix}] Installing dependencies from requirements.txt...`); + const result = spawnSync(python, ['-m', 'pip', 'install', '-r', 'requirements.txt'], { + cwd: serviceDir, + env: venvEnv(venv.binDir), + stdio: 'inherit', + }); + if (result.status !== 0) { + console.error(`${RED}[${prefix}] pip install -r requirements.txt failed.${NC}`); + process.exit(1); + } } function startBackend() { console.log(`[BACKEND] Starting... ${BACKEND_DIR}`); const venv = resolveVenv(BACKEND_DIR, ['.env', 'venv']); if (!venv) process.exit(1); - const uvicorn = requireVenvCmd(venv.binDir, venv.venvDir, 'uvicorn'); - if (!uvicorn) process.exit(1); + const python = venvPython(venv); + pipInstall('BACKEND', BACKEND_DIR, venv, python); const env = venvEnv(venv.binDir); - let args; if (MODE === 'prod') { console.log('[BACKEND] Starting in production mode on port 52123...'); - args = ['main:app', '--host', '0.0.0.0', '--port', '52123', '--workers', process.env.WORKERS || '1']; + spawnService('BACKEND', python, ['-m', 'uvicorn', 'main:app', '--host', '0.0.0.0', '--port', '52123', '--workers', process.env.WORKERS || '1'], { + cwd: BACKEND_DIR, + env, + detached: !IS_WINDOWS, + }); } else { + // Backend pins fastapi-cli==0.0.3, which predates `fastapi.__main__` + // (no `python -m fastapi` support), unlike sync-microservice's newer + // fastapi-cli. Use uvicorn directly here instead. console.log('[BACKEND] Starting in dev mode on port 52123...'); - args = ['main:app', '--host', '0.0.0.0', '--port', '52123', '--reload']; + spawnService('BACKEND', python, ['-m', 'uvicorn', 'main:app', '--host', '0.0.0.0', '--port', '52123', '--reload'], { + cwd: BACKEND_DIR, + env, + detached: !IS_WINDOWS, + }); } - spawnService('BACKEND', uvicorn, args, { - cwd: BACKEND_DIR, - env, - detached: !IS_WINDOWS, - }); } function startSync() { const venv = resolveVenv(SYNC_DIR, ['.sync-env', 'venv']); if (!venv) process.exit(1); + const python = venvPython(venv); + pipInstall('SYNC', SYNC_DIR, venv, python); console.log('[SYNC] Starting sync-microservice on port 52124...'); const env = venvEnv(venv.binDir); if (MODE === 'prod') { - const uvicorn = requireVenvCmd(venv.binDir, venv.venvDir, 'uvicorn'); - if (!uvicorn) process.exit(1); - spawnService('SYNC', uvicorn, ['main:app', '--host', '0.0.0.0', '--port', '52124'], { + spawnService('SYNC', python, ['-m', 'uvicorn', 'main:app', '--host', '0.0.0.0', '--port', '52124'], { cwd: SYNC_DIR, env, detached: !IS_WINDOWS, }); } else { - const fastapi = requireVenvCmd(venv.binDir, venv.venvDir, 'fastapi'); - if (!fastapi) process.exit(1); - spawnService('SYNC', fastapi, ['dev', '--port', '52124'], { + spawnService('SYNC', python, ['-m', 'fastapi', 'dev', '--port', '52124'], { cwd: SYNC_DIR, env, detached: !IS_WINDOWS, diff --git a/scripts/run.sh b/scripts/run.sh index 1b9c86a3b..bf0457d11 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -74,48 +74,51 @@ require_cmd "node" "Node.js not found. Install it from https://nodejs.org, or $S require_cmd "npm" "npm not found (usually bundled with Node.js). Install Node.js from https://nodejs.org, or $SETUP_HINT" require_cmd "cargo" "Rust/Cargo not found (required by 'npm run tauri dev'). Install it from https://rustup.rs, or $SETUP_HINT" -# --- venv activation helper --- -# Tries each candidate venv folder name in order, using the correct -# activate path per OS (bin/activate on Unix, Scripts/activate on Windows). -activate_venv() { +# --- venv resolution helper --- +# Tries each candidate venv folder name in order and prepends its bin dir +# (bin/ on Unix, Scripts/ on Windows) to PATH directly. Deliberately does +# NOT `source /activate`: that script bakes in an absolute path at +# creation time (see its VIRTUAL_ENV= line and pyvenv.cfg), and silently +# breaks -- falling through to system Python with no error -- if the venv +# folder is ever renamed or moved after creation, which happens easily +# since ".env"/".venv" naming is inconsistent across tooling. Resolving +# the bin dir fresh from its current location every run sidesteps that. +resolve_venv() { local base_dir="$1" shift local candidates=("$@") + local bin_subdir="bin" + [[ "$OS_TYPE" == "windows" ]] && bin_subdir="Scripts" for name in "${candidates[@]}"; do local venv_dir="$base_dir/$name" - if [[ "$OS_TYPE" == "windows" ]]; then - if [[ -f "$venv_dir/Scripts/activate" ]]; then - source "$venv_dir/Scripts/activate" - return 0 - fi - else - if [[ -f "$venv_dir/bin/activate" ]]; then - source "$venv_dir/bin/activate" - return 0 - fi + if [[ -f "$venv_dir/$bin_subdir/activate" ]]; then + echo "$venv_dir/$bin_subdir" + return 0 fi done - echo -e "${RED}Could not find a venv in $base_dir (looked for: ${candidates[*]})${NC}" - echo -e "${YELLOW}${SETUP_HINT}${NC}" + echo -e "${RED}Could not find a venv in $base_dir (looked for: ${candidates[*]})${NC}" >&2 + echo -e "${YELLOW}${SETUP_HINT}${NC}" >&2 return 1 } -# Verifies a command exists inside the *currently activated* venv, with -# guidance to reinstall dependencies rather than a bare "command not found". -require_venv_cmd() { - local cmd="$1" - local venv_dir="$2" - if ! command -v "$cmd" &> /dev/null; then - echo -e "${RED}'$cmd' not found in venv at $venv_dir.${NC}" - echo -e "${YELLOW}The venv exists but looks incomplete. Try:${NC}" - if [[ "$OS_TYPE" == "windows" ]]; then - echo -e "${YELLOW} source \"$venv_dir/Scripts/activate\" && pip install -r requirements.txt${NC}" - else - echo -e "${YELLOW} source \"$venv_dir/bin/activate\" && pip install -r requirements.txt${NC}" - fi - echo -e "${YELLOW}...or rerun: $SETUP_HINT${NC}" +# pip/uvicorn/fastapi console-script launchers generated inside a venv have +# proven unreliable on some platforms (see scripts/run.js for the Windows +# case that motivated this). Invoking everything as `python -m ` +# sidesteps those launcher stubs entirely. +export PYTHONUNBUFFERED=1 +export PYTHONUTF8=1 + +# Installs a service's Python dependencies before starting it, matching +# `source /activate && pip install -r requirements.txt` from the +# project's README. Blocking by design: the server must not start against +# a venv with missing/outdated packages. +pip_install() { + local prefix="$1" + echo "[$prefix] Installing dependencies from requirements.txt..." + if ! python -m pip install -r requirements.txt; then + echo -e "${RED}[$prefix] pip install -r requirements.txt failed.${NC}" return 1 fi return 0 @@ -138,14 +141,18 @@ start_backend() { ( echo "[BACKEND] Starting... ${BACKEND_DIR}" cd "$BACKEND_DIR" - activate_venv "$BACKEND_DIR" ".env" "venv" || exit 1 - require_venv_cmd "uvicorn" "$BACKEND_DIR/.env" || exit 1 + bin_dir=$(resolve_venv "$BACKEND_DIR" ".env" "venv") || exit 1 + export PATH="$bin_dir:$PATH" + pip_install "BACKEND" || exit 1 if [[ "$MODE" == "prod" ]]; then echo "[BACKEND] Starting in production mode on port 52123..." - uvicorn main:app --host 0.0.0.0 --port 52123 --workers "${WORKERS:-1}" + python -m uvicorn main:app --host 0.0.0.0 --port 52123 --workers "${WORKERS:-1}" else + # Backend pins fastapi-cli==0.0.3, which predates + # `fastapi.__main__` (no `python -m fastapi` support), unlike + # sync-microservice's newer fastapi-cli. Use uvicorn directly. echo "[BACKEND] Starting in dev mode on port 52123..." - uvicorn main:app --host 0.0.0.0 --port 52123 --reload + python -m uvicorn main:app --host 0.0.0.0 --port 52123 --reload fi ) 2>&1 | sed -u 's/^/[BACKEND] /' & PIDS+=($!) @@ -154,14 +161,14 @@ start_backend() { start_sync() { ( cd "$SYNC_DIR" - activate_venv "$SYNC_DIR" ".sync-env" "venv" || exit 1 + bin_dir=$(resolve_venv "$SYNC_DIR" ".sync-env" "venv") || exit 1 + export PATH="$bin_dir:$PATH" + pip_install "SYNC" || exit 1 echo "[SYNC] Starting sync-microservice on port 52124..." if [[ "$MODE" == "prod" ]]; then - require_venv_cmd "uvicorn" "$SYNC_DIR/.sync-env" || exit 1 - uvicorn main:app --host 0.0.0.0 --port 52124 + python -m uvicorn main:app --host 0.0.0.0 --port 52124 else - require_venv_cmd "fastapi" "$SYNC_DIR/.sync-env" || exit 1 - fastapi dev --port 52124 + python -m fastapi dev --port 52124 fi ) 2>&1 | sed -u 's/^/[SYNC] /' & PIDS+=($!) From 05d717e226df10df6857b6fa5d685f2c63a7717b Mon Sep 17 00:00:00 2001 From: Arman Thakur Date: Tue, 21 Jul 2026 21:18:54 +0530 Subject: [PATCH 3/3] feat: enhance virtual environment management in run scripts - Added automatic creation of Python virtual environments in both JavaScript and shell scripts if none exist, improving setup experience. - Updated the venv resolution logic to include a prefix for better context in error messages. - Enhanced error handling for Python command checks and virtual environment creation, ensuring clearer feedback for users. --- scripts/run.js | 34 +++++++++++++++++++++++++++------- scripts/run.sh | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/scripts/run.js b/scripts/run.js index 6fee9a622..57a197647 100644 --- a/scripts/run.js +++ b/scripts/run.js @@ -94,12 +94,31 @@ requireCmd('node', `Node.js not found. Install it from https://nodejs.org, or ${ requireCmd('npm', `npm not found (usually bundled with Node.js). Install Node.js from https://nodejs.org, or ${SETUP_HINT}`); requireCmd('cargo', `Rust/Cargo not found (required by 'npm run tauri dev'). Install it from https://rustup.rs, or ${SETUP_HINT}`); +// Creates a venv at `venvDir` using whatever `python`/`python3` is on PATH. +// A venv is just an empty container (unlike a .env secrets file, there's no +// "wrong value" risk), so auto-creating a missing one on first run is safe +// and matches how poetry/uv/pipenv behave. +function createVenv(prefix, venvDir) { + const pythonCmd = commandExists('python') ? 'python' : commandExists('python3') ? 'python3' : null; + if (!pythonCmd) { + console.error(`${RED}[${prefix}] 'python' not found. Install Python 3, or ${SETUP_HINT}${NC}`); + process.exit(1); + } + console.log(`[${prefix}] Creating virtual environment at ${venvDir}...`); + const result = spawnSync(pythonCmd, ['-m', 'venv', venvDir], { stdio: 'inherit' }); + if (result.status !== 0) { + console.error(`${RED}[${prefix}] Failed to create virtual environment.${NC}`); + process.exit(1); + } +} + // --- venv resolution helper --- // Tries each candidate venv folder name in order, using the correct // bin dir per OS (bin/ on Unix, Scripts/ on Windows). Instead of "sourcing" // activate (there's no such thing for a child process spawned from Node), -// the resolved bin dir is prepended to PATH for that child. -function resolveVenv(baseDir, candidates) { +// the resolved bin dir is prepended to PATH for that child. Auto-creates +// one under the first candidate name if none of them exist yet. +function resolveVenv(prefix, baseDir, candidates) { for (const name of candidates) { const venvDir = path.join(baseDir, name); const binDir = path.join(venvDir, IS_WINDOWS ? 'Scripts' : 'bin'); @@ -107,9 +126,10 @@ function resolveVenv(baseDir, candidates) { return { venvDir, binDir }; } } - console.error(`${RED}Could not find a venv in ${baseDir} (looked for: ${candidates.join(', ')})${NC}`); - console.error(`${YELLOW}${SETUP_HINT}${NC}`); - return null; + const venvDir = path.join(baseDir, candidates[0]); + createVenv(prefix, venvDir); + const binDir = path.join(venvDir, IS_WINDOWS ? 'Scripts' : 'bin'); + return { venvDir, binDir }; } // --- process orchestration --- @@ -240,7 +260,7 @@ function pipInstall(prefix, serviceDir, venv, python) { function startBackend() { console.log(`[BACKEND] Starting... ${BACKEND_DIR}`); - const venv = resolveVenv(BACKEND_DIR, ['.env', 'venv']); + const venv = resolveVenv('BACKEND', BACKEND_DIR, ['.env', 'venv']); if (!venv) process.exit(1); const python = venvPython(venv); pipInstall('BACKEND', BACKEND_DIR, venv, python); @@ -267,7 +287,7 @@ function startBackend() { } function startSync() { - const venv = resolveVenv(SYNC_DIR, ['.sync-env', 'venv']); + const venv = resolveVenv('SYNC', SYNC_DIR, ['.sync-env', 'venv']); if (!venv) process.exit(1); const python = venvPython(venv); pipInstall('SYNC', SYNC_DIR, venv, python); diff --git a/scripts/run.sh b/scripts/run.sh index bf0457d11..6e30171aa 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -74,6 +74,31 @@ require_cmd "node" "Node.js not found. Install it from https://nodejs.org, or $S require_cmd "npm" "npm not found (usually bundled with Node.js). Install Node.js from https://nodejs.org, or $SETUP_HINT" require_cmd "cargo" "Rust/Cargo not found (required by 'npm run tauri dev'). Install it from https://rustup.rs, or $SETUP_HINT" +# Creates a venv at `venv_dir` using whatever `python`/`python3` is on PATH. +# A venv is just an empty container (unlike a .env secrets file, there's no +# "wrong value" risk), so auto-creating a missing one on first run is safe +# and matches how poetry/uv/pipenv behave. All output goes to stderr since +# resolve_venv's stdout is captured via command substitution by its callers. +create_venv() { + local prefix="$1" + local venv_dir="$2" + local py_cmd="python" + if ! command -v python &> /dev/null; then + if command -v python3 &> /dev/null; then + py_cmd="python3" + else + echo -e "${RED}[$prefix] 'python' not found. Install Python 3, or $SETUP_HINT${NC}" >&2 + return 1 + fi + fi + echo "[$prefix] Creating virtual environment at $venv_dir..." >&2 + if ! "$py_cmd" -m venv "$venv_dir" >&2; then + echo -e "${RED}[$prefix] Failed to create virtual environment.${NC}" >&2 + return 1 + fi + return 0 +} + # --- venv resolution helper --- # Tries each candidate venv folder name in order and prepends its bin dir # (bin/ on Unix, Scripts/ on Windows) to PATH directly. Deliberately does @@ -83,9 +108,11 @@ require_cmd "cargo" "Rust/Cargo not found (required by 'npm run tauri dev'). Ins # folder is ever renamed or moved after creation, which happens easily # since ".env"/".venv" naming is inconsistent across tooling. Resolving # the bin dir fresh from its current location every run sidesteps that. +# Auto-creates one under the first candidate name if none of them exist yet. resolve_venv() { - local base_dir="$1" - shift + local prefix="$1" + local base_dir="$2" + shift 2 local candidates=("$@") local bin_subdir="bin" [[ "$OS_TYPE" == "windows" ]] && bin_subdir="Scripts" @@ -98,9 +125,10 @@ resolve_venv() { fi done - echo -e "${RED}Could not find a venv in $base_dir (looked for: ${candidates[*]})${NC}" >&2 - echo -e "${YELLOW}${SETUP_HINT}${NC}" >&2 - return 1 + local venv_dir="$base_dir/${candidates[0]}" + create_venv "$prefix" "$venv_dir" || return 1 + echo "$venv_dir/$bin_subdir" + return 0 } # pip/uvicorn/fastapi console-script launchers generated inside a venv have @@ -141,7 +169,7 @@ start_backend() { ( echo "[BACKEND] Starting... ${BACKEND_DIR}" cd "$BACKEND_DIR" - bin_dir=$(resolve_venv "$BACKEND_DIR" ".env" "venv") || exit 1 + bin_dir=$(resolve_venv "BACKEND" "$BACKEND_DIR" ".env" "venv") || exit 1 export PATH="$bin_dir:$PATH" pip_install "BACKEND" || exit 1 if [[ "$MODE" == "prod" ]]; then @@ -161,7 +189,7 @@ start_backend() { start_sync() { ( cd "$SYNC_DIR" - bin_dir=$(resolve_venv "$SYNC_DIR" ".sync-env" "venv") || exit 1 + bin_dir=$(resolve_venv "SYNC" "$SYNC_DIR" ".sync-env" "venv") || exit 1 export PATH="$bin_dir:$PATH" pip_install "SYNC" || exit 1 echo "[SYNC] Starting sync-microservice on port 52124..."