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:
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
@@ -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
@@ -100,5 +100,6 @@
|
||||
},
|
||||
"overrides": {
|
||||
"minimist": "^1.2.8"
|
||||
}
|
||||
},
|
||||
"author": "cita-777"
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user