fix(packaging): add verified source mirror fallback

This commit is contained in:
4gray committed 2026-08-01 13:20:13 +02:00
1 parent 9f4e11d6de
commit 09091bc64f
6 files changed
+235 -23

No files matched your search

+1
View File
@@ -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 {
+8
View File
@@ -82,6 +82,14 @@ 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. 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,
+39 -21
View File
@@ -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 } = 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,7 @@ function downloadSources() {
'--strip-components',
'1',
]);
sourcePackage.sha256 = sha256File(archivePath);
sourcePackage.sha256 = sourceSha256;
}
}
@@ -0,0 +1,58 @@
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 = [];
if (fs.existsSync(archivePath)) {
const actualSha256 = sha256File(archivePath);
if (actualSha256 === expectedSha256) {
return { sourceSha256: actualSha256, sourceUrl: null };
}
failures.push(
`cached archive: ${checksumFailure(
expectedSha256,
actualSha256
)}`
);
fs.rmSync(archivePath, { force: true });
}
for (const url of urls) {
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 };
} 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,121 @@
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/
);
});
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],
});
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);
});
});
+8 -2
View File
@@ -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"]