mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-07-09 17:28:35 +00:00
🎨 clean symlinks before sign
This commit is contained in:
parent
64ea7f233f
commit
b5849338ab
5 changed files with 283 additions and 105 deletions
98
config/before-sign.cjs
Normal file
98
config/before-sign.cjs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* After pack hook - clean invalid symlinks after packing, before signing
|
||||
*/
|
||||
exports.default = async function afterPack(context) {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
const appOutDir = context.appOutDir;
|
||||
const appName = context.packager.appInfo.productName;
|
||||
const appPath = path.join(appOutDir, `${appName}.app`);
|
||||
|
||||
if (!fs.existsSync(appPath)) {
|
||||
console.log('App bundle not found, skipping symlink cleanup');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🧹 Cleaning invalid symlinks before signing...');
|
||||
|
||||
const resourcesPath = path.join(appPath, 'Contents', 'Resources');
|
||||
const prebuiltPath = path.join(resourcesPath, 'prebuilt');
|
||||
|
||||
if (!fs.existsSync(prebuiltPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean Python symlinks in venv/bin
|
||||
const venvBinDir = path.join(prebuiltPath, 'venv', 'bin');
|
||||
if (fs.existsSync(venvBinDir)) {
|
||||
const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
|
||||
const bundlePath = path.resolve(appPath);
|
||||
|
||||
for (const pythonName of pythonNames) {
|
||||
const pythonSymlink = path.join(venvBinDir, pythonName);
|
||||
|
||||
if (fs.existsSync(pythonSymlink)) {
|
||||
try {
|
||||
const stats = fs.lstatSync(pythonSymlink);
|
||||
if (stats.isSymbolicLink()) {
|
||||
const target = fs.readlinkSync(pythonSymlink);
|
||||
const resolvedPath = path.resolve(path.dirname(pythonSymlink), target);
|
||||
|
||||
// If symlink points outside bundle, remove it
|
||||
if (!resolvedPath.startsWith(bundlePath)) {
|
||||
console.log(`Removing invalid ${pythonName} symlink: ${target}`);
|
||||
fs.unlinkSync(pythonSymlink);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not process ${pythonName} symlink: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively clean other invalid symlinks
|
||||
function cleanSymlinks(dir, bundleRoot) {
|
||||
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);
|
||||
|
||||
try {
|
||||
if (entry.isSymbolicLink()) {
|
||||
const target = fs.readlinkSync(fullPath);
|
||||
const resolvedPath = path.resolve(path.dirname(fullPath), target);
|
||||
const bundlePath = path.resolve(bundleRoot);
|
||||
|
||||
if (!fs.existsSync(resolvedPath) || !resolvedPath.startsWith(bundlePath)) {
|
||||
console.log(`Removing invalid symlink: ${fullPath} -> ${target}`);
|
||||
fs.unlinkSync(fullPath);
|
||||
}
|
||||
} else if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === '__pycache__') {
|
||||
continue;
|
||||
}
|
||||
cleanSymlinks(fullPath, bundleRoot);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
cleanSymlinks(prebuiltPath, appPath);
|
||||
console.log('✅ Symlink cleanup completed');
|
||||
};
|
||||
|
|
@ -47,6 +47,7 @@
|
|||
}
|
||||
],
|
||||
"icon": "build/icon.ico",
|
||||
"afterPack": "./config/before-sign.cjs",
|
||||
"afterSign": "./config/notarize.cjs",
|
||||
"mac": {
|
||||
"icon": "build/icon.icns",
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@
|
|||
"dev": "npm run clean-cache && vite",
|
||||
"preinstall-deps": "node scripts/preinstall-deps.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 && vite build && electron-builder -- --publish always",
|
||||
"build:mac": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac",
|
||||
"build:mac": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc && vite build && electron-builder --mac",
|
||||
"build:mac:test": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc && vite build && electron-builder --mac && npm run test-signing",
|
||||
"build:win": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --win",
|
||||
"build:all": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac --win",
|
||||
"preview": "vite preview",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,41 @@ function isValidSymlink(symlinkPath, bundleRoot) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix Python symlinks in venv/bin
|
||||
* Remove symlinks that point outside the bundle (to cache directory)
|
||||
*/
|
||||
function fixPythonSymlinks(venvBinDir, bundleRoot) {
|
||||
if (!fs.existsSync(venvBinDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bundlePath = path.resolve(bundleRoot);
|
||||
const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
|
||||
|
||||
for (const pythonName of pythonNames) {
|
||||
const pythonSymlink = path.join(venvBinDir, pythonName);
|
||||
|
||||
if (fs.existsSync(pythonSymlink)) {
|
||||
try {
|
||||
const stats = fs.lstatSync(pythonSymlink);
|
||||
if (stats.isSymbolicLink()) {
|
||||
const target = fs.readlinkSync(pythonSymlink);
|
||||
const resolvedPath = path.resolve(path.dirname(pythonSymlink), target);
|
||||
|
||||
// If symlink points outside bundle (especially to cache), remove it
|
||||
if (!resolvedPath.startsWith(bundlePath)) {
|
||||
console.log(`Removing invalid ${pythonName} symlink pointing to: ${target}`);
|
||||
fs.unlinkSync(pythonSymlink);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not process ${pythonName} symlink: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove invalid symlinks recursively
|
||||
*/
|
||||
|
|
@ -92,6 +127,14 @@ function main() {
|
|||
console.log('🧹 Cleaning invalid symbolic links...');
|
||||
|
||||
const bundleRoot = path.join(projectRoot, 'resources', 'prebuilt');
|
||||
const venvBinDir = path.join(bundleRoot, 'venv', 'bin');
|
||||
|
||||
// First, try to fix Python symlinks specifically
|
||||
if (fs.existsSync(venvBinDir)) {
|
||||
fixPythonSymlinks(venvBinDir, bundleRoot);
|
||||
}
|
||||
|
||||
// Then clean all other invalid symlinks
|
||||
const removed = cleanSymlinks(bundleRoot, bundleRoot);
|
||||
|
||||
if (removed.length > 0) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fs.mkdirSync(BIN_DIR, { recursive: true });
|
|||
fs.mkdirSync(VENV_DIR, { recursive: true });
|
||||
|
||||
/**
|
||||
* 验证下载的文件是否是有效的 ZIP 文件
|
||||
* Validate if the downloaded file is a valid ZIP file
|
||||
*/
|
||||
function isValidZip(filePath) {
|
||||
try {
|
||||
|
|
@ -46,7 +46,7 @@ function isValidZip(filePath) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 验证下载的文件是否是有效的 tar.gz 文件
|
||||
* Validate if the downloaded file is a valid tar.gz file
|
||||
*/
|
||||
function isValidTarGz(filePath) {
|
||||
try {
|
||||
|
|
@ -60,7 +60,7 @@ function isValidTarGz(filePath) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 下载文件并验证完整性
|
||||
* Download file and validate integrity
|
||||
*/
|
||||
async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType = 'file') {
|
||||
const maxRetries = 2;
|
||||
|
|
@ -74,106 +74,93 @@ async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType
|
|||
await new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
const timeout = 180000; // 3 minutes
|
||||
let redirectCount = 0;
|
||||
const maxRedirects = 10;
|
||||
|
||||
const request = protocol.get(url, {
|
||||
timeout: timeout,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
}, (response) => {
|
||||
// 处理重定向
|
||||
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||
const redirectUrl = response.headers.location;
|
||||
console.log(` Following redirect...`);
|
||||
|
||||
const redirectProtocol = redirectUrl.startsWith('https') ? https : http;
|
||||
const redirectRequest = redirectProtocol.get(redirectUrl, {
|
||||
timeout: timeout,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
}, (redirectResponse) => {
|
||||
if (redirectResponse.statusCode === 200) {
|
||||
const file = fs.createWriteStream(dest);
|
||||
let downloadedSize = 0;
|
||||
const totalSize = parseInt(redirectResponse.headers['content-length'] || '0');
|
||||
|
||||
redirectResponse.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
if (totalSize > 0) {
|
||||
const progress = ((downloadedSize / totalSize) * 100).toFixed(1);
|
||||
process.stdout.write(`\r Progress: ${progress}% (${(downloadedSize / 1024 / 1024).toFixed(2)}MB / ${(totalSize / 1024 / 1024).toFixed(2)}MB)`);
|
||||
}
|
||||
});
|
||||
|
||||
redirectResponse.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
console.log(''); // 新行
|
||||
resolve();
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
file.close();
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`HTTP ${redirectResponse.statusCode}`));
|
||||
}
|
||||
});
|
||||
|
||||
redirectRequest.on('error', reject);
|
||||
redirectRequest.on('timeout', () => {
|
||||
redirectRequest.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(dest);
|
||||
let downloadedSize = 0;
|
||||
const totalSize = parseInt(response.headers['content-length'] || '0');
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
if (totalSize > 0) {
|
||||
const progress = ((downloadedSize / totalSize) * 100).toFixed(1);
|
||||
process.stdout.write(`\r Progress: ${progress}% (${(downloadedSize / 1024 / 1024).toFixed(2)}MB / ${(totalSize / 1024 / 1024).toFixed(2)}MB)`);
|
||||
const makeRequest = (requestUrl) => {
|
||||
const requestProtocol = requestUrl.startsWith('https') ? https : http;
|
||||
const request = requestProtocol.get(requestUrl, {
|
||||
timeout: timeout,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
}, (response) => {
|
||||
// Handle redirects (301, 302, 307, 308)
|
||||
if (response.statusCode === 301 || response.statusCode === 302 ||
|
||||
response.statusCode === 307 || response.statusCode === 308) {
|
||||
redirectCount++;
|
||||
if (redirectCount > maxRedirects) {
|
||||
reject(new Error(`Too many redirects (${redirectCount})`));
|
||||
return;
|
||||
}
|
||||
|
||||
const redirectUrl = response.headers.location;
|
||||
if (!redirectUrl) {
|
||||
reject(new Error(`Redirect without location header`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle relative redirects
|
||||
const absoluteRedirectUrl = redirectUrl.startsWith('http')
|
||||
? redirectUrl
|
||||
: new URL(redirectUrl, requestUrl).href;
|
||||
|
||||
console.log(` Following redirect ${redirectCount} to: ${absoluteRedirectUrl}`);
|
||||
|
||||
// Close current response
|
||||
response.destroy();
|
||||
|
||||
// Recursively handle redirects
|
||||
makeRequest(absoluteRedirectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(dest);
|
||||
let downloadedSize = 0;
|
||||
const totalSize = parseInt(response.headers['content-length'] || '0');
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
if (totalSize > 0) {
|
||||
const progress = ((downloadedSize / totalSize) * 100).toFixed(1);
|
||||
process.stdout.write(`\r Progress: ${progress}% (${(downloadedSize / 1024 / 1024).toFixed(2)}MB / ${(totalSize / 1024 / 1024).toFixed(2)}MB)`);
|
||||
}
|
||||
});
|
||||
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
console.log(''); // New line
|
||||
resolve();
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
file.close();
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
console.log(''); // 新行
|
||||
resolve();
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
file.close();
|
||||
request.on('error', (err) => {
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (err) => {
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(err);
|
||||
});
|
||||
request.on('timeout', () => {
|
||||
request.destroy();
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
};
|
||||
|
||||
request.on('timeout', () => {
|
||||
request.destroy();
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
makeRequest(url);
|
||||
});
|
||||
|
||||
// 验证下载的文件
|
||||
// Validate downloaded file
|
||||
if (!fs.existsSync(dest)) {
|
||||
throw new Error('Downloaded file does not exist');
|
||||
}
|
||||
|
|
@ -211,7 +198,7 @@ async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取 Bun 的下载 URL 列表
|
||||
* Get Bun download URL list
|
||||
*/
|
||||
function getBunUrls(platform, arch) {
|
||||
const filename = `bun-${platform}-${arch}.zip`;
|
||||
|
|
@ -222,7 +209,7 @@ function getBunUrls(platform, arch) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取 UV 的下载 URL 列表
|
||||
* Get UV download URL list
|
||||
*/
|
||||
function getUvUrls(archStr, platformStr, isWindows = false) {
|
||||
const extension = isWindows ? '.zip' : '.tar.gz';
|
||||
|
|
@ -438,18 +425,46 @@ async function installBun() {
|
|||
// Try to find bun in system PATH
|
||||
try {
|
||||
const whichCommand = platform === 'win32' ? 'where bun' : 'which bun';
|
||||
const systemBunPath = execSync(whichCommand, { encoding: 'utf-8', stdio: 'pipe' }).trim().split('\n')[0];
|
||||
if (systemBunPath && fs.existsSync(systemBunPath)) {
|
||||
console.log(`📋 Using system bun: ${systemBunPath}`);
|
||||
fs.copyFileSync(systemBunPath, bunPath);
|
||||
if (platform !== 'win32') {
|
||||
fs.chmodSync(bunPath, '755');
|
||||
const output = execSync(whichCommand, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
||||
const paths = output.split(/[\r\n]+/).map(p => p.trim()).filter(p => p && !p.includes('INFO:'));
|
||||
|
||||
for (const systemBunPath of paths) {
|
||||
if (systemBunPath && fs.existsSync(systemBunPath)) {
|
||||
console.log(`📋 Using system bun: ${systemBunPath}`);
|
||||
fs.copyFileSync(systemBunPath, bunPath);
|
||||
if (platform !== 'win32') {
|
||||
fs.chmodSync(bunPath, '755');
|
||||
}
|
||||
return bunPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Also try common Windows paths (npm global install locations)
|
||||
if (platform === 'win32') {
|
||||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
||||
const commonPaths = [
|
||||
path.join(npmPrefix, 'bun.exe'),
|
||||
path.join(npmPrefix, 'bun.cmd'),
|
||||
path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'bun.exe'),
|
||||
path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'bun.cmd'),
|
||||
path.join(os.homedir(), 'AppData', 'Local', 'npm', 'bun.exe'),
|
||||
path.join(os.homedir(), '.bun', 'bin', 'bun.exe'),
|
||||
'C:\\Program Files\\nodejs\\bun.exe',
|
||||
'C:\\Program Files\\bun\\bun.exe',
|
||||
'C:\\bun\\bun.exe',
|
||||
];
|
||||
|
||||
for (const commonPath of commonPaths) {
|
||||
if (fs.existsSync(commonPath)) {
|
||||
console.log(`📋 Using bun from common path: ${commonPath}`);
|
||||
fs.copyFileSync(commonPath, bunPath);
|
||||
return bunPath;
|
||||
}
|
||||
}
|
||||
return bunPath;
|
||||
}
|
||||
} catch (error) {
|
||||
// bun not found in PATH, continue to download
|
||||
console.log(' bun not found in system PATH, will download...');
|
||||
console.log(` bun not found in system PATH (${error.message}), will download...`);
|
||||
}
|
||||
|
||||
// Determine platform and architecture
|
||||
|
|
@ -472,8 +487,27 @@ async function installBun() {
|
|||
|
||||
console.log(` Platform: ${bunPlatform}-${bunArch}`);
|
||||
|
||||
const urlsToTry = getBunUrls(bunPlatform, bunArch);
|
||||
await downloadFileWithValidation(urlsToTry, tempFilename, isValidZip, 'ZIP');
|
||||
// Try using curl first (more reliable for redirects)
|
||||
try {
|
||||
const urlsToTry = getBunUrls(bunPlatform, bunArch);
|
||||
const url = urlsToTry[0].url;
|
||||
|
||||
console.log(` Trying to download with curl: ${url}`);
|
||||
execSync(`curl -L -o "${tempFilename}" "${url}"`, { stdio: 'inherit' });
|
||||
|
||||
if (fs.existsSync(tempFilename) && isValidZip(tempFilename)) {
|
||||
console.log(' ✅ Downloaded successfully with curl');
|
||||
} else {
|
||||
throw new Error('Downloaded file is invalid');
|
||||
}
|
||||
} catch (curlError) {
|
||||
console.log(` ⚠️ curl download failed: ${curlError.message}`);
|
||||
console.log(' Falling back to manual download...');
|
||||
|
||||
// Fallback to manual download
|
||||
const urlsToTry = getBunUrls(bunPlatform, bunArch);
|
||||
await downloadFileWithValidation(urlsToTry, tempFilename, isValidZip, 'ZIP');
|
||||
}
|
||||
|
||||
// Extract
|
||||
console.log(' Extracting...');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue