mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-06 13:54:10 +08:00
chore: 构建
This commit is contained in:
@@ -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