4 Commits

6 changed files with 193 additions and 30 deletions

View File

@@ -1,5 +1,21 @@
# Changelog # 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 <bin>_<friendly> (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
- current package.json version: 1.0.2
- git diff shows no changes; creating a no-op/metadata patch release
## 2026-02-09 - 1.0.2 - fix() ## 2026-02-09 - 1.0.2 - fix()
no changes no changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsrust", "name": "@git.zone/tsrust",
"version": "1.0.2", "version": "1.1.0",
"private": false, "private": false,
"description": "A tool for compiling Rust projects, detecting Cargo workspaces, building with cargo, and placing binaries in a conventional dist_rust directory.", "description": "A tool for compiling Rust projects, detecting Cargo workspaces, building with cargo, and placing binaries in a conventional dist_rust directory.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@@ -83,6 +83,42 @@ Run `cargo clean` before building to force a full rebuild:
tsrust --clean 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 `<binname>_<os>_<arch>`:
```
dist_rust/
├── rustproxy_linux_arm64
└── rustproxy_linux_amd64
```
`tsrust` automatically installs missing rustup targets via `rustup target add` when needed.
### 🗑️ Clean Only ### 🗑️ Clean Only
Remove all build artifacts without rebuilding: Remove all build artifacts without rebuilding:

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsrust', name: '@git.zone/tsrust',
version: '1.0.2', 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.' description: 'A tool for compiling Rust projects, detecting Cargo workspaces, building with cargo, and placing binaries in a conventional dist_rust directory.'
} }

View File

@@ -27,16 +27,21 @@ export class CargoRunner {
return result.stdout.trim(); return result.stdout.trim();
} }
public async build(options: { debug?: boolean; clean?: boolean } = {}): Promise<ICargoRunResult> { public async build(options: { debug?: boolean; clean?: boolean; target?: string } = {}): Promise<ICargoRunResult> {
if (options.clean) { if (options.clean) {
console.log('Cleaning previous build...'); console.log('Cleaning previous build...');
await this.clean(); await this.clean();
} }
const profile = options.debug ? '' : ' --release'; if (options.target) {
const command = `cd ${this.rustDir} && cargo build${profile}`; 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); const result = await this.shell.exec(command);
return { 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<void> {
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<ICargoRunResult> { public async clean(): Promise<ICargoRunResult> {
const command = `cd ${this.rustDir} && cargo clean`; const command = `cd ${this.rustDir} && cargo clean`;
const result = await this.shell.exec(command); const result = await this.shell.exec(command);

View File

@@ -4,6 +4,42 @@ import { CargoConfig } from '../mod_cargo/index.js';
import { CargoRunner } from '../mod_cargo/index.js'; import { CargoRunner } from '../mod_cargo/index.js';
import { FsHelpers } from '../mod_fs/index.js'; import { FsHelpers } from '../mod_fs/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, '_');
}
export class TsRustCli { export class TsRustCli {
private cli: plugins.smartcli.Smartcli; private cli: plugins.smartcli.Smartcli;
private cwd: string; private cwd: string;
@@ -58,42 +94,96 @@ export class TsRustCli {
console.log(`Binary targets: ${workspaceInfo.binTargets.join(', ')}`); console.log(`Binary targets: ${workspaceInfo.binTargets.join(', ')}`);
// Build
const isDebug = !!(argvArg as any).debug; const isDebug = !!(argvArg as any).debug;
const shouldClean = !!(argvArg as any).clean; 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 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) { if (targets.length > 0) {
const srcBinary = path.join(targetDir, binName); // Cross-compilation mode
const destBinary = path.join(distDir, binName); const resolvedTargets = targets.map((t: string) => ({
triple: resolveTargetAlias(t),
friendly: friendlyName(resolveTargetAlias(t)),
}));
if (!(await FsHelpers.fileExists(srcBinary))) { console.log(`Cross-compiling for: ${resolvedTargets.map((t) => `${t.friendly} (${t.triple})`).join(', ')}`);
console.warn(`Warning: Expected binary not found: ${srcBinary}`);
continue; 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/<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 (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); const targetDir = path.join(rustDir, 'target', profile);
await FsHelpers.makeExecutable(destBinary);
const size = await FsHelpers.getFileSize(destBinary); await FsHelpers.ensureEmptyDir(distDir);
console.log(`Copied ${binName} (${FsHelpers.formatFileSize(size)}) -> dist_rust/${binName}`);
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); const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`Done in ${elapsed}s`); console.log(`\nDone in ${elapsed}s`);
}); });
} }