Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9ad5775a1 | |||
| 85273e2933 | |||
| 18dc4c3a79 | |||
| 32e3928d19 | |||
| 97cfcac82f | |||
| 4a391d9ddc |
29
changelog.md
29
changelog.md
@@ -1,5 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-09 - 1.3.0 - feat(toolchain)
|
||||
add automatic bundled Rust toolchain fallback and integrate with CLI/CargoRunner
|
||||
|
||||
- Introduce ToolchainManager to download and install a minimal Rust toolchain to /tmp/tsrust_toolchain/ when system cargo is absent
|
||||
- Add getEnvPrefix() and installation/verification logic to ToolchainManager; supports Linux and macOS (x64, arm64)
|
||||
- Make CargoRunner accept an envPrefix and prepend it to cargo/rustup commands so bundled toolchain can be used transparently
|
||||
- Update CLI to resolve toolchain at runtime (use system cargo if available; otherwise auto-install bundled toolchain) and pass envPrefix to CargoRunner for builds and clean
|
||||
- Update exports to include mod_toolchain and add new ts/mod_toolchain module files
|
||||
- Document the automatic toolchain behavior in readme.md and update usage description
|
||||
- Bump dependencies: @push.rocks/smartcli ^4.0.20 and @types/node ^25.2.2
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tsrust",
|
||||
"version": "1.0.3",
|
||||
"version": "1.3.0",
|
||||
"private": false,
|
||||
"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",
|
||||
@@ -34,7 +34,8 @@
|
||||
"homepage": "https://code.foss.global/git.zone/tsrust#README",
|
||||
"dependencies": {
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
"@push.rocks/smartcli": "^4.0.19",
|
||||
"@push.rocks/npmextra": "^5.3.3",
|
||||
"@push.rocks/smartcli": "^4.0.20",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartshell": "^3.0.6",
|
||||
@@ -44,7 +45,7 @@
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.4",
|
||||
"@types/node": "^22.15.0"
|
||||
"@types/node": "^25.2.2"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@push.rocks/early':
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4
|
||||
'@push.rocks/npmextra':
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
'@push.rocks/smartcli':
|
||||
specifier: ^4.0.19
|
||||
version: 4.0.20
|
||||
@@ -37,8 +40,8 @@ importers:
|
||||
specifier: ^3.1.4
|
||||
version: 3.1.8(socks@2.8.7)(typescript@5.9.3)
|
||||
'@types/node':
|
||||
specifier: ^22.15.0
|
||||
version: 22.19.10
|
||||
specifier: ^25.2.2
|
||||
version: 25.2.2
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1395,6 +1398,9 @@ packages:
|
||||
'@types/node@22.19.10':
|
||||
resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==}
|
||||
|
||||
'@types/node@25.2.2':
|
||||
resolution: {integrity: sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==}
|
||||
|
||||
'@types/ping@0.4.4':
|
||||
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
||||
|
||||
@@ -3272,6 +3278,9 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
unified@11.0.5:
|
||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||
|
||||
@@ -5843,27 +5852,27 @@ snapshots:
|
||||
|
||||
'@types/bn.js@5.2.0':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/buffer-json@2.0.3': {}
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
@@ -5871,7 +5880,7 @@ snapshots:
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
dependencies:
|
||||
@@ -5879,7 +5888,7 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@5.1.1':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 1.2.1
|
||||
@@ -5893,7 +5902,7 @@ snapshots:
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@@ -5915,7 +5924,7 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
dependencies:
|
||||
@@ -5929,16 +5938,20 @@ snapshots:
|
||||
|
||||
'@types/mute-stream@0.0.4':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/node@22.19.10':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@25.2.2':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
'@types/ping@0.4.4': {}
|
||||
|
||||
'@types/qs@6.14.0': {}
|
||||
@@ -5953,22 +5966,22 @@ snapshots:
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/symbol-tree@3.2.5': {}
|
||||
|
||||
'@types/tar-stream@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
@@ -5994,11 +6007,11 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
@@ -6403,7 +6416,7 @@ snapshots:
|
||||
engine.io@6.6.4:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.19
|
||||
'@types/node': 22.19.10
|
||||
'@types/node': 25.2.2
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
@@ -8189,6 +8202,8 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
unified@11.0.5:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
66
readme.md
66
readme.md
@@ -20,7 +20,7 @@ Or as a project-level dev dependency:
|
||||
pnpm install --save-dev @git.zone/tsrust
|
||||
```
|
||||
|
||||
> ⚡ **Prerequisite:** You need a working Rust toolchain. Install via [rustup.rs](https://rustup.rs/) if you haven't already.
|
||||
> ⚡ **No Rust required!** If `cargo` isn't found on your system, `tsrust` automatically downloads and installs a minimal Rust toolchain to `/tmp/tsrust_toolchain/`. This gives a zero-setup experience. If you already have Rust installed, `tsrust` uses your system toolchain.
|
||||
|
||||
## The Convention
|
||||
|
||||
@@ -44,7 +44,7 @@ tsrust
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Verify that `cargo` is available
|
||||
1. Detect the Rust toolchain (system `cargo`, or auto-install to `/tmp/tsrust_toolchain/`)
|
||||
2. Locate your `rust/` directory (containing `Cargo.toml`)
|
||||
3. Parse the workspace to discover all `[[bin]]` targets
|
||||
4. Run `cargo build --release` with full streaming output
|
||||
@@ -65,6 +65,18 @@ Copied rustproxy (13.4 MB) -> dist_rust/rustproxy
|
||||
Done in 29.2s
|
||||
```
|
||||
|
||||
### Automatic Rust Toolchain
|
||||
|
||||
`tsrust` provides a zero-setup experience through automatic toolchain management:
|
||||
|
||||
1. **System toolchain detected** → uses it as-is (no download, no overhead)
|
||||
2. **No system toolchain** → checks `/tmp/tsrust_toolchain/` for a previously installed bundled toolchain
|
||||
3. **No bundled toolchain** → downloads `rustup-init` and installs a minimal Rust toolchain (~70-90 MB download) to `/tmp/tsrust_toolchain/`
|
||||
|
||||
The bundled toolchain is stored in `/tmp/`, so it's cleaned up on reboot. Subsequent runs reuse the existing installation. This is ideal for CI environments and quick starts where you don't want to manage a system-wide Rust install.
|
||||
|
||||
Supported platforms for automatic install: Linux (x64, arm64) and macOS (x64, arm64).
|
||||
|
||||
### 🐛 Debug Build
|
||||
|
||||
Build with the debug profile instead of release:
|
||||
@@ -83,6 +95,56 @@ 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 `<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
|
||||
|
||||
Remove all build artifacts without rebuilding:
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsrust',
|
||||
version: '1.0.3',
|
||||
version: '1.3.0',
|
||||
description: 'A tool for compiling Rust projects, detecting Cargo workspaces, building with cargo, and placing binaries in a conventional dist_rust directory.'
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ plugins.early.start('@git.zone/tsrust');
|
||||
export * from './mod_fs/index.js';
|
||||
export * from './mod_cargo/index.js';
|
||||
export * from './mod_cli/index.js';
|
||||
export * from './mod_toolchain/index.js';
|
||||
|
||||
plugins.early.stop();
|
||||
|
||||
@@ -9,34 +9,41 @@ export interface ICargoRunResult {
|
||||
export class CargoRunner {
|
||||
private shell: plugins.smartshell.Smartshell;
|
||||
private rustDir: string;
|
||||
private envPrefix: string;
|
||||
|
||||
constructor(rustDir: string) {
|
||||
constructor(rustDir: string, envPrefix: string = '') {
|
||||
this.rustDir = rustDir;
|
||||
this.envPrefix = envPrefix;
|
||||
this.shell = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
}
|
||||
|
||||
public async checkCargoInstalled(): Promise<boolean> {
|
||||
const result = await this.shell.execSilent('cargo --version');
|
||||
const result = await this.shell.execSilent(`${this.envPrefix}cargo --version`);
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
public async getCargoVersion(): Promise<string> {
|
||||
const result = await this.shell.execSilent('cargo --version');
|
||||
const result = await this.shell.execSilent(`${this.envPrefix}cargo --version`);
|
||||
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) {
|
||||
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 = `${this.envPrefix}cd ${this.rustDir} && cargo build${profile}${targetFlag}`;
|
||||
|
||||
console.log(`Running: cargo build${profile}${targetFlag}`);
|
||||
const result = await this.shell.exec(command);
|
||||
|
||||
return {
|
||||
@@ -46,8 +53,24 @@ 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(`${this.envPrefix}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(`${this.envPrefix}rustup target add ${triple}`);
|
||||
if (addResult.exitCode !== 0) {
|
||||
throw new Error(`Failed to install rustup target ${triple}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async clean(): Promise<ICargoRunResult> {
|
||||
const command = `cd ${this.rustDir} && cargo clean`;
|
||||
const command = `${this.envPrefix}cd ${this.rustDir} && cargo clean`;
|
||||
const result = await this.shell.exec(command);
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,14 +3,58 @@ 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();
|
||||
}
|
||||
|
||||
@@ -19,18 +63,33 @@ export class TsRustCli {
|
||||
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();
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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}`);
|
||||
|
||||
@@ -58,42 +117,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), 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) {
|
||||
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, 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);
|
||||
}
|
||||
|
||||
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`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,8 +215,9 @@ export class TsRustCli {
|
||||
// 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);
|
||||
const runner = new CargoRunner(rustDir, envPrefix);
|
||||
await runner.clean();
|
||||
console.log('Cargo clean complete.');
|
||||
}
|
||||
|
||||
119
ts/mod_toolchain/classes.toolchainmanager.ts
Normal file
119
ts/mod_toolchain/classes.toolchainmanager.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export class ToolchainManager {
|
||||
public static TOOLCHAIN_DIR = '/tmp/tsrust_toolchain';
|
||||
public static RUSTUP_HOME = '/tmp/tsrust_toolchain/rustup';
|
||||
public static CARGO_HOME = '/tmp/tsrust_toolchain/cargo';
|
||||
public static BIN_DIR = '/tmp/tsrust_toolchain/cargo/bin';
|
||||
|
||||
private shell: plugins.smartshell.Smartshell;
|
||||
|
||||
constructor() {
|
||||
this.shell = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Rust host triple for the current platform.
|
||||
*/
|
||||
public getHostTriple(): string {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
if (platform === 'linux' && arch === 'x64') return 'x86_64-unknown-linux-gnu';
|
||||
if (platform === 'linux' && arch === 'arm64') return 'aarch64-unknown-linux-gnu';
|
||||
if (platform === 'darwin' && arch === 'x64') return 'x86_64-apple-darwin';
|
||||
if (platform === 'darwin' && arch === 'arm64') return 'aarch64-apple-darwin';
|
||||
|
||||
throw new Error(
|
||||
`Unsupported platform: ${platform}/${arch}. Please install Rust manually via https://rustup.rs/`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the bundled toolchain is already installed at TOOLCHAIN_DIR.
|
||||
*/
|
||||
public async isInstalled(): Promise<boolean> {
|
||||
const cargoPath = path.join(ToolchainManager.BIN_DIR, 'cargo');
|
||||
try {
|
||||
const stat = await fs.promises.stat(cargoPath);
|
||||
return stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads rustup-init and installs a minimal Rust toolchain to TOOLCHAIN_DIR.
|
||||
*/
|
||||
public async install(): Promise<void> {
|
||||
const triple = this.getHostTriple();
|
||||
const rustupInitUrl = `https://static.rust-lang.org/rustup/dist/${triple}/rustup-init`;
|
||||
const rustupInitPath = path.join(ToolchainManager.TOOLCHAIN_DIR, 'rustup-init');
|
||||
|
||||
// Ensure toolchain directory exists
|
||||
await fs.promises.mkdir(ToolchainManager.TOOLCHAIN_DIR, { recursive: true });
|
||||
|
||||
console.log(`Downloading rustup-init for ${triple}...`);
|
||||
const downloadResult = await this.shell.exec(
|
||||
`curl -sSf -o ${rustupInitPath} ${rustupInitUrl} && chmod +x ${rustupInitPath}`
|
||||
);
|
||||
if (downloadResult.exitCode !== 0) {
|
||||
throw new Error(`Failed to download rustup-init: ${downloadResult.stdout}`);
|
||||
}
|
||||
|
||||
console.log('Installing minimal Rust toolchain to /tmp/tsrust_toolchain/...');
|
||||
const installCmd = [
|
||||
`RUSTUP_HOME="${ToolchainManager.RUSTUP_HOME}"`,
|
||||
`CARGO_HOME="${ToolchainManager.CARGO_HOME}"`,
|
||||
rustupInitPath,
|
||||
'-y',
|
||||
'--default-toolchain stable',
|
||||
'--profile minimal',
|
||||
'--no-modify-path',
|
||||
].join(' ');
|
||||
|
||||
const installResult = await this.shell.exec(installCmd);
|
||||
if (installResult.exitCode !== 0) {
|
||||
throw new Error(`Failed to install Rust toolchain: ${installResult.stdout}`);
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
const verifyResult = await this.shell.execSilent(
|
||||
`${this.getEnvPrefix()}cargo --version`
|
||||
);
|
||||
if (verifyResult.exitCode !== 0) {
|
||||
throw new Error('Rust toolchain installation verification failed.');
|
||||
}
|
||||
console.log(`Installed: ${verifyResult.stdout.trim()}`);
|
||||
|
||||
// Clean up rustup-init binary
|
||||
await fs.promises.unlink(rustupInitPath).catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the bundled toolchain is installed. Downloads if not present.
|
||||
*/
|
||||
public async ensureInstalled(): Promise<void> {
|
||||
if (await this.isInstalled()) {
|
||||
console.log('Using bundled Rust toolchain from /tmp/tsrust_toolchain/');
|
||||
return;
|
||||
}
|
||||
await this.install();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shell prefix string that sets RUSTUP_HOME, CARGO_HOME, and PATH
|
||||
* to point at the bundled toolchain. Prepend this to any shell command.
|
||||
*/
|
||||
public getEnvPrefix(): string {
|
||||
return (
|
||||
`export RUSTUP_HOME="${ToolchainManager.RUSTUP_HOME}" CARGO_HOME="${ToolchainManager.CARGO_HOME}" && ` +
|
||||
`export PATH="${ToolchainManager.BIN_DIR}:$PATH" && `
|
||||
);
|
||||
}
|
||||
}
|
||||
1
ts/mod_toolchain/index.ts
Normal file
1
ts/mod_toolchain/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.toolchainmanager.js';
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as early from '@push.rocks/early';
|
||||
import * as npmextra from '@push.rocks/npmextra';
|
||||
import * as smartcli from '@push.rocks/smartcli';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
@@ -6,6 +7,7 @@ import * as smartshell from '@push.rocks/smartshell';
|
||||
|
||||
export {
|
||||
early,
|
||||
npmextra,
|
||||
smartcli,
|
||||
smartfile,
|
||||
smartpath,
|
||||
|
||||
Reference in New Issue
Block a user