mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-05 11:09:57 +08:00
Build complete platform tar.gz bundles with production dependencies for 13 Linux architecture/libc targets, including ARMv6 glibc and musl. Share archive, payload, runtime and dependency verification across online preparation and manual offline installation while retaining the existing Node runtime. Publish readiness only after an isolated backend starts successfully. Repair verified dependency failures once with the frozen lockfile; offline repair requires explicit opt-in. Preserve configured proxies and bound memory, concurrency, process lifetimes and cleanup on small devices. Verify the running build after explicit restart, restore prior application files and dependencies on failure, and retain backups when the failed service cannot stop. Complete cancellation recovery outside the aborted signal, reap descendants as their supervisor exits, and keep operation-only upgrade variables out of both Bash and CLI service startup environments. Keep dialog/sidebar states consistent, hide completed notices, and retain menu/user controls in narrow desktop viewports with long-name truncation. Validation: 791 backend/frontend tests passed (8 platform skips), 294 CLI tests passed (5 platform skips), backend/CLI type checks and standalone offline installer build. On dev, 29 additional Linux fault regressions passed without external networking, including real Node/PM2 SIGTERM rollback. Earlier full bundle application and ARM1176/glibc prestart acceptance remain recorded. Make the mirror notification timing regression respect Bash SECONDS integer resolution: require two clock ticks and assert the logged retry threshold. Validated with Node 20 full regression and 12 repeated boundary runs.
142 lines
4.6 KiB
JavaScript
142 lines
4.6 KiB
JavaScript
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const tar = require('tar');
|
|
const bundle = require('../cli/src/internal/maintenance/platformBundle.cjs');
|
|
const {
|
|
verifyUpgrade,
|
|
} = require('../cli/src/internal/maintenance/upgradeArtifacts.cjs');
|
|
|
|
async function normalizeModes(directory) {
|
|
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
const filename = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
await fs.chmod(filename, 0o755);
|
|
await normalizeModes(filename);
|
|
} else if (entry.isFile()) {
|
|
const stat = await fs.stat(filename);
|
|
await fs.chmod(filename, 0o644 | (stat.mode & 0o111));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function buildBundle(root, output) {
|
|
root = path.resolve(root);
|
|
output = path.resolve(output);
|
|
const target = await bundle.runtimeTarget();
|
|
if (process.platform !== 'linux')
|
|
throw new Error('Release bundles must be built and probed on Linux');
|
|
const build = await verifyUpgrade(root, path.join(root, 'static'));
|
|
await bundle.smokeDependencies(root);
|
|
await fs.mkdir(output, { recursive: true });
|
|
const workspace = await fs.mkdtemp(path.join(output, '.bundle-'));
|
|
try {
|
|
const payload = path.join(workspace, 'qinglong');
|
|
const source = path.join(payload, 'source'),
|
|
staticRoot = path.join(payload, 'static');
|
|
await fs.mkdir(source, { recursive: true });
|
|
const excluded = new Set([
|
|
'.git',
|
|
'.tmp',
|
|
'.env',
|
|
'data',
|
|
'static',
|
|
'output',
|
|
'node_modules',
|
|
]);
|
|
for (const name of await fs.readdir(root)) {
|
|
if (excluded.has(name)) continue;
|
|
await fs.cp(path.join(root, name), path.join(source, name), {
|
|
recursive: true,
|
|
verbatimSymlinks: true,
|
|
});
|
|
}
|
|
await fs.cp(
|
|
path.join(root, 'node_modules'),
|
|
path.join(source, 'node_modules'),
|
|
{ recursive: true, verbatimSymlinks: true },
|
|
);
|
|
await fs.cp(path.join(root, 'static'), staticRoot, {
|
|
recursive: true,
|
|
verbatimSymlinks: true,
|
|
});
|
|
await normalizeModes(source);
|
|
await normalizeModes(staticRoot);
|
|
const manifest = {
|
|
format: 2,
|
|
sourceCommit: build.sourceCommit,
|
|
version: JSON.parse(
|
|
await fs.readFile(path.join(root, 'package.json'), 'utf8'),
|
|
).version,
|
|
lockfileSha256: build.lockfileSha256,
|
|
target: {
|
|
platform: target.platform,
|
|
arch: target.arch,
|
|
libc: target.libc,
|
|
libcMinimum: target.libcVersion,
|
|
endianness: target.endianness,
|
|
armFloatAbi: target.armFloatAbi,
|
|
},
|
|
nodeMinimum: '20.18.1',
|
|
nodeApi: 6,
|
|
files: await bundle.inventory({ source, static: staticRoot }),
|
|
};
|
|
await fs.writeFile(
|
|
path.join(payload, 'bundle.json'),
|
|
JSON.stringify(manifest),
|
|
);
|
|
console.log('Bundle inventory verified; writing archive');
|
|
const filename = `qinglong-${build.sourceCommit}-${bundle.targetKey(
|
|
target,
|
|
)}.tar.gz`;
|
|
await tar.c(
|
|
{
|
|
gzip: true,
|
|
portable: true,
|
|
noMtime: true,
|
|
cwd: workspace,
|
|
file: path.join(output, filename),
|
|
},
|
|
['qinglong'],
|
|
);
|
|
// Verify the bytes users receive, not just the pre-archive build directory.
|
|
const roundtrip = path.join(workspace, 'roundtrip');
|
|
await fs.mkdir(roundtrip);
|
|
console.log('Verifying archived paths, permissions and checksums');
|
|
await bundle.extractArchive(path.join(output, filename), roundtrip);
|
|
const extracted = {
|
|
source: path.join(roundtrip, 'qinglong/source'),
|
|
static: path.join(roundtrip, 'qinglong/static'),
|
|
};
|
|
await bundle.verifyPayload(extracted, manifest, target);
|
|
await verifyUpgrade(extracted.source, extracted.static);
|
|
await bundle.smokeDependencies(extracted.source);
|
|
console.log('Archived production dependencies and SQLite verified');
|
|
const bytes = await fs.readFile(path.join(output, filename));
|
|
const record = {
|
|
format: 2,
|
|
sourceCommit: build.sourceCommit,
|
|
target: bundle.targetKey(target),
|
|
filename,
|
|
baseImage: process.env.QL_BUNDLE_BASE_IMAGE,
|
|
size: bytes.length,
|
|
sha256: bundle.sha256(bytes),
|
|
};
|
|
await fs.writeFile(
|
|
path.join(output, filename + '.json'),
|
|
JSON.stringify(record) + '\n',
|
|
);
|
|
console.log(JSON.stringify(record));
|
|
return record;
|
|
} finally {
|
|
await fs.rm(workspace, { recursive: true, force: true });
|
|
}
|
|
}
|
|
module.exports = { buildBundle, normalizeModes };
|
|
if (require.main === module)
|
|
buildBundle(process.argv[2] || '/ql', process.argv[3] || '/out').catch(
|
|
(e) => {
|
|
console.error(e);
|
|
process.exitCode = 1;
|
|
},
|
|
);
|