mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-10-19 08:53:50 +00:00
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
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:
parent
ecbbe8d779
commit
38f5fbc8cc
11 changed files with 1269 additions and 115 deletions
18
.github/workflows/build-view.yml
vendored
18
.github/workflows/build-view.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
21
package.json
21
package.json
|
|
@ -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
47
scripts/compile-babel.js
Normal 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
395
scripts/fix-symlinks.js
Normal 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
158
scripts/fix-venv-paths.js
Normal 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();
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
209
scripts/test-prebuilt-deps.js
Normal file
209
scripts/test-prebuilt-deps.js
Normal 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
87
scripts/test-venv-fix.js
Normal 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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue