mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
265 lines
10 KiB
JavaScript
265 lines
10 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */
|
|
const { chmodSync, existsSync, readdirSync, rmSync } = require('node:fs')
|
|
const { execFileSync } = require('node:child_process')
|
|
const path = require('node:path')
|
|
const asar = require('@electron/asar')
|
|
const { readBinaryArchitectures } = require('./binary-arch.cjs')
|
|
|
|
const REQUIRED_RUNTIME_PACKAGES = [
|
|
'@electron-toolkit/preload',
|
|
'@electron-toolkit/utils',
|
|
'archiver',
|
|
'electron-updater',
|
|
'ffmpeg-static',
|
|
'fs-extra',
|
|
'jsonrepair',
|
|
'koffi'
|
|
]
|
|
|
|
function getRuntimeResources(context) {
|
|
const productName = context.packager.appInfo.productFilename
|
|
return context.electronPlatformName === 'darwin'
|
|
? path.join(context.appOutDir, `${productName}.app`, 'Contents', 'Resources')
|
|
: path.join(context.appOutDir, 'resources')
|
|
}
|
|
|
|
function validateSilkWasmRuntime(runtimeResources) {
|
|
const packagePath = path.join(runtimeResources, 'app.asar.unpacked', 'node_modules', 'silk-wasm')
|
|
const requiredFiles = [
|
|
path.join(packagePath, 'package.json'),
|
|
path.join(packagePath, 'lib', 'index.cjs'),
|
|
path.join(packagePath, 'lib', 'silk.wasm')
|
|
]
|
|
const missingFiles = requiredFiles.filter((filePath) => !existsSync(filePath))
|
|
if (missingFiles.length > 0) {
|
|
throw new Error(`Missing unpacked silk-wasm runtime: ${missingFiles.join(', ')}`)
|
|
}
|
|
}
|
|
|
|
function validateFfmpegRuntime(runtimeResources, platform = process.platform) {
|
|
const executable = platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'
|
|
const ffmpegPath = path.join(
|
|
runtimeResources,
|
|
'app.asar.unpacked',
|
|
'node_modules',
|
|
'ffmpeg-static',
|
|
executable
|
|
)
|
|
if (!existsSync(ffmpegPath)) {
|
|
throw new Error(`Missing unpacked ffmpeg-static runtime: ${ffmpegPath}`)
|
|
}
|
|
if (platform !== 'win32') chmodSync(ffmpegPath, 0o755)
|
|
return ffmpegPath
|
|
}
|
|
|
|
function validateSherpaRuntime(runtimeResources, platform, arch) {
|
|
const platformName = platform === 'win32' ? 'win' : platform
|
|
const basePath = path.join(
|
|
runtimeResources,
|
|
'app.asar.unpacked',
|
|
'node_modules',
|
|
'sherpa-onnx-node'
|
|
)
|
|
const nativePath = path.join(
|
|
runtimeResources,
|
|
'app.asar.unpacked',
|
|
'node_modules',
|
|
`sherpa-onnx-${platformName}-${arch}`
|
|
)
|
|
const requiredFiles = [
|
|
path.join(basePath, 'package.json'),
|
|
path.join(basePath, 'sherpa-onnx.js'),
|
|
path.join(nativePath, 'package.json'),
|
|
path.join(nativePath, 'sherpa-onnx.node')
|
|
]
|
|
const missingFiles = requiredFiles.filter((filePath) => !existsSync(filePath))
|
|
if (missingFiles.length > 0) {
|
|
throw new Error(`Missing unpacked sherpa-onnx runtime: ${missingFiles.join(', ')}`)
|
|
}
|
|
}
|
|
|
|
function normalizeBuilderArch(arch) {
|
|
if (typeof arch === 'string') return arch
|
|
return { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }[arch] || String(arch)
|
|
}
|
|
|
|
/**
|
|
* A foreign-architecture binary only fails once the user touches the feature
|
|
* that needs it, so verify the ones whose filename is shared across
|
|
* architectures (ffmpeg-static keeps a single "ffmpeg" per platform) and fail
|
|
* the build instead of shipping a broken bundle.
|
|
*/
|
|
function validateRuntimeBinaryArchitecture(filePath, platform, arch, label) {
|
|
if (platform !== 'darwin' && platform !== 'win32') return
|
|
if (arch === 'universal') return
|
|
const architectures = readBinaryArchitectures(filePath)
|
|
if (!architectures.length || architectures.includes(arch)) return
|
|
throw new Error(
|
|
`${label} is ${architectures.join('/')} but this bundle targets ${arch}: ${filePath}`
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The Intel Mac key helper is an x86_64 executable that only the x64 (or
|
|
* universal) macOS bundle can run. Every other target — Apple Silicon macOS,
|
|
* Windows, Linux — would otherwise ship a ~34MB binary it can never execute,
|
|
* so it is dropped from those bundles. x64/universal builds fail fast instead
|
|
* of silently shipping an Intel Mac app that cannot read keys.
|
|
*/
|
|
function pruneIntelMacKeyTool(runtimeResources, platform, arch) {
|
|
const keyToolDirectory = path.join(runtimeResources, 'resources', 'macos-key-tool')
|
|
const usable = platform === 'darwin' && (arch === 'x64' || arch === 'universal')
|
|
if (!usable) {
|
|
rmSync(keyToolDirectory, { recursive: true, force: true })
|
|
return null
|
|
}
|
|
const helperPath = path.join(keyToolDirectory, 'intel_mac_key_helper')
|
|
if (!existsSync(helperPath)) {
|
|
throw new Error(`Missing Intel Mac key helper in a ${arch} bundle: ${helperPath}`)
|
|
}
|
|
return helperPath
|
|
}
|
|
|
|
function validateAsarRuntimeDependencies(runtimeResources) {
|
|
const asarPath = path.join(runtimeResources, 'app.asar')
|
|
if (!existsSync(asarPath)) throw new Error(`Missing packaged application archive: ${asarPath}`)
|
|
|
|
// @electron/asar returns platform-native separators. Normalize to POSIX
|
|
// paths so validation behaves consistently on Windows and macOS/Linux.
|
|
const entries = new Set(asar.listPackage(asarPath).map((entry) => entry.replaceAll('\\', '/')))
|
|
const missingPackages = REQUIRED_RUNTIME_PACKAGES.filter(
|
|
(packageName) => !entries.has(`/node_modules/${packageName}/package.json`)
|
|
)
|
|
if (missingPackages.length > 0) {
|
|
throw new Error(
|
|
`Missing packaged runtime dependencies: ${missingPackages.join(', ')}. ` +
|
|
'Use pnpm 7.33.7 so electron-builder can read pnpm-lock.yaml.'
|
|
)
|
|
}
|
|
}
|
|
function validateReaderSkillRuntime(runtimeResources) {
|
|
const skillPath = path.join(runtimeResources, 'skill', 'tracememo-reader', 'SKILL.md')
|
|
if (!existsSync(skillPath)) {
|
|
throw new Error(`Missing bundled TraceMemo Reader Skill: ${skillPath}`)
|
|
}
|
|
return skillPath
|
|
}
|
|
|
|
/**
|
|
* Native runtime packages are published once per platform-arch pair, and pnpm
|
|
* installs all of them, so every bundle ends up carrying the native libraries
|
|
* of every platform (measured: ~129MB of speech models plus ~16MB of koffi).
|
|
* The loaders pick their package from process.platform/arch, so the siblings
|
|
* are dead weight — drop them.
|
|
*/
|
|
const NATIVE_RUNTIME_PACKAGES = [
|
|
{
|
|
modules: [],
|
|
prefix: 'sherpa-onnx',
|
|
platformName: (platform) => (platform === 'win32' ? 'win' : platform)
|
|
},
|
|
{
|
|
modules: ['@koromix'],
|
|
prefix: 'koffi',
|
|
platformName: (platform) => platform
|
|
}
|
|
]
|
|
|
|
function pruneForeignArchNativeRuntimes(runtimeResources, platform, arch) {
|
|
if (arch === 'universal') return []
|
|
const unpackedRoot = path.join(runtimeResources, 'app.asar.unpacked', 'node_modules')
|
|
if (!existsSync(unpackedRoot)) return []
|
|
const removed = []
|
|
for (const runtime of NATIVE_RUNTIME_PACKAGES) {
|
|
const modulesRoot = path.join(unpackedRoot, ...runtime.modules)
|
|
if (!existsSync(modulesRoot)) continue
|
|
const expected = `${runtime.prefix}-${runtime.platformName(platform)}-${arch}`
|
|
const foreign = new RegExp(`^${runtime.prefix}-[a-z0-9]+-(arm64|x64|ia32|loong64|riscv64)$`)
|
|
for (const entry of readdirSync(modulesRoot, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || entry.name === expected || !foreign.test(entry.name)) continue
|
|
rmSync(path.join(modulesRoot, entry.name), { recursive: true, force: true })
|
|
removed.push(runtime.modules.length ? `${runtime.modules.join('/')}/${entry.name}` : entry.name)
|
|
}
|
|
}
|
|
return removed
|
|
}
|
|
|
|
/**
|
|
* Bundled native directories under resources/connectors are named
|
|
* "<platform>-<arch>". Cross-building both macOS architectures leaves both on
|
|
* disk, but a bundle can only execute its own, so drop the foreign ones
|
|
* instead of shipping every connector twice.
|
|
*/
|
|
function pruneForeignArchConnectors(runtimeResources, platform, arch) {
|
|
if (arch === 'universal') return []
|
|
const connectorsRoot = path.join(runtimeResources, 'resources', 'connectors')
|
|
if (!existsSync(connectorsRoot)) return []
|
|
const expected = `${platform}-${arch}`
|
|
const removed = []
|
|
for (const packageEntry of readdirSync(connectorsRoot, { withFileTypes: true })) {
|
|
if (!packageEntry.isDirectory()) continue
|
|
const packageRoot = path.join(connectorsRoot, packageEntry.name)
|
|
for (const targetEntry of readdirSync(packageRoot, { withFileTypes: true })) {
|
|
if (!targetEntry.isDirectory() || targetEntry.name === expected) continue
|
|
if (!/^[a-z0-9]+-(arm64|x64|ia32)$/.test(targetEntry.name)) continue
|
|
rmSync(path.join(packageRoot, targetEntry.name), { recursive: true, force: true })
|
|
removed.push(`${packageEntry.name}/${targetEntry.name}`)
|
|
}
|
|
}
|
|
return removed
|
|
}
|
|
|
|
exports.default = async function afterPack(context) {
|
|
const runtimeResources = getRuntimeResources(context)
|
|
const arch = normalizeBuilderArch(context.arch)
|
|
validateAsarRuntimeDependencies(runtimeResources)
|
|
validateReaderSkillRuntime(runtimeResources)
|
|
validateSilkWasmRuntime(runtimeResources)
|
|
const ffmpegPath = validateFfmpegRuntime(runtimeResources, context.electronPlatformName)
|
|
validateRuntimeBinaryArchitecture(
|
|
ffmpegPath,
|
|
context.electronPlatformName,
|
|
arch,
|
|
'Bundled ffmpeg'
|
|
)
|
|
validateSherpaRuntime(runtimeResources, context.electronPlatformName, arch)
|
|
pruneIntelMacKeyTool(runtimeResources, context.electronPlatformName, arch)
|
|
pruneForeignArchConnectors(runtimeResources, context.electronPlatformName, arch)
|
|
pruneForeignArchNativeRuntimes(runtimeResources, context.electronPlatformName, arch)
|
|
|
|
if (context.electronPlatformName === 'darwin') {
|
|
execFileSync('/usr/bin/codesign', ['--force', '--sign', '-', ffmpegPath], {
|
|
stdio: 'ignore'
|
|
})
|
|
}
|
|
|
|
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.validateAsarRuntimeDependencies = validateAsarRuntimeDependencies
|
|
exports.validateReaderSkillRuntime = validateReaderSkillRuntime
|
|
exports.validateFfmpegRuntime = validateFfmpegRuntime
|
|
exports.validateSilkWasmRuntime = validateSilkWasmRuntime
|
|
exports.validateSherpaRuntime = validateSherpaRuntime
|
|
exports.pruneIntelMacKeyTool = pruneIntelMacKeyTool
|
|
exports.pruneForeignArchConnectors = pruneForeignArchConnectors
|
|
exports.pruneForeignArchNativeRuntimes = pruneForeignArchNativeRuntimes
|
|
exports.validateRuntimeBinaryArchitecture = validateRuntimeBinaryArchitecture
|