chore: 构建

This commit is contained in:
电摇小子
2026-09-29 18:02:09 +07:00
parent c3629255ea
commit 194dd0035b
5 changed files with 430 additions and 49 deletions
+4
View File
@@ -75,8 +75,10 @@
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"@koromix/koffi-darwin-x64": "3.1.0",
"@koromix/koffi-win32-x64": "3.1.0", "@koromix/koffi-win32-x64": "3.1.0",
"@napi-rs/system-ocr": "1.2.0", "@napi-rs/system-ocr": "1.2.0",
"@napi-rs/system-ocr-darwin-x64": "1.2.0",
"@napi-rs/system-ocr-win32-x64-msvc": "1.2.0", "@napi-rs/system-ocr-win32-x64-msvc": "1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.23", "@radix-ui/react-alert-dialog": "^1.1.23",
"@radix-ui/react-checkbox": "^1.3.11", "@radix-ui/react-checkbox": "^1.3.11",
@@ -108,7 +110,9 @@
"parse5": "^8.0.0", "parse5": "^8.0.0",
"pinyin-pro": "^3.26.0", "pinyin-pro": "^3.26.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"sherpa-onnx-darwin-x64": "1.13.3",
"sherpa-onnx-node": "1.13.3", "sherpa-onnx-node": "1.13.3",
"sherpa-onnx-win-x64": "1.13.4",
"silk-wasm": "^3.7.1", "silk-wasm": "^3.7.1",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"unzipper": "^0.12.0", "unzipper": "^0.12.0",
+38 -31
View File
@@ -11,8 +11,10 @@ specifiers:
'@electron-toolkit/preload': ^3.0.2 '@electron-toolkit/preload': ^3.0.2
'@electron-toolkit/tsconfig': ^2.0.0 '@electron-toolkit/tsconfig': ^2.0.0
'@electron-toolkit/utils': ^4.0.0 '@electron-toolkit/utils': ^4.0.0
'@koromix/koffi-darwin-x64': 3.1.0
'@koromix/koffi-win32-x64': 3.1.0 '@koromix/koffi-win32-x64': 3.1.0
'@napi-rs/system-ocr': 1.2.0 '@napi-rs/system-ocr': 1.2.0
'@napi-rs/system-ocr-darwin-x64': 1.2.0
'@napi-rs/system-ocr-win32-x64-msvc': 1.2.0 '@napi-rs/system-ocr-win32-x64-msvc': 1.2.0
'@playwright/test': ^1.62.1 '@playwright/test': ^1.62.1
'@radix-ui/react-alert-dialog': ^1.1.23 '@radix-ui/react-alert-dialog': ^1.1.23
@@ -72,7 +74,9 @@ specifiers:
react: ^19.2.1 react: ^19.2.1
react-dom: ^19.2.1 react-dom: ^19.2.1
sass: ^1.102.0 sass: ^1.102.0
sherpa-onnx-darwin-x64: 1.13.3
sherpa-onnx-node: 1.13.3 sherpa-onnx-node: 1.13.3
sherpa-onnx-win-x64: 1.13.4
silk-wasm: ^3.7.1 silk-wasm: ^3.7.1
tailwind-merge: ^3.6.0 tailwind-merge: ^3.6.0
tailwindcss: 3.4.17 tailwindcss: 3.4.17
@@ -87,8 +91,10 @@ specifiers:
dependencies: dependencies:
'@electron-toolkit/preload': 3.0.2_electron@43.1.0 '@electron-toolkit/preload': 3.0.2_electron@43.1.0
'@electron-toolkit/utils': 4.0.0_electron@43.1.0 '@electron-toolkit/utils': 4.0.0_electron@43.1.0
'@koromix/koffi-darwin-x64': 3.1.0
'@koromix/koffi-win32-x64': 3.1.0 '@koromix/koffi-win32-x64': 3.1.0
'@napi-rs/system-ocr': 1.2.0 '@napi-rs/system-ocr': 1.2.0
'@napi-rs/system-ocr-darwin-x64': 1.2.0
'@napi-rs/system-ocr-win32-x64-msvc': 1.2.0 '@napi-rs/system-ocr-win32-x64-msvc': 1.2.0
'@radix-ui/react-alert-dialog': 1.1.23_eijghdl4n2x4hz6j4cg7ctgbuu '@radix-ui/react-alert-dialog': 1.1.23_eijghdl4n2x4hz6j4cg7ctgbuu
'@radix-ui/react-checkbox': 1.3.11_eijghdl4n2x4hz6j4cg7ctgbuu '@radix-ui/react-checkbox': 1.3.11_eijghdl4n2x4hz6j4cg7ctgbuu
@@ -120,7 +126,9 @@ dependencies:
parse5: 8.0.1 parse5: 8.0.1
pinyin-pro: 3.29.3 pinyin-pro: 3.29.3
qrcode: 1.5.4 qrcode: 1.5.4
sherpa-onnx-darwin-x64: 1.13.3
sherpa-onnx-node: 1.13.3 sherpa-onnx-node: 1.13.3
sherpa-onnx-win-x64: 1.13.4
silk-wasm: 3.7.1 silk-wasm: 3.7.1
tailwind-merge: 3.6.0 tailwind-merge: 3.6.0
unzipper: 0.12.5 unzipper: 0.12.5
@@ -1585,7 +1593,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
dev: false dev: false
optional: true
/@koromix/koffi-freebsd-arm64/3.1.0: /@koromix/koffi-freebsd-arm64/3.1.0:
resolution: {integrity: sha512-vazoPYIhOAlXZksVIqDRMIID4VeUZKx8F3dR90hOobT2ATyOkqNS5dv5UCV7Q7DSq22lQTrdbvENBAhROzCp0w==} resolution: {integrity: sha512-vazoPYIhOAlXZksVIqDRMIID4VeUZKx8F3dR90hOobT2ATyOkqNS5dv5UCV7Q7DSq22lQTrdbvENBAhROzCp0w==}
@@ -1689,6 +1696,36 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@napi-rs/system-ocr-darwin-arm64/1.2.0:
resolution: {integrity: sha512-cK8dcDBEl3P4A04xmFJSHEJQxfDytaAIFyDCLqavTp92FVU5plESttWzZsqtTkS81/kzKiBfHyPQffSIndfWbQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
dev: false
optional: true
/@napi-rs/system-ocr-darwin-x64/1.2.0:
resolution: {integrity: sha512-u3TBvBGrhmT5Os6AfaxbUEg6VHe8lvrFJNPgThJgshJHyRXUx/wCfTyOroJ22KdVCP5AE4GpwS5tFHMb6p6iaQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
dev: false
/@napi-rs/system-ocr-win32-arm64-msvc/1.2.0:
resolution: {integrity: sha512-7ej8uMvmXomw3NXo5gZ5p2Nl6UKsHI+VRU3ELv0mhcxR0sJ6wFifYTu5bJrM1TGcz1/RsaX+TjWMmsDq8vriKQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
dev: false
optional: true
/@napi-rs/system-ocr-win32-x64-msvc/1.2.0:
resolution: {integrity: sha512-oOoCj3FPWDVctTxx98vMBiMI6m51U+w7SMmMefvmtpcpLelzZ/zYTqdwtWZFAjShaHO+RdaKkcpeVcQuBQiVbA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
dev: false
/@napi-rs/system-ocr/1.2.0: /@napi-rs/system-ocr/1.2.0:
resolution: {integrity: sha512-r0f2xNH6U+sth44qF+lUP+2WuHSGUBAry5KSCNuaLDGRbgslFqeROr/qJJ/fb6AjBp3Ov+CJP5MdrOWoaoM3cw==} resolution: {integrity: sha512-r0f2xNH6U+sth44qF+lUP+2WuHSGUBAry5KSCNuaLDGRbgslFqeROr/qJJ/fb6AjBp3Ov+CJP5MdrOWoaoM3cw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@@ -1699,34 +1736,6 @@ packages:
'@napi-rs/system-ocr-win32-x64-msvc': 1.2.0 '@napi-rs/system-ocr-win32-x64-msvc': 1.2.0
dev: false dev: false
/@napi-rs/system-ocr-darwin-arm64/1.2.0:
resolution: {integrity: sha512-cK8dcDBEl3P4A04xmFJSHEJQxfDytaAIFyDCLqavTp92FVU5plESttWzZsqtTkS81/kzKiBfHyPQffSIndfWbQ==}
cpu: [arm64]
os: [darwin]
engines: {node: '>= 10'}
dev: false
/@napi-rs/system-ocr-darwin-x64/1.2.0:
resolution: {integrity: sha512-u3TBvBGrhmT5Os6AfaxbUEg6VHe8lvrFJNPgThJgshJHyRXUx/wCfTyOroJ22KdVCP5AE4GpwS5tFHMb6p6iaQ==}
cpu: [x64]
os: [darwin]
engines: {node: '>= 10'}
dev: false
/@napi-rs/system-ocr-win32-arm64-msvc/1.2.0:
resolution: {integrity: sha512-7ej8uMvmXomw3NXo5gZ5p2Nl6UKsHI+VRU3ELv0mhcxR0sJ6wFifYTu5bJrM1TGcz1/RsaX+TjWMmsDq8vriKQ==}
cpu: [arm64]
os: [win32]
engines: {node: '>= 10'}
dev: false
/@napi-rs/system-ocr-win32-x64-msvc/1.2.0:
resolution: {integrity: sha512-oOoCj3FPWDVctTxx98vMBiMI6m51U+w7SMmMefvmtpcpLelzZ/zYTqdwtWZFAjShaHO+RdaKkcpeVcQuBQiVbA==}
cpu: [x64]
os: [win32]
engines: {node: '>= 10'}
dev: false
/@nodelib/fs.scandir/2.1.5: /@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -7589,7 +7598,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
dev: false dev: false
optional: true
/sherpa-onnx-linux-arm64/1.13.4: /sherpa-onnx-linux-arm64/1.13.4:
resolution: {integrity: sha512-RMjMRqT82BgTXypNNGmLe6ZFYhc3WEvnAGl3DdkK7qB/kuXwkL3iHhV31wAecbnWPsnEpUoD+8cFovWSBzsCuw==} resolution: {integrity: sha512-RMjMRqT82BgTXypNNGmLe6ZFYhc3WEvnAGl3DdkK7qB/kuXwkL3iHhV31wAecbnWPsnEpUoD+8cFovWSBzsCuw==}
@@ -7628,7 +7636,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
dev: false dev: false
optional: true
/side-channel-list/1.0.0: /side-channel-list/1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+134 -18
View File
@@ -125,13 +125,62 @@ function validateSystemOcrRuntime(runtimeResources, platform, arch) {
} }
} }
/**
* koffi 运行期按 `${process.platform}-${process.arch}` 拼出原生包目录名
* (node_modules/koffi/src/koffi/index.cjs:153/175),找不到就直接抛
* "Cannot find the native Koffi module; did you bundle it correctly?"。
* pnpm 7 不支持 supportedArchitectures,会静默跳过外平台可选依赖,所以每个目标平台的
* koffi 原生包都必须在 package.json 里显式声明;这里再兜一层,缺了就让构建失败,
* 而不是发出一个装得上、却打不开 WCDB 的包。
*/
function koffiNativeTarget(platform, arch) {
if (platform === 'win32') {
return arch === 'x64'
? { label: 'Windows', segments: ['@koromix', 'koffi-win32-x64', 'win32_x64', 'koffi.node'] }
: null
}
if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) {
return {
label: 'macOS',
segments: ['@koromix', `koffi-darwin-${arch}`, `darwin_${arch}`, 'koffi.node']
}
}
return null
}
function validateKoffiRuntime(runtimeResources, platform, arch) {
const target = koffiNativeTarget(platform, arch)
if (!target) return
const nativePath = path.join(
runtimeResources,
'app.asar.unpacked',
'node_modules',
...target.segments
)
if (!existsSync(nativePath)) {
throw new Error(`Missing ${target.label} Koffi native module: ${nativePath}`)
}
}
function normalizeBuilderArch(arch) { function normalizeBuilderArch(arch) {
if (typeof arch === 'string') return arch if (typeof arch === 'string') return arch
return { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }[arch] || String(arch) return { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }[arch] || String(arch)
} }
function runCodesign(args) { function runCodesign(args) {
execFileSync('/usr/bin/codesign', args, { stdio: 'ignore' }) try {
execFileSync('/usr/bin/codesign', args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
})
} catch (error) {
const stderr =
error && typeof error === 'object' && 'stderr' in error ? String(error.stderr) : ''
if (stderr.trim() && error instanceof Error) {
error.message += `\n${stderr.trim()}`
}
throw error
}
} }
function isMacosCodeValid(targetPath, run = runCodesign) { function isMacosCodeValid(targetPath, run = runCodesign) {
@@ -174,8 +223,89 @@ function signMacosHelpers(runtimeResources, run = runCodesign) {
return helperPaths return helperPaths
} }
/**
* codesign 只把这些位置当作「嵌套代码」并要求它们先各自签好,才肯签外层 app。
* 只遍历这一组根目录,而不是整个 bundle:Contents/Resources 下的
* app.asar.unpacked 里成千上万个原生文件不属于嵌套代码,逐个签既慢又无意义。
*/
const MACOS_CODE_LOCATIONS = [
'Frameworks',
'MacOS',
'PlugIns',
'XPCServices',
'Helpers',
'Library/LoginItems'
]
function collectNestedMacosCode(dir, depth, targets) {
let entries
try {
entries = readdirSync(dir, { withFileTypes: true })
} catch {
return
}
for (const entry of entries) {
const entryPath = path.join(dir, entry.name)
// framework 里的 Mantle -> Versions/Current/Mantle 这类符号链接指向真实文件,
// 真实文件会在更深的层级被走到;这里跳过以免重复签名。
if (entry.isSymbolicLink()) continue
if (entry.isDirectory()) {
if (/\.(app|framework|xpc)$/.test(entry.name)) {
targets.push({ path: entryPath, depth, bundle: true })
}
collectNestedMacosCode(entryPath, depth + 1, targets)
continue
}
if (!entry.isFile()) continue
if (readBinaryArchitectures(entryPath).length === 0) continue
targets.push({ path: entryPath, depth, bundle: false })
}
}
/**
* 返回嵌套代码的签名顺序:深度大的先签(framework 内部的 dylib、无扩展名的
* crashpad handler 先于 framework 本身,helper 的可执行文件先于 helper app),
* 同深度时文件先于 bundle。
*/
function findNestedMacosCodePaths(appBundlePath) {
const targets = []
for (const location of MACOS_CODE_LOCATIONS) {
const root = path.join(appBundlePath, 'Contents', ...location.split('/'))
if (existsSync(root)) collectNestedMacosCode(root, 1, targets)
}
return targets
.map((target, index) => ({ ...target, index }))
.sort((a, b) => {
if (a.depth !== b.depth) return b.depth - a.depth
if (a.bundle !== b.bundle) return a.bundle ? 1 : -1
return a.index - b.index
})
.map((target) => target.path)
}
/**
* Electron 43.1.0 的 darwin-x64 官方 zip(sha256 与上游 SHASUMS256.txt 一致)
* 里所有嵌套 Mach-O 都是未签名状态,darwin-arm64 那份则是 linker-signed。
* codesign 签外层 bundle 时要求子组件已签,否则直接报
* "code object is not signed at all" + "In subcomponent: ...",
* 所以 x64 出包时只签外层必然失败,必须先由内向外补签一遍。
*
* 这里不采用 `--deep`(Apple 已标记 deprecated):它会把外层的签名选项套用到
* 所有子组件上,将来接上 Developer ID + entitlements 时会把 app 的 entitlements
* 一并套到 helper 上,属于已知的坑。
*/
function signMacosAppBundle(appBundlePath, run = runCodesign) { function signMacosAppBundle(appBundlePath, run = runCodesign) {
if (isMacosCodeValid(appBundlePath, run)) return appBundlePath if (isMacosCodeValid(appBundlePath, run)) return appBundlePath
for (const nestedPath of findNestedMacosCodePaths(appBundlePath)) {
try {
run(['--force', '--sign', '-', nestedPath])
} catch (error) {
throw new Error(
'macOS nested code signing failed: ' + path.relative(appBundlePath, nestedPath),
{ cause: error }
)
}
}
run(['--force', '--sign', '-', appBundlePath]) run(['--force', '--sign', '-', appBundlePath])
try { try {
run(['--verify', '--strict', appBundlePath]) run(['--verify', '--strict', appBundlePath])
@@ -401,6 +531,7 @@ exports.default = async function afterPack(context) {
) )
validateSherpaRuntime(runtimeResources, context.electronPlatformName, arch) validateSherpaRuntime(runtimeResources, context.electronPlatformName, arch)
validateSystemOcrRuntime(runtimeResources, context.electronPlatformName, arch) validateSystemOcrRuntime(runtimeResources, context.electronPlatformName, arch)
validateKoffiRuntime(runtimeResources, context.electronPlatformName, arch)
pruneIntelMacKeyTool(runtimeResources, context.electronPlatformName, arch) pruneIntelMacKeyTool(runtimeResources, context.electronPlatformName, arch)
pruneForeignArchConnectors(runtimeResources, context.electronPlatformName, arch) pruneForeignArchConnectors(runtimeResources, context.electronPlatformName, arch)
pruneForeignArchNativeRuntimes(runtimeResources, context.electronPlatformName, arch) pruneForeignArchNativeRuntimes(runtimeResources, context.electronPlatformName, arch)
@@ -413,23 +544,6 @@ exports.default = async function afterPack(context) {
const productName = context.packager.appInfo.productFilename const productName = context.packager.appInfo.productFilename
signMacosAppBundle(path.join(context.appOutDir, productName + '.app')) signMacosAppBundle(path.join(context.appOutDir, productName + '.app'))
} }
if (context.electronPlatformName === 'win32') {
const koffiNative = path.join(
context.appOutDir,
'resources',
'app.asar.unpacked',
'node_modules',
'@koromix',
'koffi-win32-x64',
'win32_x64',
'koffi.node'
)
if (!existsSync(koffiNative)) {
throw new Error(`Missing Windows Koffi native module: ${koffiNative}`)
}
return
}
} }
exports.getRuntimeResources = getRuntimeResources exports.getRuntimeResources = getRuntimeResources
@@ -439,6 +553,7 @@ exports.validateFfmpegRuntime = validateFfmpegRuntime
exports.validateSilkWasmRuntime = validateSilkWasmRuntime exports.validateSilkWasmRuntime = validateSilkWasmRuntime
exports.validateSherpaRuntime = validateSherpaRuntime exports.validateSherpaRuntime = validateSherpaRuntime
exports.validateSystemOcrRuntime = validateSystemOcrRuntime exports.validateSystemOcrRuntime = validateSystemOcrRuntime
exports.validateKoffiRuntime = validateKoffiRuntime
exports.pruneIntelMacKeyTool = pruneIntelMacKeyTool exports.pruneIntelMacKeyTool = pruneIntelMacKeyTool
exports.pruneForeignArchConnectors = pruneForeignArchConnectors exports.pruneForeignArchConnectors = pruneForeignArchConnectors
exports.pruneForeignArchNativeRuntimes = pruneForeignArchNativeRuntimes exports.pruneForeignArchNativeRuntimes = pruneForeignArchNativeRuntimes
@@ -447,6 +562,7 @@ exports.findMacosHelperPaths = findMacosHelperPaths
exports.isMacosCodeValid = isMacosCodeValid exports.isMacosCodeValid = isMacosCodeValid
exports.signMacosHelpers = signMacosHelpers exports.signMacosHelpers = signMacosHelpers
exports.signMacosAppBundle = signMacosAppBundle exports.signMacosAppBundle = signMacosAppBundle
exports.findNestedMacosCodePaths = findNestedMacosCodePaths
exports.sendRuntimeLocations = sendRuntimeLocations exports.sendRuntimeLocations = sendRuntimeLocations
exports.findSendRuntime = findSendRuntime exports.findSendRuntime = findSendRuntime
exports.enforceSendRuntimeBoundary = enforceSendRuntimeBoundary exports.enforceSendRuntimeBoundary = enforceSendRuntimeBoundary
+161
View File
@@ -0,0 +1,161 @@
import { createRequire } from 'module'
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterAll, describe, expect, it } from 'vitest'
const nodeRequire = createRequire(import.meta.url)
const { findNestedMacosCodePaths, signMacosAppBundle } = nodeRequire(
'../../scripts/after-pack.cjs'
) as {
findNestedMacosCodePaths: (appBundlePath: string) => string[]
signMacosAppBundle: (appBundlePath: string, run?: (args: string[]) => void) => string
}
const root = mkdtempSync(join(tmpdir(), 'wxe-after-pack-sign-'))
/** 最小可用的 64 位 thin Mach-O 头,足以让 readBinaryArchitectures 判出 x64。 */
function macho(): Buffer {
const buffer = Buffer.alloc(32)
buffer.writeUInt32LE(0xfeedfacf, 0)
buffer.writeUInt32LE(0x01000007, 4)
return buffer
}
function file(...segments: string[]): string {
const target = join(root, ...segments)
mkdirSync(join(target, '..'), { recursive: true })
writeFileSync(target, macho())
return target
}
/** 复刻 Electron 43.1.0 darwin-x64 的未签名 bundle 形状。 */
function fixtureApp(): string {
const app = join(root, 'TraceMemo.app')
file('TraceMemo.app', 'Contents', 'MacOS', 'TraceMemo')
file('TraceMemo.app', 'Contents', 'Frameworks', 'Mantle.framework', 'Versions', 'A', 'Mantle')
file(
'TraceMemo.app',
'Contents',
'Frameworks',
'Electron Framework.framework',
'Versions',
'A',
'Electron Framework'
)
file(
'TraceMemo.app',
'Contents',
'Frameworks',
'Electron Framework.framework',
'Versions',
'A',
'Libraries',
'libffmpeg.dylib'
)
file(
'TraceMemo.app',
'Contents',
'Frameworks',
'Electron Framework.framework',
'Versions',
'A',
'Helpers',
'chrome_crashpad_handler'
)
file('TraceMemo.app', 'Contents', 'Frameworks', 'Helper.app', 'Contents', 'MacOS', 'Helper')
// framework 内指向 Versions/A 的符号链接:真实文件在更深层级被走到,这里要跳过。
symlinkSync(
'A',
join(
root,
'TraceMemo.app',
'Contents',
'Frameworks',
'Mantle.framework',
'Versions',
'Current'
),
'dir'
)
// Contents/Resources 下的原生文件不是「嵌套代码」,不参与签名。
file(
'TraceMemo.app',
'Contents',
'Resources',
'app.asar.unpacked',
'node_modules',
'sherpa-onnx-darwin-x64',
'sherpa-onnx.node'
)
// 非原生文件必须被忽略。
writeFileSync(join(root, 'TraceMemo.app', 'Contents', 'Frameworks', 'README.md'), 'not a binary')
return app
}
const app = fixtureApp()
const relative = (target: string): string => target.slice(app.length + 1)
describe('macOS nested code signing order', () => {
afterAll(() => rmSync(root, { recursive: true, force: true }))
it('signs nested code inside-out and ignores resources and symlinks', () => {
const paths = findNestedMacosCodePaths(app).map(relative)
expect(paths).toContain(join('Contents', 'MacOS', 'TraceMemo'))
expect(paths).not.toContain(app)
expect(paths.some((entry) => entry.includes('Resources'))).toBe(false)
expect(paths.some((entry) => entry.endsWith('.md'))).toBe(false)
expect(paths.some((entry) => entry.includes('Versions/Current'))).toBe(false)
const index = (needle: string): number => paths.indexOf(needle)
const handler =
'Contents/Frameworks/Electron Framework.framework/Versions/A/Helpers/chrome_crashpad_handler'
const frameworkBinary =
'Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework'
const framework = 'Contents/Frameworks/Electron Framework.framework'
const helperBinary = 'Contents/Frameworks/Helper.app/Contents/MacOS/Helper'
const helperApp = 'Contents/Frameworks/Helper.app'
expect(index(handler)).toBeGreaterThanOrEqual(0)
// 内层可执行文件先于其所属 bundle,helper 的可执行文件先于 helper app。
expect(index(handler)).toBeLessThan(index(framework))
expect(index(frameworkBinary)).toBeLessThan(index(framework))
expect(index(helperBinary)).toBeLessThan(index(helperApp))
// 最深的目标排在最前。
expect(paths[0]).toBe(handler)
})
it('re-signs the app bundle after every nested target', () => {
const calls: string[][] = []
let verifyCalls = 0
const run = (args: string[]): void => {
if (args[0] === '--verify') {
verifyCalls += 1
// 未签名来源包:外层首次校验必然失败,补签之后才允许通过。
if (verifyCalls === 1) throw new Error('code object is not signed at all')
return
}
calls.push(args)
}
signMacosAppBundle(app, run)
const signed = calls.map((args) => args[args.length - 1])
expect(signed[signed.length - 1]).toBe(app)
expect(signed.slice(0, -1)).toEqual(findNestedMacosCodePaths(app))
expect(verifyCalls).toBe(2)
})
it('leaves an already valid bundle untouched', () => {
const calls: string[][] = []
const run = (args: string[]): void => {
calls.push(args)
}
signMacosAppBundle(app, run)
// 只有首次 --verify,没有任何 --sign。
expect(calls).toEqual([['--verify', '--strict', app]])
})
})
+93
View File
@@ -22,18 +22,26 @@ const { hasWindowsSherpaRuntime } = nodeRequire('../../scripts/prepare-win-runti
const { const {
validateAsarRuntimeDependencies, validateAsarRuntimeDependencies,
validateFfmpegRuntime, validateFfmpegRuntime,
validateKoffiRuntime,
validateReaderSkillRuntime, validateReaderSkillRuntime,
validateSherpaRuntime, validateSherpaRuntime,
validateSilkWasmRuntime, validateSilkWasmRuntime,
validateSystemOcrRuntime,
findMacosHelperPaths, findMacosHelperPaths,
signMacosHelpers, signMacosHelpers,
signMacosAppBundle signMacosAppBundle
} = nodeRequire('../../scripts/after-pack.cjs') as { } = nodeRequire('../../scripts/after-pack.cjs') as {
validateAsarRuntimeDependencies: (runtimeResources: string) => void validateAsarRuntimeDependencies: (runtimeResources: string) => void
validateFfmpegRuntime: (runtimeResources: string, platform?: NodeJS.Platform) => void validateFfmpegRuntime: (runtimeResources: string, platform?: NodeJS.Platform) => void
validateKoffiRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void
validateReaderSkillRuntime: (runtimeResources: string) => string validateReaderSkillRuntime: (runtimeResources: string) => string
validateSherpaRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void validateSherpaRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void
validateSilkWasmRuntime: (runtimeResources: string) => void validateSilkWasmRuntime: (runtimeResources: string) => void
validateSystemOcrRuntime: (
runtimeResources: string,
platform: NodeJS.Platform,
arch: string
) => void
findMacosHelperPaths: (runtimeResources: string) => string[] findMacosHelperPaths: (runtimeResources: string) => string[]
signMacosHelpers: (runtimeResources: string, run?: CodesignRunner) => string[] signMacosHelpers: (runtimeResources: string, run?: CodesignRunner) => string[]
signMacosAppBundle: (appBundlePath: string, run?: CodesignRunner) => string signMacosAppBundle: (appBundlePath: string, run?: CodesignRunner) => string
@@ -265,6 +273,91 @@ describe('production runtime packaging', () => {
expect(config).toContain('node_modules/sherpa-onnx-*/**') expect(config).toContain('node_modules/sherpa-onnx-*/**')
}) })
it('requires the matching System OCR native runtime', () => {
const resources = join(root, 'system-ocr-resources')
const modules = join(resources, 'app.asar.unpacked', 'node_modules', '@napi-rs')
const base = join(modules, 'system-ocr')
expect(() => validateSystemOcrRuntime(resources, 'darwin', 'arm64')).toThrow(
/Missing unpacked System OCR runtime:.*system-ocr/
)
mkdirSync(base, { recursive: true })
writeFileSync(join(base, 'package.json'), '{}')
writeFileSync(join(base, 'index.js'), 'module.exports = {}')
const mac = join(modules, 'system-ocr-darwin-arm64')
mkdirSync(mac, { recursive: true })
writeFileSync(join(mac, 'package.json'), '{}')
writeFileSync(join(mac, 'system-ocr.darwin-arm64.node'), 'fixture')
expect(() => validateSystemOcrRuntime(resources, 'darwin', 'arm64')).not.toThrow()
// Windows 的原生包名带 -msvc 后缀,查找规则必须跟着改。
expect(() => validateSystemOcrRuntime(resources, 'win32', 'x64')).toThrow(/win32-x64-msvc/)
const windows = join(modules, 'system-ocr-win32-x64-msvc')
mkdirSync(windows, { recursive: true })
writeFileSync(join(windows, 'package.json'), '{}')
writeFileSync(join(windows, 'system-ocr.win32-x64-msvc.node'), 'fixture')
expect(() => validateSystemOcrRuntime(resources, 'win32', 'x64')).not.toThrow()
// Linux 不是 supported target,不应做硬校验。
expect(() => validateSystemOcrRuntime(resources, 'linux', 'x64')).not.toThrow()
})
it('requires the koffi native module for the packaged platform', () => {
const resources = join(root, 'koffi-resources')
const modules = join(resources, 'app.asar.unpacked', 'node_modules', '@koromix')
expect(() => validateKoffiRuntime(resources, 'darwin', 'x64')).toThrow(
/Missing macOS Koffi native module:.*koffi-darwin-x64/
)
expect(() => validateKoffiRuntime(resources, 'darwin', 'arm64')).toThrow(
/koffi-darwin-arm64[/\\]darwin_arm64[/\\]koffi\.node/
)
expect(() => validateKoffiRuntime(resources, 'win32', 'x64')).toThrow(
/Missing Windows Koffi native module:.*koffi-win32-x64/
)
// koffi 运行期按 `${platform}-${arch}` 拼目录名,darwin 用 darwin_<arch>,
// win32 用 win32_x64(见 node_modules/koffi/src/koffi/index.cjs)。
for (const segments of [
['koffi-darwin-x64', 'darwin_x64'],
['koffi-darwin-arm64', 'darwin_arm64'],
['koffi-win32-x64', 'win32_x64']
]) {
const nativeDirectory = join(modules, ...segments)
mkdirSync(nativeDirectory, { recursive: true })
writeFileSync(join(nativeDirectory, 'koffi.node'), 'fixture')
}
expect(() => validateKoffiRuntime(resources, 'darwin', 'x64')).not.toThrow()
expect(() => validateKoffiRuntime(resources, 'darwin', 'arm64')).not.toThrow()
expect(() => validateKoffiRuntime(resources, 'win32', 'x64')).not.toThrow()
// 没有对应原生包的组合应静默跳过,而不是误报。
expect(() => validateKoffiRuntime(resources, 'linux', 'x64')).not.toThrow()
expect(() => validateKoffiRuntime(resources, 'win32', 'arm64')).not.toThrow()
})
it('declares the cross-arch native runtimes pnpm 7 would otherwise skip', () => {
const packageJson = JSON.parse(
readFileSync(resolve(__dirname, '../../package.json'), 'utf8')
) as { dependencies: Record<string, string> }
// pnpm 7.33.7 不支持 supportedArchitectures,非宿主平台的可选依赖会被静默跳过,
// 而这些原生包必须在 dependencies 里显式声明,否则打包阶段才在 afterPack 报缺。
for (const name of [
'sherpa-onnx-darwin-x64',
'sherpa-onnx-win-x64',
'@napi-rs/system-ocr-darwin-x64',
'@napi-rs/system-ocr-win32-x64-msvc',
'@koromix/koffi-darwin-x64',
'@koromix/koffi-win32-x64'
]) {
expect(packageJson.dependencies).toHaveProperty(name)
}
})
it('finds only the macOS helpers that exist in packaged resources', () => { it('finds only the macOS helpers that exist in packaged resources', () => {
const resources = join(root, 'helper-detect-resources', 'resources') const resources = join(root, 'helper-detect-resources', 'resources')
mkdirSync(resources, { recursive: true }) mkdirSync(resources, { recursive: true })