chdman
Version:
💿 chdman binaries and wrapper for Node.js.
204 lines • 8.72 kB
JavaScript
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