Skip to content

Commit

Permalink
Add front-end implementation for conda-standalone uninstall subcomm…
Browse files Browse the repository at this point in the history
…and (#897)

* Add option to use standalone binary for uninstallation

* Add conda-standalone uninstall to NSIS template

* Use AbortRetryNSExecWait to call conda-standalone uninstaller

* Add uninstallation options panel

* Improve typing for uninstall command templating

* Implement conda-standalone uninstall command into Windows uninstaller workflow

* Add standalone uninstaller options panel

* Update documentation

* Make /RemoceCondaRcs CLI parsing logic more concise

* Add tests

* Add news file

* Replace pipe operator with Union

* Ensure that ON_CI=False for CI="0"

* Update uninstallation commands

* Update CLI options documentation

* Update uninstaller documentation

* Update description of uninstallation options in the GUI

* Replace UNINSTALL_MENUS with UNINSTALL_COMMANDS

* Fix jinja syntax

* Document uninstaller subcommand for Unix

* Debug: print directory contents for standalone uninstaller

* Always remove installation directory

---------

Co-authored-by: jaimergp <jaimergp@users.noreply.github.com>
  • Loading branch information
marcoesters and jaimergp authored Dec 5, 2024
1 parent 51357eb commit e462caf
Show file tree
Hide file tree
Showing 11 changed files with 466 additions and 34 deletions.
8 changes: 8 additions & 0 deletions CONSTRUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,14 @@ Allowed keys are:
license text. Only relevant if include_text is True. Any str accepted by open()'s 'errors'
argument is valid. See https://docs.python.org/3/library/functions.html#open.

### `uninstall_with_conda_exe`

_required:_ no<br/>
_type:_ boolean<br/>

Use the standalone binary to perform the uninstallation.
Requires conda-standalone 24.11.0 or newer.


## Available selectors
- `aarch64`
Expand Down
5 changes: 5 additions & 0 deletions constructor/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,11 @@
- `text_errors` (optional str, default=`None`): How to handle decoding errors when reading the
license text. Only relevant if include_text is True. Any str accepted by open()'s 'errors'
argument is valid. See https://docs.python.org/3/library/functions.html#open.
'''),

('uninstall_with_conda_exe', False, bool, '''
Use the standalone binary to perform the uninstallation.
Requires conda-standalone 24.11.0 or newer.
'''),
]

Expand Down
10 changes: 9 additions & 1 deletion constructor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
# TODO: Investigate errors on Windows and re-enable
sys.exit("Error: micromamba is not supported on Windows installers.")

if (
info.get("uninstall_with_conda_exe")
and not (
exe_type == StandaloneExe.CONDA and exe_version and exe_version >= Version("24.11.0")
)
):
sys.exit("Error: uninstalling with conda.exe requires conda-standalone 24.11.0 or newer.")

logger.debug('conda packages download: %s', info['_download_dir'])

for key in ('welcome_image_text', 'header_image_text'):
Expand Down Expand Up @@ -185,7 +193,7 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
"Will assume it is compatible with shortcuts."
)
elif sys.platform != "win32" and (
exe_type != StandaloneExe.CONDA or exe_version < Version("23.11.0")
exe_type != StandaloneExe.CONDA or (exe_version and exe_version < Version("23.11.0"))
):
logger.warning("conda-standalone 23.11.0 or above is required for shortcuts on Unix.")
info['_enable_shortcuts'] = "incompatible"
Expand Down
108 changes: 108 additions & 0 deletions constructor/nsis/StandaloneUninstallerOptions.nsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
var UninstCustomOptions
var UninstCustomOptions.RemoveConfigFiles_User
var UninstCustomOptions.RemoveConfigFiles_System
var UninstCustomOptions.RemoveUserData
var UninstCustomOptions.RemoveCaches

# These are the checkbox states, to be used by the uninstaller
var UninstRemoveConfigFiles_User_State
var UninstRemoveConfigFiles_System_State
var UninstRemoveUserData_State
var UninstRemoveCaches_State

Function un.UninstCustomOptions_InitDefaults
StrCpy $UninstRemoveConfigFiles_User_State ${BST_UNCHECKED}
StrCpy $UninstRemoveConfigFiles_System_State ${BST_UNCHECKED}
StrCpy $UninstRemoveUserData_State ${BST_UNCHECKED}
StrCpy $UninstRemoveCaches_State ${BST_UNCHECKED}
FunctionEnd

Function un.UninstCustomOptions_Show
${If} $UninstRemoveCaches_State == ""
Abort
${EndIf}
# Create dialog
nsDialogs::Create 1018
Pop $UninstCustomOptions
${If} $UninstCustomOptions == error
Abort
${EndIf}

!insertmacro MUI_HEADER_TEXT \
"Advanced uninstallation options" \
"Remove configuration, data, and cache files"

# We will use $5 as the y axis accumulator, starting at 0
# We sum the the number of 'u' units added by 'NSD_Create*' functions
IntOp $5 0 + 0

# Option to remove configuration files
${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove user configuration files."
IntOp $5 $5 + 11
Pop $UninstCustomOptions.RemoveConfigFiles_User
${NSD_SetState} $UninstCustomOptions.RemoveConfigFiles_User $UninstRemoveConfigFiles_User_State
${NSD_OnClick} $UninstCustomOptions.RemoveConfigFiles_User un.UninstRemoveConfigFiles_User_Onclick
${NSD_CreateLabel} 5% "$5u" 90% 10u \
"This removes configuration files such as .condarc files in the Users directory."
IntOp $5 $5 + 10

${If} ${UAC_IsAdmin}
${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove system-wide configuration files."
IntOp $5 $5 + 11
Pop $UninstCustomOptions.RemoveConfigFiles_System
${NSD_SetState} $UninstCustomOptions.RemoveConfigFiles_System $UninstRemoveConfigFiles_System_State
${NSD_OnClick} $UninstCustomOptions.RemoveConfigFiles_System un.UninstRemoveConfigFiles_System_Onclick
${NSD_CreateLabel} 5% "$5u" 90% 10u \
"This removes configuration files such as .condarc files in the ProgramData directory."
IntOp $5 $5 + 10
${EndIf}

# Option to remove user data files
${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove user data."
IntOp $5 $5 + 11
Pop $UninstCustomOptions.RemoveUserData
${NSD_SetState} $UninstCustomOptions.RemoveUserData $UninstRemoveUserData_State
${NSD_OnClick} $UninstCustomOptions.RemoveUserData un.UninstRemoveUserData_Onclick
${NSD_CreateLabel} 5% "$5u" 90% 10u \
"This removes user data files such as the .conda directory inside the Users folder."
IntOp $5 $5 + 10

# Option to remove caches
${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove caches."
IntOp $5 $5 + 11
Pop $UninstCustomOptions.RemoveCaches
${NSD_SetState} $UninstCustomOptions.RemoveCaches $UninstRemoveCaches_State
${NSD_OnClick} $UninstCustomOptions.RemoveCaches un.UninstRemoveCaches_Onclick
${NSD_CreateLabel} 5% "$5u" 90% 10u \
"This removes cache directories such as package caches and notices."
IntOp $5 $5 + 20

IntOp $5 $5 + 5
${NSD_CreateLabel} 0 "$5u" 100% 10u \
"These options are not recommended if multiple conda installations exist on the same system."
IntOp $5 $5 + 10
Pop $R0
SetCtlColors $R0 ff0000 transparent

nsDialogs::Show
FunctionEnd

Function un.UninstRemoveConfigFiles_User_OnClick
Pop $0
${NSD_GetState} $0 $UninstRemoveConfigFiles_User_State
FunctionEnd

Function un.UninstRemoveConfigFiles_System_OnClick
Pop $0
${NSD_GetState} $0 $UninstRemoveConfigFiles_System_State
FunctionEnd

Function un.UninstRemoveUserData_OnClick
Pop $0
${NSD_GetState} $0 $UninstRemoveUserData_State
FunctionEnd

Function un.UninstRemoveCaches_OnClick
Pop $0
${NSD_GetState} $0 $UninstRemoveCaches_State
FunctionEnd
92 changes: 77 additions & 15 deletions constructor/nsis/main.nsi.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ var /global StdOutHandleSet

!include "Utils.nsh"

{%- if uninstall_with_conda_exe %}
!include "StandaloneUninstallerOptions.nsh"
{%- endif %}

!define NAME {{ installer_name }}
!define VERSION {{ installer_version }}
!define COMPANY {{ company }}
Expand Down Expand Up @@ -110,6 +114,11 @@ var /global ARGV_NoScripts
var /global ARGV_NoShortcuts
var /global ARGV_CheckPathLength
var /global ARGV_QuietMode
{%- if uninstall_with_conda_exe %}
var /global ARGV_Uninst_RemoveConfigFiles
var /global ARGV_Uninst_RemoveUserData
var /global ARGV_Uninst_RemoveCaches
{%- endif %}

var /global IsDomainUser
var /global CheckPathLength
Expand Down Expand Up @@ -195,14 +204,17 @@ Page Custom mui_AnaCustomOptions_Show
{%- if custom_conclusion %}
# Custom conclusion file(s)
{{ CUSTOM_CONCLUSION_FILE }}
#else
{%- else %}
!insertmacro MUI_PAGE_FINISH
{%- endif %}


!insertmacro MUI_UNPAGE_WELCOME
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.OnDirectoryLeave
!insertmacro MUI_UNPAGE_CONFIRM
{%- if uninstall_with_conda_exe %}
UninstPage Custom un.UninstCustomOptions_Show
{%- endif %}
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH

Expand Down Expand Up @@ -716,7 +728,7 @@ Function .onInit
Pop $0
FunctionEnd

Function un.onInit
!macro un.ParseCommandLineArgs
ClearErrors
${GetParameters} $ARGV
${GetOptions} $ARGV "/?" $ARGV_Help
Expand All @@ -736,6 +748,11 @@ Function un.onInit
/? (show this help message)$\n\
/S (run in CLI/headless mode)$\n\
/Q (quiet mode, do not print output to console)$\n\
{%- if uninstall_with_conda_exe %}
/RemoveCaches=[0|1] [default: 0]$\n\
/RemoveConfigFiles=[none|users|system|all] [default: none]$\n\
/RemoveUserData=[0|1] [default: 0]$\n\
{%- endif %}
/_?=[installation directory] (must be last parameter)$\n\
$\n\
EXAMPLES$\n\
Expand All @@ -747,8 +764,7 @@ Function un.onInit
Closing in 10s..."
# Give it some time so users can read it the pop-up console
# The pop-up console happens because the uninstaller copies itself to
# a temporary location because actually running, so we can't get the parent
# console handle
# a temporary location, so we can't get the parent console handle
Sleep 10000
Abort
${EndIf}
Expand All @@ -758,6 +774,59 @@ Function un.onInit
${IfNot} ${Errors}
StrCpy $QuietMode "1"
${EndIf}
{%- if uninstall_with_conda_exe %}
ClearErrors
${GetOptions} $ARGV "/RemoveConfigFiles=" $ARGV_Uninst_RemoveConfigFiles
${IfNot} ${Errors}
${IfNot} ${UAC_IsAdmin}
${If} $ARGV_Uninst_RemoveConfigFiles == "all"
${OrIf} $ARGV_Uninst_RemoveConfigFiles == "system"
MessageBox MB_ICONSTOP "Removing system .condarc files requires an elevated prompt."
Abort
${EndIf}
${EndIf}
${If} $ARGV_Uninst_RemoveConfigFiles == "user"
StrCpy $UninstRemoveConfigFiles_User_State ${BST_CHECKED}
StrCpy $UninstRemoveConfigFiles_System_State ${BST_UNCHECKED}
${ElseIf} $ARGV_Uninst_RemoveConfigFiles == "system"
StrCpy $UninstRemoveConfigFiles_User_State ${BST_UNCHECKED}
StrCpy $UninstRemoveConfigFiles_System_State ${BST_CHECKED}
${ElseIf} $ARGV_Uninst_RemoveConfigFiles == "all"
StrCpy $UninstRemoveConfigFiles_User_State ${BST_CHECKED}
StrCpy $UninstRemoveConfigFiles_System_State ${BST_CHECKED}
${Else}
StrCpy $UninstRemoveConfigFiles_User_State ${BST_UNCHECKED}
StrCpy $UninstRemoveConfigFiles_System_State ${BST_UNCHECKED}
${EndIf}
${EndIf}

ClearErrors
${GetOptions} $ARGV "/RemoveUserData=" $ARGV_Uninst_RemoveUserData
${IfNot} ${Errors}
${If} $ARGV_Uninst_RemoveUserData = "1"
StrCpy $UninstRemoveUserData_State ${BST_CHECKED}
${ElseIf} $ARGV_Uninst_RemoveUserData = "0"
StrCpy $UninstRemoveUserData_State ${BST_UNCHECKED}
${EndIf}
${EndIf}

ClearErrors
${GetOptions} $ARGV "/RemoveCaches=" $ARGV_Uninst_RemoveCaches
${IfNot} ${Errors}
${If} $ARGV_Uninst_RemoveCaches = "1"
StrCpy $UninstRemoveCaches_State ${BST_CHECKED}
${ElseIf} $ARGV_Uninst_RemoveCaches = "0"
StrCpy $UninstRemoveCaches_State ${BST_UNCHECKED}
${EndIf}
${EndIf}
{%- endif %}
!macroend

Function un.onInit

{%- if uninstall_with_conda_exe %}
Call un.UninstCustomOptions_InitDefaults
{%- endif %}

Push $0
Push $1
Expand Down Expand Up @@ -1384,10 +1453,11 @@ SectionEnd

Section "Uninstall"
${LogSet} on
${If} ${Silent}
!insertmacro un.ParseCommandLineArgs
${EndIf}

# Remove menu items, path entries
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0'
{{ UNINSTALL_MENUS }}

# ensure that MSVC runtime DLLs are on PATH during uninstallation
ReadEnvStr $0 PATH
Expand Down Expand Up @@ -1437,15 +1507,7 @@ Section "Uninstall"
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0'
${EndIf}

!insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall"
!insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath"
!insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg"

${Print} "Removing files and folders..."
nsExec::Exec 'cmd.exe /D /C RMDIR /Q /S "$INSTDIR"'

# In case the last command fails, run the slow method to remove leftover
RMDir /r /REBOOTOK "$INSTDIR"
{{ UNINSTALL_COMMANDS }}

${If} $INSTALLER_NAME_FULL != ""
DeleteRegKey SHCTX "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$INSTALLER_NAME_FULL"
Expand Down
Loading

0 comments on commit e462caf

Please sign in to comment.