feat(build): add comprehensive Claude Code executable build system
Add complete build infrastructure for creating single-file Claude Code executables across all supported platforms using Bun's native compilation features. - **Cross-platform builds**: Support for Linux (glibc/musl), macOS (Intel/ARM), Windows - **CPU optimization variants**: Modern (AVX2+) and baseline (pre-2013) CPU support - **Embedded assets**: All executables include yoga.wasm and ripgrep binaries - **Optimized builds**: Minification and sourcemaps for production-ready binaries
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -12,6 +12,9 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Tauri binaries (built executables)
|
||||
src-tauri/binaries/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
@@ -27,4 +30,4 @@ temp_lib/
|
||||
.cursor/
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
*_TASK.md
|
||||
*_TASK.md
|
||||
|
@@ -6,6 +6,11 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build:executables": "bun run scripts/fetch-and-build.js",
|
||||
"build:executables:current": "bun run scripts/fetch-and-build.js current",
|
||||
"build:executables:linux": "bun run scripts/fetch-and-build.js linux",
|
||||
"build:executables:macos": "bun run scripts/fetch-and-build.js macos",
|
||||
"build:executables:windows": "bun run scripts/fetch-and-build.js windows",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
|
84
scripts/README.md
Normal file
84
scripts/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Build Scripts
|
||||
|
||||
This directory contains scripts for building Claude Code executables for all supported platforms.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `fetch-and-build.js`
|
||||
Main build script that:
|
||||
1. Downloads the `@anthropic-ai/claude-code` package from npm
|
||||
2. Extracts and copies required files (cli.js, yoga.wasm, vendor/)
|
||||
3. Builds executables for specified platforms
|
||||
4. Cleans up temporary files
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Build for all platforms
|
||||
bun run scripts/fetch-and-build.js
|
||||
|
||||
# Build for specific platform
|
||||
bun run scripts/fetch-and-build.js linux
|
||||
bun run scripts/fetch-and-build.js macos
|
||||
bun run scripts/fetch-and-build.js windows
|
||||
bun run scripts/fetch-and-build.js current # Current platform only
|
||||
```
|
||||
|
||||
### `build-executables.js`
|
||||
Low-level script that builds executables from existing source files. This is called automatically by `fetch-and-build.js`.
|
||||
|
||||
### `prepare-bundle-native.js`
|
||||
Prepares the CLI source for bundling by embedding assets using Bun's native embedding features.
|
||||
|
||||
## NPM Scripts
|
||||
|
||||
The following npm scripts are available in `package.json`:
|
||||
|
||||
```bash
|
||||
# Build executables for all platforms
|
||||
npm run build:executables
|
||||
|
||||
# Build for specific platforms
|
||||
npm run build:executables:current
|
||||
npm run build:executables:linux
|
||||
npm run build:executables:macos
|
||||
npm run build:executables:windows
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
All executables are created in the `src-tauri/binaries/` directory with the following naming convention:
|
||||
|
||||
### Linux Executables
|
||||
- `claude-code-linux-x64` - Standard Linux x64 (glibc)
|
||||
- `claude-code-linux-x64-modern` - Modern CPUs (AVX2+)
|
||||
- `claude-code-linux-x64-baseline` - Older CPUs (pre-2013)
|
||||
- `claude-code-linux-arm64` - ARM64 Linux
|
||||
- `claude-code-linux-x64-musl` - Alpine Linux (musl)
|
||||
- `claude-code-linux-x64-musl-modern` - Alpine + modern CPUs
|
||||
- `claude-code-linux-x64-musl-baseline` - Alpine + older CPUs
|
||||
- `claude-code-linux-arm64-musl` - ARM64 Alpine
|
||||
|
||||
### macOS Executables
|
||||
- `claude-code-macos-x64` - Intel Mac
|
||||
- `claude-code-macos-x64-modern` - Intel Mac (modern CPUs)
|
||||
- `claude-code-macos-x64-baseline` - Intel Mac (older CPUs)
|
||||
- `claude-code-macos-arm64` - Apple Silicon Mac
|
||||
|
||||
### Windows Executables
|
||||
- `claude-code-windows-x64.exe` - Windows x64
|
||||
- `claude-code-windows-x64-modern.exe` - Windows x64 (modern CPUs)
|
||||
- `claude-code-windows-x64-baseline.exe` - Windows x64 (older CPUs)
|
||||
|
||||
## Features
|
||||
|
||||
- **Embedded Assets**: All executables include embedded yoga.wasm and ripgrep binaries
|
||||
- **Optimizations**: Built with minification and sourcemaps
|
||||
- **Cross-platform**: Supports all major operating systems and architectures
|
||||
- **CPU Variants**: Modern variants for newer CPUs (2013+), baseline for compatibility
|
||||
- **Self-contained**: No external dependencies required at runtime
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Bun**: Required for building (uses Bun's native compilation features)
|
||||
- **npm**: Used to download the Claude Code package
|
||||
- **tar**: For extracting the package (standard on Unix systems)
|
205
scripts/build-executables.js
Executable file
205
scripts/build-executables.js
Executable file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Build script for creating single-file executables from Claude Code package
|
||||
* Uses Bun's native embedding features with all optimizations enabled
|
||||
*
|
||||
* Usage:
|
||||
* bun run build-executables.js # Build all platforms
|
||||
* bun run build-executables.js linux # Build Linux executables only
|
||||
* bun run build-executables.js macos # Build macOS executables only
|
||||
* bun run build-executables.js windows # Build Windows executables only
|
||||
* bun run build-executables.js current # Build for current platform only
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { mkdir, rm } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// All supported targets with proper output names
|
||||
const PLATFORMS = {
|
||||
linux: [
|
||||
// Linux x64 - glibc
|
||||
{ target: 'bun-linux-x64', output: 'claude-code-linux-x64' },
|
||||
{ target: 'bun-linux-x64-modern', output: 'claude-code-linux-x64-modern' },
|
||||
{ target: 'bun-linux-x64-baseline', output: 'claude-code-linux-x64-baseline' },
|
||||
|
||||
// Linux ARM64 - glibc
|
||||
{ target: 'bun-linux-arm64', output: 'claude-code-linux-arm64' },
|
||||
|
||||
// Linux x64 - musl (Alpine Linux, etc.)
|
||||
{ target: 'bun-linux-x64-musl', output: 'claude-code-linux-x64-musl' },
|
||||
{ target: 'bun-linux-x64-musl-modern', output: 'claude-code-linux-x64-musl-modern' },
|
||||
{ target: 'bun-linux-x64-musl-baseline', output: 'claude-code-linux-x64-musl-baseline' },
|
||||
|
||||
// Linux ARM64 - musl
|
||||
{ target: 'bun-linux-arm64-musl', output: 'claude-code-linux-arm64-musl' }
|
||||
],
|
||||
macos: [
|
||||
// macOS x64
|
||||
{ target: 'bun-darwin-x64', output: 'claude-code-macos-x64' },
|
||||
{ target: 'bun-darwin-x64-modern', output: 'claude-code-macos-x64-modern' },
|
||||
{ target: 'bun-darwin-x64-baseline', output: 'claude-code-macos-x64-baseline' },
|
||||
|
||||
// macOS ARM64 (Apple Silicon)
|
||||
{ target: 'bun-darwin-arm64', output: 'claude-code-macos-arm64' }
|
||||
],
|
||||
windows: [
|
||||
// Windows x64
|
||||
{ target: 'bun-windows-x64', output: 'claude-code-windows-x64.exe' },
|
||||
{ target: 'bun-windows-x64-modern', output: 'claude-code-windows-x64-modern.exe' },
|
||||
{ target: 'bun-windows-x64-baseline', output: 'claude-code-windows-x64-baseline.exe' }
|
||||
]
|
||||
};
|
||||
|
||||
async function runCommand(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Running: ${command} ${args.join(' ')}`);
|
||||
const child = spawn(command, args, { stdio: 'inherit' });
|
||||
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareBundle() {
|
||||
console.log('\nPreparing bundle with native Bun embedding...');
|
||||
await runCommand('bun', ['run', 'scripts/prepare-bundle-native.js']);
|
||||
}
|
||||
|
||||
async function buildExecutable(target, output) {
|
||||
console.log(`\nBuilding ${output}...`);
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await runCommand('bun', [
|
||||
'build',
|
||||
'--compile',
|
||||
'--minify', // Optimize size
|
||||
'--sourcemap', // Embed sourcemap for debugging
|
||||
// '--bytecode', // Commented out - experimental feature that often fails
|
||||
`--target=${target}`,
|
||||
'./cli-native-bundled.js',
|
||||
`--outfile=src-tauri/binaries/${output}`
|
||||
]);
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log(`✓ Built ${output} in ${elapsed}s`);
|
||||
} catch (error) {
|
||||
// If compilation fails, throw the error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupBundledFile() {
|
||||
const filesToClean = ['./cli-bundled.js', './cli-native-bundled.js'];
|
||||
for (const file of filesToClean) {
|
||||
if (existsSync(file)) {
|
||||
await rm(file);
|
||||
}
|
||||
}
|
||||
console.log('\n✓ Cleaned up temporary files');
|
||||
}
|
||||
|
||||
async function getCurrentPlatform() {
|
||||
const arch = process.arch === 'x64' ? 'x64' : 'arm64';
|
||||
const platform = process.platform === 'darwin' ? 'darwin' :
|
||||
process.platform === 'linux' ? 'linux' :
|
||||
process.platform === 'win32' ? 'windows' : null;
|
||||
|
||||
if (!platform) {
|
||||
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||
}
|
||||
|
||||
return `bun-${platform}-${arch}`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const arg = process.argv[2];
|
||||
|
||||
// Create src-tauri/binaries directory if it doesn't exist
|
||||
if (!existsSync('src-tauri/binaries')) {
|
||||
await mkdir('src-tauri/binaries', { recursive: true });
|
||||
}
|
||||
|
||||
let platformsToBuild = [];
|
||||
|
||||
if (!arg || arg === 'all') {
|
||||
// Build all platforms
|
||||
platformsToBuild = [
|
||||
...PLATFORMS.linux,
|
||||
...PLATFORMS.macos,
|
||||
...PLATFORMS.windows
|
||||
];
|
||||
} else if (arg === 'linux') {
|
||||
platformsToBuild = PLATFORMS.linux;
|
||||
} else if (arg === 'macos' || arg === 'darwin') {
|
||||
platformsToBuild = PLATFORMS.macos;
|
||||
} else if (arg === 'windows' || arg === 'win32') {
|
||||
platformsToBuild = PLATFORMS.windows;
|
||||
} else if (arg === 'current') {
|
||||
// Build only for current platform
|
||||
const currentTarget = await getCurrentPlatform();
|
||||
const allPlatforms = [
|
||||
...PLATFORMS.linux,
|
||||
...PLATFORMS.macos,
|
||||
...PLATFORMS.windows
|
||||
];
|
||||
const current = allPlatforms.find(p => p.target === currentTarget);
|
||||
if (current) {
|
||||
platformsToBuild = [current];
|
||||
} else {
|
||||
console.error(`Current platform ${currentTarget} not found in build targets`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
console.error('Usage: bun run build-executables.js [all|linux|macos|windows|current]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Building ${platformsToBuild.length} executable(s) with full optimizations...`);
|
||||
console.log('Optimizations enabled: --minify --sourcemap');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Prepare the bundle once with native embedding
|
||||
await prepareBundle();
|
||||
|
||||
// Build executables sequentially to avoid resource conflicts
|
||||
let successCount = 0;
|
||||
for (const platform of platformsToBuild) {
|
||||
try {
|
||||
await buildExecutable(platform.target, platform.output);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to build ${platform.output}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log(`\n✅ Successfully built ${successCount}/${platformsToBuild.length} executables in ${totalElapsed}s`);
|
||||
console.log('\nExecutables are available in the src-tauri/binaries/ directory');
|
||||
console.log('\nNotes:');
|
||||
console.log('- All executables include embedded assets (yoga.wasm, ripgrep binaries)');
|
||||
console.log('- Modern variants require CPUs from 2013+ (AVX2 support)');
|
||||
console.log('- Baseline variants support older CPUs (pre-2013)');
|
||||
console.log('- Musl variants are for Alpine Linux and similar distributions');
|
||||
console.log('- All executables are optimized with minification and sourcemaps');
|
||||
} finally {
|
||||
// Clean up temporary files
|
||||
await cleanupBundledFile();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Build failed:', error);
|
||||
process.exit(1);
|
||||
});
|
285
scripts/fetch-and-build.js
Executable file
285
scripts/fetch-and-build.js
Executable file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Fetch Claude Code package from npm and build executables for all platforms
|
||||
*
|
||||
* This script:
|
||||
* 1. Downloads the @anthropic-ai/claude-code package from npm
|
||||
* 2. Extracts it to a temporary directory
|
||||
* 3. Runs the build-executables script to create binaries for all platforms
|
||||
* 4. Cleans up temporary files
|
||||
*
|
||||
* Usage:
|
||||
* bun run fetch-and-build.js [platform]
|
||||
*
|
||||
* Where platform can be: all, linux, macos, windows, current
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { mkdir, rm, readdir, copyFile, access } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
/**
|
||||
* Execute a shell command and return a promise
|
||||
* @param {string} command - The command to execute
|
||||
* @param {string[]} args - Command arguments
|
||||
* @param {object} options - Spawn options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function runCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Running: ${command} ${args.join(' ')}`);
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
...options
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`Failed to execute command: ${error.message}`);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file or directory exists
|
||||
* @param {string} path - Path to check
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function pathExists(path) {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and extract the Claude Code package from npm
|
||||
* @returns {Promise<string>} - Path to the extracted package directory
|
||||
*/
|
||||
async function fetchClaudeCodePackage() {
|
||||
console.log('\n📦 Fetching @anthropic-ai/claude-code package from npm...');
|
||||
|
||||
const tempDir = resolve('./temp-claude-package');
|
||||
const packageDir = join(tempDir, 'package');
|
||||
|
||||
try {
|
||||
// Clean up any existing temp directory
|
||||
if (await pathExists(tempDir)) {
|
||||
console.log('Cleaning up existing temp directory...');
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Create temp directory
|
||||
await mkdir(tempDir, { recursive: true });
|
||||
|
||||
// Download the package tarball
|
||||
console.log('Downloading package tarball...');
|
||||
await runCommand('npm', ['pack', '@anthropic-ai/claude-code'], {
|
||||
cwd: tempDir
|
||||
});
|
||||
|
||||
// Find the downloaded tarball
|
||||
const files = await readdir(tempDir);
|
||||
const tarball = files.find(file => file.startsWith('anthropic-ai-claude-code-') && file.endsWith('.tgz'));
|
||||
|
||||
if (!tarball) {
|
||||
throw new Error('Failed to find downloaded tarball');
|
||||
}
|
||||
|
||||
console.log(`Found tarball: ${tarball}`);
|
||||
|
||||
// Extract the tarball
|
||||
console.log('Extracting package...');
|
||||
await runCommand('tar', ['-xzf', tarball], {
|
||||
cwd: tempDir
|
||||
});
|
||||
|
||||
// Verify extraction
|
||||
if (!(await pathExists(packageDir))) {
|
||||
throw new Error('Package extraction failed - package directory not found');
|
||||
}
|
||||
|
||||
console.log(`✓ Package extracted to: ${packageDir}`);
|
||||
return packageDir;
|
||||
|
||||
} catch (error) {
|
||||
// Clean up on error
|
||||
if (await pathExists(tempDir)) {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy required files from the Claude Code package to current directory
|
||||
* @param {string} packageDir - Path to the extracted package directory
|
||||
*/
|
||||
async function copyRequiredFiles(packageDir) {
|
||||
console.log('\n📋 Copying required files from Claude Code package...');
|
||||
|
||||
const filesToCopy = [
|
||||
'cli.js',
|
||||
'yoga.wasm'
|
||||
];
|
||||
|
||||
const directoriesToCopy = [
|
||||
'vendor'
|
||||
];
|
||||
|
||||
// Copy individual files
|
||||
for (const file of filesToCopy) {
|
||||
const srcPath = join(packageDir, file);
|
||||
const destPath = resolve(file);
|
||||
|
||||
if (await pathExists(srcPath)) {
|
||||
console.log(`Copying ${file}...`);
|
||||
await copyFile(srcPath, destPath);
|
||||
} else {
|
||||
console.warn(`Warning: ${file} not found in package`);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy directories recursively
|
||||
for (const dir of directoriesToCopy) {
|
||||
const srcPath = join(packageDir, dir);
|
||||
const destPath = resolve(dir);
|
||||
|
||||
if (await pathExists(srcPath)) {
|
||||
console.log(`Copying ${dir}/ directory...`);
|
||||
|
||||
// Remove existing directory if it exists
|
||||
if (await pathExists(destPath)) {
|
||||
await rm(destPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Copy directory recursively using cp command
|
||||
await runCommand('cp', ['-r', srcPath, destPath]);
|
||||
} else {
|
||||
console.warn(`Warning: ${dir}/ directory not found in package`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ Required files copied successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary files and directories
|
||||
* @param {string} packageDir - Path to the package directory to clean up
|
||||
*/
|
||||
async function cleanup(packageDir) {
|
||||
console.log('\n🧹 Cleaning up temporary files...');
|
||||
|
||||
const tempDir = resolve('./temp-claude-package');
|
||||
|
||||
try {
|
||||
if (await pathExists(tempDir)) {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
console.log('✓ Temporary package directory cleaned up');
|
||||
}
|
||||
|
||||
// Clean up copied files that are no longer needed
|
||||
const filesToCleanup = [
|
||||
'./cli.js',
|
||||
'./cli-bundled.js',
|
||||
'./cli-native-bundled.js',
|
||||
'./yoga.wasm'
|
||||
];
|
||||
|
||||
for (const file of filesToCleanup) {
|
||||
if (await pathExists(file)) {
|
||||
await rm(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up vendor directory
|
||||
const vendorDir = './vendor';
|
||||
if (await pathExists(vendorDir)) {
|
||||
await rm(vendorDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log('✓ Cleanup completed');
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Cleanup failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build executables for the specified platform(s)
|
||||
* @param {string} platform - Platform to build for (all, linux, macos, windows, current)
|
||||
*/
|
||||
async function buildExecutables(platform = 'all') {
|
||||
console.log(`\n🔨 Building executables for platform: ${platform}`);
|
||||
|
||||
// Ensure src-tauri/binaries directory exists
|
||||
if (!await pathExists('./src-tauri/binaries')) {
|
||||
await mkdir('./src-tauri/binaries', { recursive: true });
|
||||
}
|
||||
|
||||
// Run the build-executables script
|
||||
const args = platform === 'all' ? [] : [platform];
|
||||
await runCommand('bun', ['run', './scripts/build-executables.js', ...args]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution function
|
||||
*/
|
||||
async function main() {
|
||||
const platform = process.argv[2] || 'all';
|
||||
const validPlatforms = ['all', 'linux', 'macos', 'darwin', 'windows', 'win32', 'current'];
|
||||
|
||||
if (!validPlatforms.includes(platform)) {
|
||||
console.error(`Invalid platform: ${platform}`);
|
||||
console.error(`Valid platforms: ${validPlatforms.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🚀 Starting Claude Code fetch and build process...');
|
||||
console.log(`Target platform: ${platform}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
let packageDir;
|
||||
|
||||
try {
|
||||
// Step 1: Fetch and extract the package
|
||||
packageDir = await fetchClaudeCodePackage();
|
||||
|
||||
// Step 2: Copy required files
|
||||
await copyRequiredFiles(packageDir);
|
||||
|
||||
// Step 3: Build executables
|
||||
await buildExecutables(platform);
|
||||
|
||||
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log(`\n✅ Build process completed successfully in ${totalTime}s`);
|
||||
console.log('\n📁 Executables are available in the src-tauri/binaries/ directory');
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n❌ Build process failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Always clean up, even if there was an error
|
||||
if (packageDir) {
|
||||
await cleanup(packageDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch(error => {
|
||||
console.error('Unexpected error:', error);
|
||||
process.exit(1);
|
||||
});
|
145
scripts/prepare-bundle-native.js
Normal file
145
scripts/prepare-bundle-native.js
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Prepare the CLI for bundling using Bun's native embedding features
|
||||
* This modifies the source to use embedded files directly
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// Read the original CLI file
|
||||
const cliPath = './cli.js';
|
||||
let cliContent = readFileSync(cliPath, 'utf-8');
|
||||
|
||||
console.log('Preparing CLI for native Bun embedding...');
|
||||
|
||||
// 1. Build list of embedded imports based on what files actually exist
|
||||
const embeddedImports = [];
|
||||
const embeddedFilesMapping = [];
|
||||
|
||||
// Define all possible ripgrep files
|
||||
const ripgrepFiles = [
|
||||
{ path: './vendor/ripgrep/arm64-darwin/rg', var: '__embeddedRgDarwinArm64' },
|
||||
{ path: './vendor/ripgrep/arm64-darwin/ripgrep.node', var: '__embeddedRgNodeDarwinArm64' },
|
||||
{ path: './vendor/ripgrep/arm64-linux/rg', var: '__embeddedRgLinuxArm64' },
|
||||
{ path: './vendor/ripgrep/arm64-linux/ripgrep.node', var: '__embeddedRgNodeLinuxArm64' },
|
||||
{ path: './vendor/ripgrep/x64-darwin/rg', var: '__embeddedRgDarwinX64' },
|
||||
{ path: './vendor/ripgrep/x64-darwin/ripgrep.node', var: '__embeddedRgNodeDarwinX64' },
|
||||
{ path: './vendor/ripgrep/x64-linux/rg', var: '__embeddedRgLinuxX64' },
|
||||
{ path: './vendor/ripgrep/x64-linux/ripgrep.node', var: '__embeddedRgNodeLinuxX64' },
|
||||
{ path: './vendor/ripgrep/x64-win32/rg.exe', var: '__embeddedRgWin32' },
|
||||
{ path: './vendor/ripgrep/x64-win32/ripgrep.node', var: '__embeddedRgNodeWin32' },
|
||||
];
|
||||
|
||||
// Always include yoga.wasm
|
||||
if (existsSync('./yoga.wasm')) {
|
||||
embeddedImports.push('import __embeddedYogaWasm from "./yoga.wasm" with { type: "file" };');
|
||||
embeddedFilesMapping.push(" 'yoga.wasm': __embeddedYogaWasm,");
|
||||
} else {
|
||||
console.error('Warning: yoga.wasm not found');
|
||||
}
|
||||
|
||||
// Only import ripgrep files that exist
|
||||
for (const file of ripgrepFiles) {
|
||||
if (existsSync(file.path)) {
|
||||
embeddedImports.push(`import ${file.var} from "${file.path}" with { type: "file" };`);
|
||||
const key = file.path.replace('./', '');
|
||||
embeddedFilesMapping.push(` '${key}': ${file.var},`);
|
||||
}
|
||||
}
|
||||
|
||||
const embeddedCode = `
|
||||
// Embedded files using Bun's native embedding
|
||||
${embeddedImports.join('\n')}
|
||||
|
||||
const __embeddedFiles = {
|
||||
${embeddedFilesMapping.join('\n')}
|
||||
};
|
||||
|
||||
`;
|
||||
|
||||
// Add imports after the shebang
|
||||
const shebangMatch = cliContent.match(/^#!.*\n/);
|
||||
if (shebangMatch) {
|
||||
cliContent = shebangMatch[0] + embeddedCode + cliContent.substring(shebangMatch[0].length);
|
||||
} else {
|
||||
cliContent = embeddedCode + cliContent;
|
||||
}
|
||||
|
||||
// 2. Replace yoga.wasm loading - handle top-level await properly
|
||||
// Original: var k81=await nUA(await VP9(CP9(import.meta.url).resolve("./yoga.wasm")));
|
||||
// Since this uses top-level await, we need to preserve that structure
|
||||
const yogaLoadPattern = /var k81=await nUA\(await VP9\(CP9\(import\.meta\.url\)\.resolve\("\.\/yoga\.wasm"\)\)\);/;
|
||||
// Use an IIFE to handle the async loading
|
||||
const yogaLoadReplacement = `var k81=await(async()=>{return await nUA(await Bun.file(__embeddedYogaWasm).arrayBuffer())})();`;
|
||||
|
||||
if (yogaLoadPattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(yogaLoadPattern, yogaLoadReplacement);
|
||||
console.log('✓ Replaced yoga.wasm loading with embedded version');
|
||||
} else {
|
||||
console.error('Warning: Could not find yoga.wasm loading pattern');
|
||||
// Try a more general pattern
|
||||
const generalYogaPattern = /var\s+(\w+)\s*=\s*await\s+nUA\s*\(\s*await\s+VP9\s*\([^)]+\.resolve\s*\(\s*["']\.\/yoga\.wasm["']\s*\)\s*\)\s*\)/;
|
||||
if (generalYogaPattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(generalYogaPattern, (match, varName) => {
|
||||
return `var ${varName}=await(async()=>{return await nUA(await Bun.file(__embeddedYogaWasm).arrayBuffer())})()`;
|
||||
});
|
||||
console.log('✓ Replaced yoga.wasm loading with embedded version (general pattern)');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Replace ripgrep path resolution
|
||||
// Add check for embedded files in the ripgrep resolver
|
||||
const ripgrepPattern = /let B=Db\.resolve\(et9,"vendor","ripgrep"\);/;
|
||||
const ripgrepReplacement = `
|
||||
if(process.env.CLAUDE_CODE_BUNDLED || typeof __embeddedFiles !== 'undefined'){
|
||||
const platform = process.platform === "win32" ? "x64-win32" : \`\${process.arch}-\${process.platform}\`;
|
||||
const rgKey = \`vendor/ripgrep/\${platform}/rg\${process.platform === "win32" ? ".exe" : ""}\`;
|
||||
if(__embeddedFiles[rgKey]) return __embeddedFiles[rgKey];
|
||||
}
|
||||
let B=Db.resolve(et9,"vendor","ripgrep");`;
|
||||
|
||||
if (ripgrepPattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(ripgrepPattern, ripgrepReplacement);
|
||||
console.log('✓ Added embedded file handling for ripgrep');
|
||||
}
|
||||
|
||||
// 4. Replace ripgrep.node loading - handle the entire if-else structure
|
||||
// Look for the complete if-else pattern where B is assigned
|
||||
const ripgrepNodePattern = /if\(typeof Bun!=="undefined"&&Bun\.embeddedFiles\?\.length>0\)B="\.\/ripgrep\.node";else/;
|
||||
const ripgrepNodeReplacement = `if(typeof Bun!=="undefined"&&Bun.embeddedFiles?.length>0)B=(()=>{
|
||||
const platform = process.platform === "win32" ? "x64-win32" : \`\${process.arch}-\${process.platform}\`;
|
||||
const nodeKey = \`vendor/ripgrep/\${platform}/ripgrep.node\`;
|
||||
return __embeddedFiles[nodeKey] || "./ripgrep.node";
|
||||
})();else`;
|
||||
|
||||
if (ripgrepNodePattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(ripgrepNodePattern, ripgrepNodeReplacement);
|
||||
console.log('✓ Added embedded file handling for ripgrep.node');
|
||||
} else {
|
||||
// Fallback to simpler pattern if the exact pattern doesn't match
|
||||
const simplePattern = /B="\.\/ripgrep\.node"/;
|
||||
if (simplePattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(simplePattern, `B=(()=>{
|
||||
const platform = process.platform === "win32" ? "x64-win32" : \`\${process.arch}-\${process.platform}\`;
|
||||
const nodeKey = \`vendor/ripgrep/\${platform}/ripgrep.node\`;
|
||||
return __embeddedFiles[nodeKey] || "./ripgrep.node";
|
||||
})()`);
|
||||
console.log('✓ Added embedded file handling for ripgrep.node (fallback pattern)');
|
||||
}
|
||||
}
|
||||
|
||||
// Set bundled mode indicator
|
||||
cliContent = cliContent.replace(
|
||||
/process\.env\.CLAUDE_CODE_ENTRYPOINT="cli"/,
|
||||
'process.env.CLAUDE_CODE_ENTRYPOINT="cli";process.env.CLAUDE_CODE_BUNDLED="1"'
|
||||
);
|
||||
|
||||
// Write the modified content
|
||||
const outputPath = './cli-native-bundled.js';
|
||||
writeFileSync(outputPath, cliContent);
|
||||
|
||||
console.log(`\n✅ Created ${outputPath} ready for bundling with native embedding`);
|
||||
console.log('\nNow you can run:');
|
||||
console.log(` bun build --compile --minify ./cli-native-bundled.js --outfile dist/claude-code`);
|
Reference in New Issue
Block a user