mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
f603b74a99
Add target-aware engine provisioning, platform package configs, and CI/release verification for macOS arm64, Windows x64, and Linux AppImage.
164 lines
5.9 KiB
JavaScript
164 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import crypto from 'node:crypto';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(__dirname, '..');
|
|
const binariesRoot = path.join(repoRoot, 'src-tauri', 'binaries');
|
|
const outputRoot = path.join(repoRoot, 'src-tauri', 'engine-dist');
|
|
const lock = JSON.parse(fs.readFileSync(path.join(repoRoot, 'engines.lock.json'), 'utf8'));
|
|
|
|
const archMap = { x64: 'x86_64', arm64: 'aarch64' };
|
|
const platformMap = {
|
|
darwin: 'apple-darwin',
|
|
win32: 'pc-windows-msvc',
|
|
linux: 'unknown-linux-gnu',
|
|
};
|
|
|
|
function argValue(name) {
|
|
const index = process.argv.indexOf(name);
|
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
}
|
|
|
|
const hostTarget = `${archMap[os.arch()]}-${platformMap[os.platform()]}`;
|
|
const target = argValue('--target')
|
|
|| process.env.TAURI_ENV_TARGET_TRIPLE
|
|
|| process.env.FIRELINK_TARGET_TRIPLE
|
|
|| hostTarget;
|
|
const isWindowsTarget = target.includes('windows');
|
|
const suffix = isWindowsTarget ? '.exe' : '';
|
|
const engines = ['yt-dlp', 'aria2c', 'ffmpeg', 'deno'];
|
|
const expectedNames = engines.map(engine => `${engine}-${target}${suffix}`);
|
|
const targetLock = lock.targets?.[target];
|
|
|
|
const configuredSource = process.env.FIRELINK_ENGINE_SOURCE_DIR
|
|
? path.resolve(process.env.FIRELINK_ENGINE_SOURCE_DIR)
|
|
: null;
|
|
const canonicalSource = path.join(binariesRoot, target);
|
|
const provisionedSource = path.join(repoRoot, 'src-tauri', 'provisioned-engines', target);
|
|
const legacyMacSource = target.endsWith('apple-darwin') ? binariesRoot : null;
|
|
const source = [configuredSource, canonicalSource, provisionedSource, legacyMacSource]
|
|
.filter(Boolean)
|
|
.find(candidate => expectedNames.every(name => fs.existsSync(path.join(candidate, name))));
|
|
|
|
if (!source) {
|
|
console.error(`No complete engine payload found for ${target}.`);
|
|
console.error(`Expected source directory: ${canonicalSource}`);
|
|
console.error(`Expected files: ${expectedNames.join(', ')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function sha256(file) {
|
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
}
|
|
|
|
function treeDigest(root) {
|
|
const files = [];
|
|
const walk = directory => {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const file = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) walk(file);
|
|
else if (entry.isFile()) files.push(file);
|
|
}
|
|
};
|
|
walk(root);
|
|
files.sort((left, right) => left.localeCompare(right));
|
|
const digest = crypto.createHash('sha256');
|
|
for (const file of files) {
|
|
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
digest.update(`${relative}\0${sha256(file)}\n`);
|
|
}
|
|
return { files: files.length, sha256: digest.digest('hex') };
|
|
}
|
|
|
|
if (targetLock) {
|
|
for (const engine of engines) {
|
|
const name = `${engine}-${target}${suffix}`;
|
|
const expected = targetLock.engines?.[engine]?.sha256;
|
|
const actual = sha256(path.join(source, name));
|
|
if (!expected || actual !== expected) {
|
|
console.error(`Checksum mismatch for ${name}. Expected ${expected || 'missing lock'}, got ${actual}.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
for (const [runtimeDir, expected] of Object.entries(targetLock.runtimeTrees || {})) {
|
|
const sourceDir = path.join(source, runtimeDir);
|
|
if (!fs.existsSync(sourceDir)) {
|
|
console.error(`Missing locked runtime directory ${runtimeDir} for ${target}.`);
|
|
process.exit(1);
|
|
}
|
|
const actual = treeDigest(sourceDir);
|
|
if (actual.files !== expected.files || actual.sha256 !== expected.sha256) {
|
|
console.error(`Runtime checksum mismatch for ${runtimeDir}.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
const manifestPath = path.join(source, 'payload-manifest.json');
|
|
if (!fs.existsSync(manifestPath)) {
|
|
console.error(`No committed lock or payload manifest exists for ${target}.`);
|
|
process.exit(1);
|
|
}
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
if (manifest.target !== target) {
|
|
console.error(`Payload manifest target mismatch: ${manifest.target}`);
|
|
process.exit(1);
|
|
}
|
|
for (const [relative, expected] of Object.entries(manifest.files || {})) {
|
|
const file = path.join(source, relative);
|
|
if (!fs.existsSync(file) || sha256(file) !== expected) {
|
|
console.error(`Payload manifest mismatch: ${relative}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
const actualFiles = [];
|
|
const walk = directory => {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const file = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) walk(file);
|
|
else if (entry.isFile() && entry.name !== 'payload-manifest.json') {
|
|
actualFiles.push(path.relative(source, file).split(path.sep).join('/'));
|
|
}
|
|
}
|
|
};
|
|
walk(source);
|
|
const expectedFiles = Object.keys(manifest.files || {}).sort();
|
|
actualFiles.sort();
|
|
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
|
|
console.error(`Payload contains files not covered by manifest for ${target}.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
const destination = path.join(outputRoot, target);
|
|
fs.rmSync(outputRoot, { recursive: true, force: true });
|
|
fs.mkdirSync(destination, { recursive: true });
|
|
|
|
for (const name of expectedNames) {
|
|
fs.copyFileSync(path.join(source, name), path.join(destination, name));
|
|
if (!isWindowsTarget) {
|
|
fs.chmodSync(path.join(destination, name), 0o755);
|
|
}
|
|
}
|
|
|
|
for (const runtimeDir of ['_internal', 'aria2-libs']) {
|
|
const sourceDir = path.join(source, runtimeDir);
|
|
if (fs.existsSync(sourceDir)) {
|
|
fs.cpSync(sourceDir, path.join(destination, runtimeDir), {
|
|
recursive: true,
|
|
dereference: false,
|
|
preserveTimestamps: true,
|
|
});
|
|
}
|
|
}
|
|
const payloadManifest = path.join(source, 'payload-manifest.json');
|
|
if (fs.existsSync(payloadManifest)) {
|
|
fs.copyFileSync(payloadManifest, path.join(destination, 'payload-manifest.json'));
|
|
}
|
|
|
|
console.log(`Staged Firelink engines for ${target} from ${source}`);
|