mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(packaging): add verified source mirror fallback (#1325)
This commit is contained in:
1 parent
9f4e11d6de
commit
b14ce2452b
6 files changed
+273
-25
No files matched your search
@@ -483,6 +483,7 @@ jobs:
|
||||
const safeDeploymentTarget = deploymentTarget.replace(/[^A-Za-z0-9_.-]/g, '-');
|
||||
sourceHashFiles.push(
|
||||
'tools/embedded-mpv/build-macos-runtime.mjs',
|
||||
'tools/embedded-mpv/download-pinned-source.mjs',
|
||||
'tools/embedded-mpv/stage-macos-runtime.mjs'
|
||||
);
|
||||
try {
|
||||
|
||||
@@ -82,6 +82,16 @@ pnpm embedded-mpv:build-runtime:linux -- /tmp/linux-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- linux x64 /tmp/linux-prefix
|
||||
```
|
||||
|
||||
The macOS builder verifies every downloaded archive against its pinned
|
||||
SHA-256 digest before extraction. FreeType uses its official SourceForge
|
||||
distribution as the primary source and the official Savannah distribution as
|
||||
a fallback; a failed or mismatched download is discarded before the next
|
||||
mirror is attempted. The runtime manifest records the selected URL for a new
|
||||
download and the complete ordered candidate list for every archive, so
|
||||
fallback use remains visible in the source provenance. Changes to the
|
||||
downloader participate in the runtime cache key, so cached native artifacts
|
||||
cannot outlive source-acquisition policy changes.
|
||||
|
||||
The Linux builder runs only on Linux x64. It requires the tool versions and
|
||||
system development interfaces declared in `build-linux-runtime.cjs`, including
|
||||
Meson 1.6 or newer, gperf 3.1 or newer, Ninja, CMake, NASM, pkg-config,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { downloadPinnedSource } from './download-pinned-source.mjs';
|
||||
|
||||
const rawArgs = process.argv.slice(2);
|
||||
const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;
|
||||
@@ -17,31 +17,44 @@ const sourcePackages = [
|
||||
{
|
||||
id: 'freetype',
|
||||
version: '2.13.3',
|
||||
url: 'https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz',
|
||||
url: 'https://downloads.sourceforge.net/project/freetype/freetype2/2.13.3/freetype-2.13.3.tar.xz',
|
||||
mirrors: [
|
||||
'https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz',
|
||||
],
|
||||
expectedSha256:
|
||||
'0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289',
|
||||
license: 'FreeType License or GPL-2.0-or-later',
|
||||
},
|
||||
{
|
||||
id: 'fribidi',
|
||||
version: '1.0.16',
|
||||
url: 'https://github.com/fribidi/fribidi/releases/download/v1.0.16/fribidi-1.0.16.tar.xz',
|
||||
expectedSha256:
|
||||
'1b1cde5b235d40479e91be2f0e88a309e3214c8ab470ec8a2744d82a5a9ea05c',
|
||||
license: 'LGPL-2.1-or-later',
|
||||
},
|
||||
{
|
||||
id: 'harfbuzz',
|
||||
version: '8.5.0',
|
||||
url: 'https://github.com/harfbuzz/harfbuzz/releases/download/8.5.0/harfbuzz-8.5.0.tar.xz',
|
||||
expectedSha256:
|
||||
'77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27',
|
||||
license: 'MIT',
|
||||
},
|
||||
{
|
||||
id: 'libass',
|
||||
version: '0.17.3',
|
||||
url: 'https://github.com/libass/libass/releases/download/0.17.3/libass-0.17.3.tar.xz',
|
||||
expectedSha256:
|
||||
'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959',
|
||||
license: 'ISC',
|
||||
},
|
||||
{
|
||||
id: 'ffmpeg',
|
||||
version: '8.1',
|
||||
url: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
|
||||
expectedSha256:
|
||||
'b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a',
|
||||
license: 'LGPL-compatible configuration',
|
||||
},
|
||||
{
|
||||
@@ -55,6 +68,8 @@ const sourcePackages = [
|
||||
id: 'mpv',
|
||||
version: '0.41.0',
|
||||
url: 'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz',
|
||||
expectedSha256:
|
||||
'ee21092a5ee427353392360929dc64645c54479aefdb5babc5cfbb5fad626209',
|
||||
license: 'LGPL-compatible configuration with -Dgpl=false',
|
||||
},
|
||||
];
|
||||
@@ -220,12 +235,6 @@ function sourcePathFor(packageId) {
|
||||
return path.join(sourceRoot, packageId);
|
||||
}
|
||||
|
||||
function sha256File(filePath) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(fs.readFileSync(filePath));
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function runCapture(command, commandArgs, options = {}) {
|
||||
const result = spawnSync(command, commandArgs, {
|
||||
cwd: options.cwd ?? workspaceRoot,
|
||||
@@ -309,18 +318,27 @@ function downloadSources() {
|
||||
}
|
||||
|
||||
const archivePath = archivePathFor(sourcePackage);
|
||||
if (!fs.existsSync(archivePath)) {
|
||||
run('curl', [
|
||||
'-fL',
|
||||
'--retry',
|
||||
'3',
|
||||
'--retry-delay',
|
||||
'5',
|
||||
'-o',
|
||||
archivePath,
|
||||
sourcePackage.url,
|
||||
]);
|
||||
}
|
||||
const { sourceSha256, sourceUrl, sourceUrls } = downloadPinnedSource({
|
||||
archivePath,
|
||||
expectedSha256: sourcePackage.expectedSha256,
|
||||
urls: [sourcePackage.url, ...(sourcePackage.mirrors ?? [])],
|
||||
download: ({ destinationPath, url }) =>
|
||||
run('curl', [
|
||||
'--fail',
|
||||
'--location',
|
||||
'--retry',
|
||||
'3',
|
||||
'--retry-all-errors',
|
||||
'--connect-timeout',
|
||||
'30',
|
||||
'--proto',
|
||||
'=https',
|
||||
'--tlsv1.2',
|
||||
'--output',
|
||||
destinationPath,
|
||||
url,
|
||||
]),
|
||||
});
|
||||
|
||||
const packageSourcePath = sourcePathFor(sourcePackage.id);
|
||||
fs.rmSync(packageSourcePath, { recursive: true, force: true });
|
||||
@@ -333,7 +351,9 @@ function downloadSources() {
|
||||
'--strip-components',
|
||||
'1',
|
||||
]);
|
||||
sourcePackage.sha256 = sha256File(archivePath);
|
||||
sourcePackage.sha256 = sourceSha256;
|
||||
sourcePackage.sourceUrl = sourceUrl;
|
||||
sourcePackage.sourceUrls = sourceUrls;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +513,13 @@ function sourceMetadata(packageId) {
|
||||
const sourcePackage = packageById.get(packageId);
|
||||
return {
|
||||
version: sourcePackage.version,
|
||||
sourceUrl: sourcePackage.url ?? sourcePackage.gitUrl,
|
||||
sourceUrl:
|
||||
sourcePackage.sourceUrl ??
|
||||
sourcePackage.url ??
|
||||
sourcePackage.gitUrl,
|
||||
...(sourcePackage.sourceUrls
|
||||
? { sourceUrls: sourcePackage.sourceUrls }
|
||||
: {}),
|
||||
...(sourcePackage.tag ? { sourceTag: sourcePackage.tag } : {}),
|
||||
...(sourcePackage.sha256
|
||||
? { sourceSha256: sourcePackage.sha256 }
|
||||
@@ -523,7 +549,13 @@ function writeManifest() {
|
||||
sourcePackage.id,
|
||||
{
|
||||
version: sourcePackage.version,
|
||||
sourceUrl: sourcePackage.url ?? sourcePackage.gitUrl,
|
||||
sourceUrl:
|
||||
sourcePackage.sourceUrl ??
|
||||
sourcePackage.url ??
|
||||
sourcePackage.gitUrl,
|
||||
...(sourcePackage.sourceUrls
|
||||
? { sourceUrls: sourcePackage.sourceUrls }
|
||||
: {}),
|
||||
...(sourcePackage.tag
|
||||
? { sourceTag: sourcePackage.tag }
|
||||
: {}),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
|
||||
function sha256File(filePath) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(fs.readFileSync(filePath));
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function checksumFailure(expectedSha256, actualSha256) {
|
||||
return `SHA-256 mismatch: expected ${expectedSha256}, received ${actualSha256}`;
|
||||
}
|
||||
|
||||
export function downloadPinnedSource({
|
||||
archivePath,
|
||||
download,
|
||||
expectedSha256,
|
||||
urls,
|
||||
}) {
|
||||
const partialPath = `${archivePath}.partial`;
|
||||
const failures = [];
|
||||
const sourceUrls = [...urls];
|
||||
|
||||
if (fs.existsSync(archivePath)) {
|
||||
const actualSha256 = sha256File(archivePath);
|
||||
if (actualSha256 === expectedSha256) {
|
||||
return {
|
||||
sourceSha256: actualSha256,
|
||||
sourceUrl: null,
|
||||
sourceUrls,
|
||||
};
|
||||
}
|
||||
failures.push(
|
||||
`cached archive: ${checksumFailure(
|
||||
expectedSha256,
|
||||
actualSha256
|
||||
)}`
|
||||
);
|
||||
fs.rmSync(archivePath, { force: true });
|
||||
}
|
||||
|
||||
for (const url of sourceUrls) {
|
||||
fs.rmSync(partialPath, { force: true });
|
||||
try {
|
||||
download({ destinationPath: partialPath, url });
|
||||
const actualSha256 = sha256File(partialPath);
|
||||
if (actualSha256 !== expectedSha256) {
|
||||
throw new Error(
|
||||
checksumFailure(expectedSha256, actualSha256)
|
||||
);
|
||||
}
|
||||
fs.renameSync(partialPath, archivePath);
|
||||
return { sourceSha256: actualSha256, sourceUrl: url, sourceUrls };
|
||||
} catch (error) {
|
||||
failures.push(`${url}: ${error.message}`);
|
||||
fs.rmSync(partialPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to download a verified source archive:\n${failures.join('\n')}`
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
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 test from 'node:test';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const downloaderPath = path.join(currentDir, 'download-pinned-source.mjs');
|
||||
const macosBuilderPath = path.join(currentDir, 'build-macos-runtime.mjs');
|
||||
const workspaceRoot = path.resolve(currentDir, '..', '..');
|
||||
const buildWorkflowPath = path.join(
|
||||
workspaceRoot,
|
||||
'.github',
|
||||
'workflows',
|
||||
'build-and-make.yaml'
|
||||
);
|
||||
const { downloadPinnedSource } = await import(pathToFileURL(downloaderPath));
|
||||
|
||||
function sha256(value) {
|
||||
return crypto.createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
function withTemporaryDirectory(run) {
|
||||
const temporaryDirectory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'iptvnator-pinned-source-test-')
|
||||
);
|
||||
try {
|
||||
return run(temporaryDirectory);
|
||||
} finally {
|
||||
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('provides the pinned source archive downloader', () => {
|
||||
assert.equal(fs.existsSync(downloaderPath), true);
|
||||
});
|
||||
|
||||
test('pins official FreeType mirrors in the macOS runtime builder', () => {
|
||||
const builderSource = fs.readFileSync(macosBuilderPath, 'utf8');
|
||||
const sourceForgeUrl =
|
||||
'https://downloads.sourceforge.net/project/freetype/freetype2/2.13.3/freetype-2.13.3.tar.xz';
|
||||
const savannahUrl =
|
||||
'https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz';
|
||||
|
||||
assert.match(builderSource, /downloadPinnedSource/);
|
||||
assert.ok(builderSource.indexOf(sourceForgeUrl) >= 0);
|
||||
assert.ok(
|
||||
builderSource.indexOf(savannahUrl) >
|
||||
builderSource.indexOf(sourceForgeUrl)
|
||||
);
|
||||
assert.match(
|
||||
builderSource,
|
||||
/0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289/
|
||||
);
|
||||
assert.match(
|
||||
builderSource,
|
||||
/const\s*\{\s*sourceSha256,\s*sourceUrl,\s*sourceUrls\s*\}\s*=\s*downloadPinnedSource/
|
||||
);
|
||||
assert.match(builderSource, /sourcePackage\.sourceUrl = sourceUrl/);
|
||||
assert.match(builderSource, /sourcePackage\.sourceUrls = sourceUrls/);
|
||||
assert.match(
|
||||
builderSource,
|
||||
/sourceUrl:\s*sourcePackage\.sourceUrl\s*\?\?\s*sourcePackage\.url/
|
||||
);
|
||||
assert.match(
|
||||
builderSource,
|
||||
/sourcePackage\.sourceUrls\s*\?\s*\{\s*sourceUrls:\s*sourcePackage\.sourceUrls\s*\}/
|
||||
);
|
||||
});
|
||||
|
||||
test('includes the pinned downloader in the macOS runtime cache key', () => {
|
||||
assert.match(
|
||||
fs.readFileSync(buildWorkflowPath, 'utf8'),
|
||||
/tools\/embedded-mpv\/download-pinned-source\.mjs/
|
||||
);
|
||||
});
|
||||
|
||||
test('uses the next mirror when the primary source is unavailable', () => {
|
||||
withTemporaryDirectory((temporaryDirectory) => {
|
||||
const archive = Buffer.from('verified source archive');
|
||||
const archivePath = path.join(temporaryDirectory, 'source.tar.xz');
|
||||
const urls = [
|
||||
'https://primary.example/source.tar.xz',
|
||||
'https://fallback.example/source.tar.xz',
|
||||
];
|
||||
const attempts = [];
|
||||
|
||||
const result = downloadPinnedSource({
|
||||
archivePath,
|
||||
expectedSha256: sha256(archive),
|
||||
urls,
|
||||
download: ({ destinationPath, url }) => {
|
||||
attempts.push(url);
|
||||
if (url === urls[0]) {
|
||||
throw new Error('primary unavailable');
|
||||
}
|
||||
fs.writeFileSync(destinationPath, archive);
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(attempts, urls);
|
||||
assert.deepEqual(fs.readFileSync(archivePath), archive);
|
||||
assert.deepEqual(result, {
|
||||
sourceSha256: sha256(archive),
|
||||
sourceUrl: urls[1],
|
||||
sourceUrls: urls,
|
||||
});
|
||||
assert.equal(fs.existsSync(`${archivePath}.partial`), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects mirrors whose archive does not match the pinned checksum', () => {
|
||||
withTemporaryDirectory((temporaryDirectory) => {
|
||||
const archivePath = path.join(temporaryDirectory, 'source.tar.xz');
|
||||
const urls = [
|
||||
'https://primary.example/source.tar.xz',
|
||||
'https://fallback.example/source.tar.xz',
|
||||
];
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
downloadPinnedSource({
|
||||
archivePath,
|
||||
expectedSha256: sha256('expected archive'),
|
||||
urls,
|
||||
download: ({ destinationPath }) =>
|
||||
fs.writeFileSync(destinationPath, 'corrupt archive'),
|
||||
}),
|
||||
/Unable to download a verified source archive.*SHA-256 mismatch/is
|
||||
);
|
||||
assert.equal(fs.existsSync(archivePath), false);
|
||||
assert.equal(fs.existsSync(`${archivePath}.partial`), false);
|
||||
});
|
||||
});
|
||||
@@ -37,9 +37,12 @@
|
||||
"{workspaceRoot}/tools/packaging/release-snap-source-binding.cjs",
|
||||
"{workspaceRoot}/tools/packaging/verify-linux-frame-copy-runtime.mjs",
|
||||
"{workspaceRoot}/tools/packaging/verify-linux-frame-copy-runtime.test.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/build-macos-runtime.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.cjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.test.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/download-pinned-source.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/download-pinned-source.test.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/generate-linux-runtime-notices.cjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/generate-linux-runtime-notices.test.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/linux-runtime-manifest.cjs",
|
||||
@@ -52,7 +55,7 @@
|
||||
"{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/flatpak-launcher-validation.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.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/release-snap-assets.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/flatpak-launcher-validation.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.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/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/download-pinned-source.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
|
||||
"cwd": "{workspaceRoot}"
|
||||
}
|
||||
},
|
||||
@@ -70,14 +73,17 @@
|
||||
"{workspaceRoot}/tools/packaging/release-snap-assets.cjs",
|
||||
"{workspaceRoot}/tools/packaging/release-snap-assets.test.mjs",
|
||||
"{workspaceRoot}/tools/packaging/release-snap-source-binding.cjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/build-macos-runtime.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.test.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/download-pinned-source.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/download-pinned-source.test.mjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/linux-source-archive-contract.cjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/linux-source-archive-contract.d.cts",
|
||||
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs",
|
||||
"{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts"
|
||||
],
|
||||
"command": "eslint \"tools/packaging/**/*.{js,cjs,mjs,ts}\" \"tools/embedded-mpv/build-linux-runtime.{mjs,test.mjs}\" \"tools/embedded-mpv/generate-linux-runtime-notices.{cjs,test.mjs}\" \"tools/embedded-mpv/linux-source-archive-contract.cjs\" \"tools/embedded-mpv/runtime-probe-contract.cjs\""
|
||||
"command": "eslint \"tools/packaging/**/*.{js,cjs,mjs,ts}\" \"tools/embedded-mpv/build-macos-runtime.mjs\" \"tools/embedded-mpv/build-linux-runtime.{mjs,test.mjs}\" \"tools/embedded-mpv/download-pinned-source.{mjs,test.mjs}\" \"tools/embedded-mpv/generate-linux-runtime-notices.{cjs,test.mjs}\" \"tools/embedded-mpv/linux-source-archive-contract.cjs\" \"tools/embedded-mpv/runtime-probe-contract.cjs\""
|
||||
}
|
||||
},
|
||||
"tags": ["scope:tools", "domain:packaging", "type:tool"]
|
||||
|
||||
Reference in new issue
Block a user