UNPKG

chdman

Version:

💿 chdman binaries and wrapper for Node.js.

204 lines • 8.72 kB
import which from 'which'; import util from 'node:util'; import fs from 'node:fs'; import child_process from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import stream from 'node:stream'; import crypto from 'node:crypto'; import { Mutex } from 'async-mutex'; export var ChdmanBinaryPreference; (function (ChdmanBinaryPreference) { ChdmanBinaryPreference[ChdmanBinaryPreference["PREFER_BUNDLED_BINARY"] = 1] = "PREFER_BUNDLED_BINARY"; ChdmanBinaryPreference[ChdmanBinaryPreference["PREFER_PATH_BINARY"] = 2] = "PREFER_PATH_BINARY"; })(ChdmanBinaryPreference || (ChdmanBinaryPreference = {})); /** * Code to find and interact with the `chdman` binary. */ export default class ChdmanBin { static CHDMAN_BIN; static CHDMAN_BIN_MUTEX = new Mutex(); static async getBinPath(binaryPreference) { if (this.CHDMAN_BIN) { return this.CHDMAN_BIN; } return this.CHDMAN_BIN_MUTEX.runExclusive(async () => { if (this.CHDMAN_BIN) { return this.CHDMAN_BIN; } if ((binaryPreference ?? ChdmanBinaryPreference.PREFER_BUNDLED_BINARY) === ChdmanBinaryPreference.PREFER_BUNDLED_BINARY) { const pathBundled = await this.getBinPathBundled(); this.CHDMAN_BIN = pathBundled ?? (await this.getBinPathExisting()); } else { const pathExisting = await this.getBinPathExisting(); this.CHDMAN_BIN = pathExisting ?? (await this.getBinPathBundled()); } return this.CHDMAN_BIN; }); } static async getBinPathBundled() { const bunPath = await this.getBinPathBundledBun(); if (bunPath !== undefined) { return bunPath; } try { const chdman = await import(`@emmercm/chdman-${process.platform}-${process.arch}`); const prebuilt = chdman.default; try { await util.promisify(fs.stat)(prebuilt); return prebuilt; } catch { /* ignored */ } } catch { /* ignored */ } return undefined; } /** * Look for chdman binaries bundled with: * `bun build --compile --asset-naming="[name].[ext]" chdman *.dylib` */ static async getBinPathBundledBun() { try { const { embeddedFiles } = await import('bun'); // Find all files that might be chdman-related const chdmanBlob = embeddedFiles.find((blob) => { // @ts-expect-error https://github.com/oven-sh/bun/issues/20700 const blobName = blob.name; return blobName.toLowerCase().startsWith('chdman'); }); if (chdmanBlob !== undefined) { // Create the temporary directory const hash = crypto.createHash('md5'); await chdmanBlob.stream().pipeTo(new WritableStream({ write(chunk) { hash.update(chunk); }, })); const temporaryDirectory = await this.getTemporaryDirectory(`chdman-${hash.digest('hex').slice(0, 7)}`); // Find additional files that might be necessary const dylibBlobs = embeddedFiles.filter((blob) => { // @ts-expect-error https://github.com/oven-sh/bun/issues/20700 const blobName = blob.name; return blobName.toLowerCase().endsWith('.dylib'); }); // Extract all files if necessary const temporaryBlobs = await Promise.all([chdmanBlob, ...dylibBlobs].map(async (blob) => { // @ts-expect-error https://github.com/oven-sh/bun/issues/20700 const blobName = blob.name.replace(/-[\da-z]{8}\./, '.').replace(/\.+$/, ''); const temporaryBlob = path.join(temporaryDirectory, blobName); try { await util.promisify(fs.stat)(temporaryBlob); return temporaryBlob; } catch { /* ignored */ } const writableStream = stream.Writable.toWeb(fs.createWriteStream(temporaryBlob)); await blob.stream().pipeTo(writableStream); await util.promisify(fs.chmod)(temporaryBlob, 0o755); // chmod +x return temporaryBlob; })); return temporaryBlobs.find((temporaryBlob) => path.basename(temporaryBlob).startsWith('chdman')); } } catch { /* ignored */ } return undefined; } static async getTemporaryDirectory(temporaryDirectoryBasename) { const candidateDirectories = [ path.join(os.tmpdir(), temporaryDirectoryBasename), path.join(process.cwd(), '.chdman', temporaryDirectoryBasename), path.join(os.homedir(), '.chdman', temporaryDirectoryBasename), ]; /* eslint-disable no-await-in-loop */ for (const candidateDirectory of candidateDirectories) { try { try { await util.promisify(fs.stat)(candidateDirectory); } catch { await util.promisify(fs.mkdir)(candidateDirectory, { recursive: true }); } const temporaryFile = path.join(candidateDirectory, temporaryDirectoryBasename); await util.promisify(fs.writeFile)(temporaryFile, temporaryFile); await util.promisify(fs.unlink)(temporaryFile); return candidateDirectory; } catch { /* ignored */ } } throw new Error("couldn't find a suitable temporary directory"); } static async getBinPathExisting() { const resolved = await which(process.platform === 'win32' ? 'chdman.exe' : 'chdman', { nothrow: true }); if (resolved) { return resolved; } return undefined; } /** * Run chdman with some arguments. */ static async run(arguments_, options) { const chdmanBin = await ChdmanBin.getBinPath(options?.binaryPreference); if (!chdmanBin) { throw new Error('chdman not found'); } const inputIndex = arguments_.indexOf('--input'); if (inputIndex !== -1 && (inputIndex + 1) < arguments_.length) { const inputPath = arguments_[inputIndex + 1]; try { await util.promisify(fs.stat)(inputPath); } catch { throw new Error(`input file doesn't exist: ${inputPath}`); } } // if (process.platform === 'darwin' // && !fs.existsSync(path.join('Library', 'Frameworks', 'SDL2.framework'))) { // throw new Error('chdman requires the SDL2 framework to be installed on macOS'); // } return new Promise((resolve, reject) => { const proc = child_process.spawn(chdmanBin, arguments_, { windowsHide: true }); let killed = false; const chunks = []; proc.stdout.on('data', (chunk) => { if (options?.logStd) { console.log(chunk.toString()); } chunks.push(chunk); }); proc.stderr.on('data', (chunk) => { if (options?.logStd) { console.error(chunk.toString()); } chunks.push(chunk); if (chunk.toString().includes('nan% complete')) { // chdman can hang forever on input files that aren't valid (i.e. too small) proc.kill(); killed = true; } if (/(10[1-9]|1[1-9]\d|[2-9]\d\d+)\.\d+%/.test(chunk.toString())) { // chdman can waste a lot of time extracting bad files proc.kill(); killed = true; } }); proc.on('close', (code) => { const output = Buffer.concat(chunks).toString().trim(); if ((code !== null && code !== 0) || killed) { return reject(output); } return resolve(output); }); proc.on('error', () => { const output = Buffer.concat(chunks).toString().trim(); reject(output); }); }); } } //# sourceMappingURL=chdmanBin.js.map