test-windows-installer.mjs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. /** Builds an isolated native payload through the production NSIS configuration and exercises its UI. */
  2. import { execFile } from 'node:child_process'
  3. import { randomUUID } from 'node:crypto'
  4. import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
  5. import { createRequire } from 'node:module'
  6. import { join } from 'node:path'
  7. import { fileURLToPath } from 'node:url'
  8. import { promisify } from 'node:util'
  9. import { createWindowsTokenSigner, installWindowsNsisBootstrapSigner, scrubWindowsSigningEnvironment } from './windows-sign.mjs'
  10. import { loadDesktopPackageEnvironment } from './desktop-package-environment.mjs'
  11. import { createPackagingRun } from './packaging-run.mjs'
  12. if (process.platform !== 'win32' || process.arch !== 'x64') {
  13. throw new Error('Installer UI checks require an interactive Windows x64 desktop')
  14. }
  15. const execute = promisify(execFile)
  16. const appRoot = fileURLToPath(new URL('..', import.meta.url))
  17. const require = createRequire(import.meta.url)
  18. const { build, Platform, Arch } = require('electron-builder')
  19. const { getMakeNsisPath } = require('app-builder-lib/out/toolsets/windows.js')
  20. const guid = randomUUID()
  21. const id = guid.replaceAll('-', '')
  22. const productName = `Harness Installer Test ${id.slice(0, 8)}`
  23. const outputRoot = join(appRoot, '.desktop-build', 'installer-tests')
  24. await mkdir(outputRoot, { recursive: true })
  25. const output = await mkdtemp(join(outputRoot, 'run-'))
  26. const payload = join(output, 'payload')
  27. await mkdir(join(payload, 'resources'), { recursive: true })
  28. const previousEnvironment = { ...process.env }
  29. const signingEnvironment = process.argv.includes('--signed') ? loadDesktopPackageEnvironment('win32') : {}
  30. const signingRun = process.argv.includes('--signed')
  31. ? createPackagingRun(join(output, 'packaging-runs'), { target: 'installer-test' }) : undefined
  32. const sign = process.argv.includes('--signed') ? createWindowsTokenSigner({
  33. certificateFile: signingEnvironment.DSH_DESKTOP_WINDOWS_CER_FILE,
  34. signTool: signingEnvironment.DSH_DESKTOP_WINDOWS_SIGNTOOL,
  35. tokenPin: signingEnvironment.DSH_DESKTOP_WINDOWS_TOKEN_PIN,
  36. keyContainer: signingEnvironment.DSH_DESKTOP_WINDOWS_KEY_CONTAINER,
  37. runDirectory: signingRun.directory,
  38. }) : undefined
  39. let succeeded = false
  40. try {
  41. Object.assign(process.env, {
  42. DSH_DESKTOP_APP_ID: `com.deepseek.harness.installertest.n${id}`,
  43. DSH_DESKTOP_TARGET_PLATFORM: 'win32', DSH_DESKTOP_TARGET_ARCH: 'x64',
  44. DSH_DESKTOP_UNSIGNED: '1', CSC_IDENTITY_AUTO_DISCOVERY: 'false', ELECTRON_BUILDER_7Z_FILTER: 'BCJ',
  45. DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN: signingEnvironment.DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN
  46. ?? 'https://harness-test.deepseek.com',
  47. ...signingRun ? { DSH_DESKTOP_PACKAGING_RUN_DIR: signingRun.directory } : {},
  48. })
  49. const { createElectronBuilderConfig } = await import('../electron-builder.config.mjs')
  50. const childOptions = { env: scrubWindowsSigningEnvironment(process.env), windowsHide: true, maxBuffer: 8 * 1024 * 1024 }
  51. await execute('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
  52. join(appRoot, 'scripts', 'prepare-windows-installer.ps1'), '-OutputDirectory', join(output, 'ui'), '-CompileProgressOnly'], childOptions)
  53. const progressTest = join(output, 'ui', 'progress-test.exe')
  54. const presentationTest = join(output, 'ui', 'presentation-test.exe')
  55. if (sign) {
  56. await sign({ path: progressTest, hash: 'sha256', isNest: false })
  57. await sign({ path: presentationTest, hash: 'sha256', isNest: false })
  58. await sign({ path: join(output, 'ui', 'window-frame.dll'), hash: 'sha256', isNest: false })
  59. installWindowsNsisBootstrapSigner({ sign })
  60. }
  61. await execute(progressTest, [], childOptions)
  62. await execute(presentationTest, [], childOptions)
  63. const payloadSource = join(output, 'payload.nsi')
  64. await writeFile(payloadSource, `Unicode true
  65. RequestExecutionLevel user
  66. ManifestDPIAware true
  67. Name "${productName}"
  68. OutFile "${join(payload, `${productName}.exe`)}"
  69. SilentInstall silent
  70. Section
  71. FileOpen $0 "$EXEDIR\\launched.txt" w
  72. FileWrite $0 "launched"
  73. FileClose $0
  74. MessageBox MB_OK "Installer test application is running."
  75. SectionEnd
  76. `)
  77. const compiler = await getMakeNsisPath()
  78. await execute(compiler.path, ['/V2', payloadSource], { ...childOptions, env: { ...childOptions.env, ...compiler.env } })
  79. if (sign) await sign({ path: join(payload, `${productName}.exe`), hash: 'sha256', isNest: false })
  80. const sourceStrings = await readFile(join(appRoot, 'installer', 'strings.nsh'), 'utf8')
  81. const config = createElectronBuilderConfig()
  82. if (sign) {
  83. config.win.forceCodeSigning = true
  84. config.win.signtoolOptions.sign = sign
  85. }
  86. for (const language of ['en_US', 'zh_CN']) {
  87. const languageOutput = join(output, language)
  88. await mkdir(languageOutput)
  89. const strings = join(languageOutput, 'strings.nsh')
  90. const languageId = language === 'en_US' ? 'ENGLISH' : 'SIMPCHINESE'
  91. await writeFile(strings, sourceStrings.split('\n').filter((line) =>
  92. !line.startsWith('LangString ') || line.includes(`\${LANG_${languageId}}`)).join('\n'))
  93. const include = join(languageOutput, 'include.nsh')
  94. await writeFile(include, `!define INSTALLER_BUILD_DIR "${join(output, 'ui')}"\n!define INSTALLER_STRINGS_FILE "${strings}"\n!include "${join(appRoot, 'scripts', 'installer.nsh')}"\n`)
  95. await build({ projectDir: appRoot, prepackaged: payload, targets: Platform.WINDOWS.createTarget(['nsis'], Arch.x64), publish: 'never',
  96. config: { ...config, productName, artifactName: 'installer-test.exe', directories: { output: languageOutput },
  97. nsis: { ...config.nsis, guid, include, installerLanguages: [language] }, beforeBuild: undefined, afterPack: undefined, afterSign: undefined, artifactBuildCompleted: undefined },
  98. })
  99. const result = await execute('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
  100. join(appRoot, 'tests', 'windows-installer-smoke.ps1'), '-Installer', join(languageOutput, 'installer-test.exe'),
  101. '-ProductName', productName, '-RegistryKey', guid, '-OutputDirectory', languageOutput], childOptions)
  102. process.stdout.write(`${language}\n${result.stdout}`)
  103. }
  104. succeeded = true
  105. } finally {
  106. signingRun?.finish(succeeded)
  107. for (const name of Object.keys(process.env)) if (!(name in previousEnvironment)) delete process.env[name]
  108. Object.assign(process.env, previousEnvironment)
  109. process.stdout.write(`Installer test artifacts: ${output}\n`)
  110. }