feat: add macOS ad-hoc signing support

- Implemented afterPack script to ensure ad-hoc signature for macOS builds.
- Created macosSignUtils module to handle app path retrieval, signature inspection, and ad-hoc signing.
- Added functions to check if the app is signed and apply ad-hoc signing if necessary.
This commit is contained in:
cita
2026-03-06 19:58:38 +08:00
parent 19125d686e
commit 6c51801e50
8 changed files with 142 additions and 55 deletions
+83 -34
View File
@@ -78,6 +78,7 @@ jobs:
path: |
${{ runner.temp }}/.cache/electron
${{ runner.temp }}/.cache/electron-builder
${{ runner.temp }}/.cache/appimage-tools
~/.cache/electron
~/.cache/electron-builder
key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
@@ -100,32 +101,23 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Prefetch AppImage tools
if: runner.os == 'Linux'
shell: bash
env:
APPIMAGE_TOOLS_URL: https://github.com/electron-userland/electron-builder-binaries/releases/download/appimage%401.0.2/appimage-tools-runtime-20251108.tar.gz
run: |
set -euo pipefail
APPIMAGE_TOOLS_DIR="${{ runner.temp }}/.cache/appimage-tools"
if [ -x "$APPIMAGE_TOOLS_DIR/mksquashfs" ]; then
exit 0
fi
mkdir -p "$APPIMAGE_TOOLS_DIR"
curl -fL --retry 5 --retry-all-errors --retry-delay 5 "$APPIMAGE_TOOLS_URL" | tar -xz -C "$APPIMAGE_TOOLS_DIR"
- name: Build desktop artifacts (Windows)
if: runner.os == 'Windows'
uses: nick-fields/retry@v3
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
npm_config_cache: ${{ runner.temp }}/.npm
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: true
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
timeout_minutes: 45
max_attempts: 2
retry_wait_seconds: 90
shell: pwsh
command: npm run dist:desktop
- name: Build desktop artifacts (Unix)
if: runner.os != 'Windows'
uses: nick-fields/retry@v3
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
npm_config_cache: ${{ runner.temp }}/.npm
@@ -133,18 +125,75 @@ jobs:
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: true
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
timeout_minutes: 45
max_attempts: 2
retry_wait_seconds: 90
WIN_CSC_LINK_INPUT: ${{ secrets.CSC_LINK }}
WIN_CSC_KEY_PASSWORD_INPUT: ${{ secrets.CSC_KEY_PASSWORD }}
run: |
if ($env:WIN_CSC_LINK_INPUT) { $env:WIN_CSC_LINK = $env:WIN_CSC_LINK_INPUT }
if ($env:WIN_CSC_KEY_PASSWORD_INPUT) { $env:WIN_CSC_KEY_PASSWORD = $env:WIN_CSC_KEY_PASSWORD_INPUT }
for ($attempt = 1; $attempt -le 2; $attempt++) {
Write-Host "Desktop build attempt $attempt/2"
npm run dist:desktop
if ($LASTEXITCODE -eq 0) {
break
}
if ($attempt -eq 2) {
exit $LASTEXITCODE
}
Start-Sleep -Seconds 90
}
- name: Build desktop artifacts (Linux)
if: runner.os == 'Linux'
shell: bash
command: npm run dist:desktop
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
npm_config_cache: ${{ runner.temp }}/.npm
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
APPIMAGE_TOOLS_PATH: ${{ runner.temp }}/.cache/appimage-tools
run: |
set -euo pipefail
for attempt in 1 2; do
echo "Desktop build attempt ${attempt}/2"
if npm run dist:desktop; then
break
fi
if [ "$attempt" -eq 2 ]; then
exit 1
fi
sleep 90
done
- name: Build desktop artifacts (macOS)
if: runner.os == 'macOS'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
npm_config_cache: ${{ runner.temp }}/.npm
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
APPLE_ID_INPUT: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD_INPUT: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID_INPUT: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
if [ -n "${APPLE_ID_INPUT:-}" ]; then export APPLE_ID="$APPLE_ID_INPUT"; fi
if [ -n "${APPLE_APP_SPECIFIC_PASSWORD_INPUT:-}" ]; then export APPLE_APP_SPECIFIC_PASSWORD="$APPLE_APP_SPECIFIC_PASSWORD_INPUT"; fi
if [ -n "${APPLE_TEAM_ID_INPUT:-}" ]; then export APPLE_TEAM_ID="$APPLE_TEAM_ID_INPUT"; fi
for attempt in 1 2; do
echo "Desktop build attempt ${attempt}/2"
if npm run dist:desktop; then
break
fi
if [ "$attempt" -eq 2 ]; then
exit 1
fi
sleep 90
done
- name: Upload desktop package
uses: actions/upload-artifact@v4
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

