This commit is contained in:
2026-02-09 09:32:20 +00:00
commit f23ced9c47
22 changed files with 9064 additions and 0 deletions

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitance data
*/
export const commitinfo = {
name: '@git.zone/tsrust',
version: '1.0.1',
description: 'A tool for compiling Rust projects, detecting Cargo workspaces, building with cargo, and placing binaries in a conventional dist_rust directory.',
};

8
ts/index.ts Normal file
View File

@@ -0,0 +1,8 @@
import * as plugins from './plugins.js';
plugins.early.start('@git.zone/tsrust');
export * from './mod_fs/index.js';
export * from './mod_cargo/index.js';
export * from './mod_cli/index.js';
plugins.early.stop();

View File

@@ -0,0 +1,89 @@
import * as path from 'path';
import * as fs from 'fs';
import * as smolToml from 'smol-toml';
import { FsHelpers } from '../mod_fs/index.js';
export interface ICargoWorkspaceInfo {
isWorkspace: boolean;
rustDir: string;
binTargets: string[];
}
export class CargoConfig {
private rustDir: string;
constructor(rustDir: string) {
this.rustDir = rustDir;
}
public async parse(): Promise<ICargoWorkspaceInfo> {
const cargoTomlPath = path.join(this.rustDir, 'Cargo.toml');
const content = await fs.promises.readFile(cargoTomlPath, 'utf-8');
const parsed = smolToml.parse(content);
const isWorkspace = !!(parsed as any).workspace;
let binTargets: string[] = [];
if (isWorkspace) {
binTargets = await this.collectWorkspaceBinTargets(parsed);
} else {
binTargets = this.collectCrateBinTargets(parsed, this.rustDir);
}
return {
isWorkspace,
rustDir: this.rustDir,
binTargets,
};
}
private async collectWorkspaceBinTargets(parsed: any): Promise<string[]> {
const members: string[] = parsed.workspace?.members || [];
const binTargets: string[] = [];
for (const member of members) {
const memberDir = path.join(this.rustDir, member);
const memberCargoToml = path.join(memberDir, 'Cargo.toml');
if (!(await FsHelpers.fileExists(memberCargoToml))) {
continue;
}
const memberContent = await fs.promises.readFile(memberCargoToml, 'utf-8');
const memberParsed = smolToml.parse(memberContent);
const memberBins = this.collectCrateBinTargets(memberParsed, memberDir);
binTargets.push(...memberBins);
}
return binTargets;
}
private collectCrateBinTargets(parsed: any, crateDir: string): string[] {
const binTargets: string[] = [];
// Check for explicit [[bin]] entries
if (Array.isArray(parsed.bin)) {
for (const bin of parsed.bin) {
if (bin.name) {
binTargets.push(bin.name);
}
}
}
// If no [[bin]] but package has a name and src/main.rs exists, use package name
if (binTargets.length === 0 && parsed.package?.name) {
const mainRsPath = path.join(crateDir, 'src', 'main.rs');
// Use sync check since this is called during parsing
try {
const stat = fs.statSync(mainRsPath);
if (stat.isFile()) {
binTargets.push(parsed.package.name);
}
} catch {
// No main.rs, not a binary crate
}
}
return binTargets;
}
}

View File

