'use strict'; var Releases = module.exports; var Fs = require('node:fs/promises'); var Os = require('node:os'); var path = require('path'); var cache = {}; var LEGACY_CACHE_DIR = path.join(Os.homedir(), '.cache/webi/legacy'); // Sort releases by ext preference and libc within the same version. // The cache is already sorted by version (stable before beta, newest first), // so we only re-order within the same version string. function createFormatsSorter(formats) { return function sortByExtLibc(a, b) { if (a.version !== b.version) { // Array.sort is stable (V8, ES2019), so returning 0 across // versions preserves the cache's pre-sorted version-desc order. return 0; } var aExtPri = formats.indexOf(a.ext.replace(/tar\..*/, 'tar')); var bExtPri = formats.indexOf(b.ext.replace(/tar\..*/, 'tar')); if (aExtPri > bExtPri) { return -1; } if (aExtPri < 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: '', releases: [{ version, date, os, arch, lts, channel, download}] } if (cache[pkg]) { return cache[pkg]; } let dataFile = `${LEGACY_CACHE_DIR}/${pkg}.json`; let json = await Fs.readFile(dataFile, 'utf8').catch(function (err) { if (err.code === 'ENOENT') { return null; } throw err; }); if (!json) { let empty = { download: '', releases: [] }; cache[pkg] = empty; return empty; } let all; try { all = JSON.parse(json); } catch (e) { console.error(`error: ${dataFile}:\n\t${e.message}`); let empty = { download: '', releases: [] }; cache[pkg] = empty; return empty; } cache[pkg] = all; return 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 sortByExtLibc = createFormatsSorter(rformats); var reVer = new RegExp('^' + ver + '\\b'); function selectMatches(rel) { /* jshint maxcomplexity: 25 */ if (os) { // '*' = any OS (matches anything, including windows). // 'posix' / 'posix_20xx' = any POSIX OS (matches linux, macos, // freebsd, etc., but NOT windows). let isPosix = rel.os === 'posix' || rel.os.startsWith('posix_20'); let osMatches = rel.os === '*' || rel.os === os || (isPosix && os !== 'windows'); if (!osMatches) { return false; } } if (arch) { if (rel.arch !== '*') { if (rel.arch !== arch) { return false; } } } // libc='libc' is serve-releases.js's default when the caller // didn't pin one — treat it as 'no preference', not a filter. let isMeaningfulLibc = libc && libc !== 'libc' && rel.libc !== 'none'; if (isMeaningfulLibc) { 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(sortByExtLibc); //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 Releases .getReleases({ 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)); }); }