fix(packaging): add verified source mirror fallback (#1325)

This commit is contained in:
4gray authored and GitHub committed 2026-08-01 15:15:09 +02:00
1 parent 9f4e11d6de
commit b14ce2452b
6 files changed
+273 -25

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 {
+10
View File
@@ -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,
+55 -23
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, 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);
});
});
+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"]