windows-desktop-release.mjs 4.6 KB

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