Fix win 0.0.82 prebuilt issue (#1109)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run

Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
This commit is contained in:
Tong Chen 2026-02-06 07:19:29 +08:00 • committed by GitHub
parent ecbbe8d779
commit 38f5fbc8cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1269 additions and 115 deletions

View file

@ -17,6 +17,9 @@ jobs:
- os: macos-latest
arch: arm64
artifact_name: macos-arm64
- os: macos-15-intel
arch: x64
artifact_name: macos-intel
- os: windows-latest
arch: x64
artifact_name: windows-latest
@ -186,7 +189,7 @@ jobs:
steps:
- name: Create directories
run: |
mkdir -p release/mac-arm64 release/win-x64 release/linux-x64
mkdir -p release/mac-arm64 release/mac-intel release/win-x64 release/linux-x64
- name: Download mac-arm64 artifact
uses: actions/download-artifact@v7
@ -194,6 +197,12 @@ jobs:
name: release-macos-arm64-arm64
path: temp-mac-arm64
- name: Download mac-intel artifact
uses: actions/download-artifact@v7
with:
name: release-macos-intel-x64
path: temp-mac-intel
- name: Download win-x64 artifact
uses: actions/download-artifact@v7
with:
@ -217,6 +226,13 @@ jobs:
find temp-mac-arm64 -name "*.dmg" -exec mv {} release/mac-arm64/ \; || true
fi
# mac-intel - only move dmg files
if [ -d "temp-mac-intel/release" ]; then
find temp-mac-intel/release -name "*.dmg" -exec mv {} release/mac-intel/ \; || true
else
find temp-mac-intel -name "*.dmg" -exec mv {} release/mac-intel/ \; || true
fi
# win-x64 - only move exe files
if [ -d "temp-win-x64/release" ]; then
find temp-win-x64/release -name "*.exe" -exec mv {} release/win-x64/ \; || true

View file

@ -31,6 +31,7 @@ import {
getPrebuiltVenvPath,
getUvEnv,
getVenvPath,
getVenvPythonPath,
isBinaryExists,
killProcessByName,
} from './utils/process';
@ -332,21 +333,25 @@ export async function startBackend(
}
};
const pythonPath = getVenvPythonPath(venvPath);
// Dev mode: use uv run (ensures sync); Packaged: use venv's python directly (prebuilt has deps)
const useDirectPython = app.isPackaged;
return new Promise(async (resolve, reject) => {
log.info(
`Spawning backend process: ${uv_path} run uvicorn main:api --port ${port} --loop asyncio`
);
const spawnCmd = useDirectPython
? `${pythonPath} -m uvicorn main:api --port ${port} --loop asyncio`
: `${uv_path} run python -m uvicorn main:api --port ${port} --loop asyncio`;
log.info(`Spawning backend process: ${spawnCmd}`);
log.info(`Backend working directory: ${backendPath}`);
log.info(`Using venv: ${venvPath}`);
try {
const { stdout: uvVersion } = await execAsync(`${uv_path} --version`);
log.info(`UV version check: ${uvVersion.trim()}`);
const { stdout: pythonTest } = await execAsync(
`${uv_path} run python -c "print('Python OK')"`,
{ cwd: backendPath, env: env }
);
const pythonTestCmd = useDirectPython
? `"${pythonPath}" -c "print('Python OK')"`
: `"${uv_path}" run python -c "print('Python OK')"`;
const { stdout: pythonTest } = await execAsync(pythonTestCmd, {
cwd: backendPath,
env: env,
});
log.info(`Python test output: ${pythonTest.trim()}`);
} catch (testErr: any) {
log.warn(`Pre-flight check failed, attempting repair: ${testErr}`);
@ -439,10 +444,13 @@ export async function startBackend(
});
// Retry the check
const { stdout: pythonTest } = await execAsync(
`${uv_path} run python -c "print('Python OK')"`,
{ cwd: backendPath, env: env }
);
const retryTestCmd = useDirectPython
? `"${pythonPath}" -c "print('Python OK')"`
: `"${uv_path}" run python -c "print('Python OK')"`;
const { stdout: pythonTest } = await execAsync(retryTestCmd, {
cwd: backendPath,
env: env,
});
log.info(`Python test output after repair: ${pythonTest.trim()}`);
} catch (repairErr) {
log.error(`Repair failed: ${repairErr}`);
@ -455,24 +463,45 @@ export async function startBackend(
}
}
const node_process = spawn(
uv_path,
[
'run',
'uvicorn',
'main:api',
'--port',
port.toString(),
'--loop',
'asyncio',
],
{
cwd: backendPath,
env: env,
detached: process.platform !== 'win32',
stdio: ['ignore', 'ignore', 'pipe'], // stdin=ignore, stdout=ignore, stderr=pipe (Python logs to stderr)
}
);
const node_process = useDirectPython
? spawn(
pythonPath,
[
'-m',
'uvicorn',
'main:api',
'--port',
port.toString(),
'--loop',
'asyncio',
],
{
cwd: backendPath,
env: env,
detached: process.platform !== 'win32',
stdio: ['ignore', 'ignore', 'pipe'],
}
)
: spawn(
uv_path,
[
'run',
'python',
'-m',
'uvicorn',
'main:api',
'--port',
port.toString(),
'--loop',
'asyncio',
],
{
cwd: backendPath,
env: env,
detached: process.platform !== 'win32',
stdio: ['ignore', 'ignore', 'pipe'],
}
);
// NOTE: Do NOT use unref() - we need to maintain the process reference
// to properly capture stdout/stderr and manage the process lifecycle

View file

@ -28,6 +28,7 @@ import {
getTerminalVenvPath,
getUvEnv,
getVenvPath,
getVenvPythonPath,
isBinaryExists,
runInstallScript,
TERMINAL_BASE_PACKAGES,
@ -753,13 +754,15 @@ export async function installDependencies(
`Install Dependencies completed ${message} for version ${version}`
);
console.log(`Virtual environment path: ${venvPath}`);
spawn(uv_path, ['run', 'task', 'babel'], {
cwd: backendPath,
env: {
...process.env,
UV_PROJECT_ENVIRONMENT: venvPath,
},
});
const pythonPath = getVenvPythonPath(venvPath);
spawn(
pythonPath,
['-m', 'babel.messages.frontend', 'compile', '-d', 'lang'],
{
cwd: backendPath,
env: { ...process.env },
}
);
},
notifyInstallDependenciesPage: (): boolean => {
const success = safeMainWindowSend('install-dependencies-start');

View file

@ -151,6 +151,171 @@ export function getCachePath(folder: string): string {
return cacheDir;
}
/**
* Fix pyvenv.cfg by replacing placeholder with actual Python path
* This makes prebuilt venvs portable across different machines
*/
function fixPyvenvCfgPlaceholder(pyvenvCfgPath: string): boolean {
try {
let content = fs.readFileSync(pyvenvCfgPath, 'utf-8');
if (content.includes('{{PREBUILT_PYTHON_DIR}}')) {
const prebuiltPythonDir = getPrebuiltPythonDir();
if (!prebuiltPythonDir) {
log.warn(
'[VENV] Cannot fix pyvenv.cfg: prebuilt Python directory not found'
);
return false;
}
content = content.replace(
/\{\{PREBUILT_PYTHON_DIR\}\}/g,
prebuiltPythonDir
);
const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
if (homeMatch) {
const finalHomePath = homeMatch[1].trim();
log.info(`[VENV] pyvenv.cfg home path set to: ${finalHomePath}`);
// Verify the path exists
if (!fs.existsSync(finalHomePath)) {
log.warn(
`[VENV] WARNING: home path does not exist: ${finalHomePath}`
);
} else {
log.info(`[VENV] home path verified successfully`);
}
}
fs.writeFileSync(pyvenvCfgPath, content);
log.info(
`[VENV] Fixed pyvenv.cfg placeholder with: ${prebuiltPythonDir}`
);
return true;
}
const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
if (homeMatch) {
const homePath = homeMatch[1].trim();
if (!fs.existsSync(homePath)) {
log.warn(`[VENV] pyvenv.cfg home path does not exist: ${homePath}`);
return false;
}
}
return true;
} catch (error) {
log.warn(`[VENV] Failed to fix pyvenv.cfg: ${error}`);
return false;
}
}
/**
* Fix shebang lines in venv scripts by replacing placeholder with actual Python path
* This ensures scripts can be executed directly (not just via `uv run`)
* Note: Windows doesn't use shebangs - it uses .exe wrappers instead
*/
function fixVenvScriptShebangs(venvPath: string): boolean {
const isWindows = process.platform === 'win32';
// Windows doesn't use shebangs - skip this step
if (isWindows) {
log.info(`[VENV] Skipping shebang fixes on Windows (not needed)`);
return true;
}
const binDir = path.join(venvPath, 'bin');
if (!fs.existsSync(binDir)) {
return false;
}
const pythonExe = path.join(binDir, 'python');
if (!fs.existsSync(pythonExe)) {
log.warn(`[VENV] Python executable not found: ${pythonExe}`);
return false;
}
try {
const entries = fs.readdirSync(binDir);
let fixedCount = 0;
for (const entry of entries) {
const filePath = path.join(binDir, entry);
try {
const stat = fs.lstatSync(filePath);
if (stat.isDirectory() || stat.isSymbolicLink()) {
continue;
}
// Skip .exe files (binary), .dll, .pyd (compiled Python modules)
if (
entry.endsWith('.exe') ||
entry.endsWith('.dll') ||
entry.endsWith('.pyd') ||
entry.startsWith('python') ||
entry.startsWith('activate')
) {
continue;
}
} catch {
continue;
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
// Check if file contains any placeholders
const hasVenvPythonPlaceholder = content.includes(
'{{PREBUILT_VENV_PYTHON}}'
);
const hasPythonDirPlaceholder = content.includes(
'{{PREBUILT_PYTHON_DIR}}'
);
if (hasVenvPythonPlaceholder || hasPythonDirPlaceholder) {
let newContent = content;
if (hasVenvPythonPlaceholder) {
newContent = newContent.replace(
/\{\{PREBUILT_VENV_PYTHON\}\}/g,
pythonExe
);
}
if (hasPythonDirPlaceholder) {
const prebuiltPythonDir = getPrebuiltPythonDir();
if (prebuiltPythonDir) {
newContent = newContent.replace(
/\{\{PREBUILT_PYTHON_DIR\}\}/g,
prebuiltPythonDir
);
}
}
if (newContent !== content) {
fs.writeFileSync(filePath, newContent, 'utf-8');
if (process.platform !== 'win32') {
fs.chmodSync(filePath, 0o755);
}
fixedCount++;
}
}
} catch {
// Silently skip files that can't be processed
}
}
if (fixedCount > 0) {
log.info(`[VENV] Fixed shebangs in ${fixedCount} script(s)`);
}
return true;
} catch (error) {
log.warn(`[VENV] Failed to fix script shebangs: ${error}`);
return false;
}
}
/**
* Get path to prebuilt venv (if available in packaged app)
*/
@ -160,25 +325,20 @@ export function getPrebuiltVenvPath(): string | null {
}
const prebuiltVenvPath = path.join(process.resourcesPath, 'prebuilt', 'venv');
if (fs.existsSync(prebuiltVenvPath)) {
const pyvenvCfg = path.join(prebuiltVenvPath, 'pyvenv.cfg');
if (fs.existsSync(pyvenvCfg)) {
// Verify Python executable exists (Windows: Scripts/python.exe, Unix: bin/python)
const isWindows = process.platform === 'win32';
const pythonExePath = isWindows
? path.join(prebuiltVenvPath, 'Scripts', 'python.exe')
: path.join(prebuiltVenvPath, 'bin', 'python');
const pyvenvCfgPath = path.join(prebuiltVenvPath, 'pyvenv.cfg');
if (fs.existsSync(pythonExePath)) {
log.info(`Using prebuilt venv: ${prebuiltVenvPath}`);
return prebuiltVenvPath;
} else {
log.warn(
`Prebuilt venv found but Python executable missing at: ${pythonExePath}. ` +
`Falling back to user venv.`
);
}
log.info(`[VENV] Checking prebuilt venv at: ${prebuiltVenvPath}`);
if (fs.existsSync(prebuiltVenvPath) && fs.existsSync(pyvenvCfgPath)) {
fixPyvenvCfgPlaceholder(pyvenvCfgPath);
fixVenvScriptShebangs(prebuiltVenvPath);
const pythonExePath = getVenvPythonPath(prebuiltVenvPath);
if (fs.existsSync(pythonExePath)) {
log.info(`[VENV] Using prebuilt venv: ${prebuiltVenvPath}`);
return prebuiltVenvPath;
}
log.warn(`[VENV] Prebuilt venv Python missing at: ${pythonExePath}`);
}
return null;
}
@ -249,65 +409,72 @@ export function getPrebuiltTerminalVenvPath(): string | null {
'terminal_venv'
);
if (fs.existsSync(prebuiltTerminalVenvPath)) {
const pyvenvCfg = path.join(prebuiltTerminalVenvPath, 'pyvenv.cfg');
const pyvenvCfgPath = path.join(prebuiltTerminalVenvPath, 'pyvenv.cfg');
const installedMarker = path.join(
prebuiltTerminalVenvPath,
'.packages_installed'
);
if (fs.existsSync(pyvenvCfg) && fs.existsSync(installedMarker)) {
const isWindows = process.platform === 'win32';
const pythonExePath = isWindows
? path.join(prebuiltTerminalVenvPath, 'Scripts', 'python.exe')
: path.join(prebuiltTerminalVenvPath, 'bin', 'python');
if (fs.existsSync(pyvenvCfgPath) && fs.existsSync(installedMarker)) {
fixPyvenvCfgPlaceholder(pyvenvCfgPath);
fixVenvScriptShebangs(prebuiltTerminalVenvPath);
const pythonExePath = getVenvPythonPath(prebuiltTerminalVenvPath);
if (fs.existsSync(pythonExePath)) {
log.info(`Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}`);
return prebuiltTerminalVenvPath;
} else {
// Try to fix the missing Python executable by creating a symlink to prebuilt Python
log.warn(
`Prebuilt terminal venv found but Python executable missing at: ${pythonExePath}. ` +
`Attempting to fix...`
log.info(
`[VENV] Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}`
);
const prebuiltPython = findPythonForTerminalVenv();
if (prebuiltPython && fs.existsSync(prebuiltPython)) {
try {
const binDir = isWindows
? path.join(prebuiltTerminalVenvPath, 'Scripts')
: path.join(prebuiltTerminalVenvPath, 'bin');
// Ensure bin directory exists
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
// Create symlink to prebuilt Python
if (fs.existsSync(pythonExePath)) {
// Remove existing broken symlink or file
fs.unlinkSync(pythonExePath);
}
// Calculate relative path for symlink
const relativePath = path.relative(binDir, prebuiltPython);
fs.symlinkSync(relativePath, pythonExePath);
log.info(
`Fixed terminal venv Python symlink: ${pythonExePath} -> ${prebuiltPython}`
);
return prebuiltTerminalVenvPath;
} catch (error) {
log.warn(`Failed to fix terminal venv Python symlink: ${error}`);
}
}
log.warn(`Falling back to user terminal venv.`);
return prebuiltTerminalVenvPath;
}
// Try to fix the missing Python executable by creating a symlink to prebuilt Python
const prebuiltPython = findPythonForTerminalVenv();
if (prebuiltPython && fs.existsSync(prebuiltPython)) {
try {
const binDir = path.join(
prebuiltTerminalVenvPath,
process.platform === 'win32' ? 'Scripts' : 'bin'
);
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
if (fs.existsSync(pythonExePath)) {
fs.unlinkSync(pythonExePath);
}
const relativePath = path.relative(binDir, prebuiltPython);
fs.symlinkSync(relativePath, pythonExePath);
log.info(
`[VENV] Fixed terminal venv Python symlink: ${pythonExePath} -> ${prebuiltPython}`
);
return prebuiltTerminalVenvPath;
} catch (error) {
log.warn(
`[VENV] Failed to fix terminal venv Python symlink: ${error}`
);
}
}
log.warn(
`[VENV] Prebuilt terminal venv Python missing, falling back to user venv`
);
}
}
return null;
}
/**
* Get the Python executable path from a venv directory.
* Use this to directly invoke venv's python, avoiding uv/launcher placeholder issues.
*/
export function getVenvPythonPath(venvPath: string): string {
const isWindows = process.platform === 'win32';
return isWindows
? path.join(venvPath, 'Scripts', 'python.exe')
: path.join(venvPath, 'bin', 'python');
}
export function getVenvPath(version: string): string {
// First check for prebuilt venv in packaged app
if (app.isPackaged) {
@ -446,7 +613,7 @@ export function getPrebuiltPythonDir(): string | null {
'uv_python'
);
if (fs.existsSync(prebuiltPythonDir)) {
log.info(`Using prebuilt Python: ${prebuiltPythonDir}`);
log.info(`[VENV] Using prebuilt Python: ${prebuiltPythonDir}`);
return prebuiltPythonDir;
}

View file

@ -14,18 +14,25 @@
"type": "module",
"scripts": {
"prepare": "husky",
"compile-babel": "cd backend && uv run pybabel compile -d lang",
"clean-cache": "rimraf node_modules/.vite",
"dev": "npm run clean-cache && vite",
"preinstall-deps": "node scripts/preinstall-deps.js",
"fix-venv-paths": "node scripts/fix-venv-paths.js",
"fix-symlinks": "node scripts/fix-symlinks.js",
"clean-symlinks": "node scripts/clean-symlinks.js",
"test-signing": "node scripts/test-signing.js",
"build": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc -p tsconfig.build.json && vite build && electron-builder -- --publish always",
"build:mac": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc -p tsconfig.build.json && vite build && electron-builder --mac",
"build:mac:test": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc -p tsconfig.build.json && vite build && electron-builder --mac && npm run test-signing",
"build:win": "npm run preinstall-deps && npm run compile-babel && tsc -p tsconfig.build.json && vite build && electron-builder --win",
"build:linux": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc -p tsconfig.build.json && vite build && electron-builder --linux --publish always",
"build:all": "npm run preinstall-deps && npm run compile-babel && tsc -p tsconfig.build.json && vite build && electron-builder --mac --win --linux",
"compile-babel": "node scripts/compile-babel.js",
"prebuild:deps": "npm run preinstall-deps && npm run compile-babel && npm run fix-venv-paths && npm run fix-symlinks",
"prebuild:deps:clean": "npm run prebuild:deps && npm run clean-symlinks",
"prebuild:compile": "tsc -p tsconfig.build.json && vite build",
"prebuild": "npm run prebuild:deps:clean && npm run prebuild:compile",
"prebuild:win": "npm run prebuild:deps && npm run prebuild:compile",
"build": "npm run prebuild && electron-builder -- --publish always",
"build:mac": "npm run prebuild && electron-builder --mac",
"build:mac:test": "npm run prebuild && electron-builder --mac && npm run test-signing",
"build:win": "npm run prebuild:win && electron-builder --win",
"build:linux": "npm run prebuild && electron-builder --linux --publish always",
"build:all": "npm run prebuild && electron-builder --mac --win --linux",
"preview": "vite preview",
"pretest": "vite build --mode=test",
"test": "vitest run",

47
scripts/compile-babel.js Normal file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env node
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Directly use venv's python.exe (not uv run) to avoid Windows .exe launcher
// placeholder issues - same reason we use direct python for backend/uvicorn.
/* global process, console */
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..');
const backendDir = path.join(projectRoot, 'backend');
const prebuiltVenvDir = path.join(projectRoot, 'resources', 'prebuilt', 'venv');
const isWindows = process.platform === 'win32';
// Prebuild uses resources/prebuilt/venv; dev may use backend/.venv
const devVenvDir = path.join(backendDir, '.venv');
let venvDir;
if (fs.existsSync(prebuiltVenvDir)) {
venvDir = prebuiltVenvDir;
} else if (fs.existsSync(devVenvDir)) {
venvDir = devVenvDir;
} else {
console.error(
'No Python venv found. Please run "uv sync" in the backend directory first.'
);
console.error(` Looked for: ${prebuiltVenvDir}`);
console.error(` And: ${devVenvDir}`);
process.exit(1);
}
const pythonPath = isWindows
? path.join(venvDir, 'Scripts', 'python.exe')
: path.join(venvDir, 'bin', 'python');
if (!fs.existsSync(pythonPath)) {
console.error(`Python executable not found at: ${pythonPath}`);
console.error(' The venv may be corrupted. Please recreate it.');
process.exit(1);
}
execSync(`"${pythonPath}" -m babel.messages.frontend compile -d lang`, {
cwd: backendDir,
stdio: 'inherit',
});

395
scripts/fix-symlinks.js Normal file
View file

@ -0,0 +1,395 @@
#!/usr/bin/env node
/* global console */
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
/**
* Fix Python symlinks in venv directories
*
* Problem:
* - venv/bin/python is a symlink pointing to absolute path on build machine
* - Example: /Users/builder/.../cache/uv_python/cpython-3.10.19-.../bin/python3.10
* - Points to cache which is excluded by electron-builder
* - Symlinks break on user machines
*
* Solution:
* - Remove broken absolute symlinks
* - Create relative symlinks pointing to uv_python directory
* - Works on any machine after packaging
*/
import { Buffer } from 'buffer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
/**
* Find Python executable in uv_python directory
*/
function findPythonExecutable(uvPythonDir) {
if (!fs.existsSync(uvPythonDir)) {
return null;
}
try {
const entries = fs.readdirSync(uvPythonDir, { withFileTypes: true });
// Find cpython directory
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('cpython-')) {
const possiblePaths = [
// Unix paths
path.join(uvPythonDir, entry.name, 'bin', 'python3.10'),
path.join(uvPythonDir, entry.name, 'bin', 'python'),
path.join(uvPythonDir, entry.name, 'install', 'bin', 'python3.10'),
path.join(uvPythonDir, entry.name, 'install', 'bin', 'python'),
// Windows paths
path.join(uvPythonDir, entry.name, 'Scripts', 'python.exe'),
path.join(
uvPythonDir,
entry.name,
'install',
'Scripts',
'python.exe'
),
path.join(uvPythonDir, entry.name, 'python.exe'),
];
for (const pythonPath of possiblePaths) {
if (fs.existsSync(pythonPath)) {
return {
absolutePath: pythonPath,
cpythonDir: entry.name,
// Extract relative path from uv_python
relativePath: path.relative(uvPythonDir, pythonPath),
};
}
}
}
}
} catch (error) {
console.error(`Error searching for Python: ${error.message}`);
}
return null;
}
/**
* Fix Python symlinks in a venv
*/
function fixVenvSymlinks(venvPath, venvName) {
const binDir = path.join(venvPath, 'bin');
const scriptsDir = path.join(venvPath, 'Scripts');
// Windows uses Scripts directory and doesn't need symlink fixes
if (fs.existsSync(scriptsDir) && !fs.existsSync(binDir)) {
console.log(`\n📝 Processing ${venvName}`);
console.log(
` ℹ️ Windows venv detected - skipping symlink fixes (not needed)`
);
return true;
}
if (!fs.existsSync(binDir)) {
console.log(`⚠️ ${venvName} bin directory not found: ${binDir}`);
return false;
}
// Find Python in uv_python
const uvPythonDir = path.join(
projectRoot,
'resources',
'prebuilt',
'uv_python'
);
const pythonInfo = findPythonExecutable(uvPythonDir);
if (!pythonInfo) {
console.log(`⚠️ No Python executable found in uv_python`);
return false;
}
console.log(`\n📝 Processing ${venvName}`);
console.log(` Found Python: ${pythonInfo.cpythonDir}`);
// Python symlink names to fix
const symlinkNames = ['python', 'python3', 'python3.10'];
let fixedCount = 0;
for (const symlinkName of symlinkNames) {
const symlinkPath = path.join(binDir, symlinkName);
try {
// Check if symlink exists
let needsFix = false;
let currentTarget = null;
let symlinkExists = false;
// Use lstatSync to check if symlink exists (works for broken symlinks too)
try {
const stat = fs.lstatSync(symlinkPath);
symlinkExists = stat.isSymbolicLink();
} catch {
// File/symlink doesn't exist at all
symlinkExists = false;
}
if (symlinkExists) {
try {
currentTarget = fs.readlinkSync(symlinkPath);
// Check if it's a broken symlink or points to wrong location
if (
path.isAbsolute(currentTarget) ||
currentTarget.includes('cache')
) {
needsFix = true;
console.log(
` ❌ ${symlinkName}: broken symlink -> ${currentTarget}`
);
} else {
console.log(
` ✓ ${symlinkName}: already fixed -> ${currentTarget}`
);
continue;
}
} catch (err) {
// readlinkSync failed - symlink is broken
needsFix = true;
console.log(
` ❌ ${symlinkName}: broken symlink (target doesn't exist), ${err.message}`
);
}
} else {
needsFix = true;
console.log(` ➕ ${symlinkName}: missing, will create`);
}
if (needsFix) {
// Remove existing symlink if it exists
try {
fs.lstatSync(symlinkPath);
// If lstatSync succeeds, the file/symlink exists
fs.unlinkSync(symlinkPath);
} catch {
// File doesn't exist, nothing to remove
}
// Calculate relative path from bin/ to the Python executable
// Example: ../../uv_python/cpython-3.10.19-macos-aarch64-none/bin/python3.10
const relativePath = path.relative(binDir, pythonInfo.absolutePath);
// For secondary symlinks (python3, python3.10), just point to python
let targetPath;
if (symlinkName === 'python') {
targetPath = relativePath;
} else {
targetPath = 'python';
}
// Create new relative symlink
fs.symlinkSync(targetPath, symlinkPath);
console.log(` ✅ ${symlinkName} -> ${targetPath}`);
fixedCount++;
}
} catch (error) {
console.error(` ❌ Failed to fix ${symlinkName}: ${error.message}`);
}
}
return fixedCount > 0;
}
/**
* Fix shebang lines in all executable scripts in venv/bin directory
* Note: Windows doesn't use shebangs - it uses .exe wrappers instead
*/
function fixScriptShebangs(venvPath, venvName) {
const binDir = path.join(venvPath, 'bin');
const scriptsDir = path.join(venvPath, 'Scripts');
// Windows uses Scripts directory and doesn't need shebang fixes
if (fs.existsSync(scriptsDir) && !fs.existsSync(binDir)) {
console.log(`\n📝 Processing ${venvName}`);
console.log(
` ℹ️ Windows venv detected - skipping shebang fixes (not needed)`
);
return { fixed: 0, skipped: 0 };
}
if (!fs.existsSync(binDir)) {
console.log(`⚠️ ${venvName} bin directory not found: ${binDir}`);
return { fixed: 0, skipped: 0 };
}
console.log(`\n📝 Fixing shebangs in ${venvName}/bin scripts`);
let fixedCount = 0;
let skippedCount = 0;
try {
const entries = fs.readdirSync(binDir);
for (const entry of entries) {
const filePath = path.join(binDir, entry);
// Skip directories and symlinks
try {
const stat = fs.lstatSync(filePath);
if (stat.isDirectory() || stat.isSymbolicLink()) {
continue;
}
} catch (err) {
console.error(` ❌ Failed to check ${entry}: ${err.message}`);
continue;
}
try {
// Read first line to check for shebang
const fd = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(512);
const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
fs.closeSync(fd);
if (bytesRead === 0) continue;
const content = buffer.toString('utf-8', 0, bytesRead);
const firstLineEnd = content.indexOf('\n');
if (firstLineEnd === -1) continue;
const firstLine = content.substring(0, firstLineEnd);
// Check if it's a Python shebang with absolute path
if (!firstLine.startsWith('#!') || !firstLine.includes('python')) {
continue;
}
// Check if it contains an absolute path to the venv
// Support both Unix (/resources/prebuilt/venv/) and Windows (\resources\prebuilt\venv\ or \resources\prebuilt\Scripts\)
const shebangPath = firstLine.substring(2).trim();
const hasVenvPath =
firstLine.includes('/resources/prebuilt/venv/') ||
firstLine.includes('\\resources\\prebuilt\\venv\\') ||
firstLine.includes('/resources/prebuilt/terminal_venv/') ||
firstLine.includes('\\resources\\prebuilt\\terminal_venv\\') ||
firstLine.includes('/resources/prebuilt/Scripts/') ||
firstLine.includes('\\resources\\prebuilt\\Scripts\\');
if (!path.isAbsolute(shebangPath) && !hasVenvPath) {
skippedCount++;
continue;
}
// Read full file content
const fullContent = fs.readFileSync(filePath, 'utf-8');
// Replace absolute shebang with placeholder
// This will be replaced at runtime with actual venv path
// Similar to how pyvenv.cfg is handled
const newContent = fullContent.replace(
/^#!.*python.*$/m,
'#!{{PREBUILT_VENV_PYTHON}}'
);
if (newContent !== fullContent) {
fs.writeFileSync(filePath, newContent, 'utf-8');
// Preserve executable permissions
fs.chmodSync(filePath, 0o755);
fixedCount++;
if (fixedCount <= 5) {
// Only show first 5 for brevity
console.log(` ✅ Fixed: ${entry}`);
}
}
} catch (err) {
// Silently skip files that can't be processed
console.error(` ❌ Failed to process ${entry}: ${err.message}`);
continue;
}
}
if (fixedCount > 5) {
console.log(` ✅ ... and ${fixedCount - 5} more files`);
}
console.log(
` 📊 Total: ${fixedCount} fixed, ${skippedCount} already correct`
);
} catch (error) {
console.error(`❌ Error processing ${venvName}: ${error.message}`);
}
return { fixed: fixedCount, skipped: skippedCount };
}
/**
* Main function
*/
function main() {
console.log(
'🔧 Fixing Python symlinks and placeholders in venv directories...'
);
console.log('==========================================================\n');
const venvDirs = [
{
path: path.join(projectRoot, 'resources', 'prebuilt', 'venv'),
name: 'backend venv',
},
{
path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv'),
name: 'terminal venv',
},
];
let symlinkSuccessCount = 0;
let totalCount = 0;
let totalShebangsFixed = 0;
for (const { path: venvPath, name } of venvDirs) {
if (fs.existsSync(venvPath)) {
totalCount++;
// Fix symlinks
if (fixVenvSymlinks(venvPath, name)) {
symlinkSuccessCount++;
}
// Fix shebangs in all scripts
const { fixed } = fixScriptShebangs(venvPath, name);
totalShebangsFixed += fixed;
} else {
console.log(`⚠️ ${name} directory not found: ${venvPath}`);
}
}
console.log('\n==========================================================');
if (totalCount === 0) {
console.log(
'⚠️ No venv directories found - this is OK for development builds'
);
} else {
console.log(
`✅ Fixed symlinks in ${symlinkSuccessCount}/${totalCount} venv(s)`
);
console.log(`✅ Fixed placeholders in ${totalShebangsFixed} script(s)`);
console.log('✅ Venvs are now fully portable!');
}
}
main();

158
scripts/fix-venv-paths.js Normal file
View file

@ -0,0 +1,158 @@
#!/usr/bin/env node
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
/**
* Fix pyvenv.cfg paths before packaging
*
* Problem:
* - pyvenv.cfg contains absolute paths from the build machine
* - Example: home = /Users/builder/project/resources/prebuilt/cache/uv_python/cpython-3.10.19-macos-aarch64-none/bin
* - These paths don't exist on user machines, causing venv to fail
*
* Solution:
* - Replace absolute paths with placeholder tokens
* - At runtime, replace tokens with actual paths on user's machine
* - This makes prebuilt venvs portable across different machines
*/
/* global process, console */
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
/**
* Fix pyvenv.cfg in a venv directory
*/
function fixPyvenvCfg(venvPath, venvName) {
const pyvenvCfgPath = path.join(venvPath, 'pyvenv.cfg');
if (!fs.existsSync(pyvenvCfgPath)) {
console.log(`⚠️ pyvenv.cfg not found: ${pyvenvCfgPath}`);
return false;
}
try {
let content = fs.readFileSync(pyvenvCfgPath, 'utf-8');
const originalContent = content;
// Extract the home path
const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
if (!homeMatch) {
console.log(`⚠️ No 'home' line found in ${venvName}/pyvenv.cfg`);
return false;
}
const originalHome = homeMatch[1].trim();
console.log(`\n📝 Processing ${venvName}/pyvenv.cfg`);
console.log(` Original home: ${originalHome}`);
// Extract cpython directory name from the path
// Example: /path/to/cpython-3.10.19-macos-aarch64-none/bin -> cpython-3.10.19-macos-aarch64-none + /bin
const cpythonMatch = originalHome.match(/(cpython-[\w.-]+)(.*)/);
if (!cpythonMatch) {
console.log(
`⚠️ Could not extract cpython directory from: ${originalHome}`
);
return false;
}
const cpythonDir = cpythonMatch[1];
const pathAfterCpython = cpythonMatch[2];
// Determine if this is Windows or Unix path
const isWindowsPath =
/^[A-Za-z]:\\/.test(originalHome) ||
originalHome.startsWith('\\\\') ||
originalHome.includes('\\');
// Replace with placeholder that will be substituted at runtime
// {{PREBUILT_PYTHON_DIR}} will be replaced with the actual path on user's machine
// Use appropriate path separator for the platform
const pathSep = isWindowsPath ? '\\' : '/';
const newHome = `{{PREBUILT_PYTHON_DIR}}${pathSep}${cpythonDir}${pathAfterCpython}`;
content = content.replace(/^home\s*=\s*.+$/m, `home = ${newHome}`);
// Only write if content changed
if (content !== originalContent) {
fs.writeFileSync(pyvenvCfgPath, content);
console.log(` ✅ Updated to: ${newHome}`);
console.log(
` 🔧 Runtime will replace {{PREBUILT_PYTHON_DIR}} with actual path`
);
return true;
} else {
console.log(` ℹ️ Already using placeholder, no change needed`);
return true;
}
} catch (error) {
console.error(`❌ Error processing ${venvName}/pyvenv.cfg:`, error.message);
return false;
}
}
/**
* Main function
*/
function main() {
console.log('🔧 Fixing pyvenv.cfg paths for portable venvs...');
console.log('================================================\n');
const venvDirs = [
{
path: path.join(projectRoot, 'resources', 'prebuilt', 'venv'),
name: 'backend venv',
},
{
path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv'),
name: 'terminal venv',
},
];
let successCount = 0;
let totalCount = 0;
for (const { path: venvPath, name } of venvDirs) {
if (fs.existsSync(venvPath)) {
totalCount++;
if (fixPyvenvCfg(venvPath, name)) {
successCount++;
}
} else {
console.log(`⚠️ ${name} directory not found: ${venvPath}`);
}
}
console.log('\n================================================');
if (successCount === totalCount && totalCount > 0) {
console.log(
`✅ Successfully fixed ${successCount}/${totalCount} pyvenv.cfg files`
);
console.log('✅ Venvs are now portable and will work on user machines!');
} else if (totalCount === 0) {
console.log(
'⚠️ No venv directories found - this is OK for development builds'
);
} else {
console.log(`⚠️ Fixed ${successCount}/${totalCount} pyvenv.cfg files`);
process.exit(1);
}
}
main();

View file

@ -822,8 +822,25 @@ async function installPythonDeps(uvPath) {
};
const pyvenvCfg = path.join(venvPath, 'pyvenv.cfg');
// Check if venv contains placeholder
// If so, remove it to force recreation
if (fs.existsSync(pyvenvCfg)) {
console.log('✅ Python venv exists, syncing...');
try {
const content = fs.readFileSync(pyvenvCfg, 'utf-8');
if (content.includes('{{PREBUILT_PYTHON_DIR}}')) {
console.log(
'⚠️ Venv contains placeholder from previous build, removing...'
);
fs.rmSync(venvPath, { recursive: true, force: true });
console.log('📦 Creating fresh Python venv...');
} else {
console.log('✅ Python venv exists, syncing...');
}
} catch (error) {
console.error(`Error checking venv: ${error}`);
console.log('⚠️ Failed to check venv, will try to sync anyway...');
}
} else {
console.log('📦 Creating Python venv...');
}
@ -895,9 +912,10 @@ async function installPythonDeps(uvPath) {
console.log('✅ Python dependencies installed');
console.log('📝 Compiling babel...');
execSync(`"${uvPath}" run pybabel compile -d lang`, {
execSync(`"${pythonExePath}" -m babel.messages.frontend compile -d lang`, {
cwd: BACKEND_DIR,
env: env,
env: { ...env },
stdio: 'inherit',
});
@ -922,6 +940,24 @@ async function installTerminalBaseVenv(uvPath) {
? path.join(TERMINAL_VENV_DIR, 'Scripts', 'python.exe')
: path.join(TERMINAL_VENV_DIR, 'bin', 'python');
const installedMarker = path.join(TERMINAL_VENV_DIR, '.packages_installed');
const pyvenvCfg = path.join(TERMINAL_VENV_DIR, 'pyvenv.cfg');
// Check if venv contains placeholder (from previous build)
// If so, remove it to force recreation
if (fs.existsSync(pyvenvCfg)) {
try {
const content = fs.readFileSync(pyvenvCfg, 'utf-8');
if (content.includes('{{PREBUILT_PYTHON_DIR}}')) {
console.log(
'⚠️ Terminal venv contains placeholder from previous build, removing...'
);
fs.rmSync(TERMINAL_VENV_DIR, { recursive: true, force: true });
}
} catch (error) {
console.error(`Error checking terminal venv: ${error}`);
console.log('⚠️ Failed to check terminal venv, will continue...');
}
}
// Check if already fully installed
if (fs.existsSync(pythonPath) && fs.existsSync(installedMarker)) {

View file

@ -0,0 +1,209 @@
#!/usr/bin/env node
/**
* Comprehensive test for prebuilt dependencies
* Verifies pyvenv.cfg and Python symlinks are correct
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
let testsPassed = 0;
let testsFailed = 0;
function pass(message) {
console.log(` ✅ ${message}`);
testsPassed++;
}
function fail(message) {
console.log(` ❌ ${message}`);
testsFailed++;
}
function warn(message) {
console.log(` ⚠️ ${message}`);
}
function testPyvenvCfg(venvPath, venvName) {
console.log(`\n📝 Testing ${venvName} pyvenv.cfg`);
const pyvenvCfgPath = path.join(venvPath, 'pyvenv.cfg');
if (!fs.existsSync(pyvenvCfgPath)) {
fail('pyvenv.cfg not found');
return;
}
const content = fs.readFileSync(pyvenvCfgPath, 'utf-8');
const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
if (!homeMatch) {
fail('No home line found in pyvenv.cfg');
return;
}
const homePath = homeMatch[1].trim();
// Check if using placeholder (build-time state)
if (homePath.includes('{{PREBUILT_PYTHON_DIR}}')) {
pass('Using placeholder (ready for packaging)');
pass(`Home: ${homePath}`);
}
// Check if using absolute path (should be fixed)
else if (path.isAbsolute(homePath)) {
// Check if it's a valid path on this machine
if (fs.existsSync(homePath)) {
pass('Using absolute path (runtime-fixed or local)');
pass(`Home: ${homePath}`);
} else {
fail('Using absolute path that does not exist!');
fail(`Path: ${homePath}`);
warn('Run: npm run fix-venv-paths');
}
}
// Relative path
else {
warn(`Using relative path: ${homePath}`);
}
}
function testPythonSymlinks(venvPath, venvName) {
console.log(`\n🔗 Testing ${venvName} Python symlinks`);
const binDir = path.join(venvPath, 'bin');
if (!fs.existsSync(binDir)) {
fail('bin/ directory not found');
return;
}
const symlinkNames = ['python', 'python3', 'python3.10'];
for (const symlinkName of symlinkNames) {
const symlinkPath = path.join(binDir, symlinkName);
if (!fs.existsSync(symlinkPath)) {
try {
// Check if it's a broken symlink
fs.lstatSync(symlinkPath);
fail(`${symlinkName}: broken symlink (target doesn't exist)`);
const target = fs.readlinkSync(symlinkPath);
fail(` Target: ${target}`);
warn('Run: npm run fix-symlinks');
} catch (err) {
fail(`${symlinkName}: missing`);
warn('Run: npm run fix-symlinks');
}
continue;
}
try {
const stats = fs.lstatSync(symlinkPath);
if (stats.isSymbolicLink()) {
const target = fs.readlinkSync(symlinkPath);
// Check if absolute path (bad)
if (path.isAbsolute(target)) {
fail(`${symlinkName}: using absolute path`);
fail(` Target: ${target}`);
warn('Run: npm run fix-symlinks');
}
// Check if relative path (good)
else {
const resolvedPath = path.resolve(binDir, target);
if (fs.existsSync(resolvedPath)) {
pass(`${symlinkName} -> ${target}`);
} else {
fail(`${symlinkName}: relative symlink but target doesn't exist`);
fail(` Target: ${target}`);
fail(` Resolved: ${resolvedPath}`);
}
}
} else {
// Not a symlink, might be a copy
pass(`${symlinkName}: regular file (not symlink)`);
}
} catch (err) {
fail(`${symlinkName}: error checking - ${err.message}`);
}
}
}
function testPythonExecutable(venvPath, venvName) {
console.log(`\n🐍 Testing ${venvName} Python executable`);
const binDir = path.join(venvPath, 'bin');
const pythonPath = path.join(binDir, 'python');
if (!fs.existsSync(pythonPath)) {
fail('Python executable not found');
return;
}
try {
// Verify it's executable (on Unix)
if (process.platform !== 'win32') {
const stats = fs.statSync(pythonPath);
const isExecutable = (stats.mode & fs.constants.S_IXUSR) !== 0;
if (isExecutable) {
pass('Python is executable');
} else {
fail('Python is not executable');
}
} else {
pass('Python file exists (Windows)');
}
} catch (err) {
fail(`Error checking executable: ${err.message}`);
}
}
function main() {
console.log('🧪 Testing Prebuilt Dependencies');
console.log('='.repeat(50));
const venvs = [
{
path: path.join(projectRoot, 'resources', 'prebuilt', 'venv'),
name: 'Backend venv'
},
{
path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv'),
name: 'Terminal venv'
}
];
for (const venv of venvs) {
if (!fs.existsSync(venv.path)) {
console.log(`\n⚠️ ${venv.name} not found: ${venv.path}`);
console.log(' This is OK for development builds');
continue;
}
testPyvenvCfg(venv.path, venv.name);
testPythonSymlinks(venv.path, venv.name);
testPythonExecutable(venv.path, venv.name);
}
console.log('\n' + '='.repeat(50));
console.log(`\n📊 Test Results: ${testsPassed} passed, ${testsFailed} failed`);
if (testsFailed > 0) {
console.log('\n❌ Some tests failed!');
console.log('\n🔧 To fix:');
console.log(' npm run fix-venv-paths # Fix pyvenv.cfg');
console.log(' npm run fix-symlinks # Fix Python symlinks');
process.exit(1);
} else {
console.log('\n✅ All tests passed! Prebuilt dependencies are ready.');
}
}
main();

87
scripts/test-venv-fix.js Normal file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env node
/**
* Test script to verify pyvenv.cfg fix works correctly
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
function testVenvFix() {
console.log('🧪 Testing pyvenv.cfg fix...\n');
const testCases = [
{
name: 'Backend venv',
path: path.join(projectRoot, 'resources', 'prebuilt', 'venv', 'pyvenv.cfg')
},
{
name: 'Terminal venv',
path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv', 'pyvenv.cfg')
}
];
let allPassed = true;
for (const testCase of testCases) {
console.log(`Testing ${testCase.name}...`);
if (!fs.existsSync(testCase.path)) {
console.log(` ⚠️ File not found: ${testCase.path}`);
continue;
}
const content = fs.readFileSync(testCase.path, 'utf-8');
const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
if (!homeMatch) {
console.log(` ❌ FAIL: No 'home' line found`);
allPassed = false;
continue;
}
const homePath = homeMatch[1].trim();
// Check if placeholder is used
if (homePath.includes('{{PREBUILT_PYTHON_DIR}}')) {
console.log(` ✅ PASS: Using placeholder correctly`);
console.log(` Home: ${homePath}`);
// Verify placeholder format (accept both / and \ for cross-platform)
const expectedPattern = /^\{\{PREBUILT_PYTHON_DIR\}\}[\/\\]cpython-[\w\.\-]+[\/\\](bin|Scripts)$/;
if (expectedPattern.test(homePath)) {
console.log(` ✅ PASS: Placeholder format is correct`);
} else {
console.log(` ⚠️ WARNING: Placeholder format might be incorrect`);
console.log(` Expected: {{PREBUILT_PYTHON_DIR}}/cpython-X.Y.Z-platform/bin (or \\ on Windows)`);
console.log(` Got: ${homePath}`);
}
} else {
// Check if it's an absolute path (bad)
if (path.isAbsolute(homePath)) {
console.log(` ❌ FAIL: Still using absolute path`);
console.log(` Path: ${homePath}`);
console.log(` This path won't work on user machines!`);
allPassed = false;
} else {
console.log(` ⚠️ WARNING: Using relative path: ${homePath}`);
}
}
console.log();
}
console.log('='.repeat(50));
if (allPassed) {
console.log('✅ All tests passed! Venvs are portable.');
} else {
console.log('❌ Some tests failed. Please run: npm run fix-venv-paths');
process.exit(1);
}
}
testVenvFix();