mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-30 12:29:34 +00:00
fix(ci): reuse Linux binary for AppImage packaging
This commit is contained in:
@@ -93,12 +93,11 @@ jobs:
|
|||||||
mkdir -p "$RUNNER_TEMP/firelink-native-packages/deb" "$RUNNER_TEMP/firelink-native-packages/rpm"
|
mkdir -p "$RUNNER_TEMP/firelink-native-packages/deb" "$RUNNER_TEMP/firelink-native-packages/rpm"
|
||||||
cp src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb "$RUNNER_TEMP/firelink-native-packages/deb/"
|
cp src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb "$RUNNER_TEMP/firelink-native-packages/deb/"
|
||||||
cp src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm "$RUNNER_TEMP/firelink-native-packages/rpm/"
|
cp src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm "$RUNNER_TEMP/firelink-native-packages/rpm/"
|
||||||
- name: Build Linux AppImage
|
- name: Build Linux AppImage from existing binary
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: npm run tauri build -- -vv --target ${{ matrix.target }} --bundles appimage
|
run: node scripts/build-linux-appimage.js --target ${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
APPIMAGE_EXTRACT_AND_RUN: 1
|
APPIMAGE_EXTRACT_AND_RUN: 1
|
||||||
FIRELINK_OMIT_ENGINE_DIST_FOR_TAURI_BUNDLE: '1'
|
|
||||||
- name: Install pinned appimagetool (Linux only)
|
- name: Install pinned appimagetool (Linux only)
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
@@ -53,11 +52,4 @@ process.env.VITE_BUILD_ID = `${configuredBuildId || 'artifact'}-${randomUUID()}`
|
|||||||
run(process.execPath, ['scripts/stage-engines.js']);
|
run(process.execPath, ['scripts/stage-engines.js']);
|
||||||
run(process.execPath, ['scripts/verify-binaries.js', '--staged']);
|
run(process.execPath, ['scripts/verify-binaries.js', '--staged']);
|
||||||
|
|
||||||
if (process.env.FIRELINK_OMIT_ENGINE_DIST_FOR_TAURI_BUNDLE === '1') {
|
|
||||||
const engineDist = path.join(repoRoot, 'src-tauri', 'engine-dist');
|
|
||||||
fs.rmSync(engineDist, { recursive: true, force: true });
|
|
||||||
fs.mkdirSync(engineDist, { recursive: true });
|
|
||||||
console.log('Omitted engine-dist from the initial Tauri bundle; release packaging will repack verified engines.');
|
|
||||||
}
|
|
||||||
|
|
||||||
runNpmScript('build');
|
runNpmScript('build');
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
|
let activeChild;
|
||||||
|
let receivedSignal;
|
||||||
|
|
||||||
|
export const APPIMAGE_CONFIG = JSON.stringify({
|
||||||
|
bundle: {
|
||||||
|
resources: {
|
||||||
|
'engine-dist/': null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function argValue(name) {
|
||||||
|
const index = process.argv.indexOf(name);
|
||||||
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function signalExitCode(signal) {
|
||||||
|
return {
|
||||||
|
SIGHUP: 129,
|
||||||
|
SIGINT: 130,
|
||||||
|
SIGTERM: 143,
|
||||||
|
}[signal] ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function npmInvocation(args) {
|
||||||
|
if (process.env.npm_execpath) {
|
||||||
|
return [process.execPath, [process.env.npm_execpath, ...args]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return ['cmd.exe', ['/d', '/s', '/c', 'npm', ...args]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['npm', args];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appImageBundleArguments(target) {
|
||||||
|
return [
|
||||||
|
'run',
|
||||||
|
'tauri',
|
||||||
|
'--',
|
||||||
|
'bundle',
|
||||||
|
'-vv',
|
||||||
|
'--target',
|
||||||
|
target,
|
||||||
|
'--bundles',
|
||||||
|
'appimage',
|
||||||
|
'--config',
|
||||||
|
APPIMAGE_CONFIG,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args, options = {}) {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
cwd: options.cwd ?? repoRoot,
|
||||||
|
env: { ...process.env, ...options.env },
|
||||||
|
stdio: options.stdio ?? 'inherit',
|
||||||
|
windowsHide: true,
|
||||||
|
detached: process.platform !== 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
activeChild = child;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
child.once('error', error => {
|
||||||
|
if (activeChild === child) activeChild = undefined;
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.once('close', (code, signal) => {
|
||||||
|
if (activeChild === child) activeChild = undefined;
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
resolve({ code, signal });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChecked(command, args, label = command) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await run(command, args);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to run ${label}: ${error.message}`, { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.signal) {
|
||||||
|
const error = new Error(`${label} was terminated by ${result.signal}.`);
|
||||||
|
error.exitCode = signalExitCode(result.signal);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
const error = new Error(`${label} exited with status ${result.code}.`);
|
||||||
|
error.exitCode = result.code ?? 1;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertSafeTarget(target) {
|
||||||
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(target)) {
|
||||||
|
throw new Error(`Invalid target triple: ${target}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const target = argValue('--target') || process.env.FIRELINK_TARGET_TRIPLE;
|
||||||
|
if (!target) {
|
||||||
|
throw new Error('Pass --target <triple>.');
|
||||||
|
}
|
||||||
|
assertSafeTarget(target);
|
||||||
|
|
||||||
|
// The native-package build has already staged and verified the engines.
|
||||||
|
// Verify once more before creating the AppImage so a failed preparation
|
||||||
|
// cannot produce an artifact that later appears valid only because its
|
||||||
|
// payload is absent.
|
||||||
|
await runChecked(
|
||||||
|
process.execPath,
|
||||||
|
['scripts/verify-binaries.js', '--staged', '--target', target],
|
||||||
|
'staged engine verification'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (receivedSignal) {
|
||||||
|
const error = new Error(`Build interrupted by ${receivedSignal}.`);
|
||||||
|
error.exitCode = signalExitCode(receivedSignal);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [npmCommand, npmArgs] = npmInvocation(appImageBundleArguments(target));
|
||||||
|
await runChecked(npmCommand, npmArgs, 'Tauri AppImage bundling');
|
||||||
|
|
||||||
|
if (receivedSignal) {
|
||||||
|
const error = new Error(`Build interrupted by ${receivedSignal}.`);
|
||||||
|
error.exitCode = signalExitCode(receivedSignal);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Built the Linux AppImage from the existing ${target} release binary.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||||
|
|
||||||
|
if (isMain) {
|
||||||
|
const handleSignal = signal => {
|
||||||
|
receivedSignal ??= signal;
|
||||||
|
|
||||||
|
// npm and Tauri can have Rust/packaging descendants. On POSIX, the child
|
||||||
|
// is a detached process-group leader, so signal the whole group instead of
|
||||||
|
// leaving descendants running after the wrapper exits.
|
||||||
|
if (activeChild && activeChild.exitCode === null && activeChild.signalCode === null) {
|
||||||
|
try {
|
||||||
|
if (process.platform === 'win32' || !activeChild.pid) {
|
||||||
|
activeChild.kill(signal);
|
||||||
|
} else {
|
||||||
|
process.kill(-activeChild.pid, signal);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'ESRCH') {
|
||||||
|
console.error(`[WARN] Could not terminate the AppImage build process group: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.exitCode = signalExitCode(signal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const signal of ['SIGHUP', 'SIGINT', 'SIGTERM']) {
|
||||||
|
process.once(signal, () => handleSignal(signal));
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`[FAIL] ${error.message}`);
|
||||||
|
process.exitCode = error.exitCode ?? 1;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
for (const signal of ['SIGHUP', 'SIGINT', 'SIGTERM']) {
|
||||||
|
process.removeAllListeners(signal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { APPIMAGE_CONFIG, appImageBundleArguments } from './build-linux-appimage.js';
|
||||||
|
|
||||||
|
test('AppImage config removes only engine-dist from Tauri resources', () => {
|
||||||
|
assert.deepEqual(JSON.parse(APPIMAGE_CONFIG), {
|
||||||
|
bundle: {
|
||||||
|
resources: {
|
||||||
|
'engine-dist/': null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AppImage bundling uses the existing binary instead of tauri build', () => {
|
||||||
|
const args = appImageBundleArguments('x86_64-unknown-linux-gnu');
|
||||||
|
assert.equal(args[3], 'bundle');
|
||||||
|
assert.equal(args.includes('build'), false);
|
||||||
|
assert.deepEqual(args.slice(-2), ['--config', APPIMAGE_CONFIG]);
|
||||||
|
});
|
||||||
@@ -5334,6 +5334,7 @@ async fn wait_for_aria2_stopped(port: u16, secret: &str, gid: &str) -> Result<()
|
|||||||
static NEXT_DOCK_BADGE_SESSION: std::sync::atomic::AtomicU64 =
|
static NEXT_DOCK_BADGE_SESSION: std::sync::atomic::AtomicU64 =
|
||||||
std::sync::atomic::AtomicU64::new(1);
|
std::sync::atomic::AtomicU64::new(1);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
fn should_apply_dock_badge_update(
|
fn should_apply_dock_badge_update(
|
||||||
current_session: u64,
|
current_session: u64,
|
||||||
current_generation: u64,
|
current_generation: u64,
|
||||||
@@ -8041,6 +8042,7 @@ mod tests {
|
|||||||
assert!(!is_media_artifact_name("video.f1.backup", "video.mp4", "video"));
|
assert!(!is_media_artifact_name("video.f1.backup", "video.mp4", "video"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
#[test]
|
#[test]
|
||||||
fn dock_badge_updates_reject_stale_sessions_and_generations() {
|
fn dock_badge_updates_reject_stale_sessions_and_generations() {
|
||||||
assert!(should_apply_dock_badge_update(1, 99, 2, 1));
|
assert!(should_apply_dock_badge_update(1, 99, 2, 1));
|
||||||
|
|||||||
Reference in New Issue
Block a user