Skip to content

Commit 8a0e759

Browse files
authored
Revert "enhance: Prebuilt terminal venv found but Python executable missing P…" (#1062)
1 parent 3bb75a3 commit 8a0e759

4 files changed

Lines changed: 130 additions & 100 deletions

File tree

config/before-sign.cjs

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

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-
}
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);
8988

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;
89+
for (const pythonName of pythonNames) {
90+
const pythonSymlink = path.join(venvBinDir, pythonName);
91+
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+
}
98104
}
105+
} catch (error) {
106+
console.warn(`Warning: Could not process ${pythonName} symlink: ${error.message}`);
99107
}
100108
}
101-
} catch (error) {
102-
console.warn(`Warning: Could not search for prebuilt Python: ${error.message}`);
103109
}
104-
return null;
105-
}
106-
107-
const prebuiltPython = findPrebuiltPython();
108-
if (prebuiltPython) {
109-
console.log(`Found prebuilt Python: ${prebuiltPython}`);
110110
}
111111

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-
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)) {
118115
const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
119116
const bundlePath = path.resolve(appPath);
120117

121118
for (const pythonName of pythonNames) {
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}`);
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);
140132
}
141133
}
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-
}
134+
} catch (error) {
135+
console.warn(`Warning: Could not process terminal_venv ${pythonName} symlink: ${error.message}`);
153136
}
154137
}
155138
}
156139
}
157140

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-
141+
// Recursively clean other invalid symlinks
168142
function cleanSymlinks(dir, bundleRoot) {
169-
if (!fs.existsSync(dir) || processedDirs.has(dir)) {
143+
if (!fs.existsSync(dir)) {
170144
return;
171145
}
172146

electron/main/install-deps.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
getTerminalVenvPath,
1212
getPrebuiltTerminalVenvPath,
1313
getUvEnv,
14-
findPrebuiltPythonExecutable,
14+
getPrebuiltPythonDir,
1515
cleanupOldVenvs,
1616
isBinaryExists,
1717
runInstallScript,
@@ -507,6 +507,59 @@ 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+
510563
/**
511564
* Install terminal base venv with common packages for terminal tasks.
512565
* This is a lightweight venv separate from the backend venv.

electron/main/utils/process.ts

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

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

181-
const isWindows = process.platform === 'win32';
182-
const pythonName = isWindows ? 'python.exe' : 'python';
183-
const binPath = isWindows ? '' : path.join('install', 'bin');
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[] = [];
184184

185-
// UV stores Python in cpython-* subdirectories
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)
186194
try {
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;
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+
);
196206
}
197207
}
198208
}
199209
} catch (error) {
200210
log.warn('[PROCESS] Error searching for prebuilt Python:', error);
201211
}
202212

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

@@ -226,13 +241,13 @@ export function getPrebuiltTerminalVenvPath(): string | null {
226241
log.info(`Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}`);
227242
return prebuiltTerminalVenvPath;
228243
} else {
229-
// Try to fix the missing Python executable by creating a symlink to
230-
// prebuilt Python
244+
// Try to fix the missing Python executable by creating a symlink to prebuilt Python
231245
log.warn(
232246
`Prebuilt terminal venv found but Python executable missing at: ${pythonExePath}. ` +
233247
`Attempting to fix...`
234248
);
235-
const prebuiltPython = findPrebuiltPythonExecutable();
249+
250+
const prebuiltPython = findPythonForTerminalVenv();
236251
if (prebuiltPython && fs.existsSync(prebuiltPython)) {
237252
try {
238253
const binDir = isWindows

test/mocks/environmentMocks.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -426,14 +426,10 @@ 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(),
432429
getVenvsBaseDir: vi.fn(),
433430
cleanupOldVenvs: vi.fn(),
434431
isBinaryExists: vi.fn(),
435432
getUvEnv: vi.fn(),
436-
TERMINAL_BASE_PACKAGES: ['pandas', 'numpy', 'matplotlib', 'requests', 'openpyxl', 'beautifulsoup4', 'pillow'],
437433
mockState: {} as MockEnvironmentState,
438434

439435
setup: (mockState: MockEnvironmentState) => {
@@ -486,14 +482,6 @@ export function createProcessUtilsMock() {
486482
utilsMock.getVenvPath.mockImplementation((version: string) => {
487483
return `${mockState.system.homedir}/.eigent/venvs/backend-${version}`
488484
})
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)
497485

498486
utilsMock.getVenvsBaseDir.mockReturnValue(
499487
`${mockState.system.homedir}/.eigent/venvs`

0 commit comments

Comments
 (0)