feat(rustbridge): add RustBridge and RustBinaryLocator with typed IPC interfaces, plugins, tests and mock runner; export from index; add npm registries
This commit is contained in:
140
ts/classes.rustbinarylocator.ts
Normal file
140
ts/classes.rustbinarylocator.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type { IBinaryLocatorOptions, IRustBridgeLogger } from './interfaces/index.js';
|
||||
|
||||
const defaultLogger: IRustBridgeLogger = {
|
||||
log() {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Locates a Rust binary using a priority-ordered search strategy:
|
||||
* 1. Explicit binaryPath override
|
||||
* 2. Environment variable
|
||||
* 3. Platform-specific npm package
|
||||
* 4. Local development build paths
|
||||
* 5. System PATH
|
||||
*/
|
||||
export class RustBinaryLocator {
|
||||
private options: IBinaryLocatorOptions;
|
||||
private logger: IRustBridgeLogger;
|
||||
private cachedPath: string | null = null;
|
||||
|
||||
constructor(options: IBinaryLocatorOptions, logger?: IRustBridgeLogger) {
|
||||
this.options = options;
|
||||
this.logger = logger || defaultLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the binary path.
|
||||
* Returns null if no binary is available.
|
||||
*/
|
||||
public async findBinary(): Promise<string | null> {
|
||||
if (this.cachedPath !== null) {
|
||||
return this.cachedPath;
|
||||
}
|
||||
const path = await this.searchBinary();
|
||||
this.cachedPath = path;
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached binary path.
|
||||
*/
|
||||
public clearCache(): void {
|
||||
this.cachedPath = null;
|
||||
}
|
||||
|
||||
private async searchBinary(): Promise<string | null> {
|
||||
const { binaryName } = this.options;
|
||||
|
||||
// 1. Explicit binary path override
|
||||
if (this.options.binaryPath) {
|
||||
if (await this.isExecutable(this.options.binaryPath)) {
|
||||
this.logger.log('info', `Binary found via explicit path: ${this.options.binaryPath}`);
|
||||
return this.options.binaryPath;
|
||||
}
|
||||
this.logger.log('warn', `Explicit binary path not executable: ${this.options.binaryPath}`);
|
||||
}
|
||||
|
||||
// 2. Environment variable override
|
||||
if (this.options.envVarName) {
|
||||
const envPath = process.env[this.options.envVarName];
|
||||
if (envPath) {
|
||||
if (await this.isExecutable(envPath)) {
|
||||
this.logger.log('info', `Binary found via ${this.options.envVarName}: ${envPath}`);
|
||||
return envPath;
|
||||
}
|
||||
this.logger.log('warn', `${this.options.envVarName} set but not executable: ${envPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Platform-specific npm package
|
||||
if (this.options.platformPackagePrefix) {
|
||||
const platformBinary = await this.findPlatformPackageBinary();
|
||||
if (platformBinary) {
|
||||
this.logger.log('info', `Binary found in platform package: ${platformBinary}`);
|
||||
return platformBinary;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Local development build paths
|
||||
const localPaths = this.options.localPaths || [
|
||||
plugins.path.resolve(process.cwd(), `rust/target/release/${binaryName}`),
|
||||
plugins.path.resolve(process.cwd(), `rust/target/debug/${binaryName}`),
|
||||
];
|
||||
for (const localPath of localPaths) {
|
||||
if (await this.isExecutable(localPath)) {
|
||||
this.logger.log('info', `Binary found at local path: ${localPath}`);
|
||||
return localPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. System PATH
|
||||
if (this.options.searchSystemPath !== false) {
|
||||
const systemPath = await this.findInPath(binaryName);
|
||||
if (systemPath) {
|
||||
this.logger.log('info', `Binary found in system PATH: ${systemPath}`);
|
||||
return systemPath;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log('error', `No binary '${binaryName}' found. Provide an explicit path, set an env var, install the platform package, or build from source.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async findPlatformPackageBinary(): Promise<string | null> {
|
||||
const { binaryName, platformPackagePrefix } = this.options;
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
const packageName = `${platformPackagePrefix}-${platform}-${arch}`;
|
||||
|
||||
try {
|
||||
const packagePath = require.resolve(`${packageName}/${binaryName}`);
|
||||
if (await this.isExecutable(packagePath)) {
|
||||
return packagePath;
|
||||
}
|
||||
} catch {
|
||||
// Package not installed - expected for development
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async isExecutable(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await plugins.fs.promises.access(filePath, plugins.fs.constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async findInPath(binaryName: string): Promise<string | null> {
|
||||
const pathDirs = (process.env.PATH || '').split(plugins.path.delimiter);
|
||||
for (const dir of pathDirs) {
|
||||
const fullPath = plugins.path.join(dir, binaryName);
|
||||
if (await this.isExecutable(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user