mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-03 18:33:14 +08:00
672 lines
27 KiB
TypeScript
672 lines
27 KiB
TypeScript
import { createRequire } from 'module'
|
||
import {
|
||
existsSync,
|
||
mkdirSync,
|
||
mkdtempSync,
|
||
readFileSync,
|
||
rmSync,
|
||
statSync,
|
||
writeFileSync
|
||
} from 'fs'
|
||
import { tmpdir } from 'os'
|
||
import { dirname, join, resolve } from 'path'
|
||
import { afterAll, describe, expect, it } from 'vitest'
|
||
|
||
const nodeRequire = createRequire(import.meta.url)
|
||
const asar = nodeRequire('@electron/asar') as {
|
||
createPackage: (source: string, destination: string) => Promise<void>
|
||
}
|
||
const { hasWindowsSherpaRuntime } = nodeRequire('../../scripts/prepare-win-runtime.cjs') as {
|
||
hasWindowsSherpaRuntime: (runtimeRoot: string) => boolean
|
||
}
|
||
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
|
||
}
|
||
|
||
type CodesignRunner = (args: string[]) => void
|
||
|
||
function createCodesignStub(options: { verifyFails?: (args: string[]) => boolean } = {}): {
|
||
calls: string[][]
|
||
run: CodesignRunner
|
||
} {
|
||
const calls: string[][] = []
|
||
const run: CodesignRunner = (args) => {
|
||
calls.push(args)
|
||
if (options.verifyFails && args[0] === '--verify' && options.verifyFails(args)) {
|
||
throw new Error('code object is not signed at all')
|
||
}
|
||
}
|
||
return { calls, run }
|
||
}
|
||
const root = mkdtempSync(join(tmpdir(), 'wxe-runtime-package-'))
|
||
|
||
const {
|
||
pruneIntelMacKeyTool,
|
||
pruneForeignArchConnectors,
|
||
pruneForeignArchNativeRuntimes,
|
||
validateRuntimeBinaryArchitecture,
|
||
findSendRuntime,
|
||
enforceSendRuntimeBoundary,
|
||
default: afterPack
|
||
} = nodeRequire('../../scripts/after-pack.cjs') as {
|
||
pruneIntelMacKeyTool: (
|
||
runtimeResources: string,
|
||
platform: NodeJS.Platform,
|
||
arch: string
|
||
) => string | null
|
||
pruneForeignArchConnectors: (
|
||
runtimeResources: string,
|
||
platform: NodeJS.Platform,
|
||
arch: string
|
||
) => string[]
|
||
pruneForeignArchNativeRuntimes: (
|
||
runtimeResources: string,
|
||
platform: NodeJS.Platform,
|
||
arch: string
|
||
) => string[]
|
||
validateRuntimeBinaryArchitecture: (
|
||
filePath: string,
|
||
platform: NodeJS.Platform,
|
||
arch: string,
|
||
label: string
|
||
) => void
|
||
findSendRuntime: (runtimeResources: string) => string | null
|
||
enforceSendRuntimeBoundary: (
|
||
runtimeResources: string,
|
||
platform: NodeJS.Platform,
|
||
bundlesSendRuntime?: boolean
|
||
) => string | null
|
||
default: (context: AfterPackContext) => Promise<void>
|
||
}
|
||
|
||
type AfterPackContext = {
|
||
appOutDir: string
|
||
electronPlatformName: NodeJS.Platform
|
||
arch?: number
|
||
packager: { appInfo: { productFilename: string } }
|
||
}
|
||
const { readBinaryArchitectures } = nodeRequire('../../scripts/binary-arch.cjs') as {
|
||
readBinaryArchitectures: (filePath: string) => string[]
|
||
}
|
||
|
||
const CPU_TYPE_X86_64 = 0x01000007
|
||
const CPU_TYPE_ARM64 = 0x0100000c
|
||
|
||
function writeThinMachO(filePath: string, cpuType: number): void {
|
||
const buffer = Buffer.alloc(32)
|
||
buffer.writeUInt32LE(0xfeedfacf, 0)
|
||
buffer.writeUInt32LE(cpuType, 4)
|
||
writeFileSync(filePath, buffer)
|
||
}
|
||
|
||
describe('production runtime packaging', () => {
|
||
afterAll(() => rmSync(root, { recursive: true, force: true }))
|
||
|
||
it('requires the complete unpacked silk-wasm runtime', () => {
|
||
const packagePath = join(root, 'resources', 'app.asar.unpacked', 'node_modules', 'silk-wasm')
|
||
mkdirSync(join(packagePath, 'lib'), { recursive: true })
|
||
writeFileSync(join(packagePath, 'package.json'), '{}')
|
||
writeFileSync(join(packagePath, 'lib', 'index.cjs'), 'module.exports = {}')
|
||
|
||
expect(() => validateSilkWasmRuntime(join(root, 'resources'))).toThrow(/silk\.wasm/)
|
||
writeFileSync(join(packagePath, 'lib', 'silk.wasm'), Buffer.from([0, 97, 115, 109]))
|
||
expect(() => validateSilkWasmRuntime(join(root, 'resources'))).not.toThrow()
|
||
})
|
||
|
||
it('requires the bundled Reader Skill declared by extraResources', () => {
|
||
const resources = join(root, 'reader-skill-resources')
|
||
const skillPath = join(resources, 'skill', 'tracememo-reader', 'SKILL.md')
|
||
const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8')
|
||
|
||
expect(config).toContain('docs/skill/tracememo-reader')
|
||
expect(config).toContain('to: skill/tracememo-reader')
|
||
expect(() => validateReaderSkillRuntime(resources)).toThrow(
|
||
/Missing bundled TraceMemo Reader Skill/
|
||
)
|
||
|
||
mkdirSync(dirname(skillPath), { recursive: true })
|
||
writeFileSync(skillPath, '# TraceMemo Reader\n')
|
||
expect(validateReaderSkillRuntime(resources)).toBe(skillPath)
|
||
})
|
||
|
||
it('keeps silk-wasm in electron-builder asarUnpack', () => {
|
||
const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8')
|
||
expect(config).toContain('node_modules/silk-wasm/**')
|
||
})
|
||
|
||
it('uses a Windows x64-only resource set', () => {
|
||
const config = readFileSync(resolve(__dirname, '../../electron-builder.win.yml'), 'utf8')
|
||
expect(config).toContain('key/win32/x64/**')
|
||
expect(config).toContain('wcdb/win32/x64/**')
|
||
expect(config).toContain('electronLanguages:')
|
||
expect(config).not.toContain('win32-arm64')
|
||
expect(config).not.toContain('macos/')
|
||
})
|
||
|
||
it('installs cross-platform optional dependencies before Windows packaging', () => {
|
||
const packageJson = JSON.parse(
|
||
readFileSync(resolve(__dirname, '../../package.json'), 'utf8')
|
||
) as { scripts: Record<string, string> }
|
||
|
||
expect(packageJson.scripts['prepare:win-runtime']).toBe(
|
||
'node scripts/prepare-win-runtime.cjs && npm run prepare:ffmpeg:win'
|
||
)
|
||
expect(packageJson.scripts['build:win']).toContain('npm run prepare:win-runtime')
|
||
expect(packageJson.scripts['release:win']).toContain('npm run prepare:win-runtime')
|
||
})
|
||
|
||
it('detects an incomplete Windows sherpa runtime before invoking pnpm', () => {
|
||
const runtimeRoot = join(root, 'win-sherpa-runtime')
|
||
mkdirSync(runtimeRoot, { recursive: true })
|
||
expect(hasWindowsSherpaRuntime(runtimeRoot)).toBe(false)
|
||
|
||
writeFileSync(join(runtimeRoot, 'package.json'), '{}')
|
||
expect(hasWindowsSherpaRuntime(runtimeRoot)).toBe(false)
|
||
|
||
writeFileSync(join(runtimeRoot, 'sherpa-onnx.node'), 'fixture')
|
||
expect(hasWindowsSherpaRuntime(runtimeRoot)).toBe(true)
|
||
})
|
||
|
||
it('rejects an app archive with missing runtime dependencies', async () => {
|
||
const resources = join(root, 'asar-resources')
|
||
const source = join(root, 'asar-source')
|
||
mkdirSync(source, { recursive: true })
|
||
writeFileSync(join(source, 'package.json'), '{}')
|
||
mkdirSync(resources, { recursive: true })
|
||
await asar.createPackage(source, join(resources, 'app.asar'))
|
||
|
||
expect(() => validateAsarRuntimeDependencies(resources)).toThrow(
|
||
/Missing packaged runtime dependencies:.*@electron-toolkit\/utils/
|
||
)
|
||
})
|
||
|
||
it('accepts runtime dependencies when asar uses Windows path separators', async () => {
|
||
const resources = join(root, 'asar-complete-resources')
|
||
const source = join(root, 'asar-complete-source')
|
||
const packages = [
|
||
'@electron-toolkit/preload',
|
||
'@electron-toolkit/utils',
|
||
'archiver',
|
||
'electron-updater',
|
||
'ffmpeg-static',
|
||
'fs-extra',
|
||
'jsonrepair',
|
||
'koffi'
|
||
]
|
||
for (const packageName of packages) {
|
||
const packagePath = join(source, 'node_modules', ...packageName.split('/'))
|
||
mkdirSync(packagePath, { recursive: true })
|
||
writeFileSync(join(packagePath, 'package.json'), '{}')
|
||
}
|
||
mkdirSync(resources, { recursive: true })
|
||
await asar.createPackage(source, join(resources, 'app.asar'))
|
||
|
||
expect(() => validateAsarRuntimeDependencies(resources)).not.toThrow()
|
||
})
|
||
|
||
it('requires and unpacks the bundled ffmpeg-static executable', () => {
|
||
const resources = join(root, 'ffmpeg-resources')
|
||
const ffmpegPath = join(
|
||
resources,
|
||
'app.asar.unpacked',
|
||
'node_modules',
|
||
'ffmpeg-static',
|
||
'ffmpeg'
|
||
)
|
||
expect(() => validateFfmpegRuntime(resources, 'darwin')).toThrow(/ffmpeg-static/)
|
||
mkdirSync(dirname(ffmpegPath), { recursive: true })
|
||
writeFileSync(ffmpegPath, 'fixture')
|
||
expect(() => validateFfmpegRuntime(resources, 'darwin')).not.toThrow()
|
||
|
||
const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8')
|
||
expect(config).toContain('node_modules/ffmpeg-static/**')
|
||
})
|
||
|
||
it('requires the matching Windows and macOS sherpa native runtime', () => {
|
||
const resources = join(root, 'sherpa-resources')
|
||
const unpacked = join(resources, 'app.asar.unpacked', 'node_modules')
|
||
const base = join(unpacked, 'sherpa-onnx-node')
|
||
mkdirSync(base, { recursive: true })
|
||
writeFileSync(join(base, 'package.json'), '{}')
|
||
writeFileSync(join(base, 'sherpa-onnx.js'), 'module.exports = {}')
|
||
|
||
expect(() => validateSherpaRuntime(resources, 'win32', 'x64')).toThrow(/win-x64/)
|
||
const windows = join(unpacked, 'sherpa-onnx-win-x64')
|
||
mkdirSync(windows, { recursive: true })
|
||
writeFileSync(join(windows, 'package.json'), '{}')
|
||
writeFileSync(join(windows, 'sherpa-onnx.node'), 'fixture')
|
||
expect(() => validateSherpaRuntime(resources, 'win32', 'x64')).not.toThrow()
|
||
|
||
expect(() => validateSherpaRuntime(resources, 'darwin', 'arm64')).toThrow(/darwin-arm64/)
|
||
const mac = join(unpacked, 'sherpa-onnx-darwin-arm64')
|
||
mkdirSync(mac, { recursive: true })
|
||
writeFileSync(join(mac, 'package.json'), '{}')
|
||
writeFileSync(join(mac, 'sherpa-onnx.node'), 'fixture')
|
||
expect(() => validateSherpaRuntime(resources, 'darwin', 'arm64')).not.toThrow()
|
||
|
||
const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8')
|
||
expect(config).toContain('node_modules/sherpa-onnx-node/**')
|
||
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 })
|
||
expect(findMacosHelperPaths(join(root, 'helper-detect-resources'))).toEqual([])
|
||
|
||
const helperPath = join(resources, 'xkey_helper')
|
||
writeFileSync(helperPath, 'fixture')
|
||
const versionedPath = join(resources, 'xkey_helper_4_1_13')
|
||
writeFileSync(versionedPath, 'fixture')
|
||
expect(findMacosHelperPaths(join(root, 'helper-detect-resources'))).toEqual([
|
||
helperPath,
|
||
versionedPath
|
||
])
|
||
})
|
||
|
||
it('ad-hoc signs packaged helpers whose signature is missing or modified', () => {
|
||
const resources = join(root, 'helper-sign-resources', 'resources')
|
||
mkdirSync(resources, { recursive: true })
|
||
const helperPath = join(resources, 'xkey_helper')
|
||
writeFileSync(helperPath, 'fixture')
|
||
const stub = createCodesignStub({ verifyFails: (args) => !args.includes('--arch') })
|
||
|
||
expect(signMacosHelpers(join(root, 'helper-sign-resources'), stub.run)).toEqual([helperPath])
|
||
expect(stub.calls).toContainEqual(['--force', '--sign', '-', helperPath])
|
||
expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'arm64', helperPath])
|
||
expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'x86_64', helperPath])
|
||
if (process.platform !== 'win32') {
|
||
expect(statSync(helperPath).mode & 0o777).toBe(0o755)
|
||
}
|
||
})
|
||
|
||
it('keeps helpers that already verify strictly without re-signing them', () => {
|
||
const resources = join(root, 'helper-valid-resources', 'resources')
|
||
mkdirSync(resources, { recursive: true })
|
||
const helperPath = join(resources, 'xkey_helper')
|
||
writeFileSync(helperPath, 'fixture')
|
||
const stub = createCodesignStub()
|
||
|
||
expect(signMacosHelpers(join(root, 'helper-valid-resources'), stub.run)).toEqual([helperPath])
|
||
expect(stub.calls.filter((args) => args[0] === '--force')).toEqual([])
|
||
expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'arm64', helperPath])
|
||
expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'x86_64', helperPath])
|
||
})
|
||
|
||
it('fails packaging when a helper signature cannot be repaired', () => {
|
||
const resources = join(root, 'helper-broken-resources', 'resources')
|
||
mkdirSync(resources, { recursive: true })
|
||
writeFileSync(join(resources, 'xkey_helper'), 'fixture')
|
||
const stub = createCodesignStub({ verifyFails: () => true })
|
||
|
||
expect(() => signMacosHelpers(join(root, 'helper-broken-resources'), stub.run)).toThrow(
|
||
/xkey_helper \(arm64\)/
|
||
)
|
||
})
|
||
|
||
it('ad-hoc signs an invalid app bundle and verifies it strictly', () => {
|
||
const appBundle = join(root, 'TraceMemo.app')
|
||
const calls: string[][] = []
|
||
let verifyCount = 0
|
||
const run: CodesignRunner = (args) => {
|
||
calls.push(args)
|
||
if (args[0] === '--verify') {
|
||
verifyCount += 1
|
||
if (verifyCount === 1) throw new Error('bundle is not signed')
|
||
}
|
||
}
|
||
|
||
expect(signMacosAppBundle(appBundle, run)).toBe(appBundle)
|
||
expect(calls).toEqual([
|
||
['--verify', '--strict', appBundle],
|
||
['--force', '--sign', '-', appBundle],
|
||
['--verify', '--strict', appBundle]
|
||
])
|
||
})
|
||
|
||
it('keeps an app bundle that already verifies strictly', () => {
|
||
const appBundle = join(root, 'Valid.app')
|
||
const stub = createCodesignStub()
|
||
|
||
expect(signMacosAppBundle(appBundle, stub.run)).toBe(appBundle)
|
||
expect(stub.calls).toEqual([['--verify', '--strict', appBundle]])
|
||
})
|
||
|
||
it('fails packaging when the app bundle cannot be made strictly valid', () => {
|
||
const appBundle = join(root, 'Broken.app')
|
||
const stub = createCodesignStub({ verifyFails: () => true })
|
||
|
||
expect(() => signMacosAppBundle(appBundle, stub.run)).toThrow(
|
||
/app bundle signature verification failed/
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('per-architecture macOS packaging', () => {
|
||
const archRoot = mkdtempSync(join(tmpdir(), 'wxe-runtime-arch-'))
|
||
afterAll(() => rmSync(archRoot, { recursive: true, force: true }))
|
||
|
||
it('reads architectures out of Mach-O and PE binaries', () => {
|
||
const x64 = join(archRoot, 'fixture-x64')
|
||
const arm64 = join(archRoot, 'fixture-arm64')
|
||
writeThinMachO(x64, CPU_TYPE_X86_64)
|
||
writeThinMachO(arm64, CPU_TYPE_ARM64)
|
||
|
||
expect(readBinaryArchitectures(x64)).toEqual(['x64'])
|
||
expect(readBinaryArchitectures(arm64)).toEqual(['arm64'])
|
||
expect(readBinaryArchitectures(resolve(__dirname, 'runtime-packaging.test.ts'))).toEqual([])
|
||
})
|
||
|
||
it('keeps the Intel Mac key helper only in x64 macOS bundles', () => {
|
||
const resources = join(archRoot, 'key-tool', 'Contents', 'Resources')
|
||
const keyToolDirectory = join(resources, 'resources', 'macos-key-tool')
|
||
const helper = join(keyToolDirectory, 'intel_mac_key_helper')
|
||
mkdirSync(keyToolDirectory, { recursive: true })
|
||
writeFileSync(helper, 'fixture')
|
||
|
||
expect(pruneIntelMacKeyTool(resources, 'darwin', 'x64')).toBe(helper)
|
||
expect(existsSync(helper)).toBe(true)
|
||
|
||
expect(pruneIntelMacKeyTool(resources, 'darwin', 'arm64')).toBeNull()
|
||
expect(existsSync(keyToolDirectory)).toBe(false)
|
||
})
|
||
|
||
it('fails an x64 bundle that lost the Intel Mac key helper', () => {
|
||
const resources = join(archRoot, 'key-tool-missing', 'Contents', 'Resources')
|
||
mkdirSync(resources, { recursive: true })
|
||
|
||
expect(() => pruneIntelMacKeyTool(resources, 'darwin', 'x64')).toThrow(/macos-key-tool/)
|
||
expect(pruneIntelMacKeyTool(resources, 'win32', 'x64')).toBeNull()
|
||
})
|
||
|
||
it('drops bundled connectors built for another architecture', () => {
|
||
const resources = join(archRoot, 'connectors', 'Contents', 'Resources')
|
||
const foreign = join(resources, 'resources', 'connectors', 'wechat', 'darwin-arm64')
|
||
const matching = join(resources, 'resources', 'connectors', 'wechat', 'darwin-x64')
|
||
mkdirSync(foreign, { recursive: true })
|
||
mkdirSync(matching, { recursive: true })
|
||
|
||
expect(pruneForeignArchConnectors(resources, 'darwin', 'x64')).toEqual(['wechat/darwin-arm64'])
|
||
expect(existsSync(foreign)).toBe(false)
|
||
expect(existsSync(matching)).toBe(true)
|
||
|
||
expect(pruneForeignArchConnectors(resources, 'darwin', 'universal')).toEqual([])
|
||
expect(existsSync(matching)).toBe(true)
|
||
})
|
||
|
||
it('drops native runtime packages built for other platforms', () => {
|
||
const resources = join(archRoot, 'native-runtimes', 'Contents', 'Resources')
|
||
const modules = join(resources, 'app.asar.unpacked', 'node_modules')
|
||
for (const name of [
|
||
'sherpa-onnx-darwin-x64',
|
||
'sherpa-onnx-darwin-arm64',
|
||
'sherpa-onnx-linux-x64',
|
||
'sherpa-onnx-node'
|
||
]) {
|
||
mkdirSync(join(modules, name), { recursive: true })
|
||
}
|
||
for (const name of ['koffi-darwin-x64', 'koffi-win32-x64']) {
|
||
mkdirSync(join(modules, '@koromix', name), { recursive: true })
|
||
}
|
||
|
||
expect(pruneForeignArchNativeRuntimes(resources, 'darwin', 'x64').sort()).toEqual([
|
||
'@koromix/koffi-win32-x64',
|
||
'sherpa-onnx-darwin-arm64',
|
||
'sherpa-onnx-linux-x64'
|
||
])
|
||
expect(existsSync(join(modules, 'sherpa-onnx-darwin-x64'))).toBe(true)
|
||
expect(existsSync(join(modules, 'sherpa-onnx-node'))).toBe(true)
|
||
expect(existsSync(join(modules, '@koromix', 'koffi-darwin-x64'))).toBe(true)
|
||
expect(existsSync(join(modules, '@koromix', 'koffi-win32-x64'))).toBe(false)
|
||
|
||
expect(pruneForeignArchNativeRuntimes(resources, 'darwin', 'universal')).toEqual([])
|
||
expect(existsSync(join(modules, 'sherpa-onnx-darwin-x64'))).toBe(true)
|
||
})
|
||
|
||
it('rejects a bundled binary built for another architecture', () => {
|
||
const binary = join(archRoot, 'bundled-arm64')
|
||
writeThinMachO(binary, CPU_TYPE_ARM64)
|
||
|
||
expect(() =>
|
||
validateRuntimeBinaryArchitecture(binary, 'darwin', 'x64', 'Bundled ffmpeg')
|
||
).toThrow(/arm64 but this bundle targets x64/)
|
||
expect(() =>
|
||
validateRuntimeBinaryArchitecture(binary, 'darwin', 'arm64', 'Bundled ffmpeg')
|
||
).not.toThrow()
|
||
})
|
||
})
|
||
|
||
describe('send runtime packaging boundary', () => {
|
||
const boundaryRoot = mkdtempSync(join(tmpdir(), 'wxe-send-runtime-'))
|
||
afterAll(() => rmSync(boundaryRoot, { recursive: true, force: true }))
|
||
|
||
function resourcesWithSendRuntime(name: string, unpacked = false): string {
|
||
const resources = join(boundaryRoot, name, 'Contents', 'Resources')
|
||
const runtime = unpacked
|
||
? join(resources, 'app.asar.unpacked', 'resources', 'runtime', 'darwin-arm64')
|
||
: join(resources, 'resources', 'runtime', 'darwin-arm64')
|
||
mkdirSync(runtime, { recursive: true })
|
||
writeFileSync(join(runtime, 'tm-wechat-host'), 'fixture')
|
||
return resources
|
||
}
|
||
|
||
it('accepts a macOS bundle without the send runtime', () => {
|
||
const resources = join(boundaryRoot, 'clean', 'Contents', 'Resources')
|
||
mkdirSync(resources, { recursive: true })
|
||
|
||
expect(findSendRuntime(resources)).toBeNull()
|
||
expect(enforceSendRuntimeBoundary(resources, 'darwin', false)).toBeNull()
|
||
})
|
||
|
||
it('fails a bundle that still carries the send runtime', () => {
|
||
expect(() =>
|
||
enforceSendRuntimeBoundary(resourcesWithSendRuntime('dirty'), 'darwin', false)
|
||
).toThrow(/must not include the WeChat send runtime/)
|
||
})
|
||
|
||
it('detects the send runtime when it lands under app.asar.unpacked', () => {
|
||
const resources = resourcesWithSendRuntime('unpacked', true)
|
||
|
||
expect(findSendRuntime(resources)).toBe(
|
||
join(resources, 'app.asar.unpacked', 'resources', 'runtime', 'darwin-arm64')
|
||
)
|
||
expect(() => enforceSendRuntimeBoundary(resources, 'darwin', false)).toThrow(
|
||
/must not include the WeChat send runtime/
|
||
)
|
||
})
|
||
|
||
it('requires the send runtime when the build is configured to bundle it', () => {
|
||
const missing = join(boundaryRoot, 'runtime-missing', 'Contents', 'Resources')
|
||
mkdirSync(missing, { recursive: true })
|
||
|
||
expect(() => enforceSendRuntimeBoundary(missing, 'darwin', true)).toThrow(
|
||
/requires the WeChat send runtime/
|
||
)
|
||
|
||
const present = resourcesWithSendRuntime('runtime-ok')
|
||
expect(enforceSendRuntimeBoundary(present, 'darwin', true)).toBe(
|
||
join(present, 'resources', 'runtime', 'darwin-arm64')
|
||
)
|
||
})
|
||
|
||
it('ignores non-macOS bundles', () => {
|
||
const resources = resourcesWithSendRuntime('win32')
|
||
|
||
expect(enforceSendRuntimeBoundary(resources, 'win32', false)).toBeNull()
|
||
expect(enforceSendRuntimeBoundary(resources, 'win32', true)).toBeNull()
|
||
})
|
||
|
||
it('keeps the send runtime build off the release publishing path', () => {
|
||
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8')) as {
|
||
scripts: Record<string, string>
|
||
}
|
||
|
||
expect(Object.keys(pkg.scripts).filter((name) => name.includes('send-runtime'))).toEqual([
|
||
'build:mac:arm64:send-runtime'
|
||
])
|
||
expect(pkg.scripts['build:mac:arm64:send-runtime']).toContain('TM_SEND_RUNTIME_BUILD=1')
|
||
expect(pkg.scripts['build:mac:arm64:send-runtime']).toContain(
|
||
'electron-builder.send-runtime.yml'
|
||
)
|
||
expect(pkg.scripts['build:mac:arm64:send-runtime']).not.toContain('--publish')
|
||
|
||
for (const [name, command] of Object.entries(pkg.scripts)) {
|
||
if (!name.startsWith('release')) continue
|
||
expect(command).not.toContain('electron-builder.send-runtime.yml')
|
||
expect(command).not.toContain('TM_SEND_RUNTIME_BUILD')
|
||
}
|
||
})
|
||
|
||
it('excludes the send runtime in the default builder config', () => {
|
||
const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8')
|
||
|
||
expect(config).toContain("'!runtime/darwin-arm64/**'")
|
||
})
|
||
|
||
// 只测函数是测不出「边界校验有没有被接上去」的。这个 bundle 故意不含 app.asar 与
|
||
// Reader Skill:一旦 afterPack 把边界校验排到其它校验之后,抛出的就会是
|
||
// "Missing packaged application archive" 而不是边界错误,用例即失败。
|
||
it('applies the boundary check inside afterPack ahead of every other validation', async () => {
|
||
const appOutDir = join(boundaryRoot, 'wired')
|
||
const runtime = join(
|
||
appOutDir,
|
||
'TraceMemo.app',
|
||
'Contents',
|
||
'Resources',
|
||
'resources',
|
||
'runtime',
|
||
'darwin-arm64'
|
||
)
|
||
mkdirSync(runtime, { recursive: true })
|
||
writeFileSync(join(runtime, 'tm-wechat-host'), 'fixture')
|
||
|
||
await expect(
|
||
afterPack({
|
||
appOutDir,
|
||
electronPlatformName: 'darwin',
|
||
arch: 3,
|
||
packager: { appInfo: { productFilename: 'TraceMemo' } }
|
||
})
|
||
).rejects.toThrow(/must not include the WeChat send runtime/)
|
||
})
|
||
})
|
||
|
||
describe('release publishing policy', () => {
|
||
it('uploads GitHub releases as drafts until the notes are reviewed', () => {
|
||
for (const configFile of ['electron-builder.yml', 'electron-builder.win.yml']) {
|
||
const config = readFileSync(resolve(__dirname, `../../${configFile}`), 'utf8')
|
||
expect(config).toContain('releaseType: draft')
|
||
expect(config).not.toContain('releaseType: release')
|
||
}
|
||
})
|
||
})
|