forked from boos/BoosAPI
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: |
|
path: |
|
||||||
${{ runner.temp }}/.cache/electron
|
${{ runner.temp }}/.cache/electron
|
||||||
${{ runner.temp }}/.cache/electron-builder
|
${{ runner.temp }}/.cache/electron-builder
|
||||||
|
${{ runner.temp }}/.cache/appimage-tools
|
||||||
~/.cache/electron
|
~/.cache/electron
|
||||||
~/.cache/electron-builder
|
~/.cache/electron-builder
|
||||||
key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
|
key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
|
||||||
@@ -100,9 +101,23 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
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)
|
- name: Build desktop artifacts (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
uses: nick-fields/retry@v3
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
npm_config_cache: ${{ runner.temp }}/.npm
|
npm_config_cache: ${{ runner.temp }}/.npm
|
||||||
@@ -110,22 +125,26 @@ jobs:
|
|||||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
||||||
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
|
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/
|
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
WIN_CSC_LINK_INPUT: ${{ secrets.CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
WIN_CSC_KEY_PASSWORD_INPUT: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: true
|
run: |
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
if ($env:WIN_CSC_LINK_INPUT) { $env:WIN_CSC_LINK = $env:WIN_CSC_LINK_INPUT }
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
if ($env:WIN_CSC_KEY_PASSWORD_INPUT) { $env:WIN_CSC_KEY_PASSWORD = $env:WIN_CSC_KEY_PASSWORD_INPUT }
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
for ($attempt = 1; $attempt -le 2; $attempt++) {
|
||||||
with:
|
Write-Host "Desktop build attempt $attempt/2"
|
||||||
timeout_minutes: 45
|
npm run dist:desktop
|
||||||
max_attempts: 2
|
if ($LASTEXITCODE -eq 0) {
|
||||||
retry_wait_seconds: 90
|
break
|
||||||
shell: pwsh
|
}
|
||||||
command: npm run dist:desktop
|
if ($attempt -eq 2) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds 90
|
||||||
|
}
|
||||||
|
|
||||||
- name: Build desktop artifacts (Unix)
|
- name: Build desktop artifacts (Linux)
|
||||||
if: runner.os != 'Windows'
|
if: runner.os == 'Linux'
|
||||||
uses: nick-fields/retry@v3
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
npm_config_cache: ${{ runner.temp }}/.npm
|
npm_config_cache: ${{ runner.temp }}/.npm
|
||||||
@@ -133,18 +152,48 @@ jobs:
|
|||||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
|
||||||
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
|
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/
|
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
APPIMAGE_TOOLS_PATH: ${{ runner.temp }}/.cache/appimage-tools
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
run: |
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: true
|
set -euo pipefail
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
for attempt in 1 2; do
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
echo "Desktop build attempt ${attempt}/2"
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
if npm run dist:desktop; then
|
||||||
with:
|
break
|
||||||
timeout_minutes: 45
|
fi
|
||||||
max_attempts: 2
|
if [ "$attempt" -eq 2 ]; then
|
||||||
retry_wait_seconds: 90
|
exit 1
|
||||||
shell: bash
|
fi
|
||||||
command: npm run dist:desktop
|
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
|
- name: Upload desktop package
|
||||||
uses: actions/upload-artifact@v4
|
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}
|
artifactName: ${name}-${version}-${os}-${arch}.${ext}
|
||||||
directories:
|
directories:
|
||||||
output: release
|
output: release
|
||||||
|
electronDist: node_modules/electron/dist
|
||||||
|
icon: build/icon.png
|
||||||
files:
|
files:
|
||||||
- dist/**/*
|
- dist/**/*
|
||||||
- drizzle/**/*
|
- drizzle/**/*
|
||||||
@@ -35,4 +37,5 @@ nsis:
|
|||||||
oneClick: false
|
oneClick: false
|
||||||
perMachine: false
|
perMachine: false
|
||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
|
afterPack: scripts/desktop/afterPack.mjs
|
||||||
afterSign: scripts/desktop/afterSign.mjs
|
afterSign: scripts/desktop/afterSign.mjs
|
||||||
|
|||||||
+2
-1
@@ -100,5 +100,6 @@
|
|||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"minimist": "^1.2.8"
|
"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 { notarize } from '@electron/notarize';
|
||||||
import { execFile } from 'node:child_process';
|
import { ensureAdHocSignature, getMacAppPath } from './macosSignUtils.mjs';
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function afterSign(context) {
|
export default async function afterSign(context) {
|
||||||
if (context.electronPlatformName !== 'darwin') return;
|
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 appleId = process.env.APPLE_ID || process.env.APPLEID;
|
||||||
const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD || process.env.APPLEIDPASS;
|
const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD || process.env.APPLEIDPASS;
|
||||||
const teamId = process.env.APPLE_TEAM_ID;
|
const teamId = process.env.APPLE_TEAM_ID;
|
||||||
|
const signature = await ensureAdHocSignature(appPath);
|
||||||
const signed = await hasSignature(appPath);
|
|
||||||
if (!signed) {
|
|
||||||
console.log('[metapi-desktop] No macOS signing identity detected, applying ad-hoc signature.');
|
|
||||||
await adHocSign(appPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!appleId || !appleIdPassword || !teamId) {
|
if (!appleId || !appleIdPassword || !teamId) {
|
||||||
console.log('[metapi-desktop] Skipping notarization: APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID not fully configured.');
|
console.log('[metapi-desktop] Skipping notarization: APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID not fully configured.');
|
||||||
return;
|
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...');
|
console.log('[metapi-desktop] Notarizing macOS build...');
|
||||||
await notarize({
|
await notarize({
|
||||||
appPath,
|
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,
|
shell,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import electronUpdater from 'electron-updater';
|
||||||
import getPort, { portNumbers } from 'get-port';
|
import getPort, { portNumbers } from 'get-port';
|
||||||
import { spawn, type ChildProcess } from 'node:child_process';
|
import { spawn, type ChildProcess } from 'node:child_process';
|
||||||
import { existsSync, mkdirSync } from 'node:fs';
|
import { existsSync, mkdirSync } from 'node:fs';
|
||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
waitForServerReady,
|
waitForServerReady,
|
||||||
} from './runtime.js';
|
} from './runtime.js';
|
||||||
|
|
||||||
|
const { autoUpdater } = electronUpdater;
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
let serverProcess: ChildProcess | null = null;
|
let serverProcess: ChildProcess | null = null;
|
||||||
@@ -48,7 +50,7 @@ function getServerEntryPath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTrayIconPath() {
|
function getTrayIconPath() {
|
||||||
return join(app.getAppPath(), 'dist', 'web', 'favicon.png');
|
return join(app.getAppPath(), 'dist', 'web', 'logo.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWindowIconPath() {
|
function getWindowIconPath() {
|
||||||
|
|||||||
Reference in New Issue
Block a user