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