mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* docs: design Linux frame-copy packaging * docs: plan Linux frame-copy packaging * feat(packaging): define Linux frame-copy profiles * fix(packaging): reject inherited profile names * feat(embedded-mpv): validate staged Linux runtime * fix(embedded-mpv): require Linux source packages * fix(embedded-mpv): harden Linux runtime staging * feat(embedded-mpv): build LGPL Linux runtime * fix(embedded-mpv): pin Linux runtime inputs * feat(embedded-mpv): build relocatable Linux helper * fix(embedded-mpv): require bundled Linux runtime * fix(embedded-mpv): make Linux runtime portable * feat(packaging): ship Linux frame-copy artifacts * fix(embedded-mpv): verify Linux helper linkage * fix(packaging): enforce Linux frame-copy isolation * fix(embedded-mpv): pin Linux display data * docs(embedded-mpv): document Linux frame-copy packaging * feat(embedded-mpv): probe Linux frame-copy runtime * test(embedded-mpv): smoke packaged Linux frame-copy * docs(embedded-mpv): clarify Linux system runtime baseline * fix(embedded-mpv): harden Linux runtime capability gate * ci: verify Linux frame-copy packages * test(embedded-mpv): harden packaged Linux smoke * test(embedded-mpv): preserve packaged GL mode * test(packaging): harden Linux package probes * fix(embedded-mpv): enable private Snap shared memory * fix(embedded-mpv): sanitize Linux helper environment * fix(packaging): enforce private Snap memory semantics * fix(packaging): reject ambiguous Snap memory metadata * fix(embedded-mpv): prioritize trusted Snap GL * fix(packaging): reject advanced Snap YAML semantics * fix(packaging): reject arbitrary Snap YAML aliases * feat(packaging): ship Linux runtime license notices * docs(embedded-mpv): document Linux runtime distribution * fix(packaging): parse Snap trailing comments safely * fix(release): gate Snap publish on public source release * fix(packaging): strip VCS metadata from source bundle * docs(packaging): clarify Linux source release gate * test(embedded-mpv): smoke missing bundled libmpv * style(embedded-mpv): format final validation inputs * fix(e2e): satisfy fixture index signature typing * fix(ci): declare fontconfig gperf generator * fix(embedded-mpv): hash runtime cache identities * fix(packaging): harden Linux frame-copy delivery * fix(packaging): tighten runtime delivery gates * fix(ci): decouple Linux runtime matrix * fix(packaging): harden Linux frame-copy delivery * fix(packaging): validate Linux frame-copy runtimes * fix(packaging): scope Snap Electron library checks * feat(packaging): ship Linux frame-copy runtimes * fix(packaging): improve Linux runtime smoke diagnostics * fix(packaging): expose bounded helper probe details * test(packaging): trace Snap EGL probe failures * fix(packaging): prefer core22 ABI in Snap helper * fix(packaging): bound helper probe capture * fix(packaging): harden Linux frame-copy releases * fix(packaging): canonicalize libplacebo submodule identity * fix(packaging): make source archive inspection portable * fix(packaging): harden Snap release verification
339 lines
11 KiB
JavaScript
339 lines
11 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { createRequire } from 'node:module';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const { SOURCE_PACKAGES } = require('./build-linux-runtime.cjs');
|
|
const {
|
|
LICENSE_PATHS_BY_PACKAGE,
|
|
collectLinuxRuntimeLicenseInputs,
|
|
generateLinuxRuntimeNotices,
|
|
validateLinuxRuntimeNotices,
|
|
} = require('./generate-linux-runtime-notices.cjs');
|
|
const generatorScript = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'generate-linux-runtime-notices.cjs'
|
|
);
|
|
|
|
function runtimeManifest() {
|
|
return {
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
packages: Object.fromEntries(
|
|
SOURCE_PACKAGES.map((sourcePackage) => [
|
|
sourcePackage.id,
|
|
{
|
|
version: sourcePackage.version,
|
|
sourceUrl: sourcePackage.sourceUrl,
|
|
...(sourcePackage.sourceTag
|
|
? { sourceTag: sourcePackage.sourceTag }
|
|
: {}),
|
|
...(sourcePackage.sourceKind === 'archive'
|
|
? { sourceSha256: sourcePackage.expectedSha256 }
|
|
: {
|
|
sourceGitCommit: sourcePackage.expectedGitCommit,
|
|
sourceSubmodules: [
|
|
...sourcePackage.expectedSubmodules,
|
|
],
|
|
}),
|
|
license: sourcePackage.license,
|
|
...(sourcePackage.buildInput
|
|
? { buildInput: sourcePackage.buildInput }
|
|
: {}),
|
|
},
|
|
])
|
|
),
|
|
};
|
|
}
|
|
|
|
function createSourceFixture() {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-linux-license-sources-')
|
|
);
|
|
for (const sourcePackage of SOURCE_PACKAGES) {
|
|
const licensePaths = LICENSE_PATHS_BY_PACKAGE[sourcePackage.id];
|
|
assert.ok(
|
|
Array.isArray(licensePaths) && licensePaths.length > 0,
|
|
`missing test mapping for ${sourcePackage.id}`
|
|
);
|
|
for (const relativePath of licensePaths) {
|
|
const filePath = path.join(root, sourcePackage.id, relativePath);
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(
|
|
filePath,
|
|
`verbatim upstream ${sourcePackage.id} ${relativePath}\n`
|
|
);
|
|
}
|
|
}
|
|
return root;
|
|
}
|
|
|
|
function fileTree(root) {
|
|
const files = [];
|
|
function visit(directoryPath) {
|
|
for (const entry of fs
|
|
.readdirSync(directoryPath, { withFileTypes: true })
|
|
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
const entryPath = path.join(directoryPath, entry.name);
|
|
if (entry.isDirectory()) {
|
|
visit(entryPath);
|
|
} else {
|
|
files.push({
|
|
path: path
|
|
.relative(root, entryPath)
|
|
.split(path.sep)
|
|
.join('/'),
|
|
contents: fs.readFileSync(entryPath),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
visit(root);
|
|
return files;
|
|
}
|
|
|
|
test('collects verbatim pinned licenses and generates deterministic exact notices', (t) => {
|
|
const sourceRoot = createSourceFixture();
|
|
const fixtureRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-linux-notices-')
|
|
);
|
|
const inputRoot = path.join(fixtureRoot, 'inputs');
|
|
const firstOutput = path.join(fixtureRoot, 'first');
|
|
const secondOutput = path.join(fixtureRoot, 'second');
|
|
t.after(() => {
|
|
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
const manifest = runtimeManifest();
|
|
collectLinuxRuntimeLicenseInputs({
|
|
sourceRoot,
|
|
outputRoot: inputRoot,
|
|
runtimeManifest: manifest,
|
|
});
|
|
const first = generateLinuxRuntimeNotices({
|
|
licenseInputRoot: inputRoot,
|
|
outputRoot: firstOutput,
|
|
runtimeManifest: manifest,
|
|
});
|
|
const second = generateLinuxRuntimeNotices({
|
|
licenseInputRoot: inputRoot,
|
|
outputRoot: secondOutput,
|
|
runtimeManifest: manifest,
|
|
});
|
|
|
|
assert.deepEqual(fileTree(firstOutput), fileTree(secondOutput));
|
|
assert.deepEqual(first, second);
|
|
assert.equal(first.schemaVersion, 1);
|
|
assert.equal(first.origin, 'pinned-linux-runtime-upstream-licenses');
|
|
assert.deepEqual(
|
|
first.packages.map(({ id }) => id),
|
|
SOURCE_PACKAGES.map(({ id }) => id).sort()
|
|
);
|
|
assert.ok(first.packages.every(({ files }) => files.length >= 1));
|
|
assert.deepEqual(LICENSE_PATHS_BY_PACKAGE.libplacebo, [
|
|
'LICENSE',
|
|
'3rdparty/Vulkan-Headers/LICENSE.md',
|
|
'3rdparty/fast_float/LICENSE-APACHE',
|
|
'3rdparty/fast_float/LICENSE-BOOST',
|
|
'3rdparty/fast_float/LICENSE-MIT',
|
|
'3rdparty/glad/LICENSE',
|
|
'3rdparty/jinja/LICENSE.txt',
|
|
'3rdparty/markupsafe/LICENSE.txt',
|
|
'demos/3rdparty/nuklear/LICENSE',
|
|
]);
|
|
assert.deepEqual(validateLinuxRuntimeNotices(firstOutput, manifest), []);
|
|
|
|
const noticeContents = fs.readFileSync(
|
|
path.join(firstOutput, 'THIRD_PARTY_NOTICES.txt')
|
|
);
|
|
assert.equal(first.noticeFile.path, 'THIRD_PARTY_NOTICES.txt');
|
|
assert.equal(first.noticeFile.size, noticeContents.length);
|
|
assert.equal(
|
|
first.noticeFile.sha256,
|
|
crypto.createHash('sha256').update(noticeContents).digest('hex')
|
|
);
|
|
assert.match(
|
|
noticeContents.toString('utf8'),
|
|
/exact corresponding sources and build scripts are distributed alongside the binary release as linux-frame-copy-runtime-sources\.tar\.xz/
|
|
);
|
|
|
|
for (const packageRecord of first.packages) {
|
|
for (const fileRecord of packageRecord.files) {
|
|
const outputContents = fs.readFileSync(
|
|
path.join(firstOutput, fileRecord.path)
|
|
);
|
|
const sourcePath = fileRecord.path.slice(
|
|
`licenses/${packageRecord.id}/`.length
|
|
);
|
|
assert.ok(
|
|
LICENSE_PATHS_BY_PACKAGE[packageRecord.id].includes(sourcePath)
|
|
);
|
|
assert.deepEqual(
|
|
outputContents,
|
|
fs.readFileSync(
|
|
path.join(sourceRoot, packageRecord.id, sourcePath)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('rejects symlinked license sources and path escapes', (t) => {
|
|
const sourceRoot = createSourceFixture();
|
|
const outputRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-linux-license-inputs-')
|
|
);
|
|
const outsidePath = path.join(outputRoot, 'outside-license');
|
|
fs.writeFileSync(outsidePath, 'outside\n');
|
|
t.after(() => {
|
|
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
|
fs.rmSync(outputRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
const freetypeLicense = path.join(
|
|
sourceRoot,
|
|
'freetype',
|
|
LICENSE_PATHS_BY_PACKAGE.freetype[0]
|
|
);
|
|
fs.rmSync(freetypeLicense);
|
|
fs.symlinkSync(outsidePath, freetypeLicense);
|
|
|
|
assert.throws(
|
|
() =>
|
|
collectLinuxRuntimeLicenseInputs({
|
|
sourceRoot,
|
|
outputRoot: path.join(outputRoot, 'collected'),
|
|
runtimeManifest: runtimeManifest(),
|
|
}),
|
|
/symbolic link|outside/i
|
|
);
|
|
});
|
|
|
|
test('rejects missing, tampered, and undeclared cached license inputs', (t) => {
|
|
const sourceRoot = createSourceFixture();
|
|
const fixtureRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-linux-license-cache-')
|
|
);
|
|
const inputRoot = path.join(fixtureRoot, 'inputs');
|
|
t.after(() => {
|
|
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
const manifest = runtimeManifest();
|
|
collectLinuxRuntimeLicenseInputs({
|
|
sourceRoot,
|
|
outputRoot: inputRoot,
|
|
runtimeManifest: manifest,
|
|
});
|
|
const collectedManifest = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(inputRoot, 'linux-runtime-license-inputs.json'),
|
|
'utf8'
|
|
)
|
|
);
|
|
const firstLicensePath = path.join(
|
|
inputRoot,
|
|
collectedManifest.packages[0].files[0].path
|
|
);
|
|
fs.appendFileSync(firstLicensePath, 'tampered\n');
|
|
assert.throws(
|
|
() =>
|
|
generateLinuxRuntimeNotices({
|
|
licenseInputRoot: inputRoot,
|
|
outputRoot: path.join(fixtureRoot, 'tampered-output'),
|
|
runtimeManifest: manifest,
|
|
}),
|
|
/(?:Size|SHA-256) mismatch/
|
|
);
|
|
|
|
collectLinuxRuntimeLicenseInputs({
|
|
sourceRoot,
|
|
outputRoot: inputRoot,
|
|
runtimeManifest: manifest,
|
|
});
|
|
fs.writeFileSync(path.join(inputRoot, 'licenses', 'undeclared.txt'), 'x');
|
|
assert.throws(
|
|
() =>
|
|
generateLinuxRuntimeNotices({
|
|
licenseInputRoot: inputRoot,
|
|
outputRoot: path.join(fixtureRoot, 'extra-output'),
|
|
runtimeManifest: manifest,
|
|
}),
|
|
/undeclared license input/
|
|
);
|
|
|
|
fs.rmSync(path.join(inputRoot, 'licenses', 'undeclared.txt'));
|
|
fs.rmSync(
|
|
path.join(inputRoot, collectedManifest.packages[0].files[0].path)
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
generateLinuxRuntimeNotices({
|
|
licenseInputRoot: inputRoot,
|
|
outputRoot: path.join(fixtureRoot, 'missing-output'),
|
|
runtimeManifest: manifest,
|
|
}),
|
|
/Missing .*license input/
|
|
);
|
|
});
|
|
|
|
test('CLI collects immutable inputs and regenerates the packaged notice bundle', (t) => {
|
|
const sourceRoot = createSourceFixture();
|
|
const fixtureRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-linux-notices-cli-')
|
|
);
|
|
const manifestPath = path.join(fixtureRoot, 'runtime-manifest.json');
|
|
const inputRoot = path.join(fixtureRoot, 'inputs');
|
|
const outputRoot = path.join(fixtureRoot, 'notices');
|
|
fs.writeFileSync(
|
|
manifestPath,
|
|
`${JSON.stringify(runtimeManifest(), null, 2)}\n`
|
|
);
|
|
t.after(() => {
|
|
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
for (const args of [
|
|
[
|
|
'collect',
|
|
'--runtime-manifest',
|
|
manifestPath,
|
|
'--source-root',
|
|
sourceRoot,
|
|
'--output-root',
|
|
inputRoot,
|
|
],
|
|
[
|
|
'generate',
|
|
'--runtime-manifest',
|
|
manifestPath,
|
|
'--license-input-root',
|
|
inputRoot,
|
|
'--output-root',
|
|
outputRoot,
|
|
],
|
|
]) {
|
|
const result = spawnSync(process.execPath, [generatorScript, ...args], {
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(
|
|
result.status,
|
|
0,
|
|
[result.stdout, result.stderr].filter(Boolean).join('\n')
|
|
);
|
|
}
|
|
assert.deepEqual(
|
|
validateLinuxRuntimeNotices(outputRoot, runtimeManifest()),
|
|
[]
|
|
);
|
|
});
|