UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

247 lines 10.7 kB
// 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