mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(packaging): strip VCS metadata from source bundle
This commit is contained in:
1 parent
0865fdf464
commit
99e5731beb
5 files changed
+720
-57
No files matched your search
@@ -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
|
||||
);
|
||||
});
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user