amical/apps/desktop/scripts/download-node-binaries.ts

263 lines
No EOL
7.9 KiB
TypeScript

#!/usr/bin/env tsx
import * as https from 'node:https';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
import { pipeline } from 'node:stream/promises';
import { createWriteStream, mkdirSync, chmodSync } from 'node:fs';
// Node.js version to download
const NODE_VERSION = '22.17.0';
// Platform/arch types
type Platform = 'darwin' | 'win32' | 'linux';
type Architecture = 'arm64' | 'x64';
interface PlatformConfig {
platform: Platform;
arch: Architecture;
url: string;
binary: string;
}
// Platform configurations
const PLATFORMS: PlatformConfig[] = [
{
platform: 'darwin',
arch: 'arm64',
url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
binary: 'bin/node'
},
{
platform: 'darwin',
arch: 'x64',
url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-x64.tar.gz`,
binary: 'bin/node'
},
{
platform: 'win32',
arch: 'x64',
url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-win-x64.zip`,
binary: 'node.exe'
},
{
platform: 'linux',
arch: 'x64',
url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz`,
binary: 'bin/node'
}
];
const RESOURCES_DIR = path.join(__dirname, '..', 'resources', 'node-binaries');
// Parse command line arguments
const args = process.argv.slice(2);
const downloadAll = args.includes('--all');
async function downloadFile(url: string, dest: string): Promise<void> {
return new Promise((resolve, reject) => {
const file = createWriteStream(dest);
https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Handle redirect
const redirectUrl = response.headers.location;
if (!redirectUrl) {
reject(new Error('Redirect without location header'));
return;
}
https.get(redirectUrl, async (redirectResponse) => {
if (redirectResponse.statusCode !== 200) {
reject(new Error(`Failed to download: ${redirectResponse.statusCode}`));
return;
}
// Show download progress
const totalSize = parseInt(redirectResponse.headers['content-length'] || '0', 10);
let downloadedSize = 0;
redirectResponse.on('data', (chunk) => {
downloadedSize += chunk.length;
if (totalSize > 0) {
const percent = Math.round((downloadedSize / totalSize) * 100);
process.stdout.write(`\r Downloading: ${percent}%`);
}
});
await pipeline(redirectResponse, file);
process.stdout.write('\n');
resolve();
}).on('error', reject);
} else if (response.statusCode === 200) {
// Direct download
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
let downloadedSize = 0;
response.on('data', (chunk) => {
downloadedSize += chunk.length;
if (totalSize > 0) {
const percent = Math.round((downloadedSize / totalSize) * 100);
process.stdout.write(`\r Downloading: ${percent}%`);
}
});
pipeline(response, file).then(() => {
process.stdout.write('\n');
resolve();
}).catch(reject);
} else {
reject(new Error(`Failed to download: ${response.statusCode}`));
}
}).on('error', reject);
});
}
async function extractArchive(archivePath: string, platform: Platform): Promise<string> {
const tempDir = path.join(path.dirname(archivePath), 'temp');
mkdirSync(tempDir, { recursive: true });
console.log(' Extracting archive...');
if (platform === 'win32') {
// Use unzip command (available on macOS) to extract zip files
execSync(`unzip -q "${archivePath}" -d "${tempDir}"`, { stdio: 'inherit' });
} else {
// Use tar for Unix-like systems
execSync(`tar -xzf "${archivePath}" -C "${tempDir}"`, { stdio: 'inherit' });
}
return tempDir;
}
async function downloadNodeBinary(config: PlatformConfig): Promise<void> {
const { platform, arch, url, binary } = config;
const platformDir = path.join(RESOURCES_DIR, `${platform}-${arch}`);
const binaryPath = path.join(platformDir, platform === 'win32' ? 'node.exe' : 'node');
// Skip if already exists
if (fs.existsSync(binaryPath)) {
console.log(`${platform}-${arch} binary already exists`);
return;
}
console.log(`\nDownloading Node.js for ${platform}-${arch}...`);
// Create directory
mkdirSync(platformDir, { recursive: true });
// Download archive
const archiveExt = platform === 'win32' ? '.zip' : '.tar.gz';
const archivePath = path.join(platformDir, `node-v${NODE_VERSION}${archiveExt}`);
try {
await downloadFile(url, archivePath);
console.log(' Download complete');
// Extract archive
const tempDir = await extractArchive(archivePath, platform);
// Find the node binary in extracted files
// Windows uses different directory naming convention (win instead of win32)
const extractedDirName = platform === 'win32'
? `node-v${NODE_VERSION}-win-${arch}`
: `node-v${NODE_VERSION}-${platform}-${arch}`;
const extractedBinaryPath = path.join(tempDir, extractedDirName, binary);
// Verify binary exists
if (!fs.existsSync(extractedBinaryPath)) {
throw new Error(`Binary not found at expected path: ${extractedBinaryPath}`);
}
// Copy binary to final location
console.log(' Installing binary...');
fs.copyFileSync(extractedBinaryPath, binaryPath);
// Make executable on Unix-like systems
if (platform !== 'win32') {
chmodSync(binaryPath, '755');
}
// Clean up
fs.rmSync(tempDir, { recursive: true, force: true });
fs.unlinkSync(archivePath);
console.log(`✓ Successfully installed ${platform}-${arch} binary`);
} catch (error) {
console.error(`✗ Failed to download ${platform}-${arch}:`, error instanceof Error ? error.message : error);
// Clean up on failure
if (fs.existsSync(archivePath)) {
fs.unlinkSync(archivePath);
}
throw error;
}
}
function getCurrentPlatform(): PlatformConfig | undefined {
const currentPlatform = process.platform as string;
const currentArch = process.arch as string;
return PLATFORMS.find(p =>
p.platform === currentPlatform &&
p.arch === currentArch
);
}
async function main() {
console.log(`Node.js Binary Downloader v${NODE_VERSION}`);
console.log('=====================================\n');
// Create base directory
mkdirSync(RESOURCES_DIR, { recursive: true });
if (downloadAll) {
console.log('Mode: Download all platforms\n');
// Download binaries for all platforms
let success = 0;
let failed = 0;
for (const platform of PLATFORMS) {
try {
await downloadNodeBinary(platform);
success++;
} catch (error) {
failed++;
}
}
console.log(`\nSummary: ${success} succeeded, ${failed} failed`);
if (failed > 0) {
process.exit(1);
}
} else {
console.log('Mode: Download current platform only\n');
// Download only for current platform
const currentPlatform = getCurrentPlatform();
if (!currentPlatform) {
console.error(`✗ Unsupported platform: ${process.platform}-${process.arch}`);
console.error(' Supported platforms:');
PLATFORMS.forEach(p => {
console.error(` - ${p.platform}-${p.arch}`);
});
process.exit(1);
}
await downloadNodeBinary(currentPlatform);
}
console.log('\nDone! Node.js binaries available at:', RESOURCES_DIR);
}
// Run if called directly
if (require.main === module) {
main().catch((error) => {
console.error('\nFatal error:', error);
process.exit(1);
});
}
// Export for potential programmatic use
export { downloadNodeBinary, PLATFORMS, NODE_VERSION, getCurrentPlatform };