From 418d72d72ded0e8238ae433ac60e5c6df19d947c Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Sat, 25 May 2024 15:41:09 +0300 Subject: [PATCH] feat(bundler/nsis): add `/UPDATE` flag (#9559) * feat(bundler/nsis): add `/UPDATE` flag * typo * typo * skip webview2 installation on updating --- .changes/bundler-shortcuts-updating.md | 6 + tooling/bundler/src/bundle/windows/nsis.rs | 4 + .../bundle/windows/templates/installer.nsi | 266 ++++++------------ .../src/bundle/windows/templates/utils.nsh | 118 ++++++++ 4 files changed, 219 insertions(+), 175 deletions(-) create mode 100644 .changes/bundler-shortcuts-updating.md create mode 100644 tooling/bundler/src/bundle/windows/templates/utils.nsh diff --git a/.changes/bundler-shortcuts-updating.md b/.changes/bundler-shortcuts-updating.md new file mode 100644 index 000000000..79a3e6652 --- /dev/null +++ b/.changes/bundler-shortcuts-updating.md @@ -0,0 +1,6 @@ +--- +"tauri-bundler": "patch:enhance" +--- + +Added `/UPDATE` flag for NSIS installer which will make the installer avoid deleting app data and re-creating shortcuts. + diff --git a/tooling/bundler/src/bundle/windows/nsis.rs b/tooling/bundler/src/bundle/windows/nsis.rs index 49ec0165c..489a8a623 100644 --- a/tooling/bundler/src/bundle/windows/nsis.rs +++ b/tooling/bundler/src/bundle/windows/nsis.rs @@ -451,6 +451,10 @@ fn build_nsis_app_installer( output_path.join("FileAssociation.nsh"), include_str!("./templates/FileAssociation.nsh"), )?; + write_ut16_le_with_bom( + output_path.join("utils.nsh"), + include_str!("./templates/utils.nsh"), + )?; let installer_nsi_path = output_path.join("installer.nsi"); write_ut16_le_with_bom( diff --git a/tooling/bundler/src/bundle/windows/templates/installer.nsi b/tooling/bundler/src/bundle/windows/templates/installer.nsi index cf804f764..32beaba6a 100644 --- a/tooling/bundler/src/bundle/windows/templates/installer.nsi +++ b/tooling/bundler/src/bundle/windows/templates/installer.nsi @@ -11,6 +11,7 @@ ManifestDPIAware true !include FileFunc.nsh !include x64.nsh !include WordFunc.nsh +!include "utils.nsh" !include "FileAssociation.nsh" !include "StrFunc.nsh" !include "Win\COM.nsh" @@ -68,6 +69,7 @@ VIAddVersionKey "ProductVersion" "${VERSION}" !addplugindir "${PLUGINSPATH}" !endif +; Uninstaller signing command !if "${UNINSTALLERSIGNCOMMAND}" != "" !uninstfinalize '${UNINSTALLERSIGNCOMMAND}' !endif @@ -98,17 +100,17 @@ VIAddVersionKey "ProductVersion" "${VERSION}" !include MultiUser.nsh !endif -; installer icon +; Installer icon !if "${INSTALLERICON}" != "" !define MUI_ICON "${INSTALLERICON}" !endif -; installer sidebar image +; Installer sidebar image !if "${SIDEBARIMAGE}" != "" !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}" !endif -; installer header image +; Installer header image !if "${HEADERIMAGE}" != "" !define MUI_HEADERIMAGE !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" @@ -136,7 +138,6 @@ VIAddVersionKey "ProductVersion" "${VERSION}" !insertmacro MULTIUSER_PAGE_INSTALLMODE !endif - ; 4. Custom page to ask user if he wants to reinstall/uninstall ; only if a previous installation was detected Var ReinstallPageCheck @@ -217,6 +218,12 @@ Function PageReinstall Abort ${EndIf} + ; Skip showing the page if passive + ; + ; Note that we don't call this earlier at the begining + ; of this function because we need to populate some variables + ; related to current installed version if detected and whether + ; we are downgrading or not. Call SkipIfPassive nsDialogs::Create 1018 @@ -232,7 +239,7 @@ Function PageReinstall ${NSD_CreateRadioButton} 30u 70u -30u 8u $R3 Pop $R3 - ; disable this radio button if downgrading and downgrades are disabled + ; Disable this radio button if downgrading and downgrades are disabled !if "${ALLOWDOWNGRADES}" == "false" ${IfThen} $R0 == -1 ${|} EnableWindow $R3 0 ${|} !endif @@ -310,8 +317,8 @@ FunctionEnd !insertmacro MUI_PAGE_DIRECTORY ; 6. Start menu shortcut page -!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive Var AppStartMenuFolder +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder ; 7. Installation page @@ -337,7 +344,7 @@ Var DeleteAppDataCheckbox Var DeleteAppDataCheckboxState !define /ifndef WS_EX_LAYOUTRTL 0x00400000 !define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow -Function un.ConfirmShow +Function un.ConfirmShow ; Add add a `Delete app data` check box FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog ${If} $(^RTL) == 1 System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE}|${WS_EX_LAYOUTRTL},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 50,i 100,i 400, i 25,i$1,i0,i0,i0)i.s' @@ -366,30 +373,17 @@ FunctionEnd !include "{{this}}" {{/each}} -!macro SetContext - !if "${INSTALLMODE}" == "currentUser" - SetShellVarContext current - !else if "${INSTALLMODE}" == "perMachine" - SetShellVarContext all - !endif - - ${If} ${RunningX64} - !if "${ARCH}" == "x64" - SetRegView 64 - !else if "${ARCH}" == "arm64" - SetRegView 64 - !else - SetRegView 32 - !endif - ${EndIf} -!macroend - Var PassiveMode +Var UpdateMode Function .onInit ${GetOptions} $CMDLINE "/P" $PassiveMode IfErrors +2 0 StrCpy $PassiveMode 1 + ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode + IfErrors +2 0 + StrCpy $UpdateMode 1 + !if "${DISPLAYLANGUAGESELECTOR}" == "true" !insertmacro MUI_LANGDLL_DISPLAY !endif @@ -455,91 +449,56 @@ Section WebView2 StrCmp $4 "" 0 webview2_done StrCmp $5 "" 0 webview2_done - ; Webview2 install modes - !if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper" - Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" - DetailPrint "$(webview2Downloading)" - NSISdl::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Pop $0 - ${If} $0 == 0 - DetailPrint "$(webview2DownloadSuccess)" - ${Else} - DetailPrint "$(webview2DownloadError)" - Abort "$(webview2AbortError)" - ${EndIf} - StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Goto install_webview2 - !endif + ; Webview2 installation + ; + ; Skip if updating + ${If} $UpdateMode <> 1 + !if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper" + Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" + DetailPrint "$(webview2Downloading)" + nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Pop $0 + ${If} $0 == 0 + DetailPrint "$(webview2DownloadSuccess)" + ${Else} + DetailPrint "$(webview2DownloadError)" + Abort "$(webview2AbortError)" + ${EndIf} + StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Goto install_webview2 + !endif - !if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper" - Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" - File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}" - DetailPrint "$(installingWebview2)" - StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Goto install_webview2 - !endif + !if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper" + Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" + File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}" + DetailPrint "$(installingWebview2)" + StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Goto install_webview2 + !endif - !if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller" - Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" - File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}" - DetailPrint "$(installingWebview2)" - StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" - Goto install_webview2 - !endif + !if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller" + Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" + File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}" + DetailPrint "$(installingWebview2)" + StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" + Goto install_webview2 + !endif - Goto webview2_done + Goto webview2_done - install_webview2: - DetailPrint "$(installingWebview2)" - ; $6 holds the path to the webview2 installer - ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1 - ${If} $1 == 0 - DetailPrint "$(webview2InstallSuccess)" - ${Else} - DetailPrint "$(webview2InstallError)" - Abort "$(webview2AbortError)" - ${EndIf} - webview2_done: -SectionEnd - -!macro CheckIfAppIsRunning - !if "${INSTALLMODE}" == "currentUser" - nsis_tauri_utils::FindProcessCurrentUser "${MAINBINARYNAME}.exe" - !else - nsis_tauri_utils::FindProcess "${MAINBINARYNAME}.exe" - !endif - Pop $R0 - ${If} $R0 = 0 - IfSilent kill 0 - ${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|} - kill: - !if "${INSTALLMODE}" == "currentUser" - nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe" - !else - nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe" - !endif - Pop $R0 - Sleep 500 - ${If} $R0 = 0 - Goto app_check_done - ${Else} - IfSilent silent ui - silent: - System::Call 'kernel32::AttachConsole(i -1)i.r0' - ${If} $0 != 0 - System::Call 'kernel32::GetStdHandle(i -11)i.r0' - System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color - FileWrite $0 "$(appRunning)$\n" - ${EndIf} - Abort - ui: - Abort "$(failedToKillApp)" - ${EndIf} - cancel: - Abort "$(appRunning)" + install_webview2: + DetailPrint "$(installingWebview2)" + ; $6 holds the path to the webview2 installer + ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1 + ${If} $1 == 0 + DetailPrint "$(webview2InstallSuccess)" + ${Else} + DetailPrint "$(webview2InstallError)" + Abort "$(webview2AbortError)" + ${EndIf} + webview2_done: ${EndIf} - app_check_done: -!macroend +SectionEnd Section Install SetOutPath $INSTDIR @@ -606,16 +565,19 @@ Section Install !insertmacro MUI_STARTMENU_WRITE_END ; Create shortcuts for silent and passive installers, which - ; can be disabled by passing `/NS` flag - ; GUI installer has buttons for users to control creating them + ; can be disabled by passing `/NS` or `/UPDATE` flag. + ; + ; GUI installer has buttons for users to control creating them. IfSilent check_ns_flag 0 ${IfThen} $PassiveMode == 1 ${|} Goto check_ns_flag ${|} Goto shortcuts_done check_ns_flag: ${GetOptions} $CMDLINE "/NS" $R0 IfErrors 0 shortcuts_done - Call CreateDesktopShortcut - Call CreateStartMenuShortcut + ${If} $UpdateMode <> 1 + Call CreateDesktopShortcut + Call CreateStartMenuShortcut + ${EndIf} shortcuts_done: ; Auto close this page for passive mode @@ -646,36 +608,11 @@ Function un.onInit !insertmacro MUI_UNGETLANGUAGE FunctionEnd -!macro DeleteAppUserModelId - !insertmacro ComHlpr_CreateInProcInstance ${CLSID_DestinationList} ${IID_ICustomDestinationList} r1 "" - ${If} $1 P<> 0 - ${ICustomDestinationList::DeleteList} $1 '("${BUNDLEID}")' - ${IUnknown::Release} $1 "" - ${EndIf} - !insertmacro ComHlpr_CreateInProcInstance ${CLSID_ApplicationDestinations} ${IID_IApplicationDestinations} r1 "" - ${If} $1 P<> 0 - ${IApplicationDestinations::SetAppID} $1 '("${BUNDLEID}")i.r0' - ${If} $0 >= 0 - ${IApplicationDestinations::RemoveAllDestinations} $1 '' - ${EndIf} - ${IUnknown::Release} $1 "" - ${EndIf} -!macroend - -; From https://stackoverflow.com/a/42816728/16993372 -!macro UnpinShortcut shortcut - !insertmacro ComHlpr_CreateInProcInstance ${CLSID_StartMenuPin} ${IID_IStartMenuPinnedList} r0 "" - ${If} $0 P<> 0 - System::Call 'SHELL32::SHCreateItemFromParsingName(ws, p0, g "${IID_IShellItem}", *p0r1)' "${shortcut}" - ${If} $1 P<> 0 - ${IStartMenuPinnedList::RemoveFromList} $0 '(r1)' - ${IUnknown::Release} $1 "" - ${EndIf} - ${IUnknown::Release} $0 "" - ${EndIf} -!macroend - Section Uninstall + ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode + IfErrors +2 0 + StrCpy $UpdateMode 1 + !insertmacro CheckIfAppIsRunning ; Delete the app directory and its content from disk @@ -716,17 +653,20 @@ Section Uninstall {{/each}} RMDir "$INSTDIR" - !insertmacro DeleteAppUserModelId - !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" - !insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" + ; Remove shortcuts if not updating + ${If} $UpdateMode <> 1 + !insertmacro DeleteAppUserModelId - ; Remove start menu shortcut - !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder - Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" - RMDir "$SMPROGRAMS\$AppStartMenuFolder" + ; Remove start menu shortcut + !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder + !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" + Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" + RMDir "$SMPROGRAMS\$AppStartMenuFolder" - ; Remove desktop shortcuts - Delete "$DESKTOP\${MAINBINARYNAME}.lnk" + ; Remove desktop shortcuts + !insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" + Delete "$DESKTOP\${MAINBINARYNAME}.lnk" + ${EndIf} ; Remove registry information for add/remove programs !if "${INSTALLMODE}" == "both" @@ -739,8 +679,10 @@ Section Uninstall DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language" - ; Delete app data - ${If} $DeleteAppDataCheckboxState == 1 + ; Delete app data if the checkbox is selected + ; and if not updating + ${If} $DeleteAppDataCheckboxState = 1 + ${AndIf} $UpdateMode <> 1 SetShellVarContext current RmDir /r "$APPDATA\${BUNDLEID}" RmDir /r "$LOCALAPPDATA\${BUNDLEID}" @@ -761,32 +703,6 @@ Function SkipIfPassive ${IfThen} $PassiveMode == 1 ${|} Abort ${|} FunctionEnd -!macro SetLnkAppUserModelId shortcut - !insertmacro ComHlpr_CreateInProcInstance ${CLSID_ShellLink} ${IID_IShellLink} r0 "" - ${If} $0 P<> 0 - ${IUnknown::QueryInterface} $0 '("${IID_IPersistFile}",.r1)' - ${If} $1 P<> 0 - ${IPersistFile::Load} $1 '("${shortcut}", ${STGM_READWRITE})' - ${IUnknown::QueryInterface} $0 '("${IID_IPropertyStore}",.r2)' - ${If} $2 P<> 0 - System::Call 'Oleaut32::SysAllocString(w "${BUNDLEID}") i.r3' - System::Call '*${SYSSTRUCT_PROPERTYKEY}(${PKEY_AppUserModel_ID})p.r4' - System::Call '*${SYSSTRUCT_PROPVARIANT}(${VT_BSTR},,&i4 $3)p.r5' - ${IPropertyStore::SetValue} $2 '($4,$5)' - - System::Call 'Oleaut32::SysFreeString($3)' - System::Free $4 - System::Free $5 - ${IPropertyStore::Commit} $2 "" - ${IUnknown::Release} $2 "" - ${IPersistFile::Save} $1 '("${shortcut}",1)' - ${EndIf} - ${IUnknown::Release} $1 "" - ${EndIf} - ${IUnknown::Release} $0 "" - ${EndIf} -!macroend - Function CreateDesktopShortcut CreateShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" !insertmacro SetLnkAppUserModelId "$DESKTOP\${MAINBINARYNAME}.lnk" diff --git a/tooling/bundler/src/bundle/windows/templates/utils.nsh b/tooling/bundler/src/bundle/windows/templates/utils.nsh new file mode 100644 index 000000000..6679d748e --- /dev/null +++ b/tooling/bundler/src/bundle/windows/templates/utils.nsh @@ -0,0 +1,118 @@ +; Change shell and registry context based on running +; architecture and chosen install mode. +!macro SetContext + !if "${INSTALLMODE}" == "currentUser" + SetShellVarContext current + !else if "${INSTALLMODE}" == "perMachine" + SetShellVarContext all + !endif + + ${If} ${RunningX64} + !if "${ARCH}" == "x64" + SetRegView 64 + !else if "${ARCH}" == "arm64" + SetRegView 64 + !else + SetRegView 32 + !endif + ${EndIf} +!macroend + +; Checks whether app is running or not and prompts to kill it. +!macro CheckIfAppIsRunning + !if "${INSTALLMODE}" == "currentUser" + nsis_tauri_utils::FindProcessCurrentUser "${MAINBINARYNAME}.exe" + !else + nsis_tauri_utils::FindProcess "${MAINBINARYNAME}.exe" + !endif + Pop $R0 + ${If} $R0 = 0 + IfSilent kill 0 + ${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|} + kill: + !if "${INSTALLMODE}" == "currentUser" + nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe" + !else + nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe" + !endif + Pop $R0 + Sleep 500 + ${If} $R0 = 0 + Goto app_check_done + ${Else} + IfSilent silent ui + silent: + System::Call 'kernel32::AttachConsole(i -1)i.r0' + ${If} $0 != 0 + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color + FileWrite $0 "$(appRunning)$\n" + ${EndIf} + Abort + ui: + Abort "$(failedToKillApp)" + ${EndIf} + cancel: + Abort "$(appRunning)" + ${EndIf} + app_check_done: +!macroend + +; Sets AppUserModelId on a shortcut +!macro SetLnkAppUserModelId shortcut + !insertmacro ComHlpr_CreateInProcInstance ${CLSID_ShellLink} ${IID_IShellLink} r0 "" + ${If} $0 P<> 0 + ${IUnknown::QueryInterface} $0 '("${IID_IPersistFile}",.r1)' + ${If} $1 P<> 0 + ${IPersistFile::Load} $1 '("${shortcut}", ${STGM_READWRITE})' + ${IUnknown::QueryInterface} $0 '("${IID_IPropertyStore}",.r2)' + ${If} $2 P<> 0 + System::Call 'Oleaut32::SysAllocString(w "${BUNDLEID}") i.r3' + System::Call '*${SYSSTRUCT_PROPERTYKEY}(${PKEY_AppUserModel_ID})p.r4' + System::Call '*${SYSSTRUCT_PROPVARIANT}(${VT_BSTR},,&i4 $3)p.r5' + ${IPropertyStore::SetValue} $2 '($4,$5)' + + System::Call 'Oleaut32::SysFreeString($3)' + System::Free $4 + System::Free $5 + ${IPropertyStore::Commit} $2 "" + ${IUnknown::Release} $2 "" + ${IPersistFile::Save} $1 '("${shortcut}",1)' + ${EndIf} + ${IUnknown::Release} $1 "" + ${EndIf} + ${IUnknown::Release} $0 "" + ${EndIf} +!macroend + +; Deletes jump list entries and recent destinations +!macro DeleteAppUserModelId + !insertmacro ComHlpr_CreateInProcInstance ${CLSID_DestinationList} ${IID_ICustomDestinationList} r1 "" + ${If} $1 P<> 0 + ${ICustomDestinationList::DeleteList} $1 '("${BUNDLEID}")' + ${IUnknown::Release} $1 "" + ${EndIf} + !insertmacro ComHlpr_CreateInProcInstance ${CLSID_ApplicationDestinations} ${IID_IApplicationDestinations} r1 "" + ${If} $1 P<> 0 + ${IApplicationDestinations::SetAppID} $1 '("${BUNDLEID}")i.r0' + ${If} $0 >= 0 + ${IApplicationDestinations::RemoveAllDestinations} $1 '' + ${EndIf} + ${IUnknown::Release} $1 "" + ${EndIf} +!macroend + +; Unpins a shortcut from Start menu and Taskbar +; +; From https://stackoverflow.com/a/42816728/16993372 +!macro UnpinShortcut shortcut + !insertmacro ComHlpr_CreateInProcInstance ${CLSID_StartMenuPin} ${IID_IStartMenuPinnedList} r0 "" + ${If} $0 P<> 0 + System::Call 'SHELL32::SHCreateItemFromParsingName(ws, p0, g "${IID_IShellItem}", *p0r1)' "${shortcut}" + ${If} $1 P<> 0 + ${IStartMenuPinnedList::RemoveFromList} $0 '(r1)' + ${IUnknown::Release} $1 "" + ${EndIf} + ${IUnknown::Release} $0 "" + ${EndIf} +!macroend