test-windows-installer.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  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, 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. if (process.platform !== 'win32' || process.arch !== 'x64') {
  10. throw new Error('Installer UI checks require an interactive Windows x64 desktop')
  11. }
  12. const execute = promisify(execFile)
  13. const appRoot = fileURLToPath(new URL('..', import.meta.url))
  14. const require = createRequire(import.meta.url)
  15. const { build, Platform, Arch } = require('electron-builder')
  16. const { getMakeNsisPath } = require('app-builder-lib/out/toolsets/windows.js')
  17. const guid = randomUUID()
  18. const id = guid.replaceAll('-', '')
  19. const productName = `Harness Installer Test ${id.slice(0, 8)}`
  20. const outputRoot = join(appRoot, '.desktop-build', 'installer-tests')
  21. await mkdir(outputRoot, { recursive: true })
  22. const output = await mkdtemp(join(outputRoot, 'run-'))
  23. const payload = join(output, 'payload')
  24. await mkdir(join(payload, 'resources'), { recursive: true })
  25. const previousEnvironment = { ...process.env }
  26. try {
  27. Object.assign(process.env, {
  28. DSH_DESKTOP_APP_ID: `com.deepseek.harness.installertest.n${id}`,
  29. DSH_DESKTOP_TARGET_PLATFORM: 'win32', DSH_DESKTOP_TARGET_ARCH: 'x64',
  30. DSH_DESKTOP_UNSIGNED: '1', CSC_IDENTITY_AUTO_DISCOVERY: 'false', ELECTRON_BUILDER_7Z_FILTER: 'BCJ',
  31. })
  32. const { createElectronBuilderConfig } = await import('../electron-builder.config.mjs')
  33. const { scrubWindowsSigningEnvironment } = await import('./windows-sign.mjs')
  34. const childOptions = { env: scrubWindowsSigningEnvironment(process.env), windowsHide: true, maxBuffer: 8 * 1024 * 1024 }
  35. await execute('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
  36. join(appRoot, 'scripts', 'prepare-windows-installer.ps1'), '-OutputDirectory', join(output, 'ui'), '-TestProgress'], childOptions)
  37. const payloadSource = join(output, 'payload.nsi')
  38. await writeFile(payloadSource, `Unicode true
  39. RequestExecutionLevel user
  40. ManifestDPIAware true
  41. Name "${productName}"
  42. OutFile "${join(payload, `${productName}.exe`)}"
  43. SilentInstall silent
  44. Section
  45. FileOpen $0 "$EXEDIR\\launched.txt" w
  46. FileWrite $0 "launched"
  47. FileClose $0
  48. MessageBox MB_OK "Installer test application is running."
  49. SectionEnd
  50. `)
  51. const compiler = await getMakeNsisPath()
  52. await execute(compiler.path, ['/V2', payloadSource], { ...childOptions, env: { ...childOptions.env, ...compiler.env } })
  53. const include = join(output, 'include.nsh')
  54. await writeFile(include, `!define INSTALLER_BUILD_DIR "${join(output, 'ui')}"\n!include "${join(appRoot, 'scripts', 'installer.nsh')}"\n`)
  55. const config = createElectronBuilderConfig()
  56. await build({ projectDir: appRoot, prepackaged: payload, targets: Platform.WINDOWS.createTarget(['nsis'], Arch.x64), publish: 'never',
  57. config: { ...config, productName, artifactName: 'installer-test.exe', directories: { output },
  58. nsis: { ...config.nsis, guid, include }, beforeBuild: undefined, afterPack: undefined, afterSign: undefined, artifactBuildCompleted: undefined },
  59. })
  60. const result = await execute('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
  61. join(appRoot, 'tests', 'windows-installer-smoke.ps1'), '-Installer', join(output, 'installer-test.exe'),
  62. '-ProductName', productName, '-RegistryKey', guid, '-OutputDirectory', output], childOptions)
  63. process.stdout.write(result.stdout)
  64. } finally {
  65. for (const name of Object.keys(process.env)) if (!(name in previousEnvironment)) delete process.env[name]
  66. Object.assign(process.env, previousEnvironment)
  67. process.stdout.write(`Installer test artifacts: ${output}\n`)
  68. }