Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57bf6e1670 | |||
| cd99e15583 | |||
| 838b3a9feb | |||
| b85abf7b93 | |||
| e9ad5775a1 | |||
| 85273e2933 |
24
changelog.md
24
changelog.md
@@ -1,5 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-24 - 1.3.2 - fix(config)
|
||||
migrate project metadata to .smartconfig.json and update related dependencies
|
||||
|
||||
- replace npmextra.json with .smartconfig.json in packaged files and documentation
|
||||
- bump smartconfig, smartshell, smol-toml, tsbuild, tstest, and @types/node versions
|
||||
|
||||
## 2026-03-24 - 1.3.1 - fix(cli)
|
||||
replace npmextra config loading with smartconfig
|
||||
|
||||
- updates the CLI to read tsrust configuration via @push.rocks/smartconfig
|
||||
- replaces the @push.rocks/npmextra dependency with @push.rocks/smartconfig
|
||||
- adjusts config fallback documentation to reference smartconfig.json
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tsrust",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.2",
|
||||
"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,18 +34,18 @@
|
||||
"homepage": "https://code.foss.global/git.zone/tsrust#README",
|
||||
"dependencies": {
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
"@push.rocks/npmextra": "^5.3.3",
|
||||
"@push.rocks/smartcli": "^4.0.19",
|
||||
"@push.rocks/smartcli": "^4.0.20",
|
||||
"@push.rocks/smartconfig": "^6.0.1",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartshell": "^3.0.6",
|
||||
"smol-toml": "^1.3.1"
|
||||
"@push.rocks/smartshell": "^3.3.8",
|
||||
"smol-toml": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.4",
|
||||
"@types/node": "^22.15.0"
|
||||
"@git.zone/tstest": "^3.5.1",
|
||||
"@types/node": "^25.5.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -54,7 +54,7 @@
|
||||
"dist_ts/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
|
||||
3127
pnpm-lock.yaml
generated
3127
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
24
readme.md
24
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:
|
||||
@@ -119,9 +131,9 @@ dist_rust/
|
||||
|
||||
`tsrust` automatically installs missing rustup targets via `rustup target add` when needed.
|
||||
|
||||
### Configuration via npmextra.json
|
||||
### Configuration via .smartconfig.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:
|
||||
You can set default cross-compilation targets in your project's `.smartconfig.json` file so you don't need to pass `--target` flags every time:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -131,7 +143,7 @@ You can set default cross-compilation targets in your project's `npmextra.json`
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
When targets are configured in `.smartconfig.json`, simply running `tsrust` will cross-compile for all listed targets. CLI `--target` flags take full precedence — if any `--target` is provided, the `.smartconfig.json` targets are ignored entirely.
|
||||
|
||||
### 🗑️ Clean Only
|
||||
|
||||
@@ -209,7 +221,7 @@ console.log(FsHelpers.formatFileSize(size)); // "13.4 MB"
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license.md) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsrust',
|
||||
version: '1.2.0',
|
||||
version: '1.3.2',
|
||||
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,21 +9,23 @@ 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();
|
||||
}
|
||||
|
||||
@@ -39,7 +41,7 @@ export class CargoRunner {
|
||||
|
||||
const profile = options.debug ? '' : ' --release';
|
||||
const targetFlag = options.target ? ` --target ${options.target}` : '';
|
||||
const command = `cd ${this.rustDir} && cargo build${profile}${targetFlag}`;
|
||||
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);
|
||||
@@ -55,20 +57,20 @@ 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 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(`rustup target add ${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,6 +3,7 @@ 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> = {
|
||||
@@ -52,8 +53,8 @@ export class TsRustCli {
|
||||
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: [] });
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig(this.cwd);
|
||||
this.config = smartconfigInstance.dataFor<ITsrustConfig>('@git.zone/tsrust', { targets: [] });
|
||||
this.registerCommands();
|
||||
}
|
||||
|
||||
@@ -62,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}`);
|
||||
|
||||
@@ -106,7 +122,7 @@ export class TsRustCli {
|
||||
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
|
||||
// Parse --target flag (can appear multiple times), fall back to smartconfig.json config
|
||||
const cliTargets = (argvArg as any).target;
|
||||
const targets: string[] = cliTargets
|
||||
? (Array.isArray(cliTargets) ? cliTargets : [cliTargets])
|
||||
@@ -125,7 +141,7 @@ export class TsRustCli {
|
||||
|
||||
for (const { triple, friendly } of resolvedTargets) {
|
||||
console.log(`\n--- Building for ${friendly} (${triple}) ---`);
|
||||
const cargoRunner = new CargoRunner(rustDir);
|
||||
const cargoRunner = new CargoRunner(rustDir, envPrefix);
|
||||
const buildResult = await cargoRunner.build({ debug: isDebug, clean: shouldClean, target: triple });
|
||||
|
||||
if (!buildResult.success) {
|
||||
@@ -159,8 +175,8 @@ export class TsRustCli {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Native build (unchanged behavior)
|
||||
const cargoRunner = new CargoRunner(rustDir);
|
||||
// Native build
|
||||
const cargoRunner = new CargoRunner(rustDir, envPrefix);
|
||||
const buildResult = await cargoRunner.build({ debug: isDebug, clean: shouldClean });
|
||||
|
||||
if (!buildResult.success) {
|
||||
@@ -199,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,5 +1,5 @@
|
||||
import * as early from '@push.rocks/early';
|
||||
import * as npmextra from '@push.rocks/npmextra';
|
||||
import * as smartconfig from '@push.rocks/smartconfig';
|
||||
import * as smartcli from '@push.rocks/smartcli';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
@@ -7,7 +7,7 @@ import * as smartshell from '@push.rocks/smartshell';
|
||||
|
||||
export {
|
||||
early,
|
||||
npmextra,
|
||||
smartconfig,
|
||||
smartcli,
|
||||
smartfile,
|
||||
smartpath,
|
||||
|
||||
Reference in New Issue
Block a user