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 { 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 { 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 { const { binaryName, platformPackagePrefix } = this.options; const platform = process.platform; const arch = process.arch; const packageName = `${platformPackagePrefix}-${platform}-${arch}`; try { const resolved = import.meta.resolve(`${packageName}/${binaryName}`); const packagePath = plugins.url.fileURLToPath(resolved); if (await this.isExecutable(packagePath)) { return packagePath; } } catch { // Package not installed - expected for development } return null; } private async isExecutable(filePath: string): Promise { try { await plugins.fs.promises.access(filePath, plugins.fs.constants.X_OK); return true; } catch { return false; } } private async findInPath(binaryName: string): Promise { 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; } }