windows-directory-installer.mjs 4.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. /** Adapt the pinned NSIS template to stage and rename complete application directories. */
  2. import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
  3. import { createRequire } from 'node:module'
  4. import { dirname, join } from 'node:path'
  5. const require = createRequire(import.meta.url)
  6. const templates = join(dirname(require.resolve('app-builder-lib/package.json')), 'templates/nsis')
  7. const patched = Symbol.for('@deepseek-ai/dsh-desktop/directory-installer')
  8. function replaceOnce(source, before, after) {
  9. if (source.split(before).length !== 2) throw new Error(`Desktop NSIS template changed: ${before}`)
  10. return source.replace(before, after)
  11. }
  12. /**
  13. * Preserve upstream registration and uninstall UI while replacing payload installation.
  14. * @param {string} source - Pinned electron-builder installSection.nsh contents.
  15. * @returns {string} Section with staging before shutdown and directory promotion before registration.
  16. */
  17. export function directoryInstallSection(source) {
  18. let result = source.replaceAll('\r\n', '\n')
  19. result = replaceOnce(result, '!include installer.nsh', `!include installer.nsh
  20. !macroundef extractUsing7za
  21. !macro extractUsing7za FILE
  22. !insertmacro dshExtractPayload "\${FILE}"
  23. !macroend
  24. !macroundef uninstallOldVersion
  25. !macro uninstallOldVersion ROOT_KEY
  26. !insertmacro readReg $R4 "\${ROOT_KEY}" "\${INSTALL_REGISTRY_KEY}" InstallLocation
  27. \${If} $R4 == $INSTDIR
  28. StrCpy $R0 0
  29. ClearErrors
  30. \${Else}
  31. Push "\${ROOT_KEY}"
  32. Call uninstallOldVersion
  33. \${EndIf}
  34. !macroend`)
  35. result = replaceOnce(result, '!insertmacro setLinkVars', `!insertmacro setLinkVars
  36. !insertmacro dshStageApplication`)
  37. result = replaceOnce(result, '!insertmacro installApplicationFiles', 'Call dshPromoteDirectories\nIfErrors 0 +4\n SetErrorLevel 2\n MessageBox MB_OK|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDOK\n Quit')
  38. result = replaceOnce(result, '!ifdef UNINSTALLER_ICON\n File /oname=uninstallerIcon.ico "${UNINSTALLER_ICON}"\n!endif\n', '')
  39. // The staging macro uses the upstream installer macro, including its signed uninstaller.
  40. return result
  41. }
  42. /**
  43. * Clean staged directories on upstream Quit paths, including silent installers.
  44. * @param {string} source - Pinned NSIS helper source.
  45. * @returns {string} Helper with cleanup before installer exits.
  46. */
  47. export function directoryInstallerExits(source) {
  48. return source.replaceAll(/^(\s*)Quit\s*$/gm, '$1!ifndef BUILD_UNINSTALLER\n$1Call dshCleanupDirectories\n$1!endif\n$1Quit')
  49. }
  50. /** Install the build-only NSIS adapter without replacing electron-builder's signed bootstrap path. */
  51. export function installWindowsDirectoryInstaller() {
  52. // The package entry initializes the platform classes before their mutually dependent leaves.
  53. require('app-builder-lib')
  54. const { NsisTarget } = require('app-builder-lib/out/targets/nsis/NsisTarget.js')
  55. const { getPath7za } = require('app-builder-lib/out/toolsets/7zip.js')
  56. const prototype = NsisTarget.prototype
  57. if (prototype[patched]) return
  58. prototype[patched] = true
  59. const compute = prototype.computeFinalScript
  60. prototype.computeFinalScript = async function (source, ...args) {
  61. const directory = join(this.outDir, '.nsis-directory-installer')
  62. await mkdir(directory, { recursive: true })
  63. const section = join(directory, 'installSection.nsh')
  64. await writeFile(section, directoryInstallSection(await readFile(join(templates, 'installSection.nsh'), 'utf8')))
  65. const sourceTool = await getPath7za()
  66. const tool = join(directory, '7za.exe')
  67. await copyFile(sourceTool, tool)
  68. await this.packager.signIf(tool)
  69. let adapted = replaceOnce(source, '!include "installSection.nsh"', `!include "${section}"`)
  70. for (const helper of ['allowOnlyOneInstallerInstance.nsh', 'installUtil.nsh']) {
  71. const path = join(directory, helper)
  72. await writeFile(path, directoryInstallerExits(await readFile(join(templates, 'include', helper), 'utf8')))
  73. adapted = replaceOnce(adapted, `!include "${helper}"`, `!include "${path}"`)
  74. }
  75. const uninstaller = join(directory, 'uninstaller.nsh')
  76. await writeFile(uninstaller, replaceOnce(await readFile(join(templates, 'uninstaller.nsh'), 'utf8'),
  77. 'RMDir /r $INSTDIR', 'RMDir /r "\\\\?\\$INSTDIR"'))
  78. adapted = replaceOnce(adapted, '!include "uninstaller.nsh"', `!include "${uninstaller}"`)
  79. return `!define DSH_SEVENZIP_PATH "${tool}"\n!define DSH_SEVENZIP_LICENSE_DIR "${dirname(dirname(sourceTool))}"\n${await compute.call(this, adapted, ...args)}`
  80. }
  81. }