@@ -0,0 +1,59 @@
import * as plugins from '../plugins.js';
export interface ICargoRunResult {
success: boolean;
exitCode: number;
stdout: string;
}
export class CargoRunner {
private shell: plugins.smartshell.Smartshell;
private rustDir: string;
constructor(rustDir: string) {
this.rustDir = rustDir;
this.shell = new plugins.smartshell.Smartshell({
executor: 'bash',
});
}
public async checkCargoInstalled(): Promise<boolean> {
const result = await this.shell.execSilent('cargo --version');
return result.exitCode === 0;
}
public async getCargoVersion(): Promise<string> {
const result = await this.shell.execSilent('cargo --version');
return result.stdout.trim();
}
public async build(options: { debug?: boolean; clean?: boolean } = {}): 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}`;
console.log(`Running: cargo build${profile}`);
const result = await this.shell.exec(command);
return {
success: result.exitCode === 0,
exitCode: result.exitCode,
stdout: result.stdout,
};
}
public async clean(): Promise<ICargoRunResult> {
const command = `cd ${this.rustDir} && cargo clean`;
const result = await this.shell.exec(command);
return {
success: result.exitCode === 0,
exitCode: result.exitCode,
stdout: result.stdout,
};
}
}

2
ts/mod_cargo/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { CargoConfig } from './classes.cargoconfig.js';
export { CargoRunner } from './classes.cargorunner.js';

View File

@@ -0,0 +1,146 @@
import * as path from 'path';
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';
export class TsRustCli {
private cli: plugins.smartcli.Smartcli;
private cwd: string;
constructor(cwd: string = process.cwd()) {
this.cwd = cwd;
this.cli = new plugins.smartcli.Smartcli();
this.registerCommands();
}
private registerCommands(): void {
this.registerStandardCommand();
this.registerCleanCommand();
}
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);
}
const cargoVersion = await runner.getCargoVersion();
console.log(`Using ${cargoVersion}`);
// Detect rust directory
const rustDir = await this.detectRustDir();
if (!rustDir) {
console.error('Error: No rust/ or ts_rust/ directory found with a Cargo.toml.');
process.exit(1);
}
console.log(`Found Rust project at: ${path.relative(this.cwd, rustDir) || '.'}`);
// Parse Cargo.toml
const cargoConfig = new CargoConfig(rustDir);
const workspaceInfo = await cargoConfig.parse();
if (workspaceInfo.isWorkspace) {
console.log('Detected Cargo workspace');
}
if (workspaceInfo.binTargets.length === 0) {
console.error('Error: No binary targets found in Cargo.toml.');
process.exit(1);
}
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');
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`);
});
}
private registerCleanCommand(): void {
this.cli.addCommand('clean').subscribe(async (_argvArg) => {
// Clean cargo build
const rustDir = await this.detectRustDir();
if (rustDir) {
console.log('Running cargo clean...');
const runner = new CargoRunner(rustDir);
await runner.clean();
console.log('Cargo clean complete.');
}
// Remove dist_rust/
const distDir = path.join(this.cwd, 'dist_rust');
if (await FsHelpers.directoryExists(distDir)) {
await FsHelpers.removeDirectory(distDir);
console.log('Removed dist_rust/');
}
console.log('Clean complete.');
});
}
private async detectRustDir(): Promise<string | null> {
// Check rust/ first
const rustDir = path.join(this.cwd, 'rust');
if (await FsHelpers.fileExists(path.join(rustDir, 'Cargo.toml'))) {
return rustDir;
}
// Fallback to ts_rust/
const tsRustDir = path.join(this.cwd, 'ts_rust');
if (await FsHelpers.fileExists(path.join(tsRustDir, 'Cargo.toml'))) {
return tsRustDir;
}
return null;
}
public run(): void {
this.cli.startParse();
}
}
export const runCli = async (): Promise<void> => {
const cli = new TsRustCli();
cli.run();
};

1
ts/mod_cli/index.ts Normal file
View File

@@ -0,0 +1 @@
export { TsRustCli, runCli } from './classes.tsrustcli.js';

View File

@@ -0,0 +1,56 @@
import * as fs from 'fs';
import * as path from 'path';
export class FsHelpers {
public static async fileExists(filePath: string): Promise<boolean> {
try {
const stat = await fs.promises.stat(filePath);
return stat.isFile();
} catch {
return false;
}
}
public static async directoryExists(dirPath: string): Promise<boolean> {
try {
const stat = await fs.promises.stat(dirPath);
return stat.isDirectory();
} catch {
return false;
}
}
public static async ensureEmptyDir(dirPath: string): Promise<void> {
if (await FsHelpers.directoryExists(dirPath)) {
await fs.promises.rm(dirPath, { recursive: true, force: true });
}
await fs.promises.mkdir(dirPath, { recursive: true });
}
public static async copyFile(src: string, dest: string): Promise<void> {
const destDir = path.dirname(dest);
await fs.promises.mkdir(destDir, { recursive: true });
await fs.promises.copyFile(src, dest);
}
public static async makeExecutable(filePath: string): Promise<void> {
await fs.promises.chmod(filePath, 0o755);
}
public static async getFileSize(filePath: string): Promise<number> {
const stat = await fs.promises.stat(filePath);
return stat.size;
}
public static async removeDirectory(dirPath: string): Promise<void> {
if (await FsHelpers.directoryExists(dirPath)) {
await fs.promises.rm(dirPath, { recursive: true, force: true });
}
}
public static formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
}

1
ts/mod_fs/index.ts Normal file
View File

@@ -0,0 +1 @@
export { FsHelpers } from './classes.fshelpers.js';

13
ts/plugins.ts Normal file
View File

@@ -0,0 +1,13 @@
import * as early from '@push.rocks/early';
import * as smartcli from '@push.rocks/smartcli';
import * as smartfile from '@push.rocks/smartfile';
import * as smartpath from '@push.rocks/smartpath';
import * as smartshell from '@push.rocks/smartshell';
export {
early,
smartcli,
smartfile,
smartpath,
smartshell,
};