mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-16 22:38:20 +00:00
chore(release): ad-hoc sign macOS builds
Configure Tauri to ad-hoc sign macOS app and DMG builds without Apple Developer credentials. Add a macOS signing verifier that checks the built app, the DMG payload, Mach-O signatures, quarantine xattrs, and expected Gatekeeper behavior. Tighten README copy while keeping the platform install pills and accurate non-notarized macOS guidance.
This commit is contained in:
@@ -91,6 +91,9 @@ jobs:
|
|||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
APP="src-tauri/target/${{ matrix.target }}/release/bundle/macos/Firelink.app"
|
APP="src-tauri/target/${{ matrix.target }}/release/bundle/macos/Firelink.app"
|
||||||
|
DMG="$(find src-tauri/target/${{ matrix.target }}/release/bundle/dmg -name '*.dmg' -print -quit)"
|
||||||
|
test -n "$DMG"
|
||||||
|
npm run verify:macos-signing -- --app "$APP" --dmg "$DMG"
|
||||||
node scripts/verify-binaries.js --search-root "$APP" --target ${{ matrix.target }}
|
node scripts/verify-binaries.js --search-root "$APP" --target ${{ matrix.target }}
|
||||||
node scripts/smoke-packaged-app.js --executable "$APP/Contents/MacOS/firelink"
|
node scripts/smoke-packaged-app.js --executable "$APP/Contents/MacOS/firelink"
|
||||||
- name: Verify Windows installer payload and launch
|
- name: Verify Windows installer payload and launch
|
||||||
|
|||||||
@@ -9,9 +9,6 @@
|
|||||||
[](#platforms)
|
[](#platforms)
|
||||||
[](#platforms)
|
[](#platforms)
|
||||||
[](#platforms)
|
[](#platforms)
|
||||||
[](https://tauri.app/)
|
|
||||||
[](https://www.rust-lang.org/)
|
|
||||||
[](https://react.dev/)
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/nimbold/Firelink/actions/workflows/ci.yml)
|
[](https://github.com/nimbold/Firelink/actions/workflows/ci.yml)
|
||||||
|
|
||||||
@@ -36,33 +33,33 @@
|
|||||||
|
|
||||||
## Why Firelink
|
## Why Firelink
|
||||||
|
|
||||||
Firelink is built for people who want a real desktop download manager again: fast segmented transfers, browser capture, media extraction, scheduling, recovery, and clear control over where files land. Version 1.0.0 completes the move from the earlier macOS-only Swift app to a modern Rust/Tauri application with a React and TypeScript interface.
|
Firelink is a desktop download manager for fast transfers, browser capture, media extraction, scheduling, and clear file placement.
|
||||||
|
|
||||||
The app keeps the heavy work native. Downloads are coordinated by a Rust backend, accelerated with aria2, enriched with yt-dlp and FFmpeg for media workflows, and persisted locally with SQLite so queues survive restarts and app updates.
|
It is now a cross-platform Rust/Tauri app with a React and TypeScript interface. A native backend coordinates downloads with aria2, yt-dlp, FFmpeg, Deno, and SQLite.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Fast segmented downloads** powered by aria2 with configurable connections, retries, and speed limits.
|
- **Segmented downloads** with aria2, retries, speed limits, and connection controls.
|
||||||
- **Media extraction** with yt-dlp, FFmpeg, and Deno for video/audio links and richer format selection.
|
- **Media downloads** with yt-dlp, FFmpeg, and Deno.
|
||||||
- **A real Add window** for manual, extension-captured, and media downloads, including metadata, duplicate handling, and save-location choices before downloads start.
|
- **Add window** for metadata, duplicates, location choices, and captured links.
|
||||||
- **Persistent queue management** with safe concurrency limits, pause/resume, retry, redownload, sorting, multi-select, and bulk controls.
|
- **Persistent queues** with pause, resume, retry, redownload, sorting, multi-select, and bulk actions.
|
||||||
- **Download scheduling** with start/stop windows, speed-limiter tools, and optional post-queue actions.
|
- **Scheduling** with start/stop windows, speed rules, and post-queue actions.
|
||||||
- **Smart organization** through categories, default folders, per-download overrides, and open/reveal/trash actions.
|
- **File organization** with categories, default folders, per-download overrides, and reveal/trash actions.
|
||||||
- **Private browser handoff** through authenticated local pairing with replay protection and desktop-server proof checks.
|
- **Browser handoff** through local pairing, signed requests, replay protection, and server checks.
|
||||||
- **Native desktop integration** including tray controls, notifications, completion sounds, sleep prevention, and OS keychain support where available.
|
- **Desktop integration** with tray controls, notifications, sounds, sleep prevention, and secure credential storage.
|
||||||
- **Diagnostics built in** with engine health checks, structured logs, and packaged-engine verification.
|
- **Diagnostics** with engine health checks, structured logs, and package verification.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Download the latest desktop build from [GitHub Releases](https://github.com/nimbold/Firelink/releases).
|
Download desktop builds from [GitHub Releases](https://github.com/nimbold/Firelink/releases).
|
||||||
|
|
||||||
| Platform | Package | Notes |
|
| Platform | Package | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **macOS Apple silicon** | `.dmg` | Unsigned and not notarized. Open through Finder or approve once in **System Settings -> Privacy & Security**. |
|
| **macOS Apple silicon** | `.dmg` | Not notarized. If macOS blocks the first launch, approve Firelink in **System Settings -> Privacy & Security**. |
|
||||||
| **Windows x64** | NSIS `.exe` installer | Unsigned. Windows SmartScreen may warn until code signing is added. |
|
| **Windows x64** | NSIS `.exe` installer | Unsigned. Windows SmartScreen may warn until code signing is added. |
|
||||||
| **Linux x64** | `.AppImage` | Make executable before launching if your desktop environment does not do that automatically. |
|
| **Linux x64** | `.AppImage` | Make executable before launching if your desktop environment does not do that automatically. |
|
||||||
|
|
||||||
Production bundles include the required media engines for the target platform. Users do not need to install aria2, yt-dlp, FFmpeg, Deno, Python, Homebrew, or a package manager for everyday app usage.
|
Bundles include the required engines. Users do not need aria2, yt-dlp, FFmpeg, Deno, Python, Homebrew, or another package manager.
|
||||||
|
|
||||||
## Browser Extension
|
## Browser Extension
|
||||||
|
|
||||||
@@ -72,28 +69,27 @@ Production bundles include the required media engines for the target platform. U
|
|||||||
<a href="https://github.com/nimbold/Firelink-Extension#manual-chromium-installation"><img src="https://img.shields.io/badge/Manual%20install-Chromium-4285F4?style=for-the-badge&logo=googlechrome&logoColor=white" alt="Read manual Chromium install instructions" /></a>
|
<a href="https://github.com/nimbold/Firelink-Extension#manual-chromium-installation"><img src="https://img.shields.io/badge/Manual%20install-Chromium-4285F4?style=for-the-badge&logo=googlechrome&logoColor=white" alt="Read manual Chromium install instructions" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Firelink Companion connects your browser to the desktop app so links and browser downloads can open in Firelink instead of disappearing into the browser's default download shelf.
|
Firelink Companion sends browser links and downloads to the desktop app.
|
||||||
The Chrome extension is currently distributed as a manual Chromium package while Chrome Web Store publication is pending. Follow the [manual Chrome/Chromium installation instructions](https://github.com/nimbold/Firelink-Extension#manual-chromium-installation) and download `firelink-chromium.zip` from the [Firelink-Extension releases](https://github.com/nimbold/Firelink-Extension/releases).
|
|
||||||
|
|
||||||
What it adds:
|
What it adds:
|
||||||
|
|
||||||
- **Automatic capture** for ordinary browser downloads, while still routing every captured link through Firelink's Add window.
|
- Automatic capture for regular browser downloads.
|
||||||
- **Firefox and Chromium support** for Firefox, Chrome, Edge, Brave, Vivaldi, Opera, and other compatible desktop Chromium browsers.
|
- Context-menu actions for links and selected text.
|
||||||
- **Context-menu actions** for "Download with Firelink" and selected links.
|
- Firefox and Chromium support.
|
||||||
- **Signed local requests** using the pairing token from **Settings -> Integrations**.
|
- Signed local requests using the token from **Settings -> Integrations**.
|
||||||
- **Server identity checks** so the extension only trusts the real local Firelink app.
|
- Fallback to the browser download when Firelink is closed or rejects a handoff.
|
||||||
- **Offline-safe behavior** that resumes browser downloads when Firelink is closed or rejects a handoff.
|
|
||||||
- **Protocol-aware compatibility** so older desktop builds are rejected before automatic capture can cancel a browser download.
|
|
||||||
|
|
||||||
Install the extension, open Firelink, then pair it from **Settings -> Integrations**. Firefox users can install from Mozilla Add-ons. Chrome and Chromium users can use the [manual load-unpacked flow](https://github.com/nimbold/Firelink-Extension#manual-chromium-installation). The extension is maintained in the [Firelink-Extension](https://github.com/nimbold/Firelink-Extension) repository and is also vendored here as the `Extensions/Firefox` submodule.
|
Install the extension, open Firelink, then pair it from **Settings -> Integrations**. Firefox users can install from Mozilla Add-ons. Chromium users can use the [manual load-unpacked flow](https://github.com/nimbold/Firelink-Extension#manual-chromium-installation) with `firelink-chromium.zip` from the [extension releases](https://github.com/nimbold/Firelink-Extension/releases).
|
||||||
|
|
||||||
|
The extension lives in [Firelink-Extension](https://github.com/nimbold/Firelink-Extension). This repo also vendors it as the `Extensions/Firefox` submodule.
|
||||||
|
|
||||||
## Platforms
|
## Platforms
|
||||||
|
|
||||||
| Target | Status |
|
| Target | Status |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **macOS arm64** | Supported. Automated native build, engine validation, packaged launch smoke test, and unsigned DMG packaging. |
|
| **macOS arm64** | Supported. Native build, engine checks, launch smoke test, ad-hoc-signed DMG workflow. |
|
||||||
| **Windows x64** | Supported. Native GitHub Actions build, engine validation, silent installer smoke test, and NSIS packaging. |
|
| **Windows x64** | Supported. Native build, engine checks, silent installer smoke test, NSIS installer. |
|
||||||
| **Linux x64** | Supported. Native GitHub Actions build, engine validation, AppImage repackaging, and xvfb launch smoke test. |
|
| **Linux x64** | Supported. Native build, engine checks, xvfb launch smoke test, AppImage. |
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -134,7 +130,7 @@ Create a production bundle:
|
|||||||
npm run tauri build
|
npm run tauri build
|
||||||
```
|
```
|
||||||
|
|
||||||
macOS development uses locked payloads in `src-tauri/binaries`. Windows and Linux payloads are provisioned from checksum-pinned archives:
|
macOS uses locked payloads in `src-tauri/binaries`. Provision Windows and Linux payloads from checksum-pinned archives:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
node scripts/provision-engines.js --target x86_64-pc-windows-msvc
|
node scripts/provision-engines.js --target x86_64-pc-windows-msvc
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bindings": "cd src-tauri && cargo test export_bindings --lib",
|
"bindings": "cd src-tauri && cargo test export_bindings --lib",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"check:updates": "node scripts/check-updates.js",
|
"check:updates": "node scripts/check-updates.js",
|
||||||
|
"verify:macos-signing": "node scripts/verify-macos-signing.js",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { execFileSync, spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
|
function argValue(name) {
|
||||||
|
const index = process.argv.indexOf(name);
|
||||||
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appArg = argValue('--app');
|
||||||
|
const dmgArg = argValue('--dmg');
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
console.error('macOS signing verification must run on a macOS host.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appArg && !dmgArg) {
|
||||||
|
console.error('Pass --app <Firelink.app> and/or --dmg <Firelink.dmg>.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let exitCode = 0;
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(`[FAIL] ${message}`);
|
||||||
|
exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ok(message) {
|
||||||
|
console.log(`[OK] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function note(message) {
|
||||||
|
console.log(`[INFO] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args, options = {}) {
|
||||||
|
return execFileSync(command, args, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runResult(command, args) {
|
||||||
|
return spawnSync(command, args, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAppPath(appPath, label) {
|
||||||
|
if (!fs.existsSync(appPath)) {
|
||||||
|
fail(`${label} does not exist at ${appPath}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(appPath);
|
||||||
|
if (!stat.isDirectory() || path.extname(appPath) !== '.app') {
|
||||||
|
fail(`${label} is not an .app bundle: ${appPath}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(`${label} exists: ${appPath}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function codesignDetails(targetPath) {
|
||||||
|
const result = runResult('codesign', ['-dv', '--verbose=4', targetPath]);
|
||||||
|
return {
|
||||||
|
ok: result.status === 0,
|
||||||
|
output: `${result.stdout || ''}${result.stderr || ''}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyCodeSignature(targetPath, label, options = {}) {
|
||||||
|
const {
|
||||||
|
deep = false,
|
||||||
|
quietOk = false,
|
||||||
|
requireAdhoc = false,
|
||||||
|
warnOnVerifyFailure = false,
|
||||||
|
} = options;
|
||||||
|
const verifyArgs = ['--verify'];
|
||||||
|
if (deep) {
|
||||||
|
verifyArgs.push('--deep');
|
||||||
|
}
|
||||||
|
verifyArgs.push('--strict', '--verbose=2', targetPath);
|
||||||
|
|
||||||
|
const details = codesignDetails(targetPath);
|
||||||
|
if (!details.ok) {
|
||||||
|
fail(`${label}: no readable code signature: ${details.output.trim() || 'codesign -dv failed'}`);
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = 'verified';
|
||||||
|
try {
|
||||||
|
run('codesign', verifyArgs);
|
||||||
|
if (!quietOk) {
|
||||||
|
ok(`${label}: codesign verification passed`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const detail = error.stderr?.trim() || error.message;
|
||||||
|
if (!warnOnVerifyFailure) {
|
||||||
|
fail(`${label}: codesign verification failed: ${detail}`);
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
note(`${label}: signed, but individual verification warned: ${detail}`);
|
||||||
|
status = 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireAdhoc && !details.output.includes('Signature=adhoc')) {
|
||||||
|
fail(`${label}: expected ad-hoc signature, but codesign did not report Signature=adhoc`);
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
if (requireAdhoc && !quietOk) {
|
||||||
|
ok(`${label}: ad-hoc signature present`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkFiles(root, visitor) {
|
||||||
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
||||||
|
const entryPath = path.join(root, entry.name);
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkFiles(entryPath, visitor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.isFile()) {
|
||||||
|
visitor(entryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileBrief(filePath) {
|
||||||
|
try {
|
||||||
|
return run('file', ['--brief', filePath], { timeout: 5000 }).trim();
|
||||||
|
} catch (error) {
|
||||||
|
fail(`file(1) failed for ${filePath}: ${error.stderr?.trim() || error.message}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyMachOObjects(appPath, label) {
|
||||||
|
const machOFiles = [];
|
||||||
|
walkFiles(appPath, filePath => {
|
||||||
|
const description = fileBrief(filePath);
|
||||||
|
if (description.includes('Mach-O')) {
|
||||||
|
machOFiles.push(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (machOFiles.length === 0) {
|
||||||
|
fail(`${label}: no Mach-O files found inside app bundle`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let warningCount = 0;
|
||||||
|
let signedCount = 0;
|
||||||
|
let verifiedCount = 0;
|
||||||
|
for (const filePath of machOFiles) {
|
||||||
|
const relative = path.relative(appPath, filePath).split(path.sep).join('/');
|
||||||
|
const isPrimaryExecutable = relative === 'Contents/MacOS/firelink';
|
||||||
|
const isDirectEngine = /^Contents\/Resources\/engine-dist\/[^/]+\/(?:yt-dlp|aria2c|ffmpeg|deno)-/.test(relative);
|
||||||
|
const mayWarn = !isPrimaryExecutable && !isDirectEngine;
|
||||||
|
|
||||||
|
const result = verifyCodeSignature(filePath, `${label} ${relative}`, {
|
||||||
|
quietOk: true,
|
||||||
|
warnOnVerifyFailure: mayWarn,
|
||||||
|
});
|
||||||
|
if (result !== 'failed') {
|
||||||
|
signedCount += 1;
|
||||||
|
if (result === 'verified') {
|
||||||
|
verifiedCount += 1;
|
||||||
|
} else {
|
||||||
|
warningCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok(`${label}: found signatures on ${signedCount}/${machOFiles.length} Mach-O code object(s)`);
|
||||||
|
ok(`${label}: individually verified ${verifiedCount}/${machOFiles.length} Mach-O code object(s)`);
|
||||||
|
if (warningCount > 0) {
|
||||||
|
note(`${label}: ${warningCount} nested signed framework object(s) produced individual verification warnings; the outer bundle signature remains authoritative.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assessGatekeeper(appPath, label) {
|
||||||
|
const result = runResult('spctl', ['--assess', '--type', 'execute', '--verbose=4', appPath]);
|
||||||
|
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
||||||
|
|
||||||
|
if (result.status === 0) {
|
||||||
|
note(`${label}: Gatekeeper accepted this app (${output || 'no spctl detail'}).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = output.toLowerCase();
|
||||||
|
if (normalized.includes('not signed at all') || normalized.includes('invalid signature')) {
|
||||||
|
fail(`${label}: Gatekeeper rejection indicates a broken signature: ${output}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
note(`${label}: Gatekeeper rejected the app as expected for ad-hoc, unnotarized distribution: ${output || `exit ${result.status}`}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportQuarantine(targetPath, label) {
|
||||||
|
const result = runResult('xattr', ['-p', 'com.apple.quarantine', targetPath]);
|
||||||
|
if (result.status === 0) {
|
||||||
|
fail(`${label}: build artifact unexpectedly has com.apple.quarantine=${result.stdout.trim()}`);
|
||||||
|
} else {
|
||||||
|
ok(`${label}: no quarantine xattr on generated artifact`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyApp(appPath, label) {
|
||||||
|
const resolved = path.resolve(appPath);
|
||||||
|
if (!assertAppPath(resolved, label)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reportQuarantine(resolved, label);
|
||||||
|
verifyCodeSignature(resolved, label, { deep: true, requireAdhoc: true });
|
||||||
|
verifyMachOObjects(resolved, label);
|
||||||
|
assessGatekeeper(resolved, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachDmg(dmgPath) {
|
||||||
|
const mountPoint = fs.mkdtempSync(path.join(os.tmpdir(), 'firelink-dmg-'));
|
||||||
|
try {
|
||||||
|
const plist = run('hdiutil', [
|
||||||
|
'attach',
|
||||||
|
'-plist',
|
||||||
|
'-nobrowse',
|
||||||
|
'-readonly',
|
||||||
|
'-mountpoint',
|
||||||
|
mountPoint,
|
||||||
|
dmgPath,
|
||||||
|
], { timeout: 60000 });
|
||||||
|
return { mountPoint, plist };
|
||||||
|
} catch (error) {
|
||||||
|
fs.rmSync(mountPoint, { recursive: true, force: true });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachDmg(mountPoint) {
|
||||||
|
const result = runResult('hdiutil', ['detach', mountPoint]);
|
||||||
|
if (result.status !== 0) {
|
||||||
|
note(`Initial hdiutil detach failed, retrying with -force: ${result.stderr?.trim() || result.stdout?.trim()}`);
|
||||||
|
const forced = runResult('hdiutil', ['detach', '-force', mountPoint]);
|
||||||
|
if (forced.status !== 0) {
|
||||||
|
fail(`Failed to detach DMG mount point ${mountPoint}: ${forced.stderr?.trim() || forced.stdout?.trim()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.rmSync(mountPoint, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyDmg(dmgPath) {
|
||||||
|
const resolved = path.resolve(dmgPath);
|
||||||
|
if (!fs.existsSync(resolved)) {
|
||||||
|
fail(`DMG does not exist at ${resolved}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reportQuarantine(resolved, 'DMG');
|
||||||
|
let mount;
|
||||||
|
try {
|
||||||
|
mount = attachDmg(resolved);
|
||||||
|
ok(`DMG mounted at ${mount.mountPoint}`);
|
||||||
|
const apps = fs.readdirSync(mount.mountPoint)
|
||||||
|
.filter(name => name.endsWith('.app'))
|
||||||
|
.map(name => path.join(mount.mountPoint, name));
|
||||||
|
if (apps.length !== 1) {
|
||||||
|
fail(`Expected exactly one .app inside DMG, found ${apps.length}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
verifyApp(apps[0], 'DMG app');
|
||||||
|
} catch (error) {
|
||||||
|
fail(`DMG verification failed: ${error.stderr?.trim() || error.message}`);
|
||||||
|
} finally {
|
||||||
|
if (mount) {
|
||||||
|
detachDmg(mount.mountPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appArg) {
|
||||||
|
verifyApp(appArg, 'Built app');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dmgArg) {
|
||||||
|
verifyDmg(dmgArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.error('[FAIL] macOS signing verification failed.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[PASS] macOS ad-hoc signing verification passed.');
|
||||||
@@ -22,6 +22,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"targets": ["app", "dmg"]
|
"targets": ["app", "dmg"],
|
||||||
|
"macOS": {
|
||||||
|
"signingIdentity": "-",
|
||||||
|
"hardenedRuntime": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user