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:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-15 15:59:50 -04:00
1 parent 5dafa432f6
commit 23735aee3c
6 files changed
+365 -24

No files matched your search

+31
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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"