feat(toolchain): add automatic bundled Rust toolchain fallback and integrate with CLI/CargoRunner
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsrust',
|
||||
version: '1.2.0',
|
||||
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,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> = {
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user