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>
This commit is contained in:
1 parent
5dafa432f6
commit
23735aee3c
6 files changed
+365
-24
No files matched your search
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prints NSIS macros naming what a stage installs.
|
||||
# Usage: files.sh <stage> <slug>
|
||||
set -euo pipefail
|
||||
|
||||
stage=${1:?staged folder from packaging/stage.sh}
|
||||
slug=${2:?app slug, such as ely-doc}
|
||||
names=$({ ls -A "$stage"; echo "$slug.ico"; echo uninstall.exe; } | sort -u)
|
||||
for name in $names; do
|
||||
[[ $name =~ ^[A-Za-z0-9._-]+$ ]] || {
|
||||
echo "files.sh: unsafe name $name" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
# The list the next install and the uninstaller remove.
|
||||
echo '!macro ElyWriteOwned'
|
||||
echo ' FileOpen $9 "$INSTDIR\.ely-files" w'
|
||||
for name in $names; do
|
||||
echo " FileWrite \$9 \"$name\$\\r\$\\n\""
|
||||
done
|
||||
echo ' FileClose $9'
|
||||
echo '!macroend'
|
||||
|
||||
# Names this install creates, for a rollback to remove.
|
||||
echo '!macro ElyNoteNew'
|
||||
for name in $names; do
|
||||
echo " IfFileExists \"\$INSTDIR\\$name\" +2"
|
||||
echo " FileWrite \$R2 \"$name\$\\r\$\\n\""
|
||||
done
|
||||
echo '!macroend'
|
||||
+200
-21
@@ -1,4 +1,4 @@
|
||||
; Per-user installer; pass APP_NAME, APP_SLUG, VERSION, STAGE, ICON, TYPES.
|
||||
; Per-user installer; pass each define the checks below name.
|
||||
Unicode true
|
||||
!include "MUI2.nsh"
|
||||
|
||||
@@ -21,6 +21,10 @@ Unicode true
|
||||
!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
|
||||
@@ -34,6 +38,7 @@ 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
|
||||
@@ -45,32 +50,193 @@ SetCompressor /SOLID lzma
|
||||
LangString DeleteData ${LANG_SIMPCHINESE} "同时删除 ${APP_NAME} 的缓存、账号与本地历史吗?你的文档不受影响。"
|
||||
LangString DeleteData ${LANG_ENGLISH} "Also delete the cache, accounts and local history of ${APP_NAME}? Your documents stay."
|
||||
|
||||
!macro ElyRemoveFiles
|
||||
; What stage.sh lays out, and nothing else.
|
||||
RMDir /r "$INSTDIR\libreoffice"
|
||||
RMDir /r "$INSTDIR\tessdata"
|
||||
RMDir /r "$INSTDIR\git"
|
||||
RMDir /r "$INSTDIR\content"
|
||||
RMDir /r "$INSTDIR\locales"
|
||||
RMDir /r "$INSTDIR\licenses"
|
||||
Delete "$INSTDIR\*.exe"
|
||||
Delete "$INSTDIR\*.dll"
|
||||
Delete "$INSTDIR\*.pak"
|
||||
Delete "$INSTDIR\*.bin"
|
||||
Delete "$INSTDIR\*.dat"
|
||||
Delete "$INSTDIR\*.json"
|
||||
Delete "$INSTDIR\${APP_SLUG}.ico"
|
||||
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 older install's files go first, so none linger.
|
||||
IfFileExists "$INSTDIR\${APP_SLUG}.exe" 0 fresh
|
||||
!insertmacro ElyRemoveFiles
|
||||
; 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}"
|
||||
@@ -84,13 +250,26 @@ fresh:
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
!insertmacro ElyRemoveFiles
|
||||
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)" IDNO done
|
||||
MessageBox MB_YESNO|MB_DEFBUTTON2 "$(DeleteData)" /SD IDNO IDNO done
|
||||
RMDir /r "$LOCALAPPDATA\ElyOffice\${APP_SLUG}"
|
||||
done:
|
||||
SectionEnd
|
||||
@@ -20,10 +20,12 @@ stage=$root/target/stage/$slug
|
||||
"$root/packaging/stage.sh" "$slug" "$stage"
|
||||
types=$root/target/stage/$slug-types.nsh
|
||||
"$root/packaging/windows/types.sh" "$slug" >"$types"
|
||||
files=$root/target/stage/$slug-files.nsh
|
||||
"$root/packaging/windows/files.sh" "$stage" "$slug" >"$files"
|
||||
mkdir -p "$dist"
|
||||
out=$dist/$name-$version-windows-x86_64-setup.exe
|
||||
"$makensis" -V2 -INPUTCHARSET UTF8 -DAPP_NAME="$name" -DAPP_SLUG="$slug" -DVERSION="$version" \
|
||||
-DSTAGE="$(cygpath -w "$stage")" -DICON="$(cygpath -w "$root/assets/icons/$slug.ico")" \
|
||||
-DTYPES="$(cygpath -w "$types")" -DOUTFILE="$(cygpath -w "$out")" \
|
||||
-DTYPES="$(cygpath -w "$types")" -DFILES="$(cygpath -w "$files")" -DOUTFILE="$(cygpath -w "$out")" \
|
||||
"$(cygpath -w "$root/packaging/windows/installer.nsi")"
|
||||
echo "$out"
|
||||
Reference in new issue
Block a user