install-purescript
Version:
Install PureScript to a given directory
298 lines (249 loc) • 7.38 kB
JavaScript
const {constants: {BROTLI_PARAM_SIZE_HINT}, brotliCompress, createBrotliDecompress} = require('zlib');
const {createReadStream, lstat, stat} = require('fs');
const {execFile} = require('child_process');
const {join, normalize} = require('path');
const {promisify} = require('util');
const {Writable} = require('stream');
const arch = require('arch');
const {create, Unpack} = require('tar');
const downloadOrBuildPurescript = require('download-or-build-purescript');
const {get: {info: getCacheInfo}, put: putCache, rm: {entry: removeCache}, tmp: {withTmp}, verify} = require('npcache');
const inspectWithKind = require('inspect-with-kind');
const isPlainObj = require('is-plain-obj');
const Observable = require('zen-observable');
const pump = require('pump');
const runInDir = require('run-in-dir');
function addId(obj, id) {
Object.defineProperty(obj, 'id', {
value: id,
writable: true
});
return obj;
}
const CACHE_KEY = 'install-purescript:binary';
const MAX_READ_SIZE = 30 * 1024 * 1024;
const defaultBinName = `purs${process.platform === 'win32' ? '.exe' : ''}`;
const cacheIdSuffix = `-${process.platform}-${arch()}`;
module.exports = function installPurescript(...args) {
return new Observable(observer => {
const argLen = args.length;
if (argLen > 1) {
const error = new RangeError(`Exepcted 0 or 1 argument ([<Object>]), but got ${argLen} arguments.`);
error.code = 'ERR_TOO_MANY_ARGS';
throw error;
}
const [options = {}] = args;
if (args.length === 1) {
if (!isPlainObj(options)) {
throw new TypeError(`Expected an object to set install-purescript options, but got ${
inspectWithKind(options)
}.`);
}
if (options.forceReinstall !== undefined && typeof options.forceReinstall !== 'boolean') {
throw new TypeError(`Expected \`forceReinstall\` option to be a Boolean value, but got ${
inspectWithKind(options.forceReinstall)
}.`);
}
}
const subscriptions = new Set();
function cancelInstallation() {
for (const subscription of subscriptions) {
subscription.unsubscribe();
}
}
const binName = typeof options.rename === 'function' ? normalize(`${options.rename(defaultBinName)}`) : defaultBinName;
const cwd = process.cwd();
const binPath = join(cwd, binName);
const cacheId = `${options.version || downloadOrBuildPurescript.defaultVersion}${cacheIdSuffix}`;
function main({brokenCacheFound = false} = {}) {
const cacheCleaning = (async () => {
if (brokenCacheFound) {
try {
await removeCache(CACHE_KEY);
} catch {}
}
try {
await verify();
} catch {}
})();
runInDir(cwd, () => subscriptions.add(downloadOrBuildPurescript(options).subscribe({
next(val) {
observer.next(val);
},
async error(err) {
await cacheCleaning;
observer.error(err);
},
async complete() {
const writeCacheValue = {id: 'write-cache'};
const tarBuffers = [];
const tarCreateOptions = {
cwd,
maxReadSize: MAX_READ_SIZE,
noDirRecurse: true,
strict: true,
statCache: new Map()
};
let tarSize = 0;
try {
const binStat = await promisify(lstat)(binPath);
tarCreateOptions.statCache.set(binPath, binStat);
writeCacheValue.originalSize = binStat.size;
} catch {}
observer.next(writeCacheValue);
try {
await Promise.all([
promisify(pump)(create(tarCreateOptions, [binName]), new Writable({
write(data, _, cb) {
tarBuffers.push(data);
tarSize += data.length;
cb();
}
})),
(async () => {
await cacheCleaning;
// Ensure the path where the current npm config regards as a cache directory
// is actually available, before performing long-running compression
await withTmp(async () => {});
})()
]);
const decomressed = await promisify(brotliCompress)(Buffer.concat(tarBuffers, tarSize), {
params: {
[BROTLI_PARAM_SIZE_HINT]: tarSize
}
});
await putCache(CACHE_KEY, decomressed, {
size: decomressed.size,
metadata: {
id: cacheId
}
});
} catch (err) {
observer.next({
id: 'write-cache:fail',
error: addId(err, 'write-cache')
});
observer.complete();
return;
}
observer.next({id: 'write-cache:complete'});
observer.complete();
}
})));
}
if (options.forceReinstall) {
main();
return cancelInstallation;
}
const tmpSubscription = downloadOrBuildPurescript(options).subscribe({
error(err) {
observer.error(err);
}
});
(async () => {
const searchCacheValue = {
id: 'search-cache',
found: false
};
let id;
let cachePath;
try {
const [info] = await Promise.all([
getCacheInfo(CACHE_KEY),
(async () => {
await promisify(setImmediate)();
tmpSubscription.unsubscribe();
})(),
(async () => {
try {
if ((await promisify(stat)(binPath)).isDirectory()) {
const error = new Error(`Tried to create a PureScript binary at ${binPath}, but a directory already exists there.`);
error.code = 'EISDIR';
error.path = binPath;
observer.error(error);
}
} catch (_) {}
})()
]);
id = info.metadata.id;
cachePath = info.path;
} catch (_) {
if (observer.closed) {
return;
}
observer.next(searchCacheValue);
main();
return;
}
if (observer.closed) {
return;
}
if (id !== cacheId) {
observer.next(searchCacheValue);
main({brokenCacheFound: true});
return;
}
searchCacheValue.found = true;
searchCacheValue.path = cachePath;
observer.next(searchCacheValue);
observer.next({id: 'restore-cache'});
try {
let fileCount = 0;
await promisify(pump)(createReadStream(cachePath), createBrotliDecompress(), new Unpack({
strict: true,
cwd,
filter(_, entry) {
entry.path = binName;
entry.header.path = binName;
entry.absolute = binPath;
const isFile = entry.type === 'File';
fileCount += Number(isFile);
return isFile;
}
}));
if (fileCount !== 1) {
const error = new Error(`Expected a cached PureScript binary archive ${cachePath} contains 1 file, but found ${fileCount}.`);
error.code = 'EINVALIDCACHE';
throw error;
}
} catch (err) {
observer.next({
id: 'restore-cache:fail',
error: addId(err, 'restore-cache')
});
main({brokenCacheFound: true});
return;
}
observer.next({id: 'restore-cache:complete'});
observer.next({id: 'check-binary'});
try {
await promisify(execFile)(binPath, ['--version'], {timeout: 8000, ...options});
} catch (err) {
observer.next({
id: 'check-binary:fail',
error: addId(err, 'check-binary')
});
main({brokenCacheFound: true});
return;
}
observer.next({id: 'check-binary:complete'});
observer.complete();
})();
return cancelInstallation;
});
};
Object.defineProperties(module.exports, {
cacheKey: {
enumerable: true,
value: CACHE_KEY
},
defaultVersion: {
enumerable: true,
value: downloadOrBuildPurescript.defaultVersion
},
supportedBuildFlags: {
enumerable: true,
value: downloadOrBuildPurescript.supportedBuildFlags
}
});
;