+3
View File
@@ -3,6 +3,8 @@ productName: Metapi
artifactName: ${name}-${version}-${os}-${arch}.${ext}
directories:
output: release
electronDist: node_modules/electron/dist
icon: build/icon.png
files:
- dist/**/*
- drizzle/**/*
@@ -35,4 +37,5 @@ nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
afterPack: scripts/desktop/afterPack.mjs
afterSign: scripts/desktop/afterSign.mjs
+2 -1
View File
@@ -100,5 +100,6 @@
},
"overrides": {
"minimist": "^1.2.8"
}
},
"author": "cita-777"
}
+7
View File
@@ -0,0 +1,7 @@
import { ensureAdHocSignature, getMacAppPath } from './macosSignUtils.mjs';
export default async function afterPack(context) {
if (context.electronPlatformName !== 'darwin') return;
await ensureAdHocSignature(getMacAppPath(context));
}
+8 -24
View File
@@ -1,41 +1,25 @@
import { notarize } from '@electron/notarize';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
async function hasSignature(appPath) {
try {
await execFileAsync('codesign', ['--verify', '--deep', '--strict', appPath]);
return true;
} catch {
return false;
}
}
async function adHocSign(appPath) {
await execFileAsync('codesign', ['--force', '--deep', '--sign', '-', appPath]);
}
import { ensureAdHocSignature, getMacAppPath } from './macosSignUtils.mjs';
export default async function afterSign(context) {
if (context.electronPlatformName !== 'darwin') return;
const appPath = `${context.appOutDir}/${context.packager.appInfo.productFilename}.app`;
const appPath = getMacAppPath(context);
const appleId = process.env.APPLE_ID || process.env.APPLEID;
const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD || process.env.APPLEIDPASS;
const teamId = process.env.APPLE_TEAM_ID;
const signed = await hasSignature(appPath);
if (!signed) {
console.log('[metapi-desktop] No macOS signing identity detected, applying ad-hoc signature.');
await adHocSign(appPath);
}
const signature = await ensureAdHocSignature(appPath);
if (!appleId || !appleIdPassword || !teamId) {
console.log('[metapi-desktop] Skipping notarization: APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID not fully configured.');
return;
}
if (signature.isAdHoc) {
console.log('[metapi-desktop] Skipping notarization: macOS app is only ad-hoc signed. Configure a Developer ID certificate to enable notarization.');
return;
}
console.log('[metapi-desktop] Notarizing macOS build...');
await notarize({
appPath,
+41
View File
@@ -0,0 +1,41 @@
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
export function getMacAppPath(context) {
return `${context.appOutDir}/${context.packager.appInfo.productFilename}.app`;
}
export async function inspectSignature(appPath) {
try {
const { stdout, stderr } = await execFileAsync('codesign', ['-dv', '--verbose=4', appPath]);
const output = `${stdout}\n${stderr}`;
return {
signed: true,
isAdHoc: /Signature=adhoc/i.test(output) || !/Authority=/i.test(output),
details: output,
};
} catch {
return {
signed: false,
isAdHoc: false,
details: '',
};
}
}
export async function adHocSign(appPath) {
await execFileAsync('codesign', ['--force', '--deep', '--sign', '-', appPath]);
}
export async function ensureAdHocSignature(appPath) {
const signature = await inspectSignature(appPath);
if (signature.signed) {
return signature;
}
console.log('[metapi-desktop] No macOS signing identity detected, applying ad-hoc signature.');
await adHocSign(appPath);
return inspectSignature(appPath);
}
+4 -2
View File
@@ -8,7 +8,7 @@ import {
shell,
} from 'electron';
import log from 'electron-log';
import { autoUpdater } from 'electron-updater';
import electronUpdater from 'electron-updater';
import getPort, { portNumbers } from 'get-port';
import { spawn, type ChildProcess } from 'node:child_process';
import { existsSync, mkdirSync } from 'node:fs';
@@ -21,6 +21,8 @@ import {
waitForServerReady,
} from './runtime.js';
const { autoUpdater } = electronUpdater;
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let serverProcess: ChildProcess | null = null;
@@ -48,7 +50,7 @@ function getServerEntryPath() {
}
function getTrayIconPath() {
return join(app.getAppPath(), 'dist', 'web', 'favicon.png');
return join(app.getAppPath(), 'dist', 'web', 'logo.png');
}
function getWindowIconPath() {