Files
ElyOffice/packaging/windows/installer.nsi
T
ZacharyZhang-NYandClaude Opus 5.5 23735aee3c R6.11 round 1 fix: the installer removes only what it installed
Reinstalling deleted files Ely never installed, and a busy file left a
half-removed install. The installer now writes the top-level names it
installs (.ely-files, from packaging/windows/files.sh); upgrades and
uninstall remove only those, and releases before the list only in the
folder the registry names. It checks that every listed file can be
written, moves them all into .ely-old, and on any refusal puts them
back and exits 2. A file that cannot be written fails the install,
which removes what it created and restores the old release.
tests/scripts/windows-installer.sh covers each case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:59:50 -04:00

276 lines
7.7 KiB
NSIS

; Per-user installer; pass each define the checks below name.
Unicode true
!include "MUI2.nsh"
!ifndef APP_NAME
!error "pass -DAPP_NAME"
!endif
!ifndef APP_SLUG
!error "pass -DAPP_SLUG"
!endif
!ifndef VERSION
!error "pass -DVERSION"
!endif
!ifndef STAGE
!error "pass -DSTAGE, a folder from packaging/stage.sh"
!endif
!ifndef ICON
!error "pass -DICON"
!endif
!ifndef TYPES
!error "pass -DTYPES, from packaging/windows/types.sh"
!endif
!include "${TYPES}"
!ifndef FILES
!error "pass -DFILES, from packaging/windows/files.sh"
!endif
!include "${FILES}"
!ifndef OUTFILE
!define OUTFILE "${APP_SLUG}-${VERSION}-setup.exe"
!endif
!define UNINSTALL_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Ely Office ${APP_NAME}"
!define MUI_ICON "${ICON}"
!define MUI_UNICON "${ICON}"
Name "${APP_NAME}"
OutFile "${OUTFILE}"
RequestExecutionLevel user
InstallDir "$LOCALAPPDATA\Programs\Ely Office\${APP_NAME}"
InstallDirRegKey HKCU "Software\Ely Office\${APP_NAME}" "InstallDir"
SetCompressor /SOLID lzma
AllowSkipFiles off
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "SimpChinese"
!insertmacro MUI_LANGUAGE "English"
LangString DeleteData ${LANG_SIMPCHINESE} "同时删除 ${APP_NAME} 的缓存、账号与本地历史吗?你的文档不受影响。"
LangString DeleteData ${LANG_ENGLISH} "Also delete the cache, accounts and local history of ${APP_NAME}? Your documents stay."
LangString Busy ${LANG_SIMPCHINESE} "${APP_NAME} 的文件正被占用,未作任何改动。请关闭 ${APP_NAME} 后重试:"
LangString Busy ${LANG_ENGLISH} "A file of ${APP_NAME} is in use; nothing was changed. Close ${APP_NAME} and try again:"
LangString Stuck ${LANG_SIMPCHINESE} "旧版的文件放回原处失败,留在:"
LangString Stuck ${LANG_ENGLISH} "Could not put the old files back; they are in:"
LangString Left ${LANG_SIMPCHINESE} "新版已装好,旧版的文件删不掉,留在:"
LangString Left ${LANG_ENGLISH} "The new version is installed; the old files could not be deleted from:"
LangString NoList ${LANG_SIMPCHINESE} "找不到安装清单,未删除任何文件:"
LangString NoList ${LANG_ENGLISH} "No install list found; nothing was deleted:"
!macro ElyLegacy ID PATTERN
FindFirst $R3 $R4 "$INSTDIR\${PATTERN}"
ely_legacy_${ID}:
StrCmp $R4 "" ely_legacy_end_${ID}
FileWrite $R2 "$R4$\r$\n"
FindNext $R3 $R4
Goto ely_legacy_${ID}
ely_legacy_end_${ID}:
FindClose $R3
!macroend
; Sets $R0 to what an earlier install owns.
Function ElyOwned
StrCpy $R0 ""
InitPluginsDir
; A copy, as this install rewrites the list.
IfFileExists "$INSTDIR\.ely-files" 0 unlisted
StrCpy $R0 "$PLUGINSDIR\owned.txt"
StrCpy $R7 "listed"
CopyFiles /SILENT "$INSTDIR\.ely-files" "$R0"
Goto done
unlisted:
; Older releases: only the folder our key names.
ReadRegStr $R1 HKCU "Software\Ely Office\${APP_NAME}" "InstallDir"
StrCmp $R1 "$INSTDIR" 0 done
IfFileExists "$INSTDIR\${APP_SLUG}.exe" 0 done
StrCpy $R0 "$PLUGINSDIR\legacy.txt"
FileOpen $R2 "$R0" w
FileWrite $R2 "libreoffice$\r$\ntessdata$\r$\ngit$\r$\ncontent$\r$\nlocales$\r$\nlicenses$\r$\n"
FileWrite $R2 "${APP_SLUG}.ico$\r$\n"
!insertmacro ElyLegacy exe "*.exe"
!insertmacro ElyLegacy dll "*.dll"
!insertmacro ElyLegacy pak "*.pak"
!insertmacro ElyLegacy bin "*.bin"
!insertmacro ElyLegacy dat "*.dat"
!insertmacro ElyLegacy json "*.json"
FileClose $R2
done:
FunctionEnd
!macro ElyAside UN
; Strips the line end from $R3.
Function ${UN}ElyTrim
again:
StrCpy $R6 "$R3" 1 -1
StrCmp $R6 "$\r" cut
StrCmp $R6 "$\n" cut
Return
cut:
StrCpy $R3 "$R3" -1
Goto again
FunctionEnd
; Moves $R0's names into .ely-old; $R5 names a blocker.
Function ${UN}ElyAside
StrCpy $R5 ""
StrCpy $R9 "$INSTDIR\.ely-old"
RMDir /r "$R9"
IfFileExists "$R9" 0 +3
StrCpy $R5 "$R9"
Return
CreateDirectory "$R9"
ClearErrors
FileOpen $R2 "$R0" r
IfErrors 0 check
StrCpy $R5 "$R0"
Return
check:
; A running or locked file refuses writing.
ClearErrors
FileRead $R2 $R3
IfErrors checked
Call ${UN}ElyTrim
StrCmp $R3 "" check
IfFileExists "$INSTDIR\$R3\*.*" check
IfFileExists "$INSTDIR\$R3" 0 check
ClearErrors
FileOpen $R4 "$INSTDIR\$R3" a
IfErrors refused
FileClose $R4
Goto check
checked:
FileSeek $R2 0 SET
move:
ClearErrors
FileRead $R2 $R3
IfErrors moved
Call ${UN}ElyTrim
StrCmp $R3 "" move
IfFileExists "$INSTDIR\$R3" 0 move
ClearErrors
Rename "$INSTDIR\$R3" "$R9\$R3"
IfErrors 0 move
StrCpy $R5 "$INSTDIR\$R3"
FileClose $R2
Call ${UN}ElyBack
Return
moved:
FileClose $R2
Return
refused:
StrCpy $R5 "$INSTDIR\$R3"
FileClose $R2
RMDir "$R9"
FunctionEnd
; Puts every name in $R0 back from .ely-old.
Function ${UN}ElyBack
FileOpen $R2 "$R0" r
back:
ClearErrors
FileRead $R2 $R3
IfErrors done
Call ${UN}ElyTrim
StrCmp $R3 "" back
IfFileExists "$R9\$R3" 0 back
Rename "$R9\$R3" "$INSTDIR\$R3"
Goto back
done:
FileClose $R2
RMDir "$R9"
IfFileExists "$R9" 0 +2
MessageBox MB_ICONSTOP "$(Stuck)$\r$\n$R9" /SD IDOK
FunctionEnd
!macroend
!insertmacro ElyAside ""
!insertmacro ElyAside "un."
; Removes what this install created, then restores the old.
Function .onInstFailed
StrCmp $R8 "aside" 0 done
FileOpen $R2 "$PLUGINSDIR\created.txt" r
drop:
ClearErrors
FileRead $R2 $R3
IfErrors dropped
Call ElyTrim
StrCmp $R3 "" drop
RMDir /r "$INSTDIR\$R3"
Delete "$INSTDIR\$R3"
Goto drop
dropped:
FileClose $R2
Call ElyBack
Delete "$INSTDIR\.ely-files"
StrCmp $R7 "listed" 0 done
CopyFiles /SILENT "$R0" "$INSTDIR\.ely-files"
done:
FunctionEnd
Section "Install"
; An earlier install moves aside whole, or nothing changes.
Call ElyOwned
StrCmp $R0 "" fresh
Call ElyAside
StrCmp $R5 "" 0 +2
StrCpy $R8 "aside"
StrCmp $R5 "" fresh
MessageBox MB_ICONSTOP "$(Busy)$\r$\n$R5" /SD IDOK
SetErrorLevel 2
Quit
fresh:
InitPluginsDir
FileOpen $R2 "$PLUGINSDIR\created.txt" w
!insertmacro ElyNoteNew
FileClose $R2
; A file that cannot be written fails the install.
ClearErrors
SetOutPath "$INSTDIR"
File /r "${STAGE}\*.*"
File "/oname=${APP_SLUG}.ico" "${ICON}"
WriteUninstaller "$INSTDIR\uninstall.exe"
!insertmacro ElyWriteOwned
IfErrors 0 +2
Abort
RMDir /r "$INSTDIR\.ely-old"
IfFileExists "$INSTDIR\.ely-old" 0 +2
MessageBox MB_ICONEXCLAMATION "$(Left)$\r$\n$INSTDIR\.ely-old" /SD IDOK
CreateShortcut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${APP_SLUG}.exe" "" "$INSTDIR\${APP_SLUG}.ico"
WriteRegStr HKCU "Software\Ely Office\${APP_NAME}" "InstallDir" "$INSTDIR"
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayName" "${APP_NAME}"
WriteRegStr HKCU "${UNINSTALL_KEY}" "Publisher" "ZacharyZhang"
WriteRegStr HKCU "${UNINSTALL_KEY}" "URLInfoAbout" "https://zacharyzhang.com"
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayVersion" "${VERSION}"
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\${APP_SLUG}.ico"
WriteRegStr HKCU "${UNINSTALL_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegDWORD HKCU "${UNINSTALL_KEY}" "NoModify" 1
!insertmacro ElyTypesInstall
SectionEnd
Section "Uninstall"
StrCpy $R0 "$INSTDIR\.ely-files"
IfFileExists "$R0" listed
MessageBox MB_ICONSTOP "$(NoList)$\r$\n$INSTDIR" /SD IDOK
SetErrorLevel 2
Quit
listed:
Call un.ElyAside
StrCmp $R5 "" gone
MessageBox MB_ICONSTOP "$(Busy)$\r$\n$R5" /SD IDOK
SetErrorLevel 2
Quit
gone:
RMDir /r "$INSTDIR\.ely-old"
Delete "$R0"
RMDir "$INSTDIR"
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
DeleteRegKey HKCU "Software\Ely Office\${APP_NAME}"
DeleteRegKey HKCU "${UNINSTALL_KEY}"
!insertmacro ElyTypesUninstall
MessageBox MB_YESNO|MB_DEFBUTTON2 "$(DeleteData)" /SD IDNO IDNO done
RMDir /r "$LOCALAPPDATA\ElyOffice\${APP_SLUG}"
done:
SectionEnd