261 lines
8.8 KiB
TypeScript
261 lines
8.8 KiB
TypeScript
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';
|
|
import { ToolchainManager } from '../mod_toolchain/index.js';
|
|
|
|
/** Maps friendly target names to Rust target triples */
|
|
const targetAliasMap: Record<string, string> = {
|
|
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<string, string> = {};
|
|
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<ITsrustConfig>('@git.zone/tsrust', { targets: [] });
|
|
this.registerCommands();
|
|
}
|
|
|
|
private registerCommands(): void {
|
|
this.registerStandardCommand();
|
|
this.registerCleanCommand();
|
|
}
|
|
|
|
/**
|
|
* Resolves the Rust toolchain to use. Tries system cargo first,
|
|
* then falls back to a bundled toolchain at /tmp/tsrust_toolchain/.
|
|
* Returns the envPrefix string to prepend to shell commands.
|
|
*/
|
|
private async resolveToolchain(): Promise<string> {
|
|
// 1. Try system cargo
|
|
const systemRunner = new CargoRunner(this.cwd);
|
|
if (await systemRunner.checkCargoInstalled()) {
|
|
return ''; // system toolchain, no prefix needed
|
|
}
|
|
|
|
// 2. Fall back to bundled toolchain
|
|
console.log('System cargo not found. Checking for bundled toolchain...');
|
|
const toolchain = new ToolchainManager();
|
|
await toolchain.ensureInstalled();
|
|
return toolchain.getEnvPrefix();
|
|
}
|
|
|
|
private registerStandardCommand(): void {
|
|
this.cli.standardCommand().subscribe(async (argvArg) => {
|
|
const startTime = Date.now();
|
|
|
|
// Resolve toolchain (system or bundled fallback)
|
|
const envPrefix = await this.resolveToolchain();
|
|
|
|
const runner = new CargoRunner(this.cwd, envPrefix);
|
|
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, envPrefix);
|
|
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/<triple>/<profile>/
|
|
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
|
|
const cargoRunner = new CargoRunner(rustDir, envPrefix);
|
|
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) {
|
|
const envPrefix = await this.resolveToolchain();
|
|
console.log('Running cargo clean...');
|
|
const runner = new CargoRunner(rustDir, envPrefix);
|
|
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<string | null> {
|
|
// 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<void> => {
|
|
const cli = new TsRustCli();
|
|
cli.run();
|
|
};
|