mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-05-29 12:02:52 +00:00
Compare commits
41 Commits
next
...
list-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0471206ec | ||
|
|
d0c0805a1f | ||
|
|
edfb064075 | ||
|
|
22d48c12cb | ||
|
|
3b3df248d1 | ||
|
|
00e828645b | ||
|
|
9b9dc6d4c9 | ||
|
|
5509686fba | ||
|
|
76e21b6e0a | ||
|
|
060f9b7026 | ||
|
|
f28bd86ce8 | ||
|
|
d4458c1076 | ||
|
|
619d77c6fa | ||
|
|
0111df7bb7 | ||
|
|
ce9c111fa9 | ||
|
|
48ca227feb | ||
|
|
8d8f809563 | ||
|
|
70121fc60c | ||
|
|
d0d2dce17c | ||
|
|
1eb28c7d22 | ||
|
|
c6fe0f7fb6 | ||
|
|
fb74b98960 | ||
|
|
867fe16f27 | ||
|
|
40766bea4c | ||
|
|
65c27afe49 | ||
|
|
36c3de2ffc | ||
|
|
1b10b18454 | ||
|
|
8abff3f0b9 | ||
|
|
14bb497303 | ||
|
|
2a785a86dc | ||
|
|
1d3f733cc4 | ||
|
|
1f1ea2b8ba | ||
|
|
8f92da28bb | ||
|
|
d102f18480 | ||
|
|
e85db4ecc4 | ||
|
|
f5b1de7751 | ||
|
|
f93ca34e68 | ||
|
|
de92e02bb1 | ||
|
|
c11bc55d5b | ||
|
|
f24df1a731 | ||
|
|
fca66feb3e |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "_webi/build-classifier"]
|
||||||
|
path = _webi/build-classifier
|
||||||
|
url = https://github.com/webinstall/webi-build-classifier.git
|
||||||
1
_webi/build-classifier
Submodule
1
_webi/build-classifier
Submodule
Submodule _webi/build-classifier added at f6b55f8d5a
452
_webi/builds-cacher.js
Normal file
452
_webi/builds-cacher.js
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var BuildsCacher = module.exports;
|
||||||
|
|
||||||
|
let Fs = require('node:fs/promises');
|
||||||
|
let Path = require('node:path');
|
||||||
|
|
||||||
|
let request = require('@root/request');
|
||||||
|
let Triplet = require('./build-classifier/triplet.js');
|
||||||
|
|
||||||
|
var ALIAS_RE = /^alias: (\w+)$/m;
|
||||||
|
|
||||||
|
var LEGACY_ARCH_MAP = {
|
||||||
|
'*': 'ANYARCH',
|
||||||
|
arm64: 'aarch64',
|
||||||
|
armv6l: 'armv6',
|
||||||
|
armv7l: 'armv7',
|
||||||
|
amd64: 'x86_64',
|
||||||
|
mipsle: 'mipsel',
|
||||||
|
mips64le: 'mips64el',
|
||||||
|
mipsr6le: 'mipsr6el',
|
||||||
|
mips64r6le: 'mips64r6el',
|
||||||
|
// yes... el for arm and mips, but le for ppc
|
||||||
|
// (perhaps the joke got old?)
|
||||||
|
ppc64el: 'ppc64le',
|
||||||
|
386: 'x86',
|
||||||
|
};
|
||||||
|
var LEGACY_OS_MAP = {
|
||||||
|
'*': 'ANYOS',
|
||||||
|
macos: 'darwin',
|
||||||
|
posix: 'posix_2017',
|
||||||
|
};
|
||||||
|
|
||||||
|
var TERMS_META = [
|
||||||
|
// pattern
|
||||||
|
'{ARCH}',
|
||||||
|
'{EXT}',
|
||||||
|
'{LIBC}',
|
||||||
|
'{NAME}',
|
||||||
|
'{OS}',
|
||||||
|
'{VENDOR}',
|
||||||
|
// // os-/arch-indepedent
|
||||||
|
// 'ANYARCH',
|
||||||
|
// 'ANYOS',
|
||||||
|
// // libc
|
||||||
|
// 'none',
|
||||||
|
// channel
|
||||||
|
'beta',
|
||||||
|
'dev',
|
||||||
|
'preview',
|
||||||
|
'stable',
|
||||||
|
];
|
||||||
|
|
||||||
|
async function getPartialHeader(path) {
|
||||||
|
let readme = `${path}/README.md`;
|
||||||
|
let head = await readFirstBytes(readme).catch(function (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
console.warn(`warn: ${path}: ${err.message}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let fsOpen = util.promisify(Fs.open);
|
||||||
|
// let fsRead = util.promisify(Fs.read);
|
||||||
|
async function readFirstBytes(path) {
|
||||||
|
let start = 0;
|
||||||
|
let n = 1024;
|
||||||
|
let fh = await Fs.open(path, 'r');
|
||||||
|
let buf = new Buffer.alloc(n);
|
||||||
|
let result = await fh.read(buf, start, n);
|
||||||
|
let str = result.buffer.toString('utf8');
|
||||||
|
await fh.close();
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
let promises = {};
|
||||||
|
async function getLatestBuilds(Releases, cacheDir, name, date) {
|
||||||
|
let id = `${cacheDir}/${name}`;
|
||||||
|
if (!promises[id]) {
|
||||||
|
promises[id] = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
promises[id] = promises[id].then(async function () {
|
||||||
|
return await getLatestBuildsInner(Releases, cacheDir, name, date);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await promises[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLatestBuildsInner(Releases, cacheDir, name, date) {
|
||||||
|
let data = await Releases.latest(request);
|
||||||
|
|
||||||
|
if (!date) {
|
||||||
|
date = new Date();
|
||||||
|
}
|
||||||
|
let isoDate = date.toISOString();
|
||||||
|
let yearMonth = isoDate.slice(0, 7);
|
||||||
|
|
||||||
|
// TODO hash file
|
||||||
|
let dataFile = `${cacheDir}/${yearMonth}/${name}.json`;
|
||||||
|
// TODO fsstat releases.js vs require-ing time as well
|
||||||
|
let tsFile = `${cacheDir}/${yearMonth}/${name}.updated.txt`;
|
||||||
|
|
||||||
|
let dirPath = Path.dirname(dataFile);
|
||||||
|
await Fs.mkdir(dirPath, { recursive: true });
|
||||||
|
|
||||||
|
let json = JSON.stringify(data, null, 2);
|
||||||
|
await Fs.writeFile(dataFile, json, 'utf8');
|
||||||
|
|
||||||
|
let seconds = date.valueOf();
|
||||||
|
let ms = seconds / 1000;
|
||||||
|
let msStr = ms.toFixed(3);
|
||||||
|
await Fs.writeFile(tsFile, msStr, 'utf8');
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
|
||||||
|
if (!ALL_TERMS) {
|
||||||
|
ALL_TERMS = Triplet.TERMS_PRIMARY_MAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bc = {};
|
||||||
|
bc.usedTerms = {};
|
||||||
|
bc.orphanTerms = Object.assign({}, ALL_TERMS);
|
||||||
|
bc.unknownTerms = {};
|
||||||
|
bc._triplets = {};
|
||||||
|
bc._targetsByBuildIdCache = {};
|
||||||
|
bc._caches = {};
|
||||||
|
bc._staleAge = 15 * 60 * 1000;
|
||||||
|
|
||||||
|
for (let term of TERMS_META) {
|
||||||
|
delete bc.orphanTerms[term];
|
||||||
|
}
|
||||||
|
|
||||||
|
bc.getPackages = async function () {
|
||||||
|
let dirs = {
|
||||||
|
hidden: {},
|
||||||
|
errors: {},
|
||||||
|
alias: {},
|
||||||
|
invalid: {},
|
||||||
|
selfhosted: {},
|
||||||
|
valid: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let entries = await Fs.readdir(installers, { withFileTypes: true });
|
||||||
|
for (let entry of entries) {
|
||||||
|
// skip non-installer dirs
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
dirs.alias[entry.name] = 'symlink';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
dirs.hidden[entry.name] = '!directory';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.name === 'node_modules') {
|
||||||
|
dirs.hidden[entry.name] = 'node_modules';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.name.startsWith('_')) {
|
||||||
|
dirs.hidden[entry.name] = '_*';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.name.startsWith('.')) {
|
||||||
|
dirs.hidden[entry.name] = '.*';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.name.startsWith('~')) {
|
||||||
|
dirs.hidden[entry.name] = '~*';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.name.endsWith('~')) {
|
||||||
|
dirs.hidden[entry.name] = '*~';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip invalid installers
|
||||||
|
let path = Path.join(installers, entry.name);
|
||||||
|
let head = await getPartialHeader(path);
|
||||||
|
if (!head) {
|
||||||
|
dirs.invalid[entry.name] = '!README.md';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let alias = head.match(ALIAS_RE);
|
||||||
|
if (alias) {
|
||||||
|
dirs.alias[entry.name] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let releasesPath = Path.join(path, 'releases.js');
|
||||||
|
let releases;
|
||||||
|
try {
|
||||||
|
releases = require(releasesPath);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'MODULE_NOT_FOUND') {
|
||||||
|
dirs.errors[entry.name] = err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (err.requireStack.length === 2) {
|
||||||
|
dirs.selfhosted[entry.name] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// err.requireStack.length > 1
|
||||||
|
console.error('');
|
||||||
|
console.error('PROBLEM');
|
||||||
|
console.error(` ${err.message}`);
|
||||||
|
console.error('');
|
||||||
|
console.error('SOLUTION');
|
||||||
|
console.error(' npm clean-install');
|
||||||
|
console.error('');
|
||||||
|
throw new Error(
|
||||||
|
'[SANITY FAIL] should never have missing modules in prod',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs.valid[entry.name] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Typically a package is organized by release (ex: go has 1.20, 1.21, etc),
|
||||||
|
// but we will organize by the build (ex: go1.20-darwin-arm64.tar.gz, etc).
|
||||||
|
bc.getBuilds = async function ({ Releases, name, date }) {
|
||||||
|
let cacheDir = caches;
|
||||||
|
let installerDir = installers;
|
||||||
|
|
||||||
|
if (!Releases) {
|
||||||
|
Releases = require(`${installerDir}/${name}/releases.js`);
|
||||||
|
}
|
||||||
|
// TODO update all releases files with object export
|
||||||
|
if (!Releases.latest) {
|
||||||
|
Releases.latest = Releases;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!date) {
|
||||||
|
date = new Date();
|
||||||
|
}
|
||||||
|
let isoDate = date.toISOString();
|
||||||
|
let yearMonth = isoDate.slice(0, 7);
|
||||||
|
let dataFile = `${cacheDir}/${yearMonth}/${name}.json`;
|
||||||
|
|
||||||
|
// let secondsStr = await Fs.readFile(tsFile, 'ascii').catch(function (err) {
|
||||||
|
// if (err.code !== 'ENOENT') {
|
||||||
|
// throw err;
|
||||||
|
// }
|
||||||
|
// return '0';
|
||||||
|
// });
|
||||||
|
// secondsStr = secondsStr.trim();
|
||||||
|
// let seconds = parseFloat(secondsStr) || 0;
|
||||||
|
|
||||||
|
// let age = now - seconds;
|
||||||
|
|
||||||
|
let data = bc._caches[name];
|
||||||
|
let versions = data?.versions || [];
|
||||||
|
let triplets = [];
|
||||||
|
let now = date.valueOf();
|
||||||
|
if (data) {
|
||||||
|
process.nextTick(async function () {
|
||||||
|
let age = now - data.updated;
|
||||||
|
if (age < bc._staleAge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = await getLatestBuilds(Releases, cacheDir, name);
|
||||||
|
let updated = date.valueOf();
|
||||||
|
updateVersions(data, versions);
|
||||||
|
Object.assign(data, { name, updated, versions, triplets });
|
||||||
|
bc._caches[name] = data;
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = await Fs.readFile(dataFile, 'ascii').catch(async function (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(json);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`error: ${dataFile}:\n\t${e.message}`);
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
data = await getLatestBuilds(Releases, cacheDir, name);
|
||||||
|
}
|
||||||
|
let updated = date.valueOf();
|
||||||
|
updateVersions(data, versions);
|
||||||
|
Object.assign(data, { name, updated, versions, triplets });
|
||||||
|
bc._caches[name] = data;
|
||||||
|
|
||||||
|
for (let build of data.releases) {
|
||||||
|
if (LEGACY_OS_MAP[build.os]) {
|
||||||
|
build.os = LEGACY_OS_MAP[build.os];
|
||||||
|
}
|
||||||
|
if (LEGACY_ARCH_MAP[build.arch]) {
|
||||||
|
build.arch = LEGACY_ARCH_MAP[build.arch];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// - sort version
|
||||||
|
// - tag channels
|
||||||
|
// - @beta not install older than stable
|
||||||
|
function updateVersions(data, versions) {
|
||||||
|
for (let release of data.releases) {
|
||||||
|
let hasVersion = versions.includes(release.version);
|
||||||
|
if (!hasVersion) {
|
||||||
|
versions.unshift(release.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes sure that packages are updated once an hour, on average
|
||||||
|
bc._staleNames = [];
|
||||||
|
bc._freshenTimeout = null;
|
||||||
|
bc.freshenRandomPackage = async function (minDelay) {
|
||||||
|
if (!minDelay) {
|
||||||
|
minDelay = 15 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bc._staleNames.length === 0) {
|
||||||
|
let dirs = await bc.getPackages();
|
||||||
|
bc._staleNames = Object.keys(dirs.valid);
|
||||||
|
bc._staleNames.sort(function () {
|
||||||
|
return 0.5 - Math.random();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = bc._staleNames.pop();
|
||||||
|
let Releases = require(`${installers}/${name}/releases.js`);
|
||||||
|
//data = await getLatestBuilds(Releases, cacheDir, name);
|
||||||
|
void (await bc.getBuilds({
|
||||||
|
Releases: Releases,
|
||||||
|
name: name,
|
||||||
|
date: new Date(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let hour = 60 * 60 * 1000;
|
||||||
|
let delay = minDelay;
|
||||||
|
let spread = hour / bc._staleNames.length;
|
||||||
|
let seed = Math.random();
|
||||||
|
delay += seed * spread;
|
||||||
|
|
||||||
|
clearTimeout(bc._freshenTimeout);
|
||||||
|
bc._freshenTimeout = setTimeout(bc.freshenRandomPackage, delay);
|
||||||
|
bc._freshenTimeout.unref();
|
||||||
|
};
|
||||||
|
|
||||||
|
bc.classify = function (pkg, build) {
|
||||||
|
let maybeInstallable = Triplet.maybeInstallable(pkg, build);
|
||||||
|
if (!maybeInstallable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// because some packages are shimmed to match a single download against
|
||||||
|
let preTarget = Object.assign({ os: '', arch: '', libc: '' }, build);
|
||||||
|
|
||||||
|
let targetId = `${preTarget.os}:${preTarget.arch}:${preTarget.libc}`;
|
||||||
|
let buildId = `${pkg.name}:${targetId}@${build.download}`;
|
||||||
|
let target = bc._targetsByBuildIdCache[buildId];
|
||||||
|
if (target) {
|
||||||
|
Object.assign(build, { target: target, triplet: target.triplet });
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pattern = Triplet.toPattern(pkg, build);
|
||||||
|
if (!pattern) {
|
||||||
|
let err = new Error(`no pattern generated for ${name}`);
|
||||||
|
err.code = 'E_BUILD_NO_PATTERN';
|
||||||
|
target = { error: err };
|
||||||
|
bc._targetsByBuildIdCache[buildId] = target;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawTerms = pattern.split(/[_\{\}\/\.\-]+/g);
|
||||||
|
for (let term of rawTerms) {
|
||||||
|
delete bc.orphanTerms[term];
|
||||||
|
bc.usedTerms[term] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// {NAME}/{NAME}-{VER}-Windows-x86_64_v2-musl.exe =>
|
||||||
|
// {NAME}.windows.x86_64v2.musl.exe
|
||||||
|
let terms = Triplet.patternToTerms(pattern);
|
||||||
|
if (!terms.length) {
|
||||||
|
let err = new Error(`'${terms}' was trimmed to ''`);
|
||||||
|
target = { error: err };
|
||||||
|
bc._targetsByBuildIdCache[buildId] = target;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let term of terms) {
|
||||||
|
if (!term) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ALL_TERMS[term]) {
|
||||||
|
delete bc.orphanTerms[term];
|
||||||
|
bc.usedTerms[term] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bc.unknownTerms[term] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// {NAME}.windows.x86_64v2.musl.exe
|
||||||
|
// windows-x86_64_v2-musl
|
||||||
|
target = { triplet: '' };
|
||||||
|
void Triplet.termsToTarget(target, pkg, build, terms);
|
||||||
|
|
||||||
|
target.triplet = `${target.arch}-${target.vendor}-${target.os}-${target.libc}`;
|
||||||
|
let hasTriplet = pkg.triplets.includes(target.triplet);
|
||||||
|
if (!hasTriplet) {
|
||||||
|
// TODO I don't love this hidden behavior
|
||||||
|
// perhaps classify should just happen when the package is loaded
|
||||||
|
// (and the sanity error should be removed, or thrown after the loop is complete)
|
||||||
|
pkg.triplets.push(target.triplet);
|
||||||
|
}
|
||||||
|
|
||||||
|
bc._triplets[target.triplet] = true;
|
||||||
|
bc._targetsByBuildIdCache[buildId] = target;
|
||||||
|
|
||||||
|
let triple = [target.arch, target.vendor, target.os, target.libc];
|
||||||
|
for (let term of triple) {
|
||||||
|
if (!ALL_TERMS[term]) {
|
||||||
|
throw new Error(
|
||||||
|
`[SANITY FAIL] '${pkg.name}' '${target.triplet}' generated unknown term '${term}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete bc.orphanTerms[term];
|
||||||
|
bc.usedTerms[term] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
return bc;
|
||||||
|
};
|
||||||
470
_webi/lint-builds.js
Normal file
470
_webi/lint-builds.js
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let Fs = require('node:fs/promises');
|
||||||
|
let Path = require('node:path');
|
||||||
|
|
||||||
|
let BuildsCacher = require('./builds-cacher.js');
|
||||||
|
let HostTargets = require('./build-classifier/host-targets.js');
|
||||||
|
let Parallel = require('./parallel.js');
|
||||||
|
|
||||||
|
var INSTALLERS_DIR = Path.join(__dirname, '..');
|
||||||
|
var CACHE_DIR = Path.join(__dirname, '../_cache');
|
||||||
|
|
||||||
|
let UserAgentsMap = require('./build-classifier/uas.json');
|
||||||
|
let uas = Object.keys(UserAgentsMap);
|
||||||
|
let uaTargetsMap = {};
|
||||||
|
for (let ua of uas) {
|
||||||
|
let terms = ua.split(/[\s\/]+/g);
|
||||||
|
let target = {};
|
||||||
|
void HostTargets.termsToTarget(target, terms);
|
||||||
|
if (!target) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (target.errors.length) {
|
||||||
|
throw target.errors[0];
|
||||||
|
}
|
||||||
|
if (!target.os) {
|
||||||
|
// TODO make target null, or create error for this
|
||||||
|
console.warn(`no os for terms: ${terms}`);
|
||||||
|
//throw new Error(`terms: ${terms}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!target.arch) {
|
||||||
|
// TODO make target null, or create error for this
|
||||||
|
console.warn(`no arch for terms: ${terms}`);
|
||||||
|
//throw new Error(`terms: ${terms}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!target.libc) {
|
||||||
|
// TODO make target null, or create error for this
|
||||||
|
console.warn(`no libc for terms: ${terms}`);
|
||||||
|
//throw new Error(`terms: ${terms}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let triplet = `${target.os}-${target.arch}-${target.libc}`;
|
||||||
|
uaTargetsMap[triplet] = target;
|
||||||
|
}
|
||||||
|
let uaTargets = [];
|
||||||
|
let triplets = Object.keys(uaTargetsMap);
|
||||||
|
for (let triplet of triplets) {
|
||||||
|
let target = uaTargetsMap[triplet];
|
||||||
|
uaTargets.push(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDirs(dirs) {
|
||||||
|
{
|
||||||
|
let errors = Object.keys(dirs.errors);
|
||||||
|
console.error('');
|
||||||
|
console.error(`Errors: ${errors.length}`);
|
||||||
|
for (let name of errors) {
|
||||||
|
let err = dirs.errors[name];
|
||||||
|
console.error(`${name}/: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let hidden = Object.keys(dirs.hidden);
|
||||||
|
console.debug('');
|
||||||
|
console.debug(`Hidden: ${hidden.length}`);
|
||||||
|
for (let name of hidden) {
|
||||||
|
let kind = dirs.hidden[name];
|
||||||
|
if (kind === '!directory') {
|
||||||
|
console.debug(` ${name}`);
|
||||||
|
} else {
|
||||||
|
console.debug(` ${name}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let alias = Object.keys(dirs.alias);
|
||||||
|
console.debug('');
|
||||||
|
console.debug(`Alias: ${alias.length}`);
|
||||||
|
for (let name of alias) {
|
||||||
|
let kind = dirs.alias[name];
|
||||||
|
if (kind === 'symlink') {
|
||||||
|
console.debug(` ${name} => ...`);
|
||||||
|
} else {
|
||||||
|
console.debug(` ${name}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let invalids = Object.keys(dirs.invalid);
|
||||||
|
console.warn('');
|
||||||
|
console.warn(`Invalid: ${invalids.length}`);
|
||||||
|
for (let name of invalids) {
|
||||||
|
console.warn(` ${name}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let selfhosted = Object.keys(dirs.selfhosted);
|
||||||
|
console.info('');
|
||||||
|
console.info(`Self-Hosted: ${selfhosted.length}`);
|
||||||
|
for (let name of selfhosted) {
|
||||||
|
console.info(` ${name}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let valids = Object.keys(dirs.valid);
|
||||||
|
console.info('');
|
||||||
|
console.info(`Found: ${valids.length}`);
|
||||||
|
for (let name of valids) {
|
||||||
|
console.info(` ${name}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bc = BuildsCacher.create({
|
||||||
|
caches: CACHE_DIR,
|
||||||
|
installers: INSTALLERS_DIR,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getPackagesWithBuilds(installersDir, pkgNames, parallel = 25) {
|
||||||
|
let packages = [];
|
||||||
|
|
||||||
|
await Parallel.run(parallel, pkgNames, getAll);
|
||||||
|
|
||||||
|
async function getAll(name, i) {
|
||||||
|
let Releases = require(`${installersDir}/${name}/releases.js`);
|
||||||
|
let pkg = await bc.getBuilds({
|
||||||
|
Releases: Releases,
|
||||||
|
name: name,
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
packages[i] = pkg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBuildsByTarget(packages) {
|
||||||
|
let packagesByName = {};
|
||||||
|
|
||||||
|
for (let pkg of packages) {
|
||||||
|
let buildsByOs = getBuildsByOs(pkg);
|
||||||
|
packagesByName[pkg.name] = buildsByOs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packagesByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBuildsByOs(pkg) {
|
||||||
|
let buildsByOs = {};
|
||||||
|
|
||||||
|
for (let build of pkg.releases) {
|
||||||
|
// TODO check targets cache
|
||||||
|
let target = bc.classify(pkg, build);
|
||||||
|
if (!target) {
|
||||||
|
// ignore known, non-package extensions
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.error) {
|
||||||
|
let err = target.error;
|
||||||
|
let code = err.code || '';
|
||||||
|
console.error(`[ERROR]: ${code} ${pkg.name}: ${build.name}`);
|
||||||
|
console.error(`>>> ${err.message} <<<`);
|
||||||
|
console.error(pkg);
|
||||||
|
console.error(build);
|
||||||
|
console.error(`^^^ ${err.message} ^^^`);
|
||||||
|
console.error(err.stack);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let buildsByRelease = getBuildsByRelease(build, buildsByOs, target);
|
||||||
|
buildsByRelease.push(build);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildsByOs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBuildsByRelease(build, buildsByOs, target) {
|
||||||
|
let archLibc = `${target.arch}-${target.libc}`;
|
||||||
|
|
||||||
|
if (!buildsByOs[target.os]) {
|
||||||
|
buildsByOs[target.os] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let buildsByVersion = buildsByOs[target.os];
|
||||||
|
if (!buildsByVersion[build.version]) {
|
||||||
|
buildsByVersion[build.version] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let buildsByArchLibc = buildsByVersion[build.version];
|
||||||
|
if (!buildsByArchLibc[archLibc]) {
|
||||||
|
buildsByArchLibc[archLibc] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let buildsByRelease = buildsByArchLibc[archLibc];
|
||||||
|
return buildsByRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchBuildsByTarget(pkg, buildsTree, target) {
|
||||||
|
let oses = [];
|
||||||
|
let targetOs = target.os;
|
||||||
|
if (target.os === 'windows') {
|
||||||
|
oses = ['ANYOS', 'windows'];
|
||||||
|
//buildsByOs = buildsTree.ANYOS || buildsTree[target.os];
|
||||||
|
} else if (target.os === 'android') {
|
||||||
|
oses = ['ANYOS', 'posix_2017', 'android', 'linux'];
|
||||||
|
// buildsByOs =
|
||||||
|
// buildsTree.ANYOS || buildsTree.posix_2017 || buildsTree[target.os];
|
||||||
|
// if (!buildsByOs) {
|
||||||
|
// targetOs = 'linux';
|
||||||
|
// buildsByOs = buildsTree.linux;
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
oses = ['ANYOS', 'posix_2017', target.os];
|
||||||
|
// buildsByOs =
|
||||||
|
// buildsTree.ANYOS || buildsTree.posix_2017 || buildsTree[target.os];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO can we move sortByOsAndArchLibc(builds, anything) down to the lib?
|
||||||
|
// and then the matcher
|
||||||
|
// and make the waterfall more optional?
|
||||||
|
|
||||||
|
let waterfall = HostTargets.WATERFALL[target.os] || {};
|
||||||
|
let arches = waterfall[target.arch] ||
|
||||||
|
HostTargets.WATERFALL.ANYOS[target.arch] || [target.arch];
|
||||||
|
arches = ['ANYARCH'].concat(arches);
|
||||||
|
let libcs = waterfall[target.libc] ||
|
||||||
|
HostTargets.WATERFALL.ANYOS[target.libc] || [target.libc];
|
||||||
|
|
||||||
|
//console.log('waterfalls', arches, libcs);
|
||||||
|
|
||||||
|
// TODO flatten earlier and precache?
|
||||||
|
let duplets = [];
|
||||||
|
for (let arch of arches) {
|
||||||
|
for (let libc of libcs) {
|
||||||
|
let duplet = `${arch}-${libc}`;
|
||||||
|
duplets.push(duplet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let duplet;
|
||||||
|
let targetBuilds;
|
||||||
|
for (let os of oses) {
|
||||||
|
let buildsByOs = buildsTree[os];
|
||||||
|
if (!buildsByOs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// - latest supported triplets
|
||||||
|
// - historical supported triplets
|
||||||
|
|
||||||
|
// TODO sort versions first, get channel (or 'stable' or 'latest') from user
|
||||||
|
for (let version of pkg.versions) {
|
||||||
|
let versionBuilds = buildsByOs[version];
|
||||||
|
if (!versionBuilds) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let _duplet of duplets) {
|
||||||
|
targetBuilds = versionBuilds[_duplet];
|
||||||
|
//console.log(` duplet: ${_duplet}`, versionBuilds, targetBuilds);
|
||||||
|
if (targetBuilds?.length > 0) {
|
||||||
|
targetOs = os;
|
||||||
|
duplet = _duplet;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetBuilds?.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetBuilds?.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetBuilds?.length) {
|
||||||
|
// console.log(' no builds:', buildsByOs);
|
||||||
|
targetBuilds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let match = { triplet: `${targetOs}-${duplet}`, builds: targetBuilds };
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// TODO
|
||||||
|
// node ./_webi/lint-builds.js caddy@beta 'x86_64/unknown Darwin libc'
|
||||||
|
//
|
||||||
|
// let [pkgName, userAgent] = process.argv[2].slice(0);
|
||||||
|
// create test case for zoxide, caddy, rg
|
||||||
|
|
||||||
|
let dirs = await bc.getPackages();
|
||||||
|
showDirs(dirs);
|
||||||
|
console.info('');
|
||||||
|
|
||||||
|
bc.freshenRandomPackage(600 * 1000);
|
||||||
|
|
||||||
|
let rows = [];
|
||||||
|
let triples = [];
|
||||||
|
let valids = Object.keys(dirs.valid);
|
||||||
|
|
||||||
|
let index = valids.indexOf('webi');
|
||||||
|
if (index >= 0) {
|
||||||
|
// TODO fix the webi faux package
|
||||||
|
// (not sure why I even created it)
|
||||||
|
void valids.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parallel = 25;
|
||||||
|
//valids = ['atomicparsley', 'caddy', 'macos'];
|
||||||
|
//valids = ['atomicparsley'];
|
||||||
|
let packages = await getPackagesWithBuilds(INSTALLERS_DIR, valids, parallel);
|
||||||
|
|
||||||
|
console.info(`Fetching builds for`);
|
||||||
|
for (let pkg of packages) {
|
||||||
|
console.info(` ${pkg.name}`);
|
||||||
|
|
||||||
|
let nStr = pkg.releases.length.toString();
|
||||||
|
let n = nStr.padStart(5, ' ');
|
||||||
|
let row = `##### ${n}\t${pkg.name}\tv`;
|
||||||
|
rows.push(row);
|
||||||
|
|
||||||
|
// ignore known, non-package extensions
|
||||||
|
for (let build of pkg.releases) {
|
||||||
|
let target = bc.classify(pkg, build);
|
||||||
|
if (!target) {
|
||||||
|
// non-build file
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (target.error) {
|
||||||
|
let e = target.error;
|
||||||
|
if (e.code === 'E_BUILD_NO_PATTERN') {
|
||||||
|
console.warn(`>>> ${e.message} <<<`);
|
||||||
|
console.warn(pkg);
|
||||||
|
console.warn(build);
|
||||||
|
console.warn(`^^^ ${e.message} ^^^`);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
triples.push(target.triplet);
|
||||||
|
// if (!build.version) {
|
||||||
|
// throw new Error(`no version for ${pkg.name} ${build.name}`);
|
||||||
|
// }
|
||||||
|
// // For debug printing versions
|
||||||
|
// console.error(build.version);
|
||||||
|
rows.push(`${target.triplet}\t${pkg.name}\t${build.version}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let packagesTree = getBuildsByTarget(packages);
|
||||||
|
//console.log(`packagesTree`, packagesTree);
|
||||||
|
|
||||||
|
for (let pkg of packages) {
|
||||||
|
console.log('');
|
||||||
|
console.log('');
|
||||||
|
console.log('pkg', pkg.name);
|
||||||
|
let buildsTree = packagesTree[pkg.name];
|
||||||
|
console.log(buildsTree);
|
||||||
|
for (let target of uaTargets) {
|
||||||
|
let libc = target.libc || 'libc';
|
||||||
|
let hostTriplet = `${target.os}-${target.arch}-${libc}`;
|
||||||
|
console.log('');
|
||||||
|
console.log(`target: ${hostTriplet}`);
|
||||||
|
let match = matchBuildsByTarget(pkg, buildsTree, target);
|
||||||
|
if (!match) {
|
||||||
|
console.log(
|
||||||
|
` pkg: ${pkg.name}: missing build for os '${target.os}'`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.builds.length === 0) {
|
||||||
|
console.log(
|
||||||
|
` pkg: ${pkg.name}: missing build for os '${target.os}-${target.arch}-${libc}'`,
|
||||||
|
);
|
||||||
|
} else if (match.triplet === hostTriplet) {
|
||||||
|
console.log(` selected ${match.builds.length}`);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
` selected ${match.builds.length} (${match.triplet} fallback)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tsv = rows.join('\n');
|
||||||
|
console.info('');
|
||||||
|
console.info('#rows', rows.length);
|
||||||
|
await Fs.writeFile('builds.tsv', tsv, 'utf8');
|
||||||
|
|
||||||
|
console.info('');
|
||||||
|
console.info('Triplets Detected:');
|
||||||
|
let triplets = Object.keys(bc._triplets);
|
||||||
|
if (triplets.length) {
|
||||||
|
triplets.sort();
|
||||||
|
console.info(' ', triplets.join('\n '));
|
||||||
|
} else {
|
||||||
|
console.info(' (none)');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('');
|
||||||
|
console.info('New / Unknown Terms:');
|
||||||
|
let unknowns = Object.keys(bc.unknownTerms);
|
||||||
|
if (unknowns.length) {
|
||||||
|
unknowns.sort();
|
||||||
|
console.warn(' ', unknowns.join('\n '));
|
||||||
|
} else {
|
||||||
|
console.info(' (none)');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('');
|
||||||
|
console.info('Unused Terms:');
|
||||||
|
let unuseds = Object.keys(bc.orphanTerms);
|
||||||
|
if (unuseds.length) {
|
||||||
|
unuseds.sort();
|
||||||
|
console.warn(' ', unuseds.join('\n '));
|
||||||
|
} else {
|
||||||
|
console.info(' (none)');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('');
|
||||||
|
// sort -u -k1 builds.tsv | rg -v '^#|^https?:' | rg -i arm
|
||||||
|
// cut -f1 builds.tsv | sort -u -k1 | rg -v '^#|^https?:' | rg -i arm
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module === require.main) {
|
||||||
|
let times = [];
|
||||||
|
let now = Date.now();
|
||||||
|
main()
|
||||||
|
.then(async function () {
|
||||||
|
let then = Date.now();
|
||||||
|
let delta = then - now;
|
||||||
|
times.push(delta);
|
||||||
|
now = then;
|
||||||
|
await main();
|
||||||
|
then = Date.now();
|
||||||
|
delta = then - now;
|
||||||
|
times.push(delta);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
console.info('');
|
||||||
|
console.info('Run times');
|
||||||
|
for (let delta of times) {
|
||||||
|
let s = delta / 1000;
|
||||||
|
console.info(` ${s}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceExit() {
|
||||||
|
console.warn(`warn: dangling event loop reference`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
let exitTimeout = setTimeout(forceExit, 250);
|
||||||
|
exitTimeout.unref();
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error(err.stack || err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
44
_webi/parallel.js
Normal file
44
_webi/parallel.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Parallel = module.exports;
|
||||||
|
Parallel.run = async function (limit, arr, fn) {
|
||||||
|
let index = 0;
|
||||||
|
let actives = [];
|
||||||
|
let results = [];
|
||||||
|
limit = Math.min(limit, arr.length);
|
||||||
|
|
||||||
|
function launch() {
|
||||||
|
let _index = index;
|
||||||
|
let p = fn(arr[_index], _index, arr);
|
||||||
|
|
||||||
|
// some tasks may be synchronous
|
||||||
|
// so we must push before removing
|
||||||
|
actives.push(p);
|
||||||
|
|
||||||
|
p.then(function _resolve(result) {
|
||||||
|
let i = actives.indexOf(p);
|
||||||
|
actives.splice(i, 1);
|
||||||
|
results[_index] = result;
|
||||||
|
});
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start tasks in parallel, up to limit
|
||||||
|
for (; actives.length < limit; ) {
|
||||||
|
launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep the task queue full
|
||||||
|
for (; index < arr.length; ) {
|
||||||
|
// wait for one task to complete
|
||||||
|
await Promise.race(actives);
|
||||||
|
// add one task again
|
||||||
|
launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for all remaining tasks
|
||||||
|
await Promise.all(actives);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user