@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
247 lines • 10.7 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import fs from 'node:fs';
import * as helpers from '../helpers.js';
import { Templates } from '../templates.js';
import { ShellRunner } from '../shell-runner.js';
import { MissingArgumentError } from '../errors/missing-argument-error.js';
import { SoloError } from '../errors/solo-error.js';
import { PathEx } from '../../business/utils/path-ex.js';
import { OperatingSystem } from '../../business/utils/operating-system.js';
import path from 'node:path';
import { SemanticVersion } from '../../business/utils/semantic-version.js';
/**
* Base class for dependency managers that download and manage CLI tools
* Common functionality for downloading, checking versions, and managing executables
*/
export class BaseDependencyManager extends ShellRunner {
downloader;
installationDirectory;
requiredVersion;
downloadBaseUrl;
osArch;
localExecutableWithPath;
globalExecutablePath = '';
artifactName;
downloadURL;
checksumURL;
executableName;
constructor(downloader, installationDirectory, osArch, requiredVersion, dependencyName, downloadBaseUrl) {
super();
this.downloader = downloader;
this.installationDirectory = installationDirectory;
this.requiredVersion = requiredVersion;
this.downloadBaseUrl = downloadBaseUrl;
if (!installationDirectory) {
throw new MissingArgumentError('installation directory is required');
}
if (!downloader) {
throw new MissingArgumentError('package downloader is required');
}
// Normalize architecture naming - many tools use 'amd64' instead of 'x64'
this.osArch = ['x64', 'x86-64'].includes(osArch) ? 'amd64' : osArch;
// Set the path to the local installation
this.localExecutableWithPath = Templates.localInstallationExecutableForDependency(dependencyName, installationDirectory);
this.executableName = path.basename(this.localExecutableWithPath);
// Set artifact name and URLs - these will be overridden by child classes
this.artifactName = this.getArtifactName();
this.downloadURL = this.getDownloadURL();
this.checksumURL = this.getChecksumURL();
}
getArch() {
let arch = this.osArch;
if (arch === 'x64') {
arch = 'amd64';
}
else if (arch === 'arm64' || arch === 'aarch64') {
arch = 'arm64';
}
return arch;
}
/**
* Get the executable to run
*/
async getExecutable() {
return this.executableName;
}
/**
* Find the global executable by scanning PATH directories directly in Node.js.
* This avoids spawning a shell subprocess (which, command -v, where) whose
* behaviour varies across shells and CI runner environments.
*/
getGlobalExecutableWithPath() {
if (this.globalExecutablePath) {
return this.globalExecutablePath;
}
const executableNames = OperatingSystem.isWin32()
? [`${this.executableName}.exe`, `${this.executableName}.cmd`, this.executableName]
: [this.executableName];
const pathDirectories = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
this.logger.debug(`Searching PATH for ${this.executableName}: [${pathDirectories.join(', ')}]`);
for (const directory of pathDirectories) {
for (const name of executableNames) {
const candidate = path.join(directory, name);
try {
// On Windows X_OK is not supported and silently degrades to F_OK;
// executability is determined by file extension (.exe/.cmd) already.
fs.accessSync(candidate, OperatingSystem.isWin32() ? fs.constants.F_OK : fs.constants.X_OK);
this.logger.debug(`Found ${this.executableName} at ${candidate}`);
this.globalExecutablePath = candidate;
return candidate;
}
catch {
// not found or not executable in this directory — continue
}
}
}
this.logger.warn(`${this.executableName} was not found in PATH`);
return false;
}
/**
* Check if the given installation meets version requirements
*/
async installationMeetsRequirements(executableWithPath) {
let version;
try {
version = await this.getVersion(executableWithPath);
}
catch (error) {
this.logger.debug(`Failed to get version for ${this.executableName} at ${executableWithPath}: ${error instanceof Error ? error.message : error}`);
return false;
}
if (new SemanticVersion(version).greaterThanOrEqual(this.getRequiredVersion())) {
return true;
}
this.logger.info(`Found version ${version} of ${this.executableName} at ${executableWithPath}, which does not meet the required version ${this.getRequiredVersion()}`);
return false;
}
/**
* Check if the tool is installed globally and meets requirements
*/
async isInstalledGloballyAndMeetsRequirements() {
const path = this.getGlobalExecutableWithPath();
try {
if (path && (await this.installationMeetsRequirements(path))) {
return true;
}
else {
this.logger.info(`${this.executableName}${path ? ` at ${path}` : ''} is not a compatible global installation`);
}
}
catch (error) {
this.logger.debug(`Global installation of ${this.executableName} does not meet version requirements: ${error instanceof Error ? error.message : error}`);
}
return false;
}
/**
* Check if the tool is installed locally and meets requirements
*/
async isInstalledLocallyAndMeetsRequirements() {
try {
if (!this.isInstalledLocally()) {
this.logger.info(`${this.executableName} is not installed locally at ${this.localExecutableWithPath}`);
return false;
}
if (await this.installationMeetsRequirements(this.localExecutableWithPath)) {
return true;
}
this.logger.info(`${this.executableName} at ${this.localExecutableWithPath} is installed locally but does not meet version requirements`);
}
catch (error) {
this.logger.debug(`Local installation of ${this.executableName} does not meet version requirements: ${error instanceof Error ? error.message : error}`);
}
return false;
}
/**
* Check if the tool is installed locally
*/
isInstalledLocally() {
return fs.existsSync(this.localExecutableWithPath);
}
/**
* Uninstall the local version
*/
uninstallLocal() {
if (this.isInstalledLocally()) {
fs.rmSync(this.localExecutableWithPath);
}
}
/**
* Hook for any pre-installation steps
*/
async preInstall() { }
/**
* Hook to determine if installation should proceed
* Child classes can override this for custom logic
*/
async shouldInstall() {
return true;
}
/**
* Determine if checksum verification should be performed
* Child classes can override this if needed
*/
getVerifyChecksum() {
return true;
}
/**
* Install the tool
*/
async install(temporaryDirectory = helpers.getTemporaryDirectory()) {
if (this.installationDirectory === temporaryDirectory) {
throw new SoloError('Installation directory cannot be the same as temporary directory');
}
if (!(await this.shouldInstall())) {
this.logger.debug(`Skipping installation of ${this.executableName}`);
return true;
}
await this.preInstall();
// Check if it is already installed locally
if (await this.isInstalledLocallyAndMeetsRequirements()) {
const localVersion = await this.getVersion(this.localExecutableWithPath).catch(() => this.getRequiredVersion());
this.logger.showUser(`Compatible ${this.executableName} v${localVersion} found at ${this.localExecutableWithPath}`);
return true;
}
// If it is installed globally and meets requirements, use the global installation
if (await this.isInstalledGloballyAndMeetsRequirements()) {
const globalVersion = await this.getVersion(this.globalExecutablePath).catch(() => this.getRequiredVersion());
this.logger.showUser(`Compatible ${this.executableName} v${globalVersion} found at ${this.globalExecutablePath}`);
return true;
}
// If not installed, download and install
this.logger.showUser(`Compatible ${this.executableName} ${this.getRequiredVersion()} was not found locally or globally. ` +
`Downloading and installing it into ${this.installationDirectory}...`);
this.logger.debug(`Downloading and installing ${this.executableName} executable...`);
const packageFile = await this.downloader.fetchPackage(this.getDownloadURL(), this.getChecksumURL(), temporaryDirectory, this.getVerifyChecksum());
const processedFiles = await this.processDownloadedPackage(packageFile, temporaryDirectory);
if (!fs.existsSync(this.installationDirectory)) {
fs.mkdirSync(this.installationDirectory, { recursive: true });
}
// In case there is an existing local installation, which did not meet the requirements - remove it
this.uninstallLocal();
try {
for (const processedFile of processedFiles) {
const fileName = path.basename(processedFile);
const localExecutable = PathEx.join(this.installationDirectory, fileName);
fs.cpSync(processedFile, localExecutable);
fs.chmodSync(localExecutable, 0o755);
}
}
catch (error) {
throw new SoloError(`Failed to install ${this.executableName}: ${error.message}`);
}
this.logger.showUser(`Installed ${this.executableName} ${this.getRequiredVersion()} into ${this.installationDirectory}.`);
return this.isInstalledLocally();
}
/**
* Get the tool's required version
*/
getRequiredVersion() {
return this.requiredVersion;
}
/**
* Hook for setting up any configuration after installation
* Child classes can override this if needed
*/
setupConfig() { }
}
//# sourceMappingURL=base-dependency-manager.js.map