141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
|
|
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;
|
||
|
|
}
|
||
|
|
}
|