From 4a391d9ddce68eed13fb7ff97da898a9f070db5a Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 9 Feb 2026 18:29:48 +0000 Subject: [PATCH] feat(cross-compile): add cross-compilation support with --target flag, friendly target aliases, and automatic rustup target installation --- changelog.md | 10 ++ readme.md | 36 ++++++++ ts/00_commitinfo_data.ts | 2 +- ts/mod_cargo/classes.cargorunner.ts | 29 +++++- ts/mod_cli/classes.tsrustcli.ts | 138 +++++++++++++++++++++++----- 5 files changed, 186 insertions(+), 29 deletions(-) diff --git a/changelog.md b/changelog.md index b145609..59a9f1d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-02-09 - 1.1.0 - feat(cross-compile) +add cross-compilation support with --target flag, friendly target aliases, and automatic rustup target installation + +- tsrust CLI: add --target flag (can be provided multiple times) to cross-compile for specified targets +- Introduce friendly target aliases (linux_amd64, linux_arm64, linux_amd64_musl, linux_arm64_musl, macos_amd64, macos_arm64) and resolve them to Rust triples +- CargoRunner.build accepts a target parameter and CargoRunner.ensureTarget installs missing rustup targets automatically +- Cross-compiled binaries are copied into dist_rust and named _ (e.g., rustproxy_linux_arm64) +- README updated with cross-compilation usage and supported target aliases +- Native build behavior is preserved when --target is not provided + ## 2026-02-09 - 1.0.3 - fix(tsrust) bump patch version due to no changes diff --git a/readme.md b/readme.md index 4deae3f..4614dbe 100644 --- a/readme.md +++ b/readme.md @@ -83,6 +83,42 @@ Run `cargo clean` before building to force a full rebuild: tsrust --clean ``` +### Cross-Compilation + +Cross-compile for different OS/architecture combinations using the `--target` flag: + +```bash +# Cross-compile for a single target +tsrust --target linux_arm64 + +# Cross-compile for multiple targets +tsrust --target linux_arm64 --target linux_amd64 + +# Full Rust triples are also accepted +tsrust --target aarch64-unknown-linux-gnu +``` + +Supported friendly target names: + +| Friendly name | Rust target triple | +|---|---| +| `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` | + +When using `--target`, output binaries are named `__`: + +``` +dist_rust/ +├── rustproxy_linux_arm64 +└── rustproxy_linux_amd64 +``` + +`tsrust` automatically installs missing rustup targets via `rustup target add` when needed. + ### 🗑️ Clean Only Remove all build artifacts without rebuilding: diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 2551c46..5a5cde3 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsrust', - version: '1.0.3', + version: '1.1.0', description: 'A tool for compiling Rust projects, detecting Cargo workspaces, building with cargo, and placing binaries in a conventional dist_rust directory.' } diff --git a/ts/mod_cargo/classes.cargorunner.ts b/ts/mod_cargo/classes.cargorunner.ts index 6c21fbd..17f94bb 100644 --- a/ts/mod_cargo/classes.cargorunner.ts +++ b/ts/mod_cargo/classes.cargorunner.ts @@ -27,16 +27,21 @@ export class CargoRunner { return result.stdout.trim(); } - public async build(options: { debug?: boolean; clean?: boolean } = {}): Promise { + public async build(options: { debug?: boolean; clean?: boolean; target?: string } = {}): Promise { if (options.clean) { console.log('Cleaning previous build...'); await this.clean(); } - const profile = options.debug ? '' : ' --release'; - const command = `cd ${this.rustDir} && cargo build${profile}`; + if (options.target) { + await this.ensureTarget(options.target); + } - console.log(`Running: cargo build${profile}`); + const profile = options.debug ? '' : ' --release'; + const targetFlag = options.target ? ` --target ${options.target}` : ''; + const command = `cd ${this.rustDir} && cargo build${profile}${targetFlag}`; + + console.log(`Running: cargo build${profile}${targetFlag}`); const result = await this.shell.exec(command); return { @@ -46,6 +51,22 @@ export class CargoRunner { }; } + /** + * Ensures a rustup target is installed. If not present, installs it via `rustup target add`. + */ + public async ensureTarget(triple: string): Promise { + const listResult = await this.shell.execSilent('rustup target list --installed'); + const installedTargets = listResult.stdout.split('\n').map((l) => l.trim()); + if (installedTargets.includes(triple)) { + return; + } + console.log(`Installing rustup target: ${triple}`); + const addResult = await this.shell.exec(`rustup target add ${triple}`); + if (addResult.exitCode !== 0) { + throw new Error(`Failed to install rustup target ${triple}`); + } + } + public async clean(): Promise { const command = `cd ${this.rustDir} && cargo clean`; const result = await this.shell.exec(command); diff --git a/ts/mod_cli/classes.tsrustcli.ts b/ts/mod_cli/classes.tsrustcli.ts index 190f106..7892a54 100644 --- a/ts/mod_cli/classes.tsrustcli.ts +++ b/ts/mod_cli/classes.tsrustcli.ts @@ -4,6 +4,42 @@ 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, '_'); +} + export class TsRustCli { private cli: plugins.smartcli.Smartcli; private cwd: string; @@ -58,42 +94,96 @@ export class TsRustCli { console.log(`Binary targets: ${workspaceInfo.binTargets.join(', ')}`); - // Build const isDebug = !!(argvArg as any).debug; const shouldClean = !!(argvArg as any).clean; - 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); - } - - // Copy binaries to dist_rust/ - const profile = isDebug ? 'debug' : 'release'; - const targetDir = path.join(rustDir, 'target', profile); const distDir = path.join(this.cwd, 'dist_rust'); + const profile = isDebug ? 'debug' : 'release'; - await FsHelpers.ensureEmptyDir(distDir); + // Parse --target flag (can appear multiple times) + const rawTargets = (argvArg as any).target; + const targets: string[] = rawTargets + ? Array.isArray(rawTargets) ? rawTargets : [rawTargets] + : []; - for (const binName of workspaceInfo.binTargets) { - const srcBinary = path.join(targetDir, binName); - const destBinary = path.join(distDir, binName); + if (targets.length > 0) { + // Cross-compilation mode + const resolvedTargets = targets.map((t: string) => ({ + triple: resolveTargetAlias(t), + friendly: friendlyName(resolveTargetAlias(t)), + })); - if (!(await FsHelpers.fileExists(srcBinary))) { - console.warn(`Warning: Expected binary not found: ${srcBinary}`); - continue; + 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); } - await FsHelpers.copyFile(srcBinary, destBinary); - await FsHelpers.makeExecutable(destBinary); + const targetDir = path.join(rustDir, 'target', profile); - const size = await FsHelpers.getFileSize(destBinary); - console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`); + 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(`Done in ${elapsed}s`); + console.log(`\nDone in ${elapsed}s`); }); }