mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-05 12:34:35 +08:00
chore: 构建
This commit is contained in:
@@ -75,8 +75,10 @@
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@koromix/koffi-darwin-x64": "3.1.0",
|
||||
"@koromix/koffi-win32-x64": "3.1.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",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.23",
|
||||
"@radix-ui/react-checkbox": "^1.3.11",
|
||||
@@ -108,7 +110,9 @@
|
||||
"parse5": "^8.0.0",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"sherpa-onnx-darwin-x64": "1.13.3",
|
||||
"sherpa-onnx-node": "1.13.3",
|
||||
"sherpa-onnx-win-x64": "1.13.4",
|
||||
"silk-wasm": "^3.7.1",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"unzipper": "^0.12.0",
|
||||
|
||||
Generated
+38
-31
@@ -11,8 +11,10 @@ specifiers:
|
||||
'@electron-toolkit/preload': ^3.0.2
|
||||
'@electron-toolkit/tsconfig': ^2.0.0
|
||||
'@electron-toolkit/utils': ^4.0.0
|
||||
'@koromix/koffi-darwin-x64': 3.1.0
|
||||
'@koromix/koffi-win32-x64': 3.1.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
|
||||
'@playwright/test': ^1.62.1
|
||||
'@radix-ui/react-alert-dialog': ^1.1.23
|
||||
@@ -72,7 +74,9 @@ specifiers:
|
||||
react: ^19.2.1
|
||||
react-dom: ^19.2.1
|
||||
sass: ^1.102.0
|
||||
sherpa-onnx-darwin-x64: 1.13.3
|
||||
sherpa-onnx-node: 1.13.3
|
||||
sherpa-onnx-win-x64: 1.13.4
|
||||
silk-wasm: ^3.7.1
|
||||
tailwind-merge: ^3.6.0
|
||||
tailwindcss: 3.4.17
|
||||
@@ -87,8 +91,10 @@ specifiers:
|
||||
dependencies:
|
||||
'@electron-toolkit/preload': 3.0.2_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
|
||||
'@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
|
||||
'@radix-ui/react-alert-dialog': 1.1.23_eijghdl4n2x4hz6j4cg7ctgbuu
|
||||
'@radix-ui/react-checkbox': 1.3.11_eijghdl4n2x4hz6j4cg7ctgbuu
|
||||
@@ -120,7 +126,9 @@ dependencies:
|
||||
parse5: 8.0.1
|
||||
pinyin-pro: 3.29.3
|
||||
qrcode: 1.5.4
|
||||
sherpa-onnx-darwin-x64: 1.13.3
|
||||
sherpa-onnx-node: 1.13.3
|
||||
sherpa-onnx-win-x64: 1.13.4
|
||||
silk-wasm: 3.7.1
|
||||
tailwind-merge: 3.6.0
|
||||
unzipper: 0.12.5
|
||||
@@ -1585,7 +1593,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@koromix/koffi-freebsd-arm64/3.1.0:
|
||||
resolution: {integrity: sha512-vazoPYIhOAlXZksVIqDRMIID4VeUZKx8F3dR90hOobT2ATyOkqNS5dv5UCV7Q7DSq22lQTrdbvENBAhROzCp0w==}
|
||||
@@ -1689,6 +1696,36 @@ packages:
|
||||
- supports-color
|
||||
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:
|
||||
resolution: {integrity: sha512-r0f2xNH6U+sth44qF+lUP+2WuHSGUBAry5KSCNuaLDGRbgslFqeROr/qJJ/fb6AjBp3Ov+CJP5MdrOWoaoM3cw==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -1699,34 +1736,6 @@ packages:
|
||||
'@napi-rs/system-ocr-win32-x64-msvc': 1.2.0
|
||||
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:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -7589,7 +7598,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/sherpa-onnx-linux-arm64/1.13.4:
|
||||
resolution: {integrity: sha512-RMjMRqT82BgTXypNNGmLe6ZFYhc3WEvnAGl3DdkK7qB/kuXwkL3iHhV31wAecbnWPsnEpUoD+8cFovWSBzsCuw==}
|
||||
@@ -7628,7 +7636,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/side-channel-list/1.0.0:
|
||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||
|
||||
+134
-18
@@ -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) {
|
||||
if (typeof arch === 'string') return arch
|
||||
return { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }[arch] || String(arch)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -174,8 +223,89 @@ function signMacosHelpers(runtimeResources, run = runCodesign) {
|
||||
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) {
|
||||
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])
|
||||
try {
|
||||
run(['--verify', '--strict', appBundlePath])
|
||||
@@ -401,6 +531,7 @@ exports.default = async function afterPack(context) {
|
||||
)
|
||||
validateSherpaRuntime(runtimeResources, context.electronPlatformName, arch)
|
||||
validateSystemOcrRuntime(runtimeResources, context.electronPlatformName, arch)
|
||||
validateKoffiRuntime(runtimeResources, context.electronPlatformName, arch)
|
||||
pruneIntelMacKeyTool(runtimeResources, context.electronPlatformName, arch)
|
||||
pruneForeignArchConnectors(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
|
||||
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
|
||||
@@ -439,6 +553,7 @@ exports.validateFfmpegRuntime = validateFfmpegRuntime
|
||||
exports.validateSilkWasmRuntime = validateSilkWasmRuntime
|
||||
exports.validateSherpaRuntime = validateSherpaRuntime
|
||||
exports.validateSystemOcrRuntime = validateSystemOcrRuntime
|
||||
exports.validateKoffiRuntime = validateKoffiRuntime
|
||||
exports.pruneIntelMacKeyTool = pruneIntelMacKeyTool
|
||||
exports.pruneForeignArchConnectors = pruneForeignArchConnectors
|
||||
exports.pruneForeignArchNativeRuntimes = pruneForeignArchNativeRuntimes
|
||||
@@ -447,6 +562,7 @@ exports.findMacosHelperPaths = findMacosHelperPaths
|
||||
exports.isMacosCodeValid = isMacosCodeValid
|
||||
exports.signMacosHelpers = signMacosHelpers
|
||||
exports.signMacosAppBundle = signMacosAppBundle
|
||||
exports.findNestedMacosCodePaths = findNestedMacosCodePaths
|
||||
exports.sendRuntimeLocations = sendRuntimeLocations
|
||||
exports.findSendRuntime = findSendRuntime
|
||||
exports.enforceSendRuntimeBoundary = enforceSendRuntimeBoundary
|
||||
|
||||
@@ -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]])
|
||||
})
|
||||
})
|
||||
@@ -22,18 +22,26 @@ const { hasWindowsSherpaRuntime } = nodeRequire('../../scripts/prepare-win-runti
|
||||
const {
|
||||
validateAsarRuntimeDependencies,
|
||||
validateFfmpegRuntime,
|
||||
validateKoffiRuntime,
|
||||
validateReaderSkillRuntime,
|
||||
validateSherpaRuntime,
|
||||
validateSilkWasmRuntime,
|
||||
validateSystemOcrRuntime,
|
||||
findMacosHelperPaths,
|
||||
signMacosHelpers,
|
||||
signMacosAppBundle
|
||||
} = nodeRequire('../../scripts/after-pack.cjs') as {
|
||||
validateAsarRuntimeDependencies: (runtimeResources: string) => void
|
||||
validateFfmpegRuntime: (runtimeResources: string, platform?: NodeJS.Platform) => void
|
||||
validateKoffiRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void
|
||||
validateReaderSkillRuntime: (runtimeResources: string) => string
|
||||
validateSherpaRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void
|
||||
validateSilkWasmRuntime: (runtimeResources: string) => void
|
||||
validateSystemOcrRuntime: (
|
||||
runtimeResources: string,
|
||||
platform: NodeJS.Platform,
|
||||
arch: string
|
||||
) => void
|
||||
findMacosHelperPaths: (runtimeResources: string) => string[]
|
||||
signMacosHelpers: (runtimeResources: 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-*/**')
|
||||
})
|
||||
|
||||
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', () => {
|
||||
const resources = join(root, 'helper-detect-resources', 'resources')
|
||||
mkdirSync(resources, { recursive: true })
|
||||
|
||||
Reference in New Issue
Block a user