diff --git a/build/installer.nsh b/build/installer.nsh new file mode 100644 index 00000000..09b0e7b5 --- /dev/null +++ b/build/installer.nsh @@ -0,0 +1,63 @@ +!ifndef BUILD_UNINSTALLER + !ifndef ONE_CLICK + !include "LogicLib.nsh" + !include "nsDialogs.nsh" + + Var DshDirectoryPage + Var DshDirectoryEdit + Var DshDirectoryNormalizationActive + + ; MUI invokes this after the assisted installer's directory page is ready. + ; Normalize a selected drive root immediately so the page does not reject it + ; before electron-builder's later install-time sanitization can run. + !define MUI_PAGE_CUSTOMFUNCTION_SHOW DshDirectoryPageShow + + Function DshDirectoryPageShow + FindWindow $DshDirectoryPage "#32770" "" $HWNDPARENT + GetDlgItem $DshDirectoryEdit $DshDirectoryPage 1019 + ${NSD_OnChange} $DshDirectoryEdit DshDirectoryChanged + Call DshNormalizeDriveRoot + FunctionEnd + + Function DshDirectoryChanged + Pop $0 + Call DshNormalizeDriveRoot + FunctionEnd + + Function DshNormalizeDriveRoot + ${If} $DshDirectoryNormalizationActive == "1" + Return + ${EndIf} + + ${NSD_GetText} $DshDirectoryEdit $0 + StrLen $1 $0 + + ; Accept both forms produced by typing or the Windows folder picker: + ; "D:" and "D:\". Any non-root directory is left untouched. + ${If} $1 == 2 + StrCpy $2 $0 1 1 + ${If} $2 != ":" + Return + ${EndIf} + StrCpy $3 "$0\${APP_FILENAME}" + ${ElseIf} $1 == 3 + StrCpy $2 $0 1 1 + ${If} $2 != ":" + Return + ${EndIf} + StrCpy $2 $0 1 2 + ${If} $2 != "\" + Return + ${EndIf} + StrCpy $3 "$0${APP_FILENAME}" + ${Else} + Return + ${EndIf} + + StrCpy $DshDirectoryNormalizationActive "1" + StrCpy $INSTDIR $3 + ${NSD_SetText} $DshDirectoryEdit $3 + StrCpy $DshDirectoryNormalizationActive "0" + FunctionEnd + !endif +!endif diff --git a/package.json b/package.json index 66b1deba..55760b49 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ }, "nsis": { "artifactName": "dsh-desktop-windows-${arch}-setup.${ext}", + "include": "build/installer.nsh", "oneClick": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, diff --git a/test/release.test.ts b/test/release.test.ts index 2d449cfd..98ce71cf 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -50,7 +50,7 @@ describe('GitHub release contract', () => { artifactName: string extraResources: Array<{ from: string; to: string }> win: { target: Array<{ target: string; arch: string[] }> } - nsis: { artifactName: string } + nsis: { artifactName: string; include: string } portable?: unknown } } @@ -71,10 +71,24 @@ describe('GitHub release contract', () => { expect(packageJson.build.nsis.artifactName).toBe( 'dsh-desktop-windows-${arch}-setup.${ext}' ) + expect(packageJson.build.nsis.include).toBe('build/installer.nsh') expect(packageJson.build.win.target).toEqual([{ target: 'nsis', arch: ['x64'] }]) expect(packageJson.build.portable).toBeUndefined() }) + it('turns a selected Windows drive root into an application directory', async () => { + const installer = await readFile( + path.join(projectRoot, 'build', 'installer.nsh'), + 'utf8' + ) + + expect(installer).toContain('!define MUI_PAGE_CUSTOMFUNCTION_SHOW DshDirectoryPageShow') + expect(installer).toContain('${NSD_OnChange} $DshDirectoryEdit DshDirectoryChanged') + expect(installer).toContain('StrCpy $3 "$0\\${APP_FILENAME}"') + expect(installer).toContain('StrCpy $3 "$0${APP_FILENAME}"') + expect(installer).toContain('${NSD_SetText} $DshDirectoryEdit $3') + }) + it('shows a packaged startup surface and pins the Electron directory picker surface', async () => { const main = await readFile(path.join(projectRoot, 'src', 'main', 'index.ts'), 'utf8') const splash = await readFile(path.join(projectRoot, 'build', 'splash.html'), 'utf8')