Files
flatpark/scripts/read-extra-data.mjs
T
KomhandClaude Opus 4.8 b1ea2acb30 ci: exercise apply_extra as an uncapable root for newly added apps
Nothing in CI reached the code path that broke six apps for every user
who installed system-wide. build-app.sh only builds, and extra-data is
fetched at install time, so a green build says nothing about whether an
app installs. A --user install misses it too: there apply_extra runs as
the invoking user, and the ownership restore that fails under root never
happens.

scripts/check-apply-extra.sh closes that gap. It downloads the pinned
artifact, verifies its sha256, and runs the app's real apply_extra.sh in
a sandbox mirroring what flatpak-system-helper imposes: uid 0, all
capabilities dropped, no network, no /proc, runtime at /usr, extra-data
bound at /app/extra. bwrap --unshare-user gives us that without needing
root: a chown to an unmapped uid fails there just as a dropped CAP_CHOWN
does on the real thing.

pr-checks runs it for apps a PR *adds* -- the moment an unpack script and
an upstream builder's uid first meet review. Scoping it there keeps the
artifact download (hundreds of MB for some apps) off every pin bump and
off refactors like the one that prompted this. The playbook asks
contributors to run it by hand whenever they touch an unpack script or
re-pin to an artifact from a new builder, which is the gap that remains.

tests/test_check_apply_extra.sh is hermetic: it builds a zip carrying an
Info-ZIP 0x7875 field with uid/gid 1001 -- what a release archive built
on CI without fakeroot looks like -- serves it over file://, and asserts
the checker rejects an unpack without --no-same-owner and accepts one
with it. Verified it fails against a mutant checker that ignores the
exit code, so it is testing what it claims to.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-10 10:31:32 +08:00

86 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
// Read a flatpak manifest and print the runtime plus the extra-data sources
// that apply to one arch, as tab-separated lines a shell can read:
//
// runtime<TAB><id><TAB><version>
// extra<TAB><filename><TAB><url><TAB><sha256>
//
// Like read-descriptor.mjs this is NOT a general YAML parser. Manifests here
// are generated and reviewed against a fixed shape, so we extract the few keys
// check-apply-extra.sh needs and stay dependency-free.
//
// Usage: read-extra-data.mjs <manifest.yml> <arch>
import { readFileSync } from 'node:fs';
const [file, arch] = process.argv.slice(2);
if (!file || !arch) {
process.stderr.write('usage: read-extra-data.mjs <manifest.yml> <arch>\n');
process.exit(2);
}
let lines;
try {
lines = readFileSync(file, 'utf8').split('\n');
} catch (e) {
process.stderr.write(`cannot read ${file}: ${e.message || e}\n`);
process.exit(1);
}
const strip = (v) => v.trim().replace(/^["'](.*)["']$/, '$1');
let runtime = '';
let runtimeVersion = '';
const sources = [];
let cur = null;
let inArches = false;
for (const raw of lines) {
const t = raw.trim();
if (!t || t.startsWith('#')) continue;
let m;
// Top-level keys only: a nested `runtime:` under build-options must not win.
if (!/^\s/.test(raw)) {
if ((m = raw.match(/^runtime:\s*(.+)$/))) runtime = strip(m[1]);
else if ((m = raw.match(/^runtime-version:\s*(.+)$/))) runtimeVersion = strip(m[1]);
}
if (t === '- type: extra-data') {
cur = { arches: [] };
sources.push(cur);
inArches = false;
continue;
}
// Any other list entry ends the record we were filling.
if (t.startsWith('- type:')) {
cur = null;
inArches = false;
continue;
}
if (!cur) continue;
if (t === 'only-arches:') { inArches = true; continue; }
if (inArches && (m = t.match(/^-\s*(\S+)$/))) { cur.arches.push(strip(m[1])); continue; }
inArches = false;
if ((m = t.match(/^filename:\s*(.+)$/))) cur.filename = strip(m[1]);
else if ((m = t.match(/^url:\s*(.+)$/))) cur.url = strip(m[1]);
else if ((m = t.match(/^sha256:\s*(.+)$/))) cur.sha256 = strip(m[1]);
}
if (!runtime || !runtimeVersion) {
process.stderr.write(`${file}: no top-level runtime / runtime-version\n`);
process.exit(1);
}
const out = [`runtime\t${runtime}\t${runtimeVersion}`];
for (const s of sources) {
if (s.arches.length && !s.arches.includes(arch)) continue;
if (!s.filename || !s.url || !s.sha256) {
process.stderr.write(`${file}: extra-data source missing filename/url/sha256\n`);
process.exit(1);
}
out.push(`extra\t${s.filename}\t${s.url}\t${s.sha256}`);
}
process.stdout.write(out.join('\n') + '\n');