Files
ElyOffice/packaging/windows/installer.nsi
T
ZacharyZhang-NYandClaude Opus 5.5 53630ce0be
release / macos (push) Failing after 1s
release / windows (push) Successful in 11m29s
release / linux (push) Successful in 7m29s
R6.11 round 3 fix: numbered backups, checked install list
Backups under .ely-old are numbered in list order, so paths near the
Windows limit still upgrade and uninstall. Uninstall checks the list
file first and puts everything back if it cannot delete it. Five
comments shortened. Round 3 was the last; ticked without review.

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

400 lines
10 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 LIST
!error "pass -DLIST, from packaging/windows/files.sh"
!endif
!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:"
LangString Taken ${LANG_SIMPCHINESE} "安装位置已有不属于 ${APP_NAME} 的文件,未作任何改动:"
LangString Taken ${LANG_ENGLISH} "A file ${APP_NAME} did not install is in the way; nothing was changed:"
; Lists a legacy folder's files and folders to $R2.
Function ElyWalk
Push "|"
Push $R1
walk:
Pop $R1
StrCmp $R1 "|" done
FileWrite $R2 "$R1\$\r$\n"
FindFirst $R3 $R4 "$INSTDIR\$R1\*"
entry:
StrCmp $R4 "" closed
StrCmp $R4 "." skip
StrCmp $R4 ".." skip
IfFileExists "$INSTDIR\$R1\$R4\*.*" 0 file
Push "$R1\$R4"
Goto skip
file:
FileWrite $R2 "$R1\$R4$\r$\n"
skip:
FindNext $R3 $R4
Goto entry
closed:
FindClose $R3
Goto walk
done:
FunctionEnd
!macro ElyLegacyDir NAME
IfFileExists "$INSTDIR\${NAME}\*.*" 0 +3
StrCpy $R1 "${NAME}"
Call ElyWalk
!macroend
!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 the list an earlier install owns.
Function ElyOwned
StrCpy $R0 ""
StrCpy $R7 ""
; A copy, as this install rewrites the list.
IfFileExists "$INSTDIR\.ely-installed" 0 unlisted
StrCpy $R0 "$PLUGINSDIR\owned.txt"
StrCpy $R7 "listed"
CopyFiles /SILENT "$INSTDIR\.ely-installed" "$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
!insertmacro ElyLegacyDir libreoffice
!insertmacro ElyLegacyDir tessdata
!insertmacro ElyLegacyDir git
!insertmacro ElyLegacyDir content
!insertmacro ElyLegacyDir locales
!insertmacro ElyLegacyDir licenses
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
; Next line of $R2: path $R3, kind $R6.
Function ${UN}ElyNext
next:
ClearErrors
FileRead $R2 $R3
IfErrors 0 trim
StrCpy $R6 ""
Return
trim:
StrCpy $R6 "$R3" 1 -1
StrCmp $R6 "$\r" cut
StrCmp $R6 "$\n" cut
StrCmp $R3 "" next
StrCmp $R6 "\" 0 +4
StrCpy $R3 "$R3" -1
StrCpy $R6 "d"
Return
StrCpy $R6 "f"
Return
cut:
StrCpy $R3 "$R3" -1
Goto trim
FunctionEnd
; Removes the empty folders $R0 lists under $R1.
Function ${UN}ElyPrune
pass:
StrCpy $8 ""
FileOpen $R2 "$R0" r
next:
Call ${UN}ElyNext
StrCmp $R6 "" passed
StrCmp $R6 "f" next
IfFileExists "$R1\$R3\*.*" 0 next
ClearErrors
RMDir "$R1\$R3"
IfErrors next
StrCpy $8 "again"
Goto next
passed:
FileClose $R2
StrCmp $8 "again" pass
FunctionEnd
; Moves $R0's files to numbered .ely-old entries; $R5: blocker.
Function ${UN}ElyAside
StrCpy $R5 ""
StrCpy $R9 "$INSTDIR\.ely-old"
RMDir /r "$R9"
IfFileExists "$R9" 0 +3
StrCpy $R5 "$R9"
Return
ClearErrors
FileOpen $R2 "$R0" r
IfErrors 0 check
StrCpy $R5 "$R0"
Return
check:
; Every file first: running or locked ones refuse writing.
Call ${UN}ElyNext
StrCmp $R6 "" checked
StrCmp $R6 "d" check
IfFileExists "$INSTDIR\$R3\*.*" check
IfFileExists "$INSTDIR\$R3" 0 check
ClearErrors
FileOpen $R4 "$INSTDIR\$R3" a
IfErrors refused
FileClose $R4
Goto check
checked:
CreateDirectory "$R9"
FileSeek $R2 0 SET
; Numbered names keep backup paths short.
StrCpy $9 0
move:
Call ${UN}ElyNext
StrCmp $R6 "" moved
StrCmp $R6 "d" move
IntOp $9 $9 + 1
IfFileExists "$INSTDIR\$R3\*.*" move
IfFileExists "$INSTDIR\$R3" 0 move
ClearErrors
Rename "$INSTDIR\$R3" "$R9\$9"
IfErrors 0 move
StrCpy $R5 "$INSTDIR\$R3"
FileClose $R2
Call ${UN}ElyBack
Return
moved:
FileClose $R2
Return
refused:
StrCpy $R5 "$INSTDIR\$R3"
FileClose $R2
FunctionEnd
; Puts $R0's files back from .ely-old.
Function ${UN}ElyBack
FileOpen $R2 "$R0" r
StrCpy $9 0
back:
Call ${UN}ElyNext
StrCmp $R6 "" done
StrCmp $R6 "d" back
IntOp $9 $9 + 1
IfFileExists "$R9\$9" 0 back
Rename "$R9\$9" "$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."
; Sets $R5 to a path in the way.
Function ElyClear
StrCpy $R5 ""
FileOpen $R2 "$PLUGINSDIR\new.txt" r
FileOpen $7 "$PLUGINSDIR\made.txt" w
next:
Call ElyNext
StrCmp $R6 "" done
StrCmp $R6 "d" folder
IfFileExists "$INSTDIR\$R3" taken next
folder:
IfFileExists "$INSTDIR\$R3\*.*" next
IfFileExists "$INSTDIR\$R3" taken
FileWrite $7 "$R3\$\r$\n"
Goto next
taken:
StrCpy $R5 "$INSTDIR\$R3"
done:
FileClose $7
FileClose $R2
FunctionEnd
; Removes what this install wrote, then restores the old.
Function .onInstFailed
StrCmp $R8 "copying" 0 done
FileOpen $R2 "$PLUGINSDIR\new.txt" r
drop:
Call ElyNext
StrCmp $R6 "" dropped
StrCmp $R6 "d" drop
Delete "$INSTDIR\$R3"
Goto drop
dropped:
FileClose $R2
Delete "$INSTDIR\.ely-installed"
Push $R0
StrCpy $R0 "$PLUGINSDIR\made.txt"
StrCpy $R1 "$INSTDIR"
Call ElyPrune
Pop $R0
StrCmp $R0 "" done
Call ElyBack
StrCmp $R7 "listed" 0 done
CopyFiles /SILENT "$R0" "$INSTDIR\.ely-installed"
done:
FunctionEnd
Section "Install"
InitPluginsDir
SetOutPath "$PLUGINSDIR"
File "/oname=new.txt" "${LIST}"
; An earlier install moves aside whole, or nothing changes.
Call ElyOwned
StrCmp $R0 "" clear
Call ElyAside
StrCmp $R5 "" clear
MessageBox MB_ICONSTOP "$(Busy)$\r$\n$R5" /SD IDOK
SetErrorLevel 2
Quit
clear:
; Never write over a file this app did not install.
Call ElyClear
StrCmp $R5 "" copy
StrCmp $R0 "" +2
Call ElyBack
MessageBox MB_ICONSTOP "$(Taken)$\r$\n$R5" /SD IDOK
SetErrorLevel 2
Quit
copy:
StrCpy $R8 "copying"
; 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"
CopyFiles /SILENT "$PLUGINSDIR\new.txt" "$INSTDIR\.ely-installed"
IfErrors 0 +2
Abort
StrCpy $R8 ""
; Old folders the new release leaves empty.
StrCmp $R0 "" +3
StrCpy $R1 "$INSTDIR"
Call ElyPrune
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 $R5 "$INSTDIR\.ely-installed"
IfFileExists "$R5" listed
MessageBox MB_ICONSTOP "$(NoList)$\r$\n$INSTDIR" /SD IDOK
SetErrorLevel 2
Quit
listed:
; The list is checked too, before anything moves.
ClearErrors
FileOpen $R4 "$R5" a
IfErrors busy
FileClose $R4
InitPluginsDir
StrCpy $R0 "$PLUGINSDIR\owned.txt"
CopyFiles /SILENT "$R5" "$R0"
Call un.ElyAside
StrCmp $R5 "" gone busy
gone:
ClearErrors
Delete "$INSTDIR\.ely-installed"
IfErrors 0 deleted
Call un.ElyBack
StrCpy $R5 "$INSTDIR\.ely-installed"
busy:
MessageBox MB_ICONSTOP "$(Busy)$\r$\n$R5" /SD IDOK
SetErrorLevel 2
Quit
deleted:
RMDir /r "$INSTDIR\.ely-old"
StrCpy $R1 "$INSTDIR"
Call un.ElyPrune
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