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>
400 lines
10 KiB
NSIS
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
|