Files
ElyOffice/packaging/windows/installer.nsi
T
ZacharyZhang-NYandClaude Opus 5.5 632a66eb46 R7.9: Explorer's New makes docx, xlsx, pptx and md on Windows
Each app installs a blank file and points ShellNew at it. Explorer
lists a type only through its default class, so a type no program
handles (no default, or one naming no class) gets the app's class;
an installed handler is never replaced. Uninstall removes only what
this install wrote.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 00:32:48 -04:00

401 lines
10 KiB
NSIS

; Per-user installer; pass each define the checks below name.
Unicode true
!include "MUI2.nsh"
!include "LogicLib.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