Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18dc4c3a79 | |||
| 32e3928d19 | |||
| 97cfcac82f | |||
| 4a391d9ddc | |||
| 7a80cc8a7a | |||
| 1dad4442d0 |
24
changelog.md
24
changelog.md
@@ -1,5 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-09 - 1.2.0 - feat(cli)
|
||||||
|
support default cross-compilation targets from npmextra.json
|
||||||
|
|
||||||
|
- Add @push.rocks/npmextra dependency and export plugin in ts/plugins.ts
|
||||||
|
- Introduce ITsrustConfig and read configuration via plugins.npmextra.Npmextra in TsRustCli
|
||||||
|
- Use npmextra.json targets as fallback when no CLI --target flags are provided
|
||||||
|
- Update README to document npmextra.json configuration for default targets
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tsrust",
|
"name": "@git.zone/tsrust",
|
||||||
"version": "1.0.2",
|
"version": "1.2.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",
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"homepage": "https://code.foss.global/git.zone/tsrust#README",
|
"homepage": "https://code.foss.global/git.zone/tsrust#README",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/early": "^4.0.4",
|
"@push.rocks/early": "^4.0.4",
|
||||||
|
"@push.rocks/npmextra": "^5.3.3",
|
||||||
"@push.rocks/smartcli": "^4.0.19",
|
"@push.rocks/smartcli": "^4.0.19",
|
||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@push.rocks/early':
|
'@push.rocks/early':
|
||||||
specifier: ^4.0.4
|
specifier: ^4.0.4
|
||||||
version: 4.0.4
|
version: 4.0.4
|
||||||
|
'@push.rocks/npmextra':
|
||||||
|
specifier: ^5.3.3
|
||||||
|
version: 5.3.3
|
||||||
'@push.rocks/smartcli':
|
'@push.rocks/smartcli':
|
||||||
specifier: ^4.0.19
|
specifier: ^4.0.19
|
||||||
version: 4.0.20
|
version: 4.0.20
|
||||||
|
|||||||
50
readme.md
50
readme.md
@@ -83,6 +83,56 @@ 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.
|
||||||
|
|
||||||
|
### Configuration via npmextra.json
|
||||||
|
|
||||||
|
You can set default cross-compilation targets in your project's `npmextra.json` file so you don't need to pass `--target` flags every time:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@git.zone/tsrust": {
|
||||||
|
"targets": ["linux_arm64", "linux_amd64"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When targets are configured in `npmextra.json`, simply running `tsrust` will cross-compile for all listed targets. CLI `--target` flags take full precedence — if any `--target` is provided, the `npmextra.json` targets are ignored entirely.
|
||||||
|
|
||||||
### 🗑️ Clean Only
|
### 🗑️ Clean Only
|
||||||
|
|
||||||
Remove all build artifacts without rebuilding:
|
Remove all build artifacts without rebuilding:
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tsrust',
|
name: '@git.zone/tsrust',
|
||||||
version: '1.0.2',
|
version: '1.2.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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -4,13 +4,56 @@ 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, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITsrustConfig {
|
||||||
|
targets?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export class TsRustCli {
|
export class TsRustCli {
|
||||||
private cli: plugins.smartcli.Smartcli;
|
private cli: plugins.smartcli.Smartcli;
|
||||||
private cwd: string;
|
private cwd: string;
|
||||||
|
private config: ITsrustConfig;
|
||||||
|
|
||||||
constructor(cwd: string = process.cwd()) {
|
constructor(cwd: string = process.cwd()) {
|
||||||
this.cwd = cwd;
|
this.cwd = cwd;
|
||||||
this.cli = new plugins.smartcli.Smartcli();
|
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();
|
this.registerCommands();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,42 +101,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), fall back to npmextra.json config
|
||||||
|
const cliTargets = (argvArg as any).target;
|
||||||
|
const targets: string[] = cliTargets
|
||||||
|
? (Array.isArray(cliTargets) ? cliTargets : [cliTargets])
|
||||||
|
: this.config.targets || [];
|
||||||
|
|
||||||
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`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as early from '@push.rocks/early';
|
import * as early from '@push.rocks/early';
|
||||||
|
import * as npmextra from '@push.rocks/npmextra';
|
||||||
import * as smartcli from '@push.rocks/smartcli';
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
@@ -6,6 +7,7 @@ import * as smartshell from '@push.rocks/smartshell';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
early,
|
early,
|
||||||
|
npmextra,
|
||||||
smartcli,
|
smartcli,
|
||||||
smartfile,
|
smartfile,
|
||||||
smartpath,
|
smartpath,
|
||||||
|
|||||||
Reference in New Issue
Block a user