Files
iptvnator/tools/embedded-mpv/linux-source-archive-contract.cjs
T
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* 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
2026-07-18 17:28:22 +02:00

182 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const { isDeepStrictEqual } = require('node:util');
const SOURCE_ARCHIVE_BINDING_SCHEMA_VERSION = 1;
const SOURCE_ARCHIVE_NAME = 'linux-frame-copy-runtime-sources.tar.xz';
const SOURCE_ARCHIVE_BINDING_NAME = 'source-archive-binding.json';
const GIT_COMMIT_PATTERN = /^[a-f0-9]{40,64}$/;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
function validateLinuxSourceArchiveBinding(
binding,
{ expectedRepositoryRevision, expectedSha256 } = {}
) {
const errors = [];
if (
binding === null ||
typeof binding !== 'object' ||
Array.isArray(binding)
) {
return ['Linux source archive binding must be an object.'];
}
if (
!isDeepStrictEqual(Object.keys(binding).sort(), [
'name',
'repositoryRevision',
'schemaVersion',
'sha256',
])
) {
errors.push(
'Linux source archive binding must contain only schemaVersion, name, sha256, and repositoryRevision.'
);
}
if (binding.schemaVersion !== SOURCE_ARCHIVE_BINDING_SCHEMA_VERSION) {
errors.push(
`Linux source archive binding schemaVersion must equal ${SOURCE_ARCHIVE_BINDING_SCHEMA_VERSION}.`
);
}
if (binding.name !== SOURCE_ARCHIVE_NAME) {
errors.push(
`Linux source archive binding name must equal ${SOURCE_ARCHIVE_NAME}.`
);
}
if (
typeof binding.sha256 !== 'string' ||
!SHA256_PATTERN.test(binding.sha256)
) {
errors.push(
'Linux source archive binding sha256 must be a lowercase SHA-256 digest.'
);
}
if (
typeof binding.repositoryRevision !== 'string' ||
!GIT_COMMIT_PATTERN.test(binding.repositoryRevision)
) {
errors.push(
'Linux source archive binding repositoryRevision must be a full Git commit.'
);
}
if (
expectedRepositoryRevision !== undefined &&
binding.repositoryRevision !== expectedRepositoryRevision
) {
errors.push(
'Linux source archive binding repositoryRevision does not match the expected release commit.'
);
}
if (expectedSha256 !== undefined && binding.sha256 !== expectedSha256) {
errors.push(
'Linux source archive binding sha256 does not match the source archive bytes.'
);
}
return errors;
}
function sha256File(filePath) {
const descriptor = fs.openSync(filePath, 'r');
const hash = crypto.createHash('sha256');
const buffer = Buffer.alloc(1024 * 1024);
try {
let bytesRead;
do {
bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, null);
if (bytesRead > 0) {
hash.update(buffer.subarray(0, bytesRead));
}
} while (bytesRead > 0);
} finally {
fs.closeSync(descriptor);
}
return hash.digest('hex');
}
function createLinuxSourceArchiveBinding({ archivePath, repositoryRevision }) {
const stat = fs.lstatSync(archivePath);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size === 0) {
throw new Error(
'Linux source archive must be a non-empty regular file.'
);
}
const binding = {
schemaVersion: SOURCE_ARCHIVE_BINDING_SCHEMA_VERSION,
name: SOURCE_ARCHIVE_NAME,
sha256: sha256File(archivePath),
repositoryRevision,
};
const errors = validateLinuxSourceArchiveBinding(binding);
if (errors.length > 0) {
throw new Error(errors.join('\n'));
}
return binding;
}
function parseArguments(argv) {
const [command, ...tokens] = argv;
if (tokens.length % 2 !== 0) {
throw new Error('Linux source archive binding arguments are invalid.');
}
const options = {};
for (let index = 0; index < tokens.length; index += 2) {
const token = tokens[index];
const value = tokens[index + 1];
if (
!token.startsWith('--') ||
value === undefined ||
Object.hasOwn(options, token.slice(2))
) {
throw new Error(
'Linux source archive binding arguments are invalid.'
);
}
options[token.slice(2)] = value;
}
return { command, options };
}
function main(argv = process.argv.slice(2)) {
const { command, options } = parseArguments(argv);
if (
command !== 'create' ||
!options.archive ||
!options['repository-revision'] ||
!options.output
) {
throw new Error(
'Usage: linux-source-archive-contract.cjs create --archive <path> --repository-revision <commit> --output <path>'
);
}
const binding = createLinuxSourceArchiveBinding({
archivePath: options.archive,
repositoryRevision: options['repository-revision'],
});
fs.writeFileSync(options.output, `${JSON.stringify(binding, null, 2)}\n`, {
mode: 0o644,
});
}
if (require.main === module) {
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exitCode = 1;
}
}
module.exports = {
SOURCE_ARCHIVE_BINDING_NAME,
SOURCE_ARCHIVE_BINDING_SCHEMA_VERSION,
SOURCE_ARCHIVE_NAME,
createLinuxSourceArchiveBinding,
sha256File,
validateLinuxSourceArchiveBinding,
};