fix(packaging): strip VCS metadata from source bundle

This commit is contained in:
4gray committed 2026-07-17 22:42:37 +02:00
1 parent 0865fdf464
commit 99e5731beb
5 files changed
+720 -57

No files matched your search

+16 -53
View File
@@ -43,7 +43,7 @@ jobs:
echo 'meson=1.7.2'
} > "${RUNNER_TEMP}/linux-runtime-toolchain.txt"
TOOLCHAIN_SHA256="$(sha256sum "${RUNNER_TEMP}/linux-runtime-toolchain.txt" | cut -d ' ' -f 1)"
SOURCE_SHA256="${{ hashFiles('tools/embedded-mpv/build-linux-runtime.cjs', 'tools/embedded-mpv/build-linux-runtime.mjs', 'tools/embedded-mpv/generate-linux-runtime-notices.cjs', 'tools/embedded-mpv/linux-runtime-manifest.cjs', 'tools/embedded-mpv/stage-runtime.mjs') }}"
SOURCE_SHA256="${{ hashFiles('tools/embedded-mpv/build-linux-runtime.cjs', 'tools/embedded-mpv/build-linux-runtime.mjs', 'tools/embedded-mpv/generate-linux-runtime-notices.cjs', 'tools/embedded-mpv/linux-runtime-manifest.cjs', 'tools/embedded-mpv/stage-runtime.mjs', 'tools/packaging/prepare-linux-runtime-source-snapshot.cjs') }}"
echo "toolchain-sha256=${TOOLCHAIN_SHA256}" >> "${GITHUB_OUTPUT}"
echo "key=linux-frame-copy-runtime-v5-ubuntu-22.04-${TOOLCHAIN_SHA256}-${SOURCE_SHA256}" >> "${GITHUB_OUTPUT}"
@@ -126,6 +126,7 @@ jobs:
export RUNTIME_ROOT="${GITHUB_WORKSPACE}/vendor/embedded-mpv/linux-x64"
export SOURCE_INPUT_ROOT="${GITHUB_WORKSPACE}/dist/linux-frame-copy-runtime-source-inputs"
export SOURCE_BUNDLE_ROOT="${RUNNER_TEMP}/linux-frame-copy-runtime-sources"
export LIBPLACEBO_SOURCE_RECORD="${RUNNER_TEMP}/libplacebo-source-record.json"
test -f "${RUNTIME_ROOT}/runtime-manifest.json"
test -d "${SOURCE_INPUT_ROOT}/archives"
@@ -147,23 +148,27 @@ jobs:
"${SOURCE_BUNDLE_ROOT}/tooling"
cp -a "${SOURCE_INPUT_ROOT}/archives/." "${SOURCE_BUNDLE_ROOT}/archives/"
cp -a "${SOURCE_INPUT_ROOT}/git/." "${SOURCE_BUNDLE_ROOT}/git/"
cp -a "${SOURCE_INPUT_ROOT}/license-inputs/." "${SOURCE_BUNDLE_ROOT}/license-inputs/"
cp -a "${RUNTIME_ROOT}/notices/." "${SOURCE_BUNDLE_ROOT}/notices/"
cp "${RUNTIME_ROOT}/runtime-manifest.json" "${SOURCE_BUNDLE_ROOT}/metadata/runtime-manifest.json"
node tools/packaging/prepare-linux-runtime-source-snapshot.cjs prepare \
--runtime-manifest "${RUNTIME_ROOT}/runtime-manifest.json" \
--checkout "${SOURCE_INPUT_ROOT}/git/libplacebo" \
--output "${SOURCE_BUNDLE_ROOT}/git/libplacebo" \
--record-output "${LIBPLACEBO_SOURCE_RECORD}"
cp \
tools/embedded-mpv/build-linux-runtime.cjs \
tools/embedded-mpv/build-linux-runtime.mjs \
tools/embedded-mpv/generate-linux-runtime-notices.cjs \
tools/embedded-mpv/linux-runtime-manifest.cjs \
tools/embedded-mpv/stage-runtime.mjs \
tools/packaging/prepare-linux-runtime-source-snapshot.cjs \
"${SOURCE_BUNDLE_ROOT}/tooling/"
test -f "${SOURCE_BUNDLE_ROOT}/notices/THIRD_PARTY_NOTICES.txt"
test -f "${SOURCE_BUNDLE_ROOT}/notices/embedded-mpv-notices.json"
git rev-parse HEAD > "${SOURCE_BUNDLE_ROOT}/metadata/iptvnator-git-revision.txt"
git diff --binary HEAD > "${SOURCE_BUNDLE_ROOT}/metadata/local-changes.patch"
node <<'NODE'
const childProcess = require('node:child_process');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
@@ -195,56 +200,15 @@ jobs:
);
}
const libplaceboCheckout = path.join(
process.env.SOURCE_BUNDLE_ROOT,
'git',
'libplacebo'
const libplacebo = JSON.parse(
fs.readFileSync(process.env.LIBPLACEBO_SOURCE_RECORD, 'utf8')
);
const gitOutput = (...args) =>
childProcess.execFileSync('git', ['-C', libplaceboCheckout, ...args], {
encoding: 'utf8',
}).trim();
const sourceGitCommit = gitOutput('rev-parse', 'HEAD');
if (sourceGitCommit !== manifest.packages.libplacebo.sourceGitCommit) {
throw new Error('Bundled libplacebo checkout commit does not match the runtime manifest.');
}
const sourceSubmodules = gitOutput('submodule', 'status', '--recursive')
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
if (
JSON.stringify(sourceSubmodules) !==
libplacebo.sourceGitCommit !== manifest.packages.libplacebo.sourceGitCommit ||
JSON.stringify(libplacebo.sourceSubmodules) !==
JSON.stringify(manifest.packages.libplacebo.sourceSubmodules)
) {
throw new Error('Bundled libplacebo submodules do not match the runtime manifest.');
}
const assertCleanCheckout = (checkoutPath, label) => {
const status = childProcess.execFileSync(
'git',
[
'-C',
checkoutPath,
'status',
'--porcelain=v1',
'--untracked-files=all',
'--ignore-submodules=none',
],
{ encoding: 'utf8' }
);
if (status.trim()) {
throw new Error(`${label} checkout contains dirty or untracked files.`);
}
};
assertCleanCheckout(libplaceboCheckout, 'libplacebo');
for (const submoduleRecord of sourceSubmodules) {
const match = submoduleRecord.match(/^[+-U]?[a-f0-9]{40,64}\s+([^\s]+)/);
if (!match) {
throw new Error(`Invalid libplacebo submodule record: ${submoduleRecord}`);
}
assertCleanCheckout(
path.join(libplaceboCheckout, match[1]),
`libplacebo submodule ${match[1]}`
);
throw new Error('Prepared libplacebo source identity does not match the runtime manifest.');
}
const notices = JSON.parse(
@@ -271,10 +235,7 @@ jobs:
repositoryRevision,
sourcePackages: manifest.packages,
archives,
libplacebo: {
sourceGitCommit,
sourceSubmodules,
},
libplacebo,
legal: {
manifest: 'notices/embedded-mpv-notices.json',
noticeFile: notices.noticeFile,
@@ -290,6 +251,8 @@ jobs:
cd "${SOURCE_BUNDLE_ROOT}/archives"
sha256sum * > "../metadata/archive-sha256.txt"
)
node tools/packaging/prepare-linux-runtime-source-snapshot.cjs assert-vcs-free \
--directory "${SOURCE_BUNDLE_ROOT}"
mkdir -p dist/compliance
rm -f dist/compliance/linux-frame-copy-runtime-sources.tar.xz
@@ -214,13 +214,36 @@ test('Linux CI builds one cached source runtime and packages three isolated prof
complianceStep,
/new Set\(expectedArchiveHashes\)\.size[\s\S]*archives\.length !== expectedArchiveHashes\.length/
);
assert.match(
assert.match(complianceStep, /prepare-linux-runtime-source-snapshot\.cjs/);
assert.doesNotMatch(
complianceStep,
/status[\s\S]*--porcelain=v1[\s\S]*--untracked-files=all[\s\S]*--ignore-submodules=none/
/cp -a "\$\{SOURCE_INPUT_ROOT\}\/git\/\."/
);
assert.ok(
complianceStep.indexOf('prepare-linux-runtime-source-snapshot.cjs') <
complianceStep.indexOf(
'--file dist/compliance/linux-frame-copy-runtime-sources.tar.xz'
)
);
assert.match(
complianceStep,
/for \(const submoduleRecord of sourceSubmodules\)/
/libplacebo-source-record\.json[\s\S]*sourceGitCommit[\s\S]*sourceSubmodules/
);
assert.match(
complianceStep,
/prepare-linux-runtime-source-snapshot\.cjs assert-vcs-free/
);
assert.ok(
complianceStep.indexOf(
'prepare-linux-runtime-source-snapshot.cjs assert-vcs-free'
) <
complianceStep.indexOf(
'--file dist/compliance/linux-frame-copy-runtime-sources.tar.xz'
)
);
assert.match(
buildWorkflow,
/hashFiles\([^\n]*prepare-linux-runtime-source-snapshot\.cjs/
);
const buildStep = workflowStep('Build and stage pinned LGPL Linux runtime');
@@ -0,0 +1,318 @@
#!/usr/bin/env node
'use strict';
const childProcess = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const { isDeepStrictEqual } = require('node:util');
const GIT_COMMIT_PATTERN = /^[a-f0-9]{40,64}$/;
function gitOutput(checkoutPath, ...args) {
try {
return childProcess
.execFileSync('git', ['-C', checkoutPath, ...args], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
})
.trim();
} catch (error) {
const stderr =
error && typeof error === 'object' && 'stderr' in error
? String(error.stderr).trim()
: '';
throw new Error(
`Unable to inspect source checkout with git ${args.join(' ')}${stderr ? `: ${stderr}` : '.'}`
);
}
}
function assertExpectedGitRecord(expected) {
if (
expected === null ||
typeof expected !== 'object' ||
typeof expected.sourceGitCommit !== 'string' ||
!GIT_COMMIT_PATTERN.test(expected.sourceGitCommit) ||
!Array.isArray(expected.sourceSubmodules) ||
expected.sourceSubmodules.some(
(record) => typeof record !== 'string' || record.length === 0
)
) {
throw new Error(
'Expected source identity must contain one commit and a submodule record array.'
);
}
}
function assertCleanCheckout(checkoutPath, label) {
const status = gitOutput(
checkoutPath,
'status',
'--porcelain=v1',
'--untracked-files=all',
'--ignore-submodules=none'
);
if (status) {
throw new Error(`${label} checkout contains dirty or untracked files.`);
}
}
function sourceSubmoduleIdentity(record) {
const match = record.match(/^([a-f0-9]{40,64})\s+([^\s]+)(?:\s|$)/);
if (!match) {
throw new Error(`Invalid source submodule record: ${record}`);
}
const submodulePath = match[2];
if (
path.isAbsolute(submodulePath) ||
submodulePath
.split('/')
.some((part) => part === '' || part === '.' || part === '..')
) {
throw new Error(`Unsafe source submodule path: ${submodulePath}`);
}
return {
commit: match[1],
path: submodulePath,
};
}
function inspectCleanGitSource(checkoutPath, expected) {
assertExpectedGitRecord(expected);
const sourceGitCommit = gitOutput(checkoutPath, 'rev-parse', 'HEAD');
if (sourceGitCommit !== expected.sourceGitCommit) {
throw new Error(
'Source checkout commit does not match the runtime manifest.'
);
}
const submoduleOutput = gitOutput(
checkoutPath,
'submodule',
'status',
'--recursive'
);
const sourceSubmodules = submoduleOutput
? submoduleOutput
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
: [];
if (!isDeepStrictEqual(sourceSubmodules, expected.sourceSubmodules)) {
throw new Error(
'Source checkout submodules do not match the runtime manifest.'
);
}
assertCleanCheckout(checkoutPath, 'Source');
for (const submoduleRecord of sourceSubmodules) {
const submodule = sourceSubmoduleIdentity(submoduleRecord);
const submoduleCheckout = path.join(
checkoutPath,
...submodule.path.split('/')
);
if (
gitOutput(submoduleCheckout, 'rev-parse', 'HEAD') !==
submodule.commit
) {
throw new Error(
`Source submodule ${submodule.path} commit does not match its recorded identity.`
);
}
assertCleanCheckout(
submoduleCheckout,
`Source submodule ${submodule.path}`
);
}
return {
sourceGitCommit,
sourceSubmodules,
};
}
function gitMetadataEntries(rootPath) {
const entries = [];
function visit(directoryPath, relativeDirectory = '') {
for (const entry of fs.readdirSync(directoryPath, {
withFileTypes: true,
})) {
const relativePath = path.posix.join(relativeDirectory, entry.name);
const absolutePath = path.join(directoryPath, entry.name);
if (entry.name === '.git') {
entries.push(relativePath);
} else if (entry.isDirectory()) {
visit(absolutePath, relativePath);
}
}
}
visit(rootPath);
return entries.sort();
}
function assertNoGitMetadata(rootPath) {
const entries = gitMetadataEntries(rootPath);
if (entries.length > 0) {
throw new Error(
`Prepared source snapshot must not contain VCS metadata: ${entries.join(', ')}`
);
}
}
function lstatIfExists(candidatePath) {
try {
return fs.lstatSync(candidatePath);
} catch (error) {
if (
error &&
typeof error === 'object' &&
'code' in error &&
error.code === 'ENOENT'
) {
return null;
}
throw error;
}
}
function copyWorkingTreeWithoutGitMetadata(checkoutPath, outputPath) {
const outputParent = path.dirname(outputPath);
const temporaryPath = fs.mkdtempSync(
path.join(outputParent, `.${path.basename(outputPath)}-`)
);
try {
fs.cpSync(checkoutPath, temporaryPath, {
recursive: true,
dereference: false,
verbatimSymlinks: true,
filter: (sourcePath) => path.basename(sourcePath) !== '.git',
});
assertNoGitMetadata(temporaryPath);
if (lstatIfExists(outputPath)) {
throw new Error(
`Prepared source snapshot output must not already exist: ${outputPath}`
);
}
fs.renameSync(temporaryPath, outputPath);
} catch (error) {
fs.rmSync(temporaryPath, { recursive: true, force: true });
throw error;
}
}
function prepareLinuxRuntimeSourceSnapshot({
checkoutPath,
outputPath,
expected,
}) {
const checkoutStat = fs.lstatSync(checkoutPath);
if (!checkoutStat.isDirectory() || checkoutStat.isSymbolicLink()) {
throw new Error('Source checkout must be a real directory.');
}
const checkoutRoot = fs.realpathSync(checkoutPath);
const outputRoot = path.resolve(outputPath);
const outputParent = path.dirname(outputRoot);
const outputParentStat = fs.lstatSync(outputParent);
if (!outputParentStat.isDirectory() || outputParentStat.isSymbolicLink()) {
throw new Error(
'Prepared source snapshot parent must be a real directory.'
);
}
if (lstatIfExists(outputRoot)) {
throw new Error(
`Prepared source snapshot output must not already exist: ${outputRoot}`
);
}
const realOutputRoot = path.join(
fs.realpathSync(outputParent),
path.basename(outputRoot)
);
const isInside = (root, candidate) => {
const relative = path.relative(root, candidate);
return (
relative === '' ||
(relative !== '..' &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative))
);
};
if (
isInside(checkoutRoot, realOutputRoot) ||
isInside(realOutputRoot, checkoutRoot)
) {
throw new Error(
'Prepared source snapshot and checkout must be separate trees.'
);
}
const record = inspectCleanGitSource(checkoutRoot, expected);
copyWorkingTreeWithoutGitMetadata(checkoutRoot, realOutputRoot);
assertNoGitMetadata(realOutputRoot);
return record;
}
function parseArguments(argv) {
const [command, ...tokens] = argv;
const options = {};
for (let index = 0; index < tokens.length; index += 2) {
const name = tokens[index];
const value = tokens[index + 1];
if (!name?.startsWith('--') || value === undefined) {
throw new Error(`Invalid command-line argument: ${name ?? ''}`);
}
options[name.slice(2)] = value;
}
return { command, options };
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
function main(argv = process.argv.slice(2)) {
const { command, options } = parseArguments(argv);
if (
command === 'prepare' &&
options['runtime-manifest'] &&
options.checkout &&
options.output &&
options['record-output']
) {
const runtimeManifest = readJson(options['runtime-manifest']);
const sourcePackage = runtimeManifest?.packages?.libplacebo;
const record = prepareLinuxRuntimeSourceSnapshot({
checkoutPath: options.checkout,
outputPath: options.output,
expected: {
sourceGitCommit: sourcePackage?.sourceGitCommit,
sourceSubmodules: sourcePackage?.sourceSubmodules,
},
});
writeJson(options['record-output'], record);
return;
}
if (command === 'assert-vcs-free' && options.directory) {
assertNoGitMetadata(options.directory);
return;
}
throw new Error(
'Usage: prepare-linux-runtime-source-snapshot.cjs prepare --runtime-manifest <path> --checkout <path> --output <path> --record-output <path> | assert-vcs-free --directory <path>'
);
}
if (require.main === module) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
module.exports = {
assertNoGitMetadata,
inspectCleanGitSource,
prepareLinuxRuntimeSourceSnapshot,
};
@@ -0,0 +1,354 @@
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import test from 'node:test';
import { fileURLToPath, pathToFileURL } from 'node:url';
const helperPath = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'prepare-linux-runtime-source-snapshot.cjs'
);
async function loadHelper() {
if (!fs.existsSync(helperPath)) {
return null;
}
return import(pathToFileURL(helperPath).href);
}
function runGit(cwd, args, env = {}) {
const result = spawnSync('git', args, {
cwd,
encoding: 'utf8',
env: {
...process.env,
...env,
},
});
assert.equal(
result.status,
0,
`git ${args.join(' ')} failed:\n${result.stderr}`
);
return result.stdout.trim();
}
function createEquivalentCheckouts(root) {
const origin = path.join(root, 'origin');
fs.mkdirSync(origin);
runGit(origin, ['init', '--quiet']);
fs.mkdirSync(path.join(origin, 'src'));
fs.writeFileSync(path.join(origin, 'src', 'renderer.c'), 'int main() {}\n');
fs.writeFileSync(path.join(origin, 'LICENSE'), 'license\n');
fs.symlinkSync('../LICENSE', path.join(origin, 'src', 'license-link'));
runGit(origin, ['add', '.']);
runGit(origin, ['commit', '--quiet', '-m', 'source fixture'], {
GIT_AUTHOR_NAME: 'Fixture',
GIT_AUTHOR_EMAIL: 'fixture@example.invalid',
GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
GIT_COMMITTER_NAME: 'Fixture',
GIT_COMMITTER_EMAIL: 'fixture@example.invalid',
GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
});
const commit = runGit(origin, ['rev-parse', 'HEAD']);
const first = path.join(root, 'first');
const second = path.join(root, 'second');
runGit(root, ['clone', '--quiet', origin, first]);
runGit(root, ['clone', '--quiet', origin, second]);
fs.appendFileSync(
path.join(first, '.git', 'config'),
'\n[fixture]\n\tvalue = first\n'
);
fs.appendFileSync(
path.join(second, '.git', 'config'),
'\n[fixture]\n\tvalue = second\n'
);
fs.writeFileSync(path.join(first, '.git', 'fixture-cache'), 'one');
fs.writeFileSync(path.join(second, '.git', 'fixture-cache'), 'two');
const differentTime = new Date('2030-01-01T00:00:00Z');
fs.utimesSync(
path.join(second, '.git', 'index'),
differentTime,
differentTime
);
return { commit, first, second };
}
function createCheckoutWithSubmodule(root) {
const submoduleOrigin = path.join(root, 'submodule-origin');
fs.mkdirSync(submoduleOrigin);
runGit(submoduleOrigin, ['init', '--quiet']);
fs.writeFileSync(
path.join(submoduleOrigin, 'submodule-source.c'),
'int submodule_source;\n'
);
runGit(submoduleOrigin, ['add', '.']);
runGit(submoduleOrigin, ['commit', '--quiet', '-m', 'submodule source'], {
GIT_AUTHOR_NAME: 'Fixture',
GIT_AUTHOR_EMAIL: 'fixture@example.invalid',
GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
GIT_COMMITTER_NAME: 'Fixture',
GIT_COMMITTER_EMAIL: 'fixture@example.invalid',
GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
});
const checkout = path.join(root, 'checkout-with-submodule');
fs.mkdirSync(checkout);
runGit(checkout, ['init', '--quiet']);
fs.writeFileSync(path.join(checkout, 'README'), 'source tree\n');
runGit(checkout, ['add', 'README']);
runGit(checkout, [
'-c',
'protocol.file.allow=always',
'submodule',
'add',
'--quiet',
submoduleOrigin,
'3rdparty/example',
]);
runGit(checkout, ['commit', '--quiet', '-m', 'source with submodule'], {
GIT_AUTHOR_NAME: 'Fixture',
GIT_AUTHOR_EMAIL: 'fixture@example.invalid',
GIT_AUTHOR_DATE: '2000-01-02T00:00:00Z',
GIT_COMMITTER_NAME: 'Fixture',
GIT_COMMITTER_EMAIL: 'fixture@example.invalid',
GIT_COMMITTER_DATE: '2000-01-02T00:00:00Z',
});
return {
checkout,
expected: {
sourceGitCommit: runGit(checkout, ['rev-parse', 'HEAD']),
sourceSubmodules: runGit(checkout, [
'submodule',
'status',
'--recursive',
])
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean),
},
};
}
function snapshotRecords(root) {
const records = [];
function visit(directory, relativeDirectory = '') {
for (const entry of fs
.readdirSync(directory, { withFileTypes: true })
.sort(({ name: left }, { name: right }) =>
left.localeCompare(right)
)) {
const relativePath = path.posix.join(relativeDirectory, entry.name);
const absolutePath = path.join(directory, entry.name);
const stat = fs.lstatSync(absolutePath);
if (entry.isDirectory()) {
records.push({
path: `${relativePath}/`,
mode: stat.mode & 0o777,
});
visit(absolutePath, relativePath);
} else if (entry.isSymbolicLink()) {
records.push({
path: relativePath,
mode: stat.mode & 0o777,
symlink: fs.readlinkSync(absolutePath),
});
} else {
records.push({
path: relativePath,
mode: stat.mode & 0o777,
sha256: crypto
.createHash('sha256')
.update(fs.readFileSync(absolutePath))
.digest('hex'),
});
}
}
}
visit(root);
return records;
}
test('prepares identical VCS-free snapshots from equivalent clean checkouts', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-snapshot-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const { commit, first, second } = createEquivalentCheckouts(root);
const firstOutput = path.join(root, 'snapshot-first');
const secondOutput = path.join(root, 'snapshot-second');
const expected = {
sourceGitCommit: commit,
sourceSubmodules: [],
};
const firstRecord = helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: firstOutput,
expected,
});
const secondRecord = helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: second,
outputPath: secondOutput,
expected,
});
assert.deepEqual(firstRecord, expected);
assert.deepEqual(secondRecord, expected);
assert.deepEqual(
snapshotRecords(firstOutput),
snapshotRecords(secondOutput)
);
assert.equal(
snapshotRecords(firstOutput).some(({ path: recordPath }) =>
recordPath.split('/').includes('.git')
),
false
);
assert.equal(
fs.readlinkSync(path.join(firstOutput, 'src', 'license-link')),
'../LICENSE'
);
});
test('validates checkout cleanliness and exact commit/submodule identities before copying', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-identity-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const { commit, first, second } = createEquivalentCheckouts(root);
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: path.join(root, 'wrong-commit'),
expected: {
sourceGitCommit: '0'.repeat(40),
sourceSubmodules: [],
},
}),
/commit does not match/
);
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: path.join(root, 'wrong-submodules'),
expected: {
sourceGitCommit: commit,
sourceSubmodules: [`${'1'.repeat(40)} 3rdparty/example`],
},
}),
/submodules do not match/
);
fs.writeFileSync(path.join(second, 'untracked-build-output'), 'dirty');
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: second,
outputPath: path.join(root, 'dirty'),
expected: {
sourceGitCommit: commit,
sourceSubmodules: [],
},
}),
/dirty or untracked files/
);
const existingOutput = path.join(root, 'existing-output');
fs.mkdirSync(existingOutput);
fs.writeFileSync(
path.join(existingOutput, 'owned-by-someone-else'),
'keep'
);
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: existingOutput,
expected: {
sourceGitCommit: commit,
sourceSubmodules: [],
},
}),
/output.*must not already exist/i
);
assert.equal(
fs.readFileSync(
path.join(existingOutput, 'owned-by-someone-else'),
'utf8'
),
'keep'
);
const existingSymlink = path.join(root, 'existing-symlink');
fs.symlinkSync('missing-target', existingSymlink);
assert.throws(
() =>
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: first,
outputPath: existingSymlink,
expected: {
sourceGitCommit: commit,
sourceSubmodules: [],
},
}),
/output.*must not already exist/i
);
assert.equal(fs.readlinkSync(existingSymlink), 'missing-target');
});
test('preserves recursive submodule sources while stripping every nested .git link', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-submodule-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const { checkout, expected } = createCheckoutWithSubmodule(root);
const output = path.join(root, 'submodule-snapshot');
assert.deepEqual(
helper.prepareLinuxRuntimeSourceSnapshot({
checkoutPath: checkout,
outputPath: output,
expected,
}),
expected
);
assert.equal(
fs.readFileSync(
path.join(output, '3rdparty', 'example', 'submodule-source.c'),
'utf8'
),
'int submodule_source;\n'
);
assert.doesNotThrow(() => helper.assertNoGitMetadata(output));
});
test('fails closed when a prepared snapshot contains any nested .git entry', async (t) => {
const helper = await loadHelper();
assert.ok(helper, 'the deterministic source snapshot helper must exist');
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-source-vcs-entry-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'nested', '.git'), { recursive: true });
fs.writeFileSync(path.join(root, 'nested', '.git', 'index'), 'metadata');
assert.throws(
() => helper.assertNoGitMetadata(root),
/must not contain VCS metadata.*nested\/\.git/i
);
});
+6 -1
View File
@@ -26,6 +26,8 @@
"{workspaceRoot}/tools/packaging/configure-linux-frame-copy-build.test.mjs",
"{workspaceRoot}/tools/packaging/linux-frame-copy-profile.cjs",
"{workspaceRoot}/tools/packaging/linux-frame-copy-profile.test.mjs",
"{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.cjs",
"{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs",
"{workspaceRoot}/tools/packaging/publish-snap-workflow.test.mjs",
"{workspaceRoot}/tools/packaging/release-snap-assets.cjs",
"{workspaceRoot}/tools/packaging/verify-linux-frame-copy-runtime.mjs",
@@ -41,14 +43,17 @@
"{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs"
],
"options": {
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"cwd": "{workspaceRoot}"
}
},
"lint": {
"inputs": [
"default",
"{workspaceRoot}/.github/workflows/build-and-make.yaml",
"{workspaceRoot}/.github/workflows/publish-snap.yaml",
"{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.cjs",
"{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs",
"{workspaceRoot}/tools/packaging/publish-snap-workflow.test.mjs",
"{workspaceRoot}/tools/packaging/release-snap-assets.cjs"
],