diff --git a/electron-builder.json b/electron-builder.json index 0ff93ab7..4028cd46 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -29,7 +29,13 @@ { "from": "resources/prebuilt", "to": "prebuilt", - "filter": ["**/*", "!cache/**/*", "!**/.npm-cache/**/*"] + "filter": [ + "**/*", + "!cache/**/*", + "!**/.npm-cache/**/*", + "!uv_python/**/*.pyc", + "!uv_python/**/__pycache__" + ] } ], "protocols": [ diff --git a/electron/main/init.ts b/electron/main/init.ts index 1ee74eaa..2b0ace8c 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -1,4 +1,4 @@ -import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, isBinaryExists, runInstallScript, killProcessByName } from "./utils/process"; +import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, isBinaryExists, runInstallScript, killProcessByName, getPrebuiltPythonDir, getPrebuiltVenvPath } from "./utils/process"; import { spawn, exec } from 'child_process' import log from 'electron-log' import fs from 'fs' @@ -225,21 +225,34 @@ export async function startBackend(setPort?: (port: number) => void): Promise { return fs.existsSync(cmd); } +/** + * Get path to prebuilt Python installation (if available in packaged app) + */ +export function getPrebuiltPythonDir(): string | null { + if (!app.isPackaged) { + return null; + } + + const prebuiltPythonDir = path.join(process.resourcesPath, 'prebuilt', 'uv_python'); + if (fs.existsSync(prebuiltPythonDir)) { + log.info(`Using prebuilt Python: ${prebuiltPythonDir}`); + return prebuiltPythonDir; + } + + return null; +} + /** * Get unified UV environment variables for consistent Python environment management. * This ensures both installation and runtime use the same paths. @@ -279,8 +309,12 @@ export async function isBinaryExists(name: string): Promise { * @returns Environment variables for UV commands */ export function getUvEnv(version: string): Record { + // Use prebuilt Python if available (packaged app) + const prebuiltPython = getPrebuiltPythonDir(); + const pythonInstallDir = prebuiltPython || getCachePath('uv_python'); + return { - UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'), + UV_PYTHON_INSTALL_DIR: pythonInstallDir, UV_TOOL_DIR: getCachePath('uv_tool'), UV_PROJECT_ENVIRONMENT: getVenvPath(version), UV_HTTP_TIMEOUT: '300', diff --git a/scripts/clean-symlinks.js b/scripts/clean-symlinks.js index e011ba45..716a4c40 100644 --- a/scripts/clean-symlinks.js +++ b/scripts/clean-symlinks.js @@ -46,7 +46,7 @@ function isValidSymlink(symlinkPath, bundleRoot) { } /** - * Fix Python symlinks in venv/bin + * Fix Python symlinks in venv/bin (Unix) or venv/Scripts (Windows) * Remove symlinks that point outside the bundle (to cache directory) */ function fixPythonSymlinks(venvBinDir, bundleRoot) { @@ -55,7 +55,10 @@ function fixPythonSymlinks(venvBinDir, bundleRoot) { } const bundlePath = path.resolve(bundleRoot); - const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12']; + const isWindows = process.platform === 'win32'; + const pythonNames = isWindows + ? ['python.exe', 'python3.exe', 'python3.10.exe', 'python3.11.exe', 'python3.12.exe'] + : ['python', 'python3', 'python3.10', 'python3.11', 'python3.12']; for (const pythonName of pythonNames) { const pythonSymlink = path.join(venvBinDir, pythonName); @@ -127,7 +130,8 @@ function main() { console.log('๐Ÿงน Cleaning invalid symbolic links...'); const bundleRoot = path.join(projectRoot, 'resources', 'prebuilt'); - const venvBinDir = path.join(bundleRoot, 'venv', 'bin'); + const isWindows = process.platform === 'win32'; + const venvBinDir = path.join(bundleRoot, 'venv', isWindows ? 'Scripts' : 'bin'); // First, try to fix Python symlinks specifically if (fs.existsSync(venvBinDir)) { diff --git a/scripts/preinstall-deps.js b/scripts/preinstall-deps.js index a5ef3311..ad04989f 100644 --- a/scripts/preinstall-deps.js +++ b/scripts/preinstall-deps.js @@ -17,8 +17,9 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, '..'); -const BIN_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'bin'); -const VENV_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'venv'); +const PREBUILT_DIR = path.join(projectRoot, 'resources', 'prebuilt'); +const BIN_DIR = path.join(PREBUILT_DIR, 'bin'); +const VENV_DIR = path.join(PREBUILT_DIR, 'venv'); const BACKEND_DIR = path.join(projectRoot, 'backend'); console.log('๐Ÿš€ Starting pre-installation of dependencies...'); @@ -197,6 +198,45 @@ async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType throw new Error(`Failed to download ${fileType} from all sources`); } +/** + * Recursively copy directory, handling symlinks properly + */ +function copyDirRecursiveSync(src, dest) { + if (!fs.existsSync(src)) { + return; + } + + // Create destination directory + fs.mkdirSync(dest, { recursive: true }); + + // Get all files and directories + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyDirRecursiveSync(srcPath, destPath); + } else if (entry.isSymbolicLink()) { + try { + const realPath = fs.realpathSync(srcPath); + const realStat = fs.statSync(realPath); + if (realStat.isDirectory()) { + copyDirRecursiveSync(realPath, destPath); + } else { + fs.copyFileSync(realPath, destPath); + } + } catch (err) { + // If symlink target doesn't exist, skip it + console.log(` Skipping broken symlink: ${srcPath}`); + } + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + /** * Get Bun download URL list */ @@ -613,11 +653,66 @@ async function installPythonDeps(uvPath) { console.log('๐Ÿ“ฆ Creating Python venv...'); } + // 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...'); + try { + execSync( + `"${uvPath}" python install 3.10`, + { cwd: BACKEND_DIR, env: env, stdio: 'inherit' } + ); + } catch (error) { + console.log('โš ๏ธ Python install command failed, continuing with sync (Python may already be installed)...'); + } + + // Use --python-preference only-managed to ensure uv uses its own managed Python + // This makes the venv more portable execSync( - `"${uvPath}" sync --no-dev --cache-dir "${cacheDir}"`, + `"${uvPath}" sync --no-dev --cache-dir "${cacheDir}" --python-preference only-managed`, { cwd: BACKEND_DIR, env: env, stdio: 'inherit' } ); + // Verify Python executable exists in the virtual environment + const isWindows = process.platform === 'win32'; + const pythonExePath = isWindows + ? path.join(venvPath, 'Scripts', 'python.exe') + : path.join(venvPath, 'bin', '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.` + ); + } + + console.log(`โœ… Python executable verified: ${pythonExePath}`); + + // Bundle the actual Python installation from UV cache into prebuilt + console.log('๐Ÿ“ฆ Bundling Python installation...'); + try { + const uvPythonDir = pythonCacheDir; + const prebuiltPythonDir = path.join(PREBUILT_DIR, 'uv_python'); + + if (fs.existsSync(uvPythonDir)) { + console.log(` Copying from: ${uvPythonDir}`); + console.log(` Copying to: ${prebuiltPythonDir}`); + + // Remove existing python dir if it exists + if (fs.existsSync(prebuiltPythonDir)) { + fs.rmSync(prebuiltPythonDir, { recursive: true, force: true }); + } + + // Copy the Python installation + copyDirRecursiveSync(uvPythonDir, prebuiltPythonDir); + console.log('โœ… Python installation bundled'); + } else { + console.log('โš ๏ธ UV Python cache not found, venv may not be portable'); + } + } catch (error) { + console.log(`โš ๏ธ Failed to bundle Python: ${error.message}`); + console.log(' The app may fail to start without internet connection'); + } + console.log('โœ… Python dependencies installed'); console.log('๐Ÿ“ Compiling babel...'); diff --git a/scripts/test-notarization.js b/scripts/test-notarization.js new file mode 100644 index 00000000..672b44a5 --- /dev/null +++ b/scripts/test-notarization.js @@ -0,0 +1,296 @@ +#!/usr/bin/env node +/** + * Test script for macOS notarization issues + * This script checks for common issues that cause notarization to fail: + * 1. .npm-cache directories + * 2. flac-mac binary (outdated SDK) + * 3. Unsigned native binaries (.node files) + * 4. Other problematic files + */ + +import fs from 'fs'; +import path from 'path'; +import { execSync } from 'child_process'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.resolve(__dirname, '..'); + +const RELEASE_DIR = path.join(projectRoot, 'release'); +const APP_BUNDLE_PATTERN = /Eigent\.app$/; + +/** + * Find the app bundle in release directory + */ +function findAppBundle() { + if (!fs.existsSync(RELEASE_DIR)) { + console.log('โŒ Release directory does not exist. Please build the app first.'); + console.log(' Run: npm run build:mac'); + return null; + } + + const entries = fs.readdirSync(RELEASE_DIR, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory() && entry.name.match(APP_BUNDLE_PATTERN)) { + return path.join(RELEASE_DIR, entry.name); + } + + // Check subdirectories (e.g., mac-arm64/Eigent.app) + if (entry.isDirectory()) { + const subDir = path.join(RELEASE_DIR, entry.name); + const subEntries = fs.readdirSync(subDir, { withFileTypes: true }); + for (const subEntry of subEntries) { + if (subEntry.isDirectory() && subEntry.name.match(APP_BUNDLE_PATTERN)) { + return path.join(subDir, subEntry.name); + } + } + } + } + + return null; +} + +/** + * Check for .npm-cache directories + */ +function checkNpmCache(bundlePath) { + console.log('\n๐Ÿ” Checking for .npm-cache directories...'); + const issues = []; + + function scanDir(dir) { + if (!fs.existsSync(dir)) { + return; + } + + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.name === '.npm-cache' && entry.isDirectory()) { + issues.push(fullPath); + } else if (entry.isDirectory()) { + // Skip node_modules to avoid deep scanning + if (entry.name !== 'node_modules' && entry.name !== '__pycache__') { + scanDir(fullPath); + } + } + } + } catch (error) { + // Ignore errors + } + } + + const resourcesPath = path.join(bundlePath, 'Contents', 'Resources'); + const prebuiltPath = path.join(resourcesPath, 'prebuilt'); + + if (fs.existsSync(prebuiltPath)) { + scanDir(prebuiltPath); + } + + if (issues.length > 0) { + console.log(`โŒ Found ${issues.length} .npm-cache directory(ies):`); + issues.forEach(issue => console.log(` - ${issue}`)); + return false; + } else { + console.log('โœ… No .npm-cache directories found'); + return true; + } +} + +/** + * Check for flac-mac binary + */ +function checkFlacMac(bundlePath) { + console.log('\n๐Ÿ” Checking for flac-mac binary...'); + const issues = []; + + const resourcesPath = path.join(bundlePath, 'Contents', 'Resources'); + const prebuiltPath = path.join(resourcesPath, 'prebuilt'); + const venvLibPath = path.join(prebuiltPath, 'venv', 'lib'); + + if (fs.existsSync(venvLibPath)) { + try { + const entries = fs.readdirSync(venvLibPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name.startsWith('python')) { + const flacMacPath = path.join(venvLibPath, entry.name, 'site-packages', 'speech_recognition', 'flac-mac'); + if (fs.existsSync(flacMacPath)) { + issues.push(flacMacPath); + } + } + } + } catch (error) { + // Ignore errors + } + } + + if (issues.length > 0) { + console.log(`โŒ Found ${issues.length} flac-mac binary(ies) (outdated SDK):`); + issues.forEach(issue => console.log(` - ${issue}`)); + return false; + } else { + console.log('โœ… No flac-mac binaries found'); + return true; + } +} + +/** + * Check for unsigned native binaries + */ +function checkUnsignedBinaries(bundlePath) { + console.log('\n๐Ÿ” Checking for unsigned native binaries (.node files)...'); + const issues = []; + + function scanForNodeFiles(dir) { + if (!fs.existsSync(dir)) { + return; + } + + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isFile() && entry.name.endsWith('.node')) { + // Check if file is signed + try { + const output = execSync(`codesign -dv "${fullPath}" 2>&1 || true`, { encoding: 'utf-8' }); + if (output.includes('code object is not signed')) { + issues.push({ + path: fullPath, + reason: 'Not signed' + }); + } + } catch (error) { + // If codesign fails, assume it's not signed + issues.push({ + path: fullPath, + reason: 'Could not verify signature' + }); + } + } else if (entry.isDirectory()) { + // Skip certain directories + if (entry.name !== 'node_modules' && entry.name !== '__pycache__' && !entry.name.startsWith('.')) { + scanForNodeFiles(fullPath); + } + } + } + } catch (error) { + // Ignore errors + } + } + + const resourcesPath = path.join(bundlePath, 'Contents', 'Resources'); + const prebuiltPath = path.join(resourcesPath, 'prebuilt'); + + if (fs.existsSync(prebuiltPath)) { + scanForNodeFiles(prebuiltPath); + } + + if (issues.length > 0) { + console.log(`โŒ Found ${issues.length} unsigned .node file(s):`); + issues.forEach(issue => { + console.log(` - ${issue.path}`); + console.log(` Reason: ${issue.reason}`); + }); + return false; + } else { + console.log('โœ… No unsigned .node files found'); + return true; + } +} + +/** + * Check app bundle size + */ +function checkBundleSize(bundlePath) { + console.log('\n๐Ÿ” Checking app bundle size...'); + + try { + function getDirSize(dir) { + let size = 0; + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isFile()) { + size += fs.statSync(fullPath).size; + } else if (entry.isDirectory()) { + size += getDirSize(fullPath); + } + } + } catch (error) { + // Ignore errors + } + return size; + } + + const size = getDirSize(bundlePath); + const sizeInMB = (size / (1024 * 1024)).toFixed(2); + console.log(` App bundle size: ${sizeInMB} MB`); + + if (size > 500 * 1024 * 1024) { + console.log(` โš ๏ธ Large bundle size (>500MB) may cause slow notarization (30-60 minutes)`); + } else if (size > 200 * 1024 * 1024) { + console.log(` โš ๏ธ Medium bundle size (200-500MB) may take 15-30 minutes to notarize`); + } else { + console.log(` โœ… Bundle size is reasonable for notarization`); + } + + return true; + } catch (error) { + console.log(` โš ๏ธ Could not calculate bundle size: ${error.message}`); + return true; + } +} + +/** + * Main function + */ +function main() { + console.log('๐Ÿงช macOS Notarization Test Script\n'); + + const appBundle = findAppBundle(); + + if (!appBundle) { + console.log('\n๐Ÿ’ก To build the app for testing:'); + console.log(' npm run build:mac'); + process.exit(1); + } + + console.log(`๐Ÿ“ฆ Found app bundle: ${appBundle}\n`); + + const results = { + npmCache: checkNpmCache(appBundle), + flacMac: checkFlacMac(appBundle), + unsignedBinaries: checkUnsignedBinaries(appBundle), + bundleSize: checkBundleSize(appBundle), + }; + + console.log('\n๐Ÿ“Š Summary:'); + console.log(` .npm-cache directories: ${results.npmCache ? 'โœ…' : 'โŒ'}`); + console.log(` flac-mac binaries: ${results.flacMac ? 'โœ…' : 'โŒ'}`); + console.log(` Unsigned .node files: ${results.unsignedBinaries ? 'โœ…' : 'โŒ'}`); + console.log(` Bundle size: ${results.bundleSize ? 'โœ…' : 'โš ๏ธ'}`); + + const allPassed = Object.values(results).every(r => r); + + if (allPassed) { + console.log('\nโœ… All checks passed! The app should be ready for notarization.'); + console.log('\n๐Ÿ’ก Note: This script only checks for common issues.'); + console.log(' Actual notarization may still fail for other reasons.'); + console.log(' To test actual notarization, you need:'); + console.log(' - Valid Apple Developer ID certificate'); + console.log(' - APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID environment variables'); + } else { + console.log('\nโŒ Some checks failed. Please fix the issues above before notarization.'); + process.exit(1); + } +} + +main();