mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-02-14 17:49:53 +00:00
435 lines
11 KiB
JavaScript
435 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
var Releases = module.exports;
|
|
|
|
var path = require('path');
|
|
var _normalize = require('./normalize.js');
|
|
|
|
var cache = {};
|
|
//var staleAge = 5 * 1000;
|
|
//var expiredAge = 15 * 1000;
|
|
var staleAge = 5 * 60 * 1000;
|
|
var expiredAge = 15 * 60 * 1000;
|
|
|
|
let installerDir = path.join(__dirname, '..');
|
|
|
|
Releases.get = async function (pkgdir) {
|
|
let get;
|
|
try {
|
|
get = require(`${pkgdir}/releases.js`);
|
|
// TODO update all releases files with module.exports.xxxx = 'foo';
|
|
if (!get.latest) {
|
|
get.latest = get;
|
|
}
|
|
} catch (e) {
|
|
let err = new Error('no releases.js for', pkgdir.split(/[\/\\]+/).pop());
|
|
err.code = 'E_NO_RELEASE';
|
|
throw err;
|
|
}
|
|
|
|
let all = await get.latest();
|
|
|
|
return _normalize(all);
|
|
};
|
|
|
|
// TODO needs a proper test, and more accurate (though perhaps far less simple) code
|
|
function createFormatsSorter(formats) {
|
|
return function sortByVerExt(a, b) {
|
|
function lexver(semver) {
|
|
// v1.20.156 => 00001.00020.00156.zzzzz
|
|
// TODO BUG: v1.20.156-rc2 => 00001.00020.00156.rc2zz
|
|
var parts = semver.split(/[+\.\-]/g);
|
|
while (parts.length < 4) {
|
|
parts.push('');
|
|
}
|
|
return parts
|
|
.map(function (num, i) {
|
|
if (3 === i) {
|
|
return num.toString().padEnd(10, 'z');
|
|
}
|
|
return num.toString().padStart(10, '0');
|
|
})
|
|
.join('.');
|
|
}
|
|
|
|
var aver = lexver(a.version);
|
|
var bver = lexver(b.version);
|
|
if (aver > bver) {
|
|
//console.log(aver, '>', bver);
|
|
return -1;
|
|
}
|
|
if (aver < bver) {
|
|
//console.log(aver, '<', bver);
|
|
return 1;
|
|
}
|
|
|
|
var aExtPri = formats.indexOf(a.ext.replace(/tar\..*/, 'tar'));
|
|
var bExtPri = formats.indexOf(b.ext.replace(/tar\..*/, 'tar'));
|
|
if (aExtPri > bExtPri) {
|
|
//console.log(a.ext, aExtPri, '>', b.ext, bExtPri);
|
|
return -1;
|
|
}
|
|
if (aExtPri < bExtPri) {
|
|
//console.log(a.ext, aExtPri, '<', b.ext, bExtPri);
|
|
return 1;
|
|
}
|
|
|
|
// rank builds that don't depend on any form of libc first
|
|
if (a.libc === 'none' && b.libc !== 'none') {
|
|
return -1;
|
|
}
|
|
if (a.libc !== 'none' && b.libc === 'none') {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
}
|
|
|
|
async function getCachedReleases(pkg) {
|
|
// returns { download: '<template string>', releases: [{ version, date, os, arch, lts, channel, download}] }
|
|
|
|
async function chainCachePromise(fn) {
|
|
cache[pkg].promise = cache[pkg].promise.then(fn);
|
|
return cache[pkg].promise;
|
|
}
|
|
|
|
async function sleep(ms) {
|
|
return await new Promise(function (resolve, reject) {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
async function putCache() {
|
|
var age = Date.now() - cache[pkg].updatedAt;
|
|
if (age < staleAge) {
|
|
//console.debug('NOT STALE ANYMORE - updated in previous promise');
|
|
return cache[pkg].all;
|
|
}
|
|
|
|
//console.debug('DOWNLOADING NEW "%s" releases', pkg);
|
|
var pkgdir = path.join(installerDir, pkg);
|
|
|
|
// workaround for request timeout seeming to not work
|
|
let complete = false;
|
|
await Promise.race([
|
|
Releases.get(pkgdir)
|
|
.catch(function (err) {
|
|
if ('E_NO_RELEASE' === err.code) {
|
|
let all = { _error: 'E_NO_RELEASE', download: '', releases: [] };
|
|
return all;
|
|
}
|
|
|
|
throw err;
|
|
})
|
|
.catch(function (err) {
|
|
let hasReleases = cache[pkg].all?.releases?.length > 1;
|
|
if (!hasReleases) {
|
|
throw err;
|
|
}
|
|
|
|
console.error(`Error: the BOOGEYMAN got us!`);
|
|
console.error(err.stack);
|
|
|
|
return cache[pkg].all;
|
|
})
|
|
.then(function (all) {
|
|
// Note: it is possible for slightly older data
|
|
// to replace slightly newer data, but this is better
|
|
// than being in a cycle where release updates _always_
|
|
// take longer than expected.
|
|
//console.debug('DOWNLOADED NEW "%s" releases', pkg);
|
|
cache[pkg].updatedAt = Date.now();
|
|
cache[pkg].all = all;
|
|
complete = true;
|
|
}),
|
|
sleep(15000).then(function () {
|
|
if (complete) {
|
|
return;
|
|
}
|
|
console.error(`request timeout waiting for '${pkg}' release info`);
|
|
}),
|
|
]);
|
|
|
|
return cache[pkg].all;
|
|
}
|
|
|
|
if (!cache[pkg]) {
|
|
cache[pkg] = {
|
|
updatedAt: 0,
|
|
all: { download: '', releases: [] },
|
|
promise: Promise.resolve(),
|
|
};
|
|
}
|
|
|
|
var bgRenewal;
|
|
var age = Date.now() - cache[pkg].updatedAt;
|
|
var fresh = age < staleAge;
|
|
if (!fresh) {
|
|
bgRenewal = chainCachePromise(putCache);
|
|
}
|
|
|
|
var tooStale = age > expiredAge;
|
|
if (!tooStale) {
|
|
return await cache[pkg].all;
|
|
}
|
|
|
|
return await Promise.race([
|
|
bgRenewal,
|
|
sleep(5000).then(function () {
|
|
return cache[pkg].all;
|
|
}),
|
|
]);
|
|
}
|
|
|
|
async function filterReleases(
|
|
all,
|
|
{ ver, os, arch, libc, lts, channel, formats, limit },
|
|
) {
|
|
// When multiple formats are downloadable (i.e. .zip and .pkg)
|
|
// sort the most compatible format first
|
|
// (i.e. so that we don't do .pkg on linux except on purpose)
|
|
var rformats = formats.slice(0).reverse();
|
|
var sortByVerExt = createFormatsSorter(rformats);
|
|
var reVer = new RegExp('^' + ver + '\\b');
|
|
|
|
function selectMatches(rel) {
|
|
if (os) {
|
|
if (rel.os !== '*') {
|
|
if (rel.os !== os) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (arch) {
|
|
if (rel.arch !== '*') {
|
|
if (rel.arch !== arch) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rel.libc !== 'none') {
|
|
let releaseRequiresMusl = rel.libc === 'musl';
|
|
// goal: handle non-glibc (Alpine / Docker / musl)
|
|
let osHasMusl = libc === 'musl';
|
|
if (osHasMusl) {
|
|
// goal: fail if dependent on libc
|
|
let releaseRequiresLibc = rel.libc === 'gnu';
|
|
if (releaseRequiresLibc) {
|
|
return false;
|
|
}
|
|
} else if (releaseRequiresMusl) {
|
|
// goal: don't use musl++ on glibc (Ubuntu, GNU, etc)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (lts) {
|
|
if (!rel.lts) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (channel) {
|
|
if (rel.channel !== channel) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// to match 'tar.gz' and 'tar.xz' with just 'tar'
|
|
function hasExt(ext) {
|
|
return rel.ext.match(ext);
|
|
}
|
|
if (formats.length) {
|
|
if (!formats.some(hasExt)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (ver) {
|
|
if (!rel.version.match(reVer)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
var sortedRels = all.releases.filter(selectMatches).sort(sortByVerExt);
|
|
//console.log(sortedRels.slice(0, 4));
|
|
|
|
return sortedRels.slice(0, limit || 1000);
|
|
}
|
|
|
|
Releases.getReleases = function ({
|
|
_count,
|
|
pkg,
|
|
ver,
|
|
os,
|
|
arch,
|
|
libc,
|
|
lts,
|
|
channel,
|
|
formats,
|
|
limit,
|
|
}) {
|
|
if (!_count) {
|
|
_count = 0;
|
|
}
|
|
return getCachedReleases(pkg).then(function (all) {
|
|
return filterReleases(all, {
|
|
ver,
|
|
os,
|
|
arch,
|
|
libc,
|
|
lts,
|
|
channel,
|
|
formats,
|
|
limit,
|
|
})
|
|
.catch(function (err) {
|
|
if ('MODULE_NOT_FOUND' === err.code) {
|
|
return null;
|
|
}
|
|
console.error(
|
|
'TODO: lib/release.js: check type of error, such as MODULE_NOT_FOUND',
|
|
);
|
|
console.error(err);
|
|
})
|
|
.then(function (releases) {
|
|
if (releases.length) {
|
|
return {
|
|
oses: all.oses,
|
|
arches: all.arches,
|
|
libcs: all.libcs,
|
|
formats: all.formats,
|
|
releases: releases,
|
|
};
|
|
}
|
|
if (_count < 1) {
|
|
// Apple Silicon M1 hacky-do workaround fix
|
|
if ('macos' === os && 'arm64' === arch) {
|
|
return Releases.getReleases({
|
|
pkg,
|
|
ver,
|
|
os,
|
|
arch: 'amd64',
|
|
libc,
|
|
lts,
|
|
channel,
|
|
formats,
|
|
limit,
|
|
});
|
|
}
|
|
// Windows ARM hacky-do workaround fix
|
|
if ('windows' === os && 'arm64' === arch) {
|
|
return Releases.getReleases({
|
|
pkg,
|
|
ver,
|
|
os,
|
|
arch: 'amd64',
|
|
libc,
|
|
lts,
|
|
channel,
|
|
formats,
|
|
limit,
|
|
});
|
|
}
|
|
// Raspberry Pi 3+ on Ubuntu arm64 (via Bionic?)
|
|
if ('linux' === os && 'arm64' === arch) {
|
|
return Releases.getReleases({
|
|
_count: _count + 1,
|
|
pkg,
|
|
ver,
|
|
os,
|
|
arch: 'armv7l',
|
|
libc,
|
|
lts,
|
|
channel,
|
|
formats,
|
|
limit,
|
|
});
|
|
}
|
|
// armv7 can run armv6
|
|
if ('linux' === os && 'armv7l' === arch) {
|
|
return Releases.getReleases({
|
|
_count: _count + 1,
|
|
pkg,
|
|
ver,
|
|
os,
|
|
arch: 'armv6l',
|
|
libc,
|
|
lts,
|
|
channel,
|
|
formats,
|
|
limit,
|
|
});
|
|
}
|
|
}
|
|
if (_count < 2) {
|
|
// Raspberry Pi 3+ on Raspbian arm7 (not Ubuntu arm64)
|
|
// hail mary
|
|
if ('linux' === os && 'armv7l' === arch) {
|
|
return Releases.getReleases({
|
|
_count: _count + 1,
|
|
pkg,
|
|
ver,
|
|
os,
|
|
arch: 'arm64',
|
|
libc,
|
|
lts,
|
|
channel,
|
|
formats,
|
|
limit,
|
|
});
|
|
}
|
|
}
|
|
releases = [
|
|
{
|
|
name: 'doesntexist.ext',
|
|
version: '0.0.0',
|
|
lts: '-',
|
|
channel: 'error',
|
|
date: '1970-01-01',
|
|
os: os || '-',
|
|
arch: arch || '-',
|
|
libc: libc || '-',
|
|
ext: 'err',
|
|
download: 'https://example.com/doesntexist.ext',
|
|
comment:
|
|
'No matches found. Could be bad or missing version info' +
|
|
',' +
|
|
"Check query parameters. Should be something like '/api/releases/{package}@{version}.tab?os={macos|linux|windows|-}&arch={amd64|x86|aarch64|arm64|armv7l|-}&libc={musl|gnu|msvc|libc|static}&limit=10'",
|
|
},
|
|
];
|
|
return {
|
|
oses: all.oses,
|
|
arches: all.arches,
|
|
libcs: all.libcs,
|
|
formats: all.formats,
|
|
releases: releases,
|
|
};
|
|
});
|
|
});
|
|
};
|
|
|
|
if (require.main === module) {
|
|
return module
|
|
.exports({
|
|
pkg: 'node',
|
|
ver: '',
|
|
os: 'macos',
|
|
arch: 'amd64',
|
|
lts: true,
|
|
libc: 'libc',
|
|
channel: 'stable',
|
|
formats: ['tar', 'exe', 'zip', 'xz', 'dmg', 'pkg'],
|
|
limit: 10,
|
|
})
|
|
.then(function (all) {
|
|
console.info(JSON.stringify(all));
|
|
});
|
|
}
|