windows-desktop-release.mjs 4.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. import { createHash } from 'node:crypto'
  2. import { createReadStream } from 'node:fs'
  3. import { readFile, writeFile, stat } from 'node:fs/promises'
  4. import { basename, join, resolve } from 'node:path'
  5. import { pathToFileURL } from 'node:url'
  6. import { load } from 'js-yaml'
  7. async function hash(path, algorithm, encoding) {
  8. const result = createHash(algorithm)
  9. for await (const bytes of createReadStream(path)) result.update(bytes)
  10. return result.digest(encoding)
  11. }
  12. export function windowsSigningOptions(env = process.env) {
  13. if (env.YAOYAO_WINDOWS_SIGNED !== '1') return { forceCodeSigning: false, win: { signAndEditExecutable: true } }
  14. if (!env.CSC_LINK || !env.YAOYAO_WINDOWS_PUBLISHER?.trim()) throw new Error('正式 Windows 包需要 CSC_LINK、CSC_KEY_PASSWORD(如适用)和 YAOYAO_WINDOWS_PUBLISHER')
  15. return { forceCodeSigning: true, win: { publisherName: env.YAOYAO_WINDOWS_PUBLISHER.trim(), verifyUpdateCodeSignature: true } }
  16. }
  17. export async function verifyWindowsArtifacts(directory, version) {
  18. const name = `Yaoyao-${version}-win-x64-setup.exe`
  19. const metadata = load(await readFile(join(directory, 'latest.yml'), 'utf8'))
  20. if (metadata?.version !== version || metadata.files?.length !== 1 || metadata.files[0].url !== name) throw new Error('Windows 更新清单与安装包不匹配')
  21. for (const file of metadata.files) {
  22. if (basename(file.url) !== file.url) throw new Error('安装包路径无效')
  23. const path = join(directory, file.url), info = await stat(path)
  24. if (!info.isFile() || info.size === 0 || info.size !== file.size || file.sha512 !== await hash(path, 'sha512', 'base64')) throw new Error('Windows 安装包大小或 SHA-512 不匹配')
  25. const exe = await readFile(path)
  26. if (exe.toString('ascii', 0, 2) !== 'MZ') throw new Error('Windows 安装包不是 PE 文件')
  27. }
  28. if (!(await stat(join(directory, `${name}.blockmap`))).size) throw new Error('Windows 差分更新文件为空')
  29. const names = [name, `${name}.blockmap`, 'latest.yml']
  30. await writeFile(join(directory, 'SHA256SUMS-win-x64.txt'), (await Promise.all(names.map(async name => `${await hash(join(directory, name), 'sha256', 'hex')} ${name}`))).join('\n') + '\n')
  31. return names
  32. }
  33. export async function packageWindowsDesktop() {
  34. if (process.platform !== 'win32' || process.arch !== 'x64') throw new Error('请在 Windows x64 CI 或 Windows 电脑打包')
  35. const root = resolve(import.meta.dirname, '..'), directory = join(root, 'desktop-release', 'windows-x64')
  36. const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'))
  37. const { build, Platform, Arch } = await import('electron-builder')
  38. const signing = windowsSigningOptions()
  39. if (process.env.YAOYAO_WINDOWS_SIGNED !== '1') process.env.CSC_IDENTITY_AUTO_DISCOVERY = 'false'
  40. await build({ projectDir: root, targets: Platform.WINDOWS.createTarget(['nsis'], Arch.x64), publish: 'never',
  41. config: { ...signing, directories: { output: directory }, electronUpdaterCompatibility: '>=2.16' } })
  42. await verifyWindowsArtifacts(directory, version)
  43. const feed = load(await readFile(join(directory, 'win-unpacked/resources/app-update.yml'), 'utf8'))
  44. if (feed.provider !== 'github' || feed.owner !== 'Lsamien' || feed.repo !== 'yaoyao') throw new Error('Windows 更新源配置无效')
  45. const info = JSON.parse(await readFile(join(root, 'build-info.json'), 'utf8'))
  46. await writeFile(join(directory, 'BUILD-INFO.json'), JSON.stringify({ version, platform: 'win32', arch: 'x64',
  47. signing: process.env.YAOYAO_WINDOWS_SIGNED === '1' ? 'signed' : 'unsigned-test', ...info }, null, 2) + '\n')
  48. await writeFile(join(directory, 'README.txt'), '夭夭 Windows x64 客户端\n' +
  49. (process.env.YAOYAO_WINDOWS_SIGNED === '1' ? '正式签名构建。\n' : '未签名测试包,Windows 可能提示未知发布者。\n') +
  50. '支持 Windows 10/11 x64;连接已有夭夭服务器。本机无需 Node、Python、Hermes。\n' +
  51. 'Windows 电脑操作需要服务器支持;更新清单随本次构建交付,尚未上传正式更新源。\n' +
  52. '完整验收步骤见 docs/windows-client.md。CI 构建不代表真实桌面控制已完成验收。\n')
  53. console.log(`Windows 安装包及更新清单已校验:${directory}(尚未发布)`)
  54. }
  55. if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) await packageWindowsDesktop()