Skip to content

Commit 3bb75a3

Browse files
authored
enhance: Prebuilt terminal venv found but Python executable missing P… (#1059)
1 parent 4c95c4c commit 3bb75a3

4 files changed

Lines changed: 100 additions & 130 deletions

File tree

config/before-sign.cjs

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -80,67 +80,93 @@ exports.default = async function afterPack(context) {
8080
}
8181
}
8282

83-
// Clean Python symlinks in venv/bin
84-
const venvBinDir = path.join(prebuiltPath, 'venv', 'bin');
85-
if (fs.existsSync(venvBinDir)) {
86-
const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
87-
const bundlePath = path.resolve(appPath);
88-
89-
for (const pythonName of pythonNames) {
90-
const pythonSymlink = path.join(venvBinDir, pythonName);
83+
// Find prebuilt Python executable in uv_python directory
84+
function findPrebuiltPython() {
85+
const uvPythonDir = path.join(prebuiltPath, 'uv_python');
86+
if (!fs.existsSync(uvPythonDir)) {
87+
return null;
88+
}
9189

92-
if (fs.existsSync(pythonSymlink)) {
93-
try {
94-
const stats = fs.lstatSync(pythonSymlink);
95-
if (stats.isSymbolicLink()) {
96-
const target = fs.readlinkSync(pythonSymlink);
97-
const resolvedPath = path.resolve(path.dirname(pythonSymlink), target);
98-
99-
// If symlink points outside bundle, remove it
100-
if (!resolvedPath.startsWith(bundlePath)) {
101-
console.log(`Removing invalid ${pythonName} symlink: ${target}`);
102-
fs.unlinkSync(pythonSymlink);
103-
}
90+
// UV stores Python in cpython-* subdirectories
91+
try {
92+
const entries = fs.readdirSync(uvPythonDir, { withFileTypes: true });
93+
for (const entry of entries) {
94+
if (entry.isDirectory() && entry.name.startsWith('cpython-')) {
95+
const pythonPath = path.join(uvPythonDir, entry.name, 'install', 'bin', 'python');
96+
if (fs.existsSync(pythonPath)) {
97+
return pythonPath;
10498
}
105-
} catch (error) {
106-
console.warn(`Warning: Could not process ${pythonName} symlink: ${error.message}`);
10799
}
108100
}
101+
} catch (error) {
102+
console.warn(`Warning: Could not search for prebuilt Python: ${error.message}`);
109103
}
104+
return null;
105+
}
106+
107+
const prebuiltPython = findPrebuiltPython();
108+
if (prebuiltPython) {
109+
console.log(`Found prebuilt Python: ${prebuiltPython}`);
110110
}
111111

112-
// Clean Python symlinks in terminal_venv/bin (same as venv/bin)
113-
const terminalVenvBinDir = path.join(prebuiltPath, 'terminal_venv', 'bin');
114-
if (fs.existsSync(terminalVenvBinDir)) {
112+
// Clean and fix Python symlinks in a venv bin directory
113+
function fixPythonSymlinks(binDir, venvName) {
114+
if (!fs.existsSync(binDir)) {
115+
return;
116+
}
117+
115118
const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
116119
const bundlePath = path.resolve(appPath);
117120

118121
for (const pythonName of pythonNames) {
119-
const pythonSymlink = path.join(terminalVenvBinDir, pythonName);
120-
121-
if (fs.existsSync(pythonSymlink)) {
122-
try {
123-
const stats = fs.lstatSync(pythonSymlink);
124-
if (stats.isSymbolicLink()) {
125-
const target = fs.readlinkSync(pythonSymlink);
126-
const resolvedPath = path.resolve(path.dirname(pythonSymlink), target);
127-
128-
// If symlink points outside bundle, remove it
129-
if (!resolvedPath.startsWith(bundlePath)) {
130-
console.log(`Removing invalid terminal_venv ${pythonName} symlink: ${target}`);
131-
fs.unlinkSync(pythonSymlink);
122+
const pythonSymlink = path.join(binDir, pythonName);
123+
124+
try {
125+
const stats = fs.lstatSync(pythonSymlink);
126+
if (stats.isSymbolicLink()) {
127+
const target = fs.readlinkSync(pythonSymlink);
128+
const resolvedPath = path.resolve(path.dirname(pythonSymlink), target);
129+
130+
// If symlink points outside bundle or is broken, remove and recreate it
131+
if (!resolvedPath.startsWith(bundlePath) || !fs.existsSync(resolvedPath)) {
132+
console.log(`Removing invalid ${venvName} ${pythonName} symlink: ${target}`);
133+
fs.unlinkSync(pythonSymlink);
134+
135+
// Recreate symlink pointing to prebuilt Python (only for main 'python')
136+
if (prebuiltPython && pythonName === 'python') {
137+
const relativePath = path.relative(binDir, prebuiltPython);
138+
fs.symlinkSync(relativePath, pythonSymlink);
139+
console.log(`Created ${venvName} ${pythonName} symlink -> ${relativePath}`);
132140
}
133141
}
134-
} catch (error) {
135-
console.warn(`Warning: Could not process terminal_venv ${pythonName} symlink: ${error.message}`);
142+
}
143+
} catch (error) {
144+
// Symlink doesn't exist, create it if this is the main python symlink
145+
if (error.code === 'ENOENT' && prebuiltPython && pythonName === 'python') {
146+
try {
147+
const relativePath = path.relative(binDir, prebuiltPython);
148+
fs.symlinkSync(relativePath, pythonSymlink);
149+
console.log(`Created missing ${venvName} ${pythonName} symlink -> ${relativePath}`);
150+
} catch (createError) {
151+
console.warn(`Warning: Could not create ${venvName} ${pythonName} symlink: ${createError.message}`);
152+
}
136153
}
137154
}
138155
}
139156
}
140157

141-
// Recursively clean other invalid symlinks
158+
// Fix Python symlinks in both venv directories
159+
fixPythonSymlinks(path.join(prebuiltPath, 'venv', 'bin'), 'venv');
160+
fixPythonSymlinks(path.join(prebuiltPath, 'terminal_venv', 'bin'), 'terminal_venv');
161+
162+
// Recursively clean other invalid symlinks (skip already-processed venv bin directories)
163+
const processedDirs = new Set([
164+
path.join(prebuiltPath, 'venv', 'bin'),
165+
path.join(prebuiltPath, 'terminal_venv', 'bin'),
166+
]);
167+
142168
function cleanSymlinks(dir, bundleRoot) {
143-
if (!fs.existsSync(dir)) {
169+
if (!fs.existsSync(dir) || processedDirs.has(dir)) {
144170
return;
145171
}
146172

electron/main/install-deps.ts

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
getTerminalVenvPath,
1212
getPrebuiltTerminalVenvPath,
1313
getUvEnv,
14-
getPrebuiltPythonDir,
14+
findPrebuiltPythonExecutable,
1515
cleanupOldVenvs,
1616
isBinaryExists,
1717
runInstallScript,
@@ -507,59 +507,6 @@ const runInstall = (extraArgs: string[], version: string) => {
507507
});
508508
};
509509

510-
/**
511-
* Find Python executable in prebuilt Python directory
512-
* UV stores Python installations in directories like: cpython-3.10.19+.../install/bin/python
513-
*/
514-
function findPrebuiltPythonExecutable(): string | null {
515-
const prebuiltPythonDir = getPrebuiltPythonDir();
516-
if (!prebuiltPythonDir) {
517-
return null;
518-
}
519-
520-
// Look for Python executable in the prebuilt directory
521-
// UV stores Python in subdirectories like: cpython-3.10.19+.../install/bin/python
522-
const possiblePaths: string[] = [];
523-
524-
// First, try common direct paths
525-
possiblePaths.push(
526-
path.join(prebuiltPythonDir, 'install', 'bin', 'python'),
527-
path.join(prebuiltPythonDir, 'install', 'python.exe'),
528-
path.join(prebuiltPythonDir, 'bin', 'python'),
529-
path.join(prebuiltPythonDir, 'python.exe'),
530-
);
531-
532-
// Then, search in subdirectories (UV stores Python in versioned directories)
533-
try {
534-
if (fs.existsSync(prebuiltPythonDir)) {
535-
const entries = fs.readdirSync(prebuiltPythonDir, { withFileTypes: true });
536-
for (const entry of entries) {
537-
if (entry.isDirectory() && entry.name.startsWith('cpython-')) {
538-
const subDir = path.join(prebuiltPythonDir, entry.name);
539-
possiblePaths.push(
540-
path.join(subDir, 'install', 'bin', 'python'),
541-
path.join(subDir, 'install', 'python.exe'),
542-
path.join(subDir, 'bin', 'python'),
543-
path.join(subDir, 'python.exe'),
544-
);
545-
}
546-
}
547-
}
548-
} catch (error) {
549-
log.warn('[DEPS INSTALL] Error searching for prebuilt Python:', error);
550-
}
551-
552-
for (const pythonPath of possiblePaths) {
553-
if (fs.existsSync(pythonPath)) {
554-
log.info(`[DEPS INSTALL] Found prebuilt Python executable: ${pythonPath}`);
555-
return pythonPath;
556-
}
557-
}
558-
559-
log.info('[DEPS INSTALL] Prebuilt Python directory found but executable not found, will use UV_PYTHON_INSTALL_DIR');
560-
return null;
561-
}
562-
563510
/**
564511
* Install terminal base venv with common packages for terminal tasks.
565512
* This is a lightweight venv separate from the backend venv.

electron/main/utils/process.ts

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -170,52 +170,37 @@ export function getPrebuiltVenvPath(): string | null {
170170
}
171171

172172
/**
173-
* Find Python executable in prebuilt Python directory for terminal venv
173+
* Find Python executable in prebuilt Python directory
174174
*/
175-
function findPythonForTerminalVenv(): string | null {
175+
export function findPrebuiltPythonExecutable(): string | null {
176176
const prebuiltPythonDir = getPrebuiltPythonDir();
177177
if (!prebuiltPythonDir) {
178178
return null;
179179
}
180180

181-
// Look for Python executable in the prebuilt directory
182-
// UV stores Python in subdirectories like: cpython-3.10.19+.../install/bin/python
183-
const possiblePaths: string[] = [];
181+
const isWindows = process.platform === 'win32';
182+
const pythonName = isWindows ? 'python.exe' : 'python';
183+
const binPath = isWindows ? '' : path.join('install', 'bin');
184184

185-
// First, try common direct paths
186-
possiblePaths.push(
187-
path.join(prebuiltPythonDir, 'install', 'bin', 'python'),
188-
path.join(prebuiltPythonDir, 'install', 'python.exe'),
189-
path.join(prebuiltPythonDir, 'bin', 'python'),
190-
path.join(prebuiltPythonDir, 'python.exe'),
191-
);
192-
193-
// Then, search in subdirectories (UV stores Python in versioned directories)
185+
// UV stores Python in cpython-* subdirectories
194186
try {
195-
if (fs.existsSync(prebuiltPythonDir)) {
196-
const entries = fs.readdirSync(prebuiltPythonDir, { withFileTypes: true });
197-
for (const entry of entries) {
198-
if (entry.isDirectory() && entry.name.startsWith('cpython-')) {
199-
const subDir = path.join(prebuiltPythonDir, entry.name);
200-
possiblePaths.push(
201-
path.join(subDir, 'install', 'bin', 'python'),
202-
path.join(subDir, 'install', 'python.exe'),
203-
path.join(subDir, 'bin', 'python'),
204-
path.join(subDir, 'python.exe'),
205-
);
187+
const entries = fs.readdirSync(prebuiltPythonDir, { withFileTypes: true });
188+
for (const entry of entries) {
189+
if (entry.isDirectory() && entry.name.startsWith('cpython-')) {
190+
const pythonPath = isWindows
191+
? path.join(prebuiltPythonDir, entry.name, 'install', pythonName)
192+
: path.join(prebuiltPythonDir, entry.name, binPath, pythonName);
193+
if (fs.existsSync(pythonPath)) {
194+
log.info(`[PROCESS] Found prebuilt Python executable: ${pythonPath}`);
195+
return pythonPath;
206196
}
207197
}
208198
}
209199
} catch (error) {
210200
log.warn('[PROCESS] Error searching for prebuilt Python:', error);
211201
}
212202

213-
for (const pythonPath of possiblePaths) {
214-
if (fs.existsSync(pythonPath)) {
215-
return pythonPath;
216-
}
217-
}
218-
203+
log.info('[PROCESS] Prebuilt Python directory found but executable not found');
219204
return null;
220205
}
221206

@@ -241,13 +226,13 @@ export function getPrebuiltTerminalVenvPath(): string | null {
241226
log.info(`Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}`);
242227
return prebuiltTerminalVenvPath;
243228
} else {
244-
// Try to fix the missing Python executable by creating a symlink to prebuilt Python
229+
// Try to fix the missing Python executable by creating a symlink to
230+
// prebuilt Python
245231
log.warn(
246232
`Prebuilt terminal venv found but Python executable missing at: ${pythonExePath}. ` +
247233
`Attempting to fix...`
248234
);
249-
250-
const prebuiltPython = findPythonForTerminalVenv();
235+
const prebuiltPython = findPrebuiltPythonExecutable();
251236
if (prebuiltPython && fs.existsSync(prebuiltPython)) {
252237
try {
253238
const binDir = isWindows

test/mocks/environmentMocks.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,10 +426,14 @@ export function createProcessUtilsMock() {
426426
getBinaryPath: vi.fn(),
427427
getCachePath: vi.fn(),
428428
getVenvPath: vi.fn(),
429+
getTerminalVenvPath: vi.fn(),
430+
getPrebuiltTerminalVenvPath: vi.fn(),
431+
findPrebuiltPythonExecutable: vi.fn(),
429432
getVenvsBaseDir: vi.fn(),
430433
cleanupOldVenvs: vi.fn(),
431434
isBinaryExists: vi.fn(),
432435
getUvEnv: vi.fn(),
436+
TERMINAL_BASE_PACKAGES: ['pandas', 'numpy', 'matplotlib', 'requests', 'openpyxl', 'beautifulsoup4', 'pillow'],
433437
mockState: {} as MockEnvironmentState,
434438

435439
setup: (mockState: MockEnvironmentState) => {
@@ -482,6 +486,14 @@ export function createProcessUtilsMock() {
482486
utilsMock.getVenvPath.mockImplementation((version: string) => {
483487
return `${mockState.system.homedir}/.eigent/venvs/backend-${version}`
484488
})
489+
490+
utilsMock.getTerminalVenvPath.mockImplementation((version: string) => {
491+
return `${mockState.system.homedir}/.eigent/venvs/terminal-${version}`
492+
})
493+
494+
utilsMock.getPrebuiltTerminalVenvPath.mockReturnValue(null)
495+
496+
utilsMock.findPrebuiltPythonExecutable.mockReturnValue(null)
485497

486498
utilsMock.getVenvsBaseDir.mockReturnValue(
487499
`${mockState.system.homedir}/.eigent/venvs`

0 commit comments

Comments
 (0)