import * as path from 'path'; import * as plugins from '../plugins.js'; import { CargoConfig } from '../mod_cargo/index.js'; import { CargoRunner } from '../mod_cargo/index.js'; import { FsHelpers } from '../mod_fs/index.js'; /** Maps friendly target names to Rust target triples */ const targetAliasMap: Record = { linux_amd64: 'x86_64-unknown-linux-gnu', linux_arm64: 'aarch64-unknown-linux-gnu', linux_amd64_musl: 'x86_64-unknown-linux-musl', linux_arm64_musl: 'aarch64-unknown-linux-musl', macos_amd64: 'x86_64-apple-darwin', macos_arm64: 'aarch64-apple-darwin', }; /** Reverse map: Rust triple → friendly name */ const tripleToFriendlyMap: Record = {}; for (const [friendly, triple] of Object.entries(targetAliasMap)) { tripleToFriendlyMap[triple] = friendly; } /** * Resolves a user-provided target name to a Rust target triple. * Accepts both friendly names (linux_arm64) and raw triples (aarch64-unknown-linux-gnu). */ function resolveTargetAlias(name: string): string { return targetAliasMap[name] || name; } /** * Derives a friendly name from a Rust target triple for use in output filenames. * Falls back to the raw triple with dashes replaced by underscores. */ function friendlyName(triple: string): string { if (tripleToFriendlyMap[triple]) { return tripleToFriendlyMap[triple]; } // Derive from triple: e.g. x86_64-unknown-linux-gnu → x86_64_unknown_linux_gnu return triple.replace(/-/g, '_'); } interface ITsrustConfig { targets?: string[]; } export class TsRustCli { private cli: plugins.smartcli.Smartcli; private cwd: string; private config: ITsrustConfig; constructor(cwd: string = process.cwd()) { this.cwd = cwd; this.cli = new plugins.smartcli.Smartcli(); const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd); this.config = npmextraInstance.dataFor('@git.zone/tsrust', { targets: [] }); this.registerCommands(); } private registerCommands(): void { this.registerStandardCommand(); this.registerCleanCommand(); } private registerStandardCommand(): void { this.cli.standardCommand().subscribe(async (argvArg) => { const startTime = Date.now(); // Check cargo is installed const runner = new CargoRunner(this.cwd); // temporary, just for version check if (!(await runner.checkCargoInstalled())) { console.error('Error: cargo is not installed or not in PATH.'); console.error('Install Rust via https://rustup.rs/'); process.exit(1); } const cargoVersion = await runner.getCargoVersion(); console.log(`Using ${cargoVersion}`); // Detect rust directory const rustDir = await this.detectRustDir(); if (!rustDir) { console.error('Error: No rust/ or ts_rust/ directory found with a Cargo.toml.'); process.exit(1); } console.log(`Found Rust project at: ${path.relative(this.cwd, rustDir) || '.'}`); // Parse Cargo.toml const cargoConfig = new CargoConfig(rustDir); const workspaceInfo = await cargoConfig.parse(); if (workspaceInfo.isWorkspace) { console.log('Detected Cargo workspace'); } if (workspaceInfo.binTargets.length === 0) { console.error('Error: No binary targets found in Cargo.toml.'); process.exit(1); } console.log(`Binary targets: ${workspaceInfo.binTargets.join(', ')}`); const isDebug = !!(argvArg as any).debug; const shouldClean = !!(argvArg as any).clean; const distDir = path.join(this.cwd, 'dist_rust'); const profile = isDebug ? 'debug' : 'release'; // Parse --target flag (can appear multiple times), fall back to npmextra.json config const cliTargets = (argvArg as any).target; const targets: string[] = cliTargets ? (Array.isArray(cliTargets) ? cliTargets : [cliTargets]) : this.config.targets || []; if (targets.length > 0) { // Cross-compilation mode const resolvedTargets = targets.map((t: string) => ({ triple: resolveTargetAlias(t), friendly: friendlyName(resolveTargetAlias(t)), })); console.log(`Cross-compiling for: ${resolvedTargets.map((t) => `${t.friendly} (${t.triple})`).join(', ')}`); await FsHelpers.ensureEmptyDir(distDir); for (const { triple, friendly } of resolvedTargets) { console.log(`\n--- Building for ${friendly} (${triple}) ---`); const cargoRunner = new CargoRunner(rustDir); const buildResult = await cargoRunner.build({ debug: isDebug, clean: shouldClean, target: triple }); if (!buildResult.success) { console.error(`Build failed for target ${triple} with exit code ${buildResult.exitCode}`); process.exit(1); } // Cross-compiled binaries go to target/// const targetDir = path.join(rustDir, 'target', triple, profile); for (const binName of workspaceInfo.binTargets) { const srcBinary = path.join(targetDir, binName); const destName = `${binName}_${friendly}`; const destBinary = path.join(distDir, destName); if (!(await FsHelpers.fileExists(srcBinary))) { console.warn(`Warning: Expected binary not found: ${srcBinary}`); continue; } await FsHelpers.copyFile(srcBinary, destBinary); await FsHelpers.makeExecutable(destBinary); const size = await FsHelpers.getFileSize(destBinary); console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${destName}`); } // Only clean on first iteration if (shouldClean) { (argvArg as any).clean = false; } } } else { // Native build (unchanged behavior) const cargoRunner = new CargoRunner(rustDir); const buildResult = await cargoRunner.build({ debug: isDebug, clean: shouldClean }); if (!buildResult.success) { console.error(`Build failed with exit code ${buildResult.exitCode}`); process.exit(1); } const targetDir = path.join(rustDir, 'target', profile); await FsHelpers.ensureEmptyDir(distDir); for (const binName of workspaceInfo.binTargets) { const srcBinary = path.join(targetDir, binName); const destBinary = path.join(distDir, binName); if (!(await FsHelpers.fileExists(srcBinary))) { console.warn(`Warning: Expected binary not found: ${srcBinary}`); continue; } await FsHelpers.copyFile(srcBinary, destBinary); await FsHelpers.makeExecutable(destBinary); const size = await FsHelpers.getFileSize(destBinary); console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`); } } const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`\nDone in ${elapsed}s`); }); } private registerCleanCommand(): void { this.cli.addCommand('clean').subscribe(async (_argvArg) => { // Clean cargo build const rustDir = await this.detectRustDir(); if (rustDir) { console.log('Running cargo clean...'); const runner = new CargoRunner(rustDir); await runner.clean(); console.log('Cargo clean complete.'); } // Remove dist_rust/ const distDir = path.join(this.cwd, 'dist_rust'); if (await FsHelpers.directoryExists(distDir)) { await FsHelpers.removeDirectory(distDir); console.log('Removed dist_rust/'); } console.log('Clean complete.'); }); } private async detectRustDir(): Promise { // Check rust/ first const rustDir = path.join(this.cwd, 'rust'); if (await FsHelpers.fileExists(path.join(rustDir, 'Cargo.toml'))) { return rustDir; } // Fallback to ts_rust/ const tsRustDir = path.join(this.cwd, 'ts_rust'); if (await FsHelpers.fileExists(path.join(tsRustDir, 'Cargo.toml'))) { return tsRustDir; } return null; } public run(): void { this.cli.startParse(); } } export const runCli = async (): Promise => { const cli = new TsRustCli(); cli.run(); };