From 6f58bb5076fdc43d535bd219676aa98fa89df13a Mon Sep 17 00:00:00 2001 From: 4pmtong Date: Mon, 19 Jan 2026 04:11:02 +0800 Subject: [PATCH] python to 3.10 --- .github/workflows/build-view.yml | 2 +- .github/workflows/build.yml | 4 +- config/before-sign.cjs | 104 ++++++++++++++++++++- electron/main/utils/process.ts | 86 ++++++++++++++++- scripts/preinstall-deps.js | 155 ++++++++++++++++++++++++------- 5 files changed, 309 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build-view.yml b/.github/workflows/build-view.yml index 3546e9fe..ba9698b0 100644 --- a/.github/workflows/build-view.yml +++ b/.github/workflows/build-view.yml @@ -33,7 +33,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.10' - name: Install Python Dependencies run: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a52d762f..f6e7f3c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version: "3.10" - name: Install Python Dependencies run: | @@ -158,4 +158,4 @@ jobs: release/mac-arm64/* release/win-x64/* env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/config/before-sign.cjs b/config/before-sign.cjs index 6b882ed6..12e58b16 100644 --- a/config/before-sign.cjs +++ b/config/before-sign.cjs @@ -103,6 +103,7 @@ exports.default = async function afterPack(context) { : ['python', 'python3', 'python3.10', 'python3.11', 'python3.12']; const bundlePath = path.resolve(appPath); + // First, clean invalid symlinks for (const pythonName of pythonNames) { const pythonPath = path.join(venvBinDir, pythonName); @@ -113,8 +114,8 @@ exports.default = async function afterPack(context) { const target = fs.readlinkSync(pythonPath); const resolvedPath = path.resolve(path.dirname(pythonPath), target); - // If symlink points outside bundle, remove it - if (!resolvedPath.startsWith(bundlePath)) { + // If symlink points outside bundle or target doesn't exist, remove it + if (!resolvedPath.startsWith(bundlePath) || !fs.existsSync(resolvedPath)) { console.log(`Removing invalid ${pythonName} symlink: ${target}`); fs.unlinkSync(pythonPath); } @@ -125,6 +126,80 @@ exports.default = async function afterPack(context) { } } + // Fix Python symlinks by finding actual Python in cache + const pythonCacheDir = path.join(prebuiltPath, 'cache', 'uv_python'); + if (fs.existsSync(pythonCacheDir)) { + try { + const entries = fs.readdirSync(pythonCacheDir); + const pythonDirs = entries + .filter(name => name.startsWith('cpython-')) + .map(name => { + const binDir = path.join(pythonCacheDir, name, 'bin'); + const pythonExe = isWindows + ? path.join(binDir, 'python.exe') + : path.join(binDir, 'python3.10'); + return { name, binDir, pythonExe }; + }) + .filter(({ pythonExe }) => fs.existsSync(pythonExe)); + + if (pythonDirs.length > 0) { + // Use the first available Python (usually the latest) + const { pythonExe, binDir } = pythonDirs[0]; + const mainPythonName = isWindows ? 'python.exe' : 'python'; + const mainPythonPath = path.join(venvBinDir, mainPythonName); + + // Check if main Python symlink exists and is valid + let needsFix = true; + if (fs.existsSync(mainPythonPath)) { + try { + const stats = fs.lstatSync(mainPythonPath); + if (stats.isSymbolicLink()) { + const target = fs.readlinkSync(mainPythonPath); + const resolvedPath = path.resolve(path.dirname(mainPythonPath), target); + if (fs.existsSync(resolvedPath) && resolvedPath === pythonExe) { + needsFix = false; + } + } + } catch (error) { + // Error reading symlink, needs fix + } + } + + if (needsFix) { + // Remove old symlink if exists + if (fs.existsSync(mainPythonPath)) { + fs.unlinkSync(mainPythonPath); + } + + // Create new symlink using relative path + const relativePath = path.relative(venvBinDir, pythonExe); + fs.symlinkSync(relativePath, mainPythonPath); + console.log(`✅ Fixed Python symlink: ${mainPythonPath} -> ${relativePath}`); + + // Also fix python3 and python3.10 symlinks on Unix + if (!isWindows) { + const python3Path = path.join(venvBinDir, 'python3'); + const python310Path = path.join(venvBinDir, 'python3.10'); + + if (fs.existsSync(python3Path)) { + fs.unlinkSync(python3Path); + } + fs.symlinkSync('python', python3Path); + + if (fs.existsSync(python310Path)) { + fs.unlinkSync(python310Path); + } + fs.symlinkSync('python', python310Path); + } + } else { + console.log(`✅ Python symlink is valid: ${mainPythonPath}`); + } + } + } catch (error) { + console.warn(`Warning: Could not fix Python symlinks: ${error.message}`); + } + } + // On Windows, verify Python executable exists and is accessible if (isWindows) { const pythonExe = path.join(venvBinDir, 'python.exe'); @@ -134,6 +209,31 @@ exports.default = async function afterPack(context) { } else { console.log(`✅ Python executable verified: ${pythonExe}`); } + } else { + // On Unix, verify Python executable + const pythonExe = path.join(venvBinDir, 'python'); + if (!fs.existsSync(pythonExe)) { + console.warn(`⚠️ Warning: Python executable not found at: ${pythonExe}`); + console.warn(` This may cause runtime errors. Ensure Python cache (uv_python) is included in the build.`); + } else { + // Verify symlink is valid + try { + const stats = fs.lstatSync(pythonExe); + if (stats.isSymbolicLink()) { + const target = fs.readlinkSync(pythonExe); + const resolvedPath = path.resolve(path.dirname(pythonExe), target); + if (fs.existsSync(resolvedPath)) { + console.log(`✅ Python executable verified: ${pythonExe} -> ${resolvedPath}`); + } else { + console.warn(`⚠️ Warning: Python symlink target does not exist: ${target}`); + } + } else { + console.log(`✅ Python executable verified: ${pythonExe}`); + } + } catch (error) { + console.warn(`⚠️ Warning: Could not verify Python executable: ${error.message}`); + } + } } } diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts index 093c4646..ba917638 100644 --- a/electron/main/utils/process.ts +++ b/electron/main/utils/process.ts @@ -139,6 +139,7 @@ export function getCachePath(folder: string): string { /** * Get path to prebuilt venv (if available in packaged app) + * Attempts to fix Python symlinks if they are broken */ export function getPrebuiltVenvPath(): string | null { if (!app.isPackaged) { @@ -151,18 +152,95 @@ export function getPrebuiltVenvPath(): string | null { if (fs.existsSync(pyvenvCfg)) { // Verify Python executable exists (Windows: Scripts/python.exe, Unix: bin/python) const isWindows = process.platform === 'win32'; + const venvBinDir = isWindows + ? path.join(prebuiltVenvPath, 'Scripts') + : path.join(prebuiltVenvPath, 'bin'); const pythonExePath = isWindows - ? path.join(prebuiltVenvPath, 'Scripts', 'python.exe') - : path.join(prebuiltVenvPath, 'bin', 'python'); + ? path.join(venvBinDir, 'python.exe') + : path.join(venvBinDir, 'python'); + // Check if Python executable exists and is valid + let pythonValid = false; if (fs.existsSync(pythonExePath)) { + try { + const stats = fs.lstatSync(pythonExePath); + if (stats.isSymbolicLink()) { + const target = fs.readlinkSync(pythonExePath); + const resolvedPath = path.resolve(path.dirname(pythonExePath), target); + pythonValid = fs.existsSync(resolvedPath); + } else { + pythonValid = true; // Regular file, assume valid + } + } catch (error) { + pythonValid = false; + } + } + + if (pythonValid) { log.info(`Using prebuilt venv: ${prebuiltVenvPath}`); return prebuiltVenvPath; } else { + // Try to fix the Python symlink before falling back log.warn( - `Prebuilt venv found but Python executable missing at: ${pythonExePath}. ` + - `Falling back to user venv.` + `Prebuilt venv found but Python executable missing or invalid at: ${pythonExePath}. ` + + `Attempting to fix...` ); + + const pythonCacheDir = path.join(process.resourcesPath, 'prebuilt', 'cache', 'uv_python'); + if (fs.existsSync(pythonCacheDir)) { + try { + const entries = fs.readdirSync(pythonCacheDir); + const pythonDirs = entries + .filter(name => name.startsWith('cpython-3.10')) + .map(name => { + const binDir = path.join(pythonCacheDir, name, 'bin'); + const pythonExe = isWindows + ? path.join(binDir, 'python.exe') + : path.join(binDir, 'python3.10'); + return { name, binDir, pythonExe }; + }) + .filter(({ pythonExe }) => fs.existsSync(pythonExe)); + + if (pythonDirs.length > 0) { + const { pythonExe } = pythonDirs[0]; + const relativePath = path.relative(venvBinDir, pythonExe); + + // Remove old symlink if exists + if (fs.existsSync(pythonExePath)) { + fs.unlinkSync(pythonExePath); + } + + // Create new symlink + fs.symlinkSync(relativePath, pythonExePath); + log.info(`Fixed Python symlink: ${pythonExePath} -> ${relativePath}`); + + // On Unix, also fix python3 and python3.10 + if (!isWindows) { + const python3Path = path.join(venvBinDir, 'python3'); + const python310Path = path.join(venvBinDir, 'python3.10'); + + if (fs.existsSync(python3Path)) { + fs.unlinkSync(python3Path); + } + fs.symlinkSync('python', python3Path); + + if (fs.existsSync(python310Path)) { + fs.unlinkSync(python310Path); + } + fs.symlinkSync('python', python310Path); + } + + log.info(`Using prebuilt venv (after fix): ${prebuiltVenvPath}`); + return prebuiltVenvPath; + } else { + log.warn('No valid Python executable found in cache, falling back to user venv.'); + } + } catch (error: any) { + log.warn(`Failed to fix Python symlink: ${error?.message || String(error)}. Falling back to user venv.`); + } + } else { + log.warn('Python cache directory not found, falling back to user venv.'); + } } } } diff --git a/scripts/preinstall-deps.js b/scripts/preinstall-deps.js index ebf44ffd..116e3cf7 100644 --- a/scripts/preinstall-deps.js +++ b/scripts/preinstall-deps.js @@ -36,10 +36,10 @@ function isValidZip(filePath) { try { const buffer = fs.readFileSync(filePath); return buffer.length > 4 && - buffer[0] === 0x50 && - buffer[1] === 0x4B && - buffer[2] === 0x03 && - buffer[3] === 0x04; + buffer[0] === 0x50 && + buffer[1] === 0x4B && + buffer[2] === 0x03 && + buffer[3] === 0x04; } catch { return false; } @@ -52,8 +52,8 @@ function isValidTarGz(filePath) { try { const buffer = fs.readFileSync(filePath); return buffer.length > 2 && - buffer[0] === 0x1F && - buffer[1] === 0x8B; + buffer[0] === 0x1F && + buffer[1] === 0x8B; } catch { return false; } @@ -87,7 +87,7 @@ async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType }, (response) => { // Handle redirects (301, 302, 307, 308) if (response.statusCode === 301 || response.statusCode === 302 || - response.statusCode === 307 || response.statusCode === 308) { + response.statusCode === 307 || response.statusCode === 308) { redirectCount++; if (redirectCount > maxRedirects) { reject(new Error(`Too many redirects (${redirectCount})`)); @@ -300,21 +300,21 @@ async function installUv() { // Find installed uv const possiblePaths = process.platform === 'win32' ? [ - path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python311', 'Scripts', 'uv.exe'), - path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python312', 'Scripts', 'uv.exe'), - path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python313', 'Scripts', 'uv.exe'), - path.join(os.homedir(), '.local', 'bin', 'uv.exe'), - 'C:\\Python311\\Scripts\\uv.exe', - 'C:\\Python312\\Scripts\\uv.exe', - 'C:\\Python313\\Scripts\\uv.exe', - ] + path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python311', 'Scripts', 'uv.exe'), + path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python312', 'Scripts', 'uv.exe'), + path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python313', 'Scripts', 'uv.exe'), + path.join(os.homedir(), '.local', 'bin', 'uv.exe'), + 'C:\\Python311\\Scripts\\uv.exe', + 'C:\\Python312\\Scripts\\uv.exe', + 'C:\\Python313\\Scripts\\uv.exe', + ] : [ - path.join(os.homedir(), '.local', 'bin', 'uv'), - path.join(os.homedir(), 'Library', 'Python', '3.11', 'bin', 'uv'), - path.join(os.homedir(), 'Library', 'Python', '3.12', 'bin', 'uv'), - path.join(os.homedir(), 'Library', 'Python', '3.13', 'bin', 'uv'), - '/usr/local/bin/uv', - ]; + path.join(os.homedir(), '.local', 'bin', 'uv'), + path.join(os.homedir(), 'Library', 'Python', '3.11', 'bin', 'uv'), + path.join(os.homedir(), 'Library', 'Python', '3.12', 'bin', 'uv'), + path.join(os.homedir(), 'Library', 'Python', '3.13', 'bin', 'uv'), + '/usr/local/bin/uv', + ]; let foundUvPath = null; try { @@ -592,15 +592,28 @@ async function installPythonDeps(uvPath) { } // Ensure Python is installed before syncing - // This is critical for Windows where Python might not be in the venv - console.log('🐍 Ensuring Python is installed...'); + // Use fixed Python version to ensure consistency across builds + // This prevents version mismatch issues when packaging + console.log('🐍 Ensuring Python is installed (using fixed version 3.10.15)...'); try { + // Use fixed minor version to ensure consistency + // This prevents issues where different builds might get different patch versions execSync( - `"${uvPath}" python install 3.10`, + `"${uvPath}" python install 3.10.15`, { cwd: BACKEND_DIR, env: env, stdio: 'inherit' } ); + console.log('✅ Python 3.10.15 installed'); } catch (error) { - console.log('⚠️ Python install command failed, continuing with sync (Python may already be installed)...'); + console.log('⚠️ Python 3.10.15 install failed, trying 3.10 (latest)...'); + try { + execSync( + `"${uvPath}" python install 3.10`, + { cwd: BACKEND_DIR, env: env, stdio: 'inherit' } + ); + console.log('✅ Python 3.10 (latest) installed'); + } catch (error2) { + console.log('⚠️ Python install command failed, continuing with sync (Python may already be installed)...'); + } } execSync( @@ -608,20 +621,96 @@ async function installPythonDeps(uvPath) { { cwd: BACKEND_DIR, env: env, stdio: 'inherit' } ); - // Verify Python executable exists in the virtual environment + // Verify and fix Python executable symlinks in the virtual environment const isWindows = process.platform === 'win32'; + const venvBinDir = isWindows ? path.join(venvPath, 'Scripts') : path.join(venvPath, 'bin'); const pythonExePath = isWindows - ? path.join(venvPath, 'Scripts', 'python.exe') - : path.join(venvPath, 'bin', 'python'); + ? path.join(venvBinDir, 'python.exe') + : path.join(venvBinDir, 'python'); if (!fs.existsSync(pythonExePath)) { - throw new Error( - `Python executable not found in virtual environment at: ${pythonExePath}\n` + - `Virtual environment may be corrupted. Please ensure uv sync completed successfully.` - ); + // Try to find Python in cache and create symlink + console.log('⚠️ Python executable not found, attempting to fix symlink...'); + + if (fs.existsSync(pythonCacheDir)) { + try { + const entries = fs.readdirSync(pythonCacheDir); + const pythonDirs = entries + .filter(name => name.startsWith('cpython-3.10')) + .map(name => { + const binDir = path.join(pythonCacheDir, name, 'bin'); + const pythonExe = isWindows + ? path.join(binDir, 'python.exe') + : path.join(binDir, 'python3.10'); + return { name, binDir, pythonExe }; + }) + .filter(({ pythonExe }) => fs.existsSync(pythonExe)); + + if (pythonDirs.length > 0) { + const { pythonExe, binDir } = pythonDirs[0]; + console.log(`Found Python at: ${pythonExe}`); + + // Create symlink using relative path + const relativePath = path.relative(venvBinDir, pythonExe); + + if (fs.existsSync(pythonExePath)) { + fs.unlinkSync(pythonExePath); + } + fs.symlinkSync(relativePath, pythonExePath); + console.log(`✅ Created Python symlink: ${pythonExePath} -> ${relativePath}`); + + // On Unix, also create python3 and python3.10 symlinks + if (!isWindows) { + const python3Path = path.join(venvBinDir, 'python3'); + const python310Path = path.join(venvBinDir, 'python3.10'); + + if (fs.existsSync(python3Path)) { + fs.unlinkSync(python3Path); + } + fs.symlinkSync('python', python3Path); + + if (fs.existsSync(python310Path)) { + fs.unlinkSync(python310Path); + } + fs.symlinkSync('python', python310Path); + } + } else { + throw new Error('No Python executable found in cache'); + } + } catch (error) { + throw new Error( + `Python executable not found in virtual environment at: ${pythonExePath}\n` + + `Virtual environment may be corrupted. Please ensure uv sync completed successfully.\n` + + `Error: ${error.message}` + ); + } + } else { + throw new Error( + `Python executable not found in virtual environment at: ${pythonExePath}\n` + + `Virtual environment may be corrupted. Please ensure uv sync completed successfully.` + ); + } + } + + // Verify the symlink is valid + try { + const stats = fs.lstatSync(pythonExePath); + if (stats.isSymbolicLink()) { + const target = fs.readlinkSync(pythonExePath); + const resolvedPath = path.resolve(path.dirname(pythonExePath), target); + if (fs.existsSync(resolvedPath)) { + console.log(`✅ Python executable verified: ${pythonExePath} -> ${resolvedPath}`); + } else { + console.warn(`⚠️ Warning: Python symlink target does not exist: ${target}`); + console.warn(` This may cause issues during packaging.`); + } + } else { + console.log(`✅ Python executable verified: ${pythonExePath}`); + } + } catch (error) { + console.warn(`⚠️ Warning: Could not verify Python executable: ${error.message}`); } - console.log(`✅ Python executable verified: ${pythonExePath}`); console.log('✅ Python dependencies installed'); console.log('📝 Compiling babel...');