From b38e95c3a56304a3eedde9aeed4c0bdb08d9508b Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 11 Oct 2024 11:13:02 -0700 Subject: [PATCH 01/22] Add option to use standalone binary for uninstallation --- constructor/construct.py | 5 +++++ constructor/main.py | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/constructor/construct.py b/constructor/construct.py index 1a3a2b9e7..df63d22d2 100644 --- a/constructor/construct.py +++ b/constructor/construct.py @@ -636,6 +636,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. '''), ] diff --git a/constructor/main.py b/constructor/main.py index 2f724bafd..e22b179a2 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -102,6 +102,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 or newer.") + logger.debug('conda packages download: %s', info['_download_dir']) for key in ('welcome_image_text', 'header_image_text'): @@ -184,7 +192,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" From c323060b6f19011af761d90269c374c3740daea3 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 11 Oct 2024 13:39:34 -0700 Subject: [PATCH 02/22] Add conda-standalone uninstall to NSIS template --- constructor/nsis/main.nsi.tmpl | 12 +------- constructor/winexe.py | 53 +++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 6ff4ecdc0..5385f8648 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -1406,9 +1406,7 @@ SectionEnd Section "Uninstall" ${LogSet} on - # 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 @@ -1453,15 +1451,7 @@ Section "Uninstall" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_PLAT", "${PLATFORM}").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_TYPE", "EXE").r0' - !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" diff --git a/constructor/winexe.py b/constructor/winexe.py index 16bcca7b5..39391e90d 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -12,6 +12,7 @@ from os.path import abspath, basename, dirname, isfile, join from pathlib import Path from subprocess import check_output, run +from textwrap import dedent from typing import List, Union from .construct import ns_platform @@ -213,6 +214,56 @@ def uninstall_menus_commands(info): return [line.strip() for line in lines] +def uninstall_commands_default(info): + return uninstall_menus_commands(info) + dedent(""" + !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" + """) + + +def uninstall_commands_conda_standalone(info): + return dedent(r""" + !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" + !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" + !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" + + ${Print} "Removing files and folders..." + $INSTDIR\_conda.exe uninstall $INSTDIR + # If only .conda_trash files are left behind, remove the directory + IfFileExists $INSTDIR 0 skip_rmdir + StrLen $R0 ".conda_trash" + IntOp $R0 0 - $R0 + FindFirst $0 $1 $INSTDIR\* + rmdir_loop: + StrCmp "" $1 end_rmdir_loop + StrCmp "." $1 advance_rmdir_loop + StrCmp ".." $1 advance_rmdir_loop + StrCpy $2 $1 "" $R0 + StrCmp $2 ".conda_trash" advance_rmdir_loop end_rmdir + advance_rmdir_loop: + FindNext $0 $1 + Goto rmdir_loop + end_rmdir_loop: + RMDir /r /REBOOTOK "$INSTDIR" + end_rmdir: + FindClose $0 + skip_rmdir: + """) + + +def uninstall_commands(info): + if info.get("uninstall_with_conda_exe"): + return uninstall_commands_conda_standalone(info) + return uninstall_commands_default(info) + + def make_nsi( info: dict, dir_path: str, @@ -378,7 +429,7 @@ def make_nsi( ('@UNINSTALL_NAME@', info.get('uninstall_name', '${NAME} ${VERSION} (Python ${PYVERSION} ${ARCH})' )), - ('@UNINSTALL_MENUS@', '\n '.join(uninstall_menus_commands(info))), + ('@UNINSTALL_COMMANDS@', '\n '.join(uninstall_commands(info))), ('@EXTRA_FILES@', '\n '.join(extra_files_commands(extra_files, dir_path))), ('@SCRIPT_ENV_VARIABLES@', '\n '.join(setup_script_env_variables(info))), ( From 21fc398adad6d1d1e769ec1de67c76368a680ac6 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 17 Oct 2024 13:22:28 -0700 Subject: [PATCH 03/22] Use AbortRetryNSExecWait to call conda-standalone uninstaller --- constructor/winexe.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/constructor/winexe.py b/constructor/winexe.py index 39391e90d..6fcda079a 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -225,7 +225,7 @@ def uninstall_commands_default(info): # In case the last command fails, run the slow method to remove leftover RMDir /r /REBOOTOK "$INSTDIR" - """) + """).splitlines() def uninstall_commands_conda_standalone(info): @@ -235,8 +235,14 @@ def uninstall_commands_conda_standalone(info): !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" ${Print} "Removing files and folders..." - $INSTDIR\_conda.exe uninstall $INSTDIR - # If only .conda_trash files are left behind, remove the directory + push '"$INSTDIR\_conda.exe" uninstall "$INSTDIR"' + push 'Failed to remove files and folders. Please see the log for more information.' + push 'WithLog' + SetDetailsPrint listonly + call un.AbortRetryNSExecWait + SetDetailsPrint both + + # If only install.log and .conda_trash files are left behind, remove the directory IfFileExists $INSTDIR 0 skip_rmdir StrLen $R0 ".conda_trash" IntOp $R0 0 - $R0 @@ -245,6 +251,7 @@ def uninstall_commands_conda_standalone(info): StrCmp "" $1 end_rmdir_loop StrCmp "." $1 advance_rmdir_loop StrCmp ".." $1 advance_rmdir_loop + StrCmp "install.log" $1 advance_rmdir_loop StrCpy $2 $1 "" $R0 StrCmp $2 ".conda_trash" advance_rmdir_loop end_rmdir advance_rmdir_loop: @@ -255,7 +262,7 @@ def uninstall_commands_conda_standalone(info): end_rmdir: FindClose $0 skip_rmdir: - """) + """).splitlines() def uninstall_commands(info): From 292e2ba729949f94bc3a1aa80bab40a4b6159dfa Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 29 Oct 2024 13:28:17 -0700 Subject: [PATCH 04/22] Add uninstallation options panel --- constructor/nsis/main.nsi.tmpl | 14 ++++++++++++-- constructor/winexe.py | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 5385f8648..0c4d47078 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -67,6 +67,10 @@ var /global StdOutHandleSet !include "Utils.nsh" +#if uninstall_with_conda_exe is True +!include "StandaloneUninstallerOptions.nsh" +#endif + !define NAME __NAME__ !define VERSION __VERSION__ !define COMPANY __COMPANY__ @@ -203,6 +207,9 @@ Page Custom mui_AnaCustomOptions_Show !insertmacro MUI_UNPAGE_WELCOME !define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.OnDirectoryLeave !insertmacro MUI_UNPAGE_CONFIRM +#if uninstall_with_conda_exe is True +UninstPage Custom un.UninstCustomOptions_Show +#endif !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH @@ -773,8 +780,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} @@ -785,6 +791,10 @@ Function un.onInit StrCpy $QuietMode "1" ${EndIf} +#if uninstall_with_conda_exe is True + Call un.UninstCustomOptions_InitDefaults +#endif + Push $0 Push $1 Push $2 diff --git a/constructor/winexe.py b/constructor/winexe.py index 6fcda079a..8fef5353f 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -409,6 +409,7 @@ def make_nsi( ppd["custom_conclusion"] = info.get("conclusion_file", "").endswith(".nsi") ppd["has_license"] = bool(info.get("license_file")) ppd["post_install_pages"] = bool(info.get("post_install_pages")) + ppd["uninstall_with_conda_exe"] = bool(info.get("uninstall_with_conda_exe")) data = preprocess(data, ppd) data = fill_template(data, replace, exceptions=nsis_predefines) From d5e11b5483c100178a21ba21b6bfc1de063a1d3a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 29 Oct 2024 14:09:06 -0700 Subject: [PATCH 05/22] Improve typing for uninstall command templating --- constructor/winexe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/constructor/winexe.py b/constructor/winexe.py index 8fef5353f..6735350a8 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -196,7 +196,7 @@ def setup_envs_commands(info, dir_path): return [line.strip() for line in lines] -def uninstall_menus_commands(info): +def uninstall_menus_commands(info: dict) -> List[str]: tmpl = r""" SetDetailsPrint both ${{Print}} "Deleting {name} menus in {env_name}..." @@ -214,7 +214,7 @@ def uninstall_menus_commands(info): return [line.strip() for line in lines] -def uninstall_commands_default(info): +def uninstall_commands_default(info: dict) -> List[str]: return uninstall_menus_commands(info) + dedent(""" !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" @@ -228,7 +228,7 @@ def uninstall_commands_default(info): """).splitlines() -def uninstall_commands_conda_standalone(info): +def uninstall_commands_conda_standalone() -> List[str]: return dedent(r""" !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" @@ -265,9 +265,9 @@ def uninstall_commands_conda_standalone(info): """).splitlines() -def uninstall_commands(info): +def uninstall_commands(info: dict) -> List[str]: if info.get("uninstall_with_conda_exe"): - return uninstall_commands_conda_standalone(info) + return uninstall_commands_conda_standalone() return uninstall_commands_default(info) From 67d36205add7dc79c67e946e10d0bb37797df30e Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 30 Oct 2024 10:03:49 -0700 Subject: [PATCH 06/22] Implement conda-standalone uninstall command into Windows uninstaller workflow --- constructor/main.py | 2 +- constructor/nsis/main.nsi.tmpl | 65 +++++++++++++++++++++++++++++++++- constructor/winexe.py | 23 +++++++++++- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/constructor/main.py b/constructor/main.py index e22b179a2..9df0d65d4 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -108,7 +108,7 @@ def main_build(dir_path, output_dir='.', platform=cc_platform, 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 or newer.") + sys.exit("Error: uninstalling with conda.exe requires conda-standalone 24.11.0 or newer.") logger.debug('conda packages download: %s', info['_download_dir']) diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 0c4d47078..d108bc258 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -114,6 +114,11 @@ var /global ARGV_NoScripts var /global ARGV_NoShortcuts var /global ARGV_CheckPathLength var /global ARGV_QuietMode +#if uninstall_with_conda_exe is True +var /global ARGV_Uninst_RemoveCondaRcs +var /global ARGV_Uninst_RemoveCaches +var /global ARGV_Uninst_CondaClean +#endif var /global IsDomainUser var /global CheckPathLength @@ -749,7 +754,7 @@ Function .onInit Pop $0 FunctionEnd -Function un.onInit +!macro un.ParseCommandLineArgs ClearErrors ${GetParameters} $ARGV ${GetOptions} $ARGV "/?" $ARGV_Help @@ -769,6 +774,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 is True + /RemoveCondaRcs=[none|users|system|all] [default: none]$\n\ + /RemoveCaches=[0|1] [default: 0]$\n\ + /CondaClean=[0|1] [default: 0]$\n\ +#endif /_?=[installation directory] (must be last parameter)$\n\ $\n\ EXAMPLES$\n\ @@ -790,6 +800,58 @@ Function un.onInit ${IfNot} ${Errors} StrCpy $QuietMode "1" ${EndIf} +#if uninstall_with_conda_exe is True + ClearErrors + ${GetOptions} $ARGV "/RemoveCondaRcs=" $ARGV_Uninst_RemoveCondaRcs + ${IfNot} ${Errors} + ${IfNot} ${UAC_IsAdmin} + ${If} $ARGV_Uninst_RemoveCondaRcs != "all" + ${If} $ARGV_Uninst_RemoveCondaRcs != "system" + goto parse_condarc_args + ${EndIf} + ${EndIf} + MessageBox MB_ICONSTOP "Removing system .condarc files requires an elevated prompt." + Abort + ${EndIf} + parse_condarc_args: + ${If} $ARGV_Uninst_RemoveCondaRcs == "user" + StrCpy $UninstRemoveCondaRcs_User_State ${BST_CHECKED} + StrCpy $UninstRemoveCondaRcs_System_State ${BST_UNCHECKED} + ${ElseIf} $ARGV_Uninst_RemoveCondaRcs == "system" + StrCpy $UninstRemoveCondaRcs_User_State ${BST_UNCHECKED} + StrCpy $UninstRemoveCondaRcs_System_State ${BST_CHECKED} + ${ElseIf} $ARGV_Uninst_RemoveCondaRcs == "all" + StrCpy $UninstRemoveCondaRcs_User_State ${BST_CHECKED} + StrCpy $UninstRemoveCondaRcs_System_State ${BST_CHECKED} + ${Else} + StrCpy $UninstRemoveCondaRcs_User_State ${BST_UNCHECKED} + StrCpy $UninstRemoveCondaRcs_System_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} + + ClearErrors + ${GetOptions} $ARGV "/CondaClean=" $ARGV_Uninst_CondaClean + ${IfNot} ${Errors} + ${If} $ARGV_Uninst_CondaClean = "1" + StrCpy $UninstCondaClean_State ${BST_CHECKED} + ${ElseIf} $ARGV_Uninst_CondaClean = "0" + StrCpy $UninstCondaClean_State ${BST_UNCHECKED} + ${EndIf} + ${EndIf} +#endif +!macroend + +Function un.onInit #if uninstall_with_conda_exe is True Call un.UninstCustomOptions_InitDefaults @@ -1415,6 +1477,7 @@ SectionEnd Section "Uninstall" ${LogSet} on + !insertmacro un.ParseCommandLineArgs System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0' diff --git a/constructor/winexe.py b/constructor/winexe.py index 6735350a8..253fa266c 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -234,8 +234,29 @@ def uninstall_commands_conda_standalone() -> List[str]: !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" + # Parse arguments + StrCpy $R0 "" + + ${If} $UninstRemoveCondaRcs_User_State == ${BST_CHECKED} + ${If} $UninstRemoveCondaRcs_System_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-condarcs=all" + ${Else} + StrCpy $R0 "$R0 --remove-condarcs=user" + ${EndIf} + ${ElseIf} $UninstRemoveCondaRcs_System_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-condarcs=system" + ${EndIf} + + ${If} $UninstRemoveCaches_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-caches" + ${EndIf} + + ${If} $UninstCondaClean_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --conda-clean" + ${EndIf} + ${Print} "Removing files and folders..." - push '"$INSTDIR\_conda.exe" uninstall "$INSTDIR"' + push '"$INSTDIR\_conda.exe" uninstall $R0 "$INSTDIR"' push 'Failed to remove files and folders. Please see the log for more information.' push 'WithLog' SetDetailsPrint listonly From 00b1608cf035d1a0a3a71deb813fadd802c1db3c Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Fri, 1 Nov 2024 09:07:21 -0700 Subject: [PATCH 07/22] Add standalone uninstaller options panel --- .../nsis/StandaloneUninstallerOptions.nsh | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 constructor/nsis/StandaloneUninstallerOptions.nsh diff --git a/constructor/nsis/StandaloneUninstallerOptions.nsh b/constructor/nsis/StandaloneUninstallerOptions.nsh new file mode 100644 index 000000000..04588d6f0 --- /dev/null +++ b/constructor/nsis/StandaloneUninstallerOptions.nsh @@ -0,0 +1,109 @@ +var UninstCustomOptions +var UninstCustomOptions.RemoveCondaRcs_User +var UninstCustomOptions.RemoveCondaRcs_System +var UninstCustomOptions.RemoveCaches +var UninstCustomOptions.CondaClean + +# These are the checkbox states, to be used by the uninstaller +var UninstRemoveCondaRcs_User_State +var UninstRemoveCondaRcs_System_State +var UninstRemoveCaches_State +var UninstCondaClean_State + +Function un.UninstCustomOptions_InitDefaults + StrCpy $UninstRemoveCondaRcs_User_State ${BST_UNCHECKED} + StrCpy $UninstRemoveCondaRcs_System_State ${BST_UNCHECKED} + StrCpy $UninstRemoveCaches_State ${BST_UNCHECKED} + StrCpy $UninstCondaClean_State ${BST_UNCHECKED} +FunctionEnd + +Function un.UninstCustomOptions_Show + ${If} $UninstCondaClean_State == "" + Abort + ${EndIf} + # Create dialog + nsDialogs::Create 1018 + Pop $UninstCustomOptions + ${If} $UninstCustomOptions == error + Abort + ${EndIf} + + !insertmacro MUI_HEADER_TEXT \ + "Advanced uninstallation options" \ + "Remove configuration 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 .condarc files + ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove user configuration files." + IntOp $5 $5 + 11 + Pop $UninstCustomOptions.RemoveCondaRcs_User + ${NSD_SetState} $UninstCustomOptions.RemoveCondaRcs_User $UninstRemoveCondaRcs_User_State + ${NSD_OnClick} $UninstCustomOptions.RemoveCondaRcs_User un.UninstRemoveCondarcs_User_Onclick + ${NSD_CreateLabel} 5% "$5u" 90% 10u \ + "This removes .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.RemoveCondaRcs_System + ${NSD_SetState} $UninstCustomOptions.RemoveCondaRcs_System $UninstRemoveCondaRcs_System_State + ${NSD_OnClick} $UninstCustomOptions.RemoveCondaRcs_System un.UninstRemoveCondarcs_System_Onclick + ${NSD_CreateLabel} 5% "$5u" 90% 10u \ + "This removes .condarc files in the ProgramData directory." + IntOp $5 $5 + 10 + ${EndIf} + + # Option to remove cache files + ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove cache files." + 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 the .conda directory in the user folder and conda system cache files." + IntOp $5 $5 + 10 + + # Option to run conda --clean + ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove index and package files." + IntOp $5 $5 + 11 + Pop $UninstCustomOptions.CondaClean + ${NSD_SetState} $UninstCustomOptions.CondaClean $UninstCondaClean_State + ${NSD_OnClick} $UninstCustomOptions.CondaClean un.UninstCondaClean_Onclick + ${NSD_CreateLabel} 5% "$5u" 90% 20u \ + "This removes index and unused package files by running conda clean --all. \ + Only useful if pkgs_dirs is set in a .condarc file." + 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.UninstRemoveCondarcs_User_OnClick + Pop $0 + ${NSD_GetState} $0 $UninstRemoveCondarcs_User_State +FunctionEnd + +Function un.UninstRemoveCondarcs_System_OnClick + Pop $0 + ${NSD_GetState} $0 $UninstRemoveCondarcs_System_State +FunctionEnd + +Function un.UninstRemoveCaches_OnClick + Pop $0 + ${NSD_GetState} $0 $UninstRemoveCaches_State +FunctionEnd + +Function un.UninstCondaClean_OnClick + Pop $0 + ${NSD_GetState} $0 $UninstCondaClean_State +FunctionEnd From a5b3ac383da8ef5db8e0cfc21a4b8106fc739d7a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 11 Nov 2024 15:02:04 -0800 Subject: [PATCH 08/22] Update documentation --- CONSTRUCT.md | 8 ++++++++ docs/source/construct-yaml.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/CONSTRUCT.md b/CONSTRUCT.md index 1c978d434..9669da69d 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -867,6 +867,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
+_type:_ boolean
+ +Use the standalone binary to perform the uninstallation. +Requires conda-standalone 24.11.0 or newer. + ## Available selectors - `aarch64` diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index 1c978d434..9669da69d 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -867,6 +867,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
+_type:_ boolean
+ +Use the standalone binary to perform the uninstallation. +Requires conda-standalone 24.11.0 or newer. + ## Available selectors - `aarch64` From 9ecf634eab32b39b979d262e20cff981df8ccfbd Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 12 Nov 2024 14:40:27 -0800 Subject: [PATCH 09/22] Make /RemoceCondaRcs CLI parsing logic more concise --- constructor/nsis/StandaloneUninstallerOptions.nsh | 4 ++-- constructor/nsis/main.nsi.tmpl | 15 +++++++-------- constructor/winexe.py | 4 +++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/constructor/nsis/StandaloneUninstallerOptions.nsh b/constructor/nsis/StandaloneUninstallerOptions.nsh index 04588d6f0..25e176090 100644 --- a/constructor/nsis/StandaloneUninstallerOptions.nsh +++ b/constructor/nsis/StandaloneUninstallerOptions.nsh @@ -29,8 +29,8 @@ Function un.UninstCustomOptions_Show ${EndIf} !insertmacro MUI_HEADER_TEXT \ - "Advanced uninstallation options" \ - "Remove configuration and cache files" + "Advanced uninstallation options" \ + "Remove configuration 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 diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index e00f94088..6cf3e0b05 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -805,15 +805,12 @@ FunctionEnd ${GetOptions} $ARGV "/RemoveCondaRcs=" $ARGV_Uninst_RemoveCondaRcs ${IfNot} ${Errors} ${IfNot} ${UAC_IsAdmin} - ${If} $ARGV_Uninst_RemoveCondaRcs != "all" - ${If} $ARGV_Uninst_RemoveCondaRcs != "system" - goto parse_condarc_args - ${EndIf} + ${If} $ARGV_Uninst_RemoveCondaRcs == "all" + ${OrIf} $ARGV_Uninst_RemoveCondaRcs == "system" + MessageBox MB_ICONSTOP "Removing system .condarc files requires an elevated prompt." + Abort ${EndIf} - MessageBox MB_ICONSTOP "Removing system .condarc files requires an elevated prompt." - Abort ${EndIf} - parse_condarc_args: ${If} $ARGV_Uninst_RemoveCondaRcs == "user" StrCpy $UninstRemoveCondaRcs_User_State ${BST_CHECKED} StrCpy $UninstRemoveCondaRcs_System_State ${BST_UNCHECKED} @@ -1482,7 +1479,9 @@ SectionEnd Section "Uninstall" ${LogSet} on - !insertmacro un.ParseCommandLineArgs + ${If} ${Silent} + !insertmacro un.ParseCommandLineArgs + ${EndIf} System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0' diff --git a/constructor/winexe.py b/constructor/winexe.py index 4d5513bd2..01f19c74d 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -265,7 +265,8 @@ def uninstall_commands_conda_standalone() -> List[str]: call un.AbortRetryNSExecWait SetDetailsPrint both - # If only install.log and .conda_trash files are left behind, remove the directory + # If only install.log, the uninstaller, and .conda_trash files are left behind, + # remove the directory IfFileExists $INSTDIR 0 skip_rmdir StrLen $R0 ".conda_trash" IntOp $R0 0 - $R0 @@ -275,6 +276,7 @@ def uninstall_commands_conda_standalone() -> List[str]: StrCmp "." $1 advance_rmdir_loop StrCmp ".." $1 advance_rmdir_loop StrCmp "install.log" $1 advance_rmdir_loop + StrCmp "Uninstall-${NAME}.exe" $1 advance_rmdir_loop StrCpy $2 $1 "" $R0 StrCmp $2 ".conda_trash" advance_rmdir_loop end_rmdir advance_rmdir_loop: From 4209d7bbc70994826b07291fcc257fcfddbcfdd4 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 12 Nov 2024 15:01:30 -0800 Subject: [PATCH 10/22] Add tests --- tests/test_examples.py | 91 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index f16200f44..d35164187 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -141,7 +141,12 @@ def _run_installer_exe(installer, install_dir, installer_input=None, timeout=420 return process -def _run_uninstaller_exe(install_dir, timeout=420, check=True): +def _run_uninstaller_exe( + install_dir: Path, + timeout: int = 420, + check: bool = True, + options: list | None = None, +) -> subprocess.CompletedProcess | None: # Now test the uninstallers if " " in str(install_dir): # TODO: We can't seem to run the uninstaller when there are spaces in the PATH @@ -150,6 +155,7 @@ def _run_uninstaller_exe(install_dir, timeout=420, check=True): "This is a known issue with our setup, to be fixed." ) return + options = options or [] # Rename install.log install_log = install_dir / "install.log" if install_log.exists(): @@ -164,11 +170,13 @@ def _run_uninstaller_exe(install_dir, timeout=420, check=True): "start", "/wait", str(uninstaller), + "/S", + *options, # We need silent mode + "uninstaller location" (_?=...) so the command can # be waited; otherwise, since the uninstaller copies itself to a different # location so it can be auto-deleted, it returns immediately and it gives # us problems with the tempdir cleanup later - f"/S _?={install_dir}", + f"_?={install_dir}", ] process = _execute(cmd, timeout=timeout, check=check) if check: @@ -908,3 +916,82 @@ def test_ignore_condarc_files(tmp_path, monkeypatch, request): # the bogus .condarc file. # pkg installers unfortunately do not output any errors into the log. assert proc.stderr.count("Bad conversion of configurable") == 4 + + +@pytest.mark.skipif( + CONDA_EXE == StandaloneExe.CONDA and CONDA_EXE_VERSION < Version("24.11.0"), + reason="Requires conda-standalone 24.11.x or newer", +) +@pytest.mark.skipif(not sys.platform == "win32", reason="Windows only") +@pytest.mark.skipif(not ON_CI, reason="CI only - Interacts with system files") +@pytest.mark.parametrize( + "remove_caches,conda_clean,remove_condarcs", + ( + pytest.param(False, False, None, id="keep files"), + pytest.param(True, True, "all", id="remove all files"), + pytest.param(False, False, "system", id="remove system .condarc files"), + pytest.param(False, False, "user", id="remove user .condarc files"), + ) +) +def test_uninstallation_standalone( + monkeypatch, + remove_caches: bool, + conda_clean: bool, + remove_condarcs: str | None, + tmp_path: Path, +): + recipe_path = _example_path("customize_controls") + input_path = tmp_path / "input" + shutil.copytree(str(recipe_path), str(input_path)) + with open(input_path / "construct.yaml", "a") as construct: + construct.write("uninstall_with_conda_exe: true\n") + installer, install_dir = next(create_installer(input_path, tmp_path)) + monkeypatch.setenv("USERPROFILE", str(tmp_path)) + _run_installer( + input_path, + installer, + install_dir, + check_subprocess=True, + uninstall=False, + ) + + # Set up files for removal. + # Since conda-standalone is extensively tested upstream, + # only set up a minimum set of files. + dot_conda_dir = tmp_path / ".conda" + assert dot_conda_dir.exists() + + # Minimum set of files needed for an index cache + pkg_cache = tmp_path / "pkgs" + (pkg_cache / "cache").mkdir(parents=True) + (pkg_cache / "urls.txt").touch() + + user_rc = tmp_path / ".condarc" + system_rc = Path("C:/ProgramData/conda/.condarc") + system_rc.parent.mkdir(parents=True) + condarc = f"pkgs_dirs:\n - {pkg_cache}\n" + user_rc.write_text(condarc) + system_rc.write_text(condarc) + + uninstall_options = [] + remove_system_rcs = False + remove_user_rcs = False + if remove_condarcs is not None: + uninstall_options.append(f"/RemoveCondaRcs={remove_condarcs}") + remove_system_rcs = remove_condarcs != "user" + remove_user_rcs = remove_condarcs != "system" + if remove_caches: + uninstall_options.append("/RemoveCaches=1") + if conda_clean: + uninstall_options.append("/CondaClean=1") + + try: + _run_uninstaller_exe(install_dir, check=False, options=uninstall_options) + assert not install_dir.exists() or not next(install_dir.glob("*.conda_trash"), None) + assert dot_conda_dir.exists() != remove_caches + assert pkg_cache.exists() != conda_clean + assert system_rc.exists() != remove_system_rcs + assert user_rc.exists() != remove_user_rcs + finally: + if system_rc.parent.exists(): + shutil.rmtree(system_rc.parent) From f6e5d83fedce09c04dedabe0322a81a0097fc3ea Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 12 Nov 2024 16:57:29 -0800 Subject: [PATCH 11/22] Add news file --- news/897-standalone-uninstallation | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 news/897-standalone-uninstallation diff --git a/news/897-standalone-uninstallation b/news/897-standalone-uninstallation new file mode 100644 index 000000000..48d6b9c5c --- /dev/null +++ b/news/897-standalone-uninstallation @@ -0,0 +1,19 @@ +### Enhancements + +* Implement feature to run uninstallation via conda-standalone (see https://github.com/conda/conda-standalone/pull/112). (#897) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 7efbc74cb08d7d945bc1dac10e75679692d17cec Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 12 Nov 2024 17:12:05 -0800 Subject: [PATCH 12/22] Replace pipe operator with Union --- tests/test_examples.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index d35164187..17dcd2e7e 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -11,7 +11,7 @@ from functools import lru_cache from pathlib import Path from plistlib import load as plist_load -from typing import Generator, Iterable, Optional, Tuple +from typing import Generator, Iterable, Optional, Tuple, Union import pytest from conda.base.context import context @@ -145,8 +145,8 @@ def _run_uninstaller_exe( install_dir: Path, timeout: int = 420, check: bool = True, - options: list | None = None, -) -> subprocess.CompletedProcess | None: + options: Union[list, None] = None, +) -> Union[subprocess.CompletedProcess, None]: # Now test the uninstallers if " " in str(install_dir): # TODO: We can't seem to run the uninstaller when there are spaces in the PATH @@ -937,7 +937,7 @@ def test_uninstallation_standalone( monkeypatch, remove_caches: bool, conda_clean: bool, - remove_condarcs: str | None, + remove_condarcs: Union[str, None], tmp_path: Path, ): recipe_path = _example_path("customize_controls") From d1f221db9b271f897d546d1f4c7725127a2aa476 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 18 Nov 2024 09:30:40 -0800 Subject: [PATCH 13/22] Ensure that ON_CI=False for CI="0" --- tests/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 17dcd2e7e..8dbeffa14 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -33,7 +33,7 @@ pytestmark = pytest.mark.examples REPO_DIR = Path(__file__).parent.parent -ON_CI = os.environ.get("CI") +ON_CI = bool(os.environ.get("CI")) and os.environ.get("CI") != "0" CONSTRUCTOR_CONDA_EXE = os.environ.get("CONSTRUCTOR_CONDA_EXE") CONDA_EXE, CONDA_EXE_VERSION = identify_conda_exe(CONSTRUCTOR_CONDA_EXE) if CONDA_EXE_VERSION is not None: From 19dc456d11b6ff4dadf67018722b7bec2bc76ed6 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 25 Nov 2024 11:18:35 -0800 Subject: [PATCH 14/22] Update uninstallation commands --- .../nsis/StandaloneUninstallerOptions.nsh | 60 +++++++++---------- constructor/nsis/main.nsi.tmpl | 56 ++++++++--------- constructor/winexe.py | 22 +++---- tests/test_examples.py | 22 +++---- 4 files changed, 80 insertions(+), 80 deletions(-) diff --git a/constructor/nsis/StandaloneUninstallerOptions.nsh b/constructor/nsis/StandaloneUninstallerOptions.nsh index 25e176090..003174aa8 100644 --- a/constructor/nsis/StandaloneUninstallerOptions.nsh +++ b/constructor/nsis/StandaloneUninstallerOptions.nsh @@ -1,24 +1,24 @@ var UninstCustomOptions -var UninstCustomOptions.RemoveCondaRcs_User -var UninstCustomOptions.RemoveCondaRcs_System +var UninstCustomOptions.RemoveConfigFiles_User +var UninstCustomOptions.RemoveConfigFiles_System +var UninstCustomOptions.RemoveUserData var UninstCustomOptions.RemoveCaches -var UninstCustomOptions.CondaClean # These are the checkbox states, to be used by the uninstaller -var UninstRemoveCondaRcs_User_State -var UninstRemoveCondaRcs_System_State +var UninstRemoveConfigFiles_User_State +var UninstRemoveConfigFiles_System_State +var UninstRemoveUserData_State var UninstRemoveCaches_State -var UninstCondaClean_State Function un.UninstCustomOptions_InitDefaults - StrCpy $UninstRemoveCondaRcs_User_State ${BST_UNCHECKED} - StrCpy $UninstRemoveCondaRcs_System_State ${BST_UNCHECKED} + StrCpy $UninstRemoveConfigFiles_User_State ${BST_UNCHECKED} + StrCpy $UninstRemoveConfigFiles_System_State ${BST_UNCHECKED} + StrCpy $UninstRemoveUserData_State ${BST_UNCHECKED} StrCpy $UninstRemoveCaches_State ${BST_UNCHECKED} - StrCpy $UninstCondaClean_State ${BST_UNCHECKED} FunctionEnd Function un.UninstCustomOptions_Show - ${If} $UninstCondaClean_State == "" + ${If} $UninstRemoveCaches_State == "" Abort ${EndIf} # Create dialog @@ -39,9 +39,9 @@ Function un.UninstCustomOptions_Show # Option to remove .condarc files ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove user configuration files." IntOp $5 $5 + 11 - Pop $UninstCustomOptions.RemoveCondaRcs_User - ${NSD_SetState} $UninstCustomOptions.RemoveCondaRcs_User $UninstRemoveCondaRcs_User_State - ${NSD_OnClick} $UninstCustomOptions.RemoveCondaRcs_User un.UninstRemoveCondarcs_User_Onclick + 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 .condarc files in the Users directory." IntOp $5 $5 + 10 @@ -49,9 +49,9 @@ Function un.UninstCustomOptions_Show ${If} ${UAC_IsAdmin} ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove system-wide configuration files." IntOp $5 $5 + 11 - Pop $UninstCustomOptions.RemoveCondaRcs_System - ${NSD_SetState} $UninstCustomOptions.RemoveCondaRcs_System $UninstRemoveCondaRcs_System_State - ${NSD_OnClick} $UninstCustomOptions.RemoveCondaRcs_System un.UninstRemoveCondarcs_System_Onclick + 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 .condarc files in the ProgramData directory." IntOp $5 $5 + 10 @@ -60,9 +60,9 @@ Function un.UninstCustomOptions_Show # Option to remove cache files ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove cache files." IntOp $5 $5 + 11 - Pop $UninstCustomOptions.RemoveCaches - ${NSD_SetState} $UninstCustomOptions.RemoveCaches $UninstRemoveCaches_State - ${NSD_OnClick} $UninstCustomOptions.RemoveCaches un.UninstRemoveCaches_Onclick + Pop $UninstCustomOptions.RemoveUserData + ${NSD_SetState} $UninstCustomOptions.RemoveUserData $UninstRemoveUserData_State + ${NSD_OnClick} $UninstCustomOptions.RemoveUserData un.UninstRemoveUserData_Onclick ${NSD_CreateLabel} 5% "$5u" 90% 10u \ "This removes the .conda directory in the user folder and conda system cache files." IntOp $5 $5 + 10 @@ -70,9 +70,9 @@ Function un.UninstCustomOptions_Show # Option to run conda --clean ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove index and package files." IntOp $5 $5 + 11 - Pop $UninstCustomOptions.CondaClean - ${NSD_SetState} $UninstCustomOptions.CondaClean $UninstCondaClean_State - ${NSD_OnClick} $UninstCustomOptions.CondaClean un.UninstCondaClean_Onclick + Pop $UninstCustomOptions.RemoveCaches + ${NSD_SetState} $UninstCustomOptions.RemoveCaches $UninstRemoveCaches_State + ${NSD_OnClick} $UninstCustomOptions.RemoveCaches un.UninstRemoveCaches_Onclick ${NSD_CreateLabel} 5% "$5u" 90% 20u \ "This removes index and unused package files by running conda clean --all. \ Only useful if pkgs_dirs is set in a .condarc file." @@ -88,22 +88,22 @@ Function un.UninstCustomOptions_Show nsDialogs::Show FunctionEnd -Function un.UninstRemoveCondarcs_User_OnClick +Function un.UninstRemoveConfigFiles_User_OnClick Pop $0 - ${NSD_GetState} $0 $UninstRemoveCondarcs_User_State + ${NSD_GetState} $0 $UninstRemoveConfigFiles_User_State FunctionEnd -Function un.UninstRemoveCondarcs_System_OnClick +Function un.UninstRemoveConfigFiles_System_OnClick Pop $0 - ${NSD_GetState} $0 $UninstRemoveCondarcs_System_State + ${NSD_GetState} $0 $UninstRemoveConfigFiles_System_State FunctionEnd -Function un.UninstRemoveCaches_OnClick +Function un.UninstRemoveUserData_OnClick Pop $0 - ${NSD_GetState} $0 $UninstRemoveCaches_State + ${NSD_GetState} $0 $UninstRemoveUserData_State FunctionEnd -Function un.UninstCondaClean_OnClick +Function un.UninstRemoveCaches_OnClick Pop $0 - ${NSD_GetState} $0 $UninstCondaClean_State + ${NSD_GetState} $0 $UninstRemoveCaches_State FunctionEnd diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 6cf3e0b05..4c908e0f1 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -115,9 +115,9 @@ var /global ARGV_NoShortcuts var /global ARGV_CheckPathLength var /global ARGV_QuietMode #if uninstall_with_conda_exe is True -var /global ARGV_Uninst_RemoveCondaRcs +var /global ARGV_Uninst_RemoveConfigFiles +var /global ARGV_Uninst_RemoveUserData var /global ARGV_Uninst_RemoveCaches -var /global ARGV_Uninst_CondaClean #endif var /global IsDomainUser @@ -775,9 +775,9 @@ FunctionEnd /S (run in CLI/headless mode)$\n\ /Q (quiet mode, do not print output to console)$\n\ #if uninstall_with_conda_exe is True - /RemoveCondaRcs=[none|users|system|all] [default: none]$\n\ /RemoveCaches=[0|1] [default: 0]$\n\ - /CondaClean=[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\ @@ -802,47 +802,47 @@ FunctionEnd ${EndIf} #if uninstall_with_conda_exe is True ClearErrors - ${GetOptions} $ARGV "/RemoveCondaRcs=" $ARGV_Uninst_RemoveCondaRcs + ${GetOptions} $ARGV "/RemoveConfigFiles=" $ARGV_Uninst_RemoveConfigFiles ${IfNot} ${Errors} ${IfNot} ${UAC_IsAdmin} - ${If} $ARGV_Uninst_RemoveCondaRcs == "all" - ${OrIf} $ARGV_Uninst_RemoveCondaRcs == "system" + ${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_RemoveCondaRcs == "user" - StrCpy $UninstRemoveCondaRcs_User_State ${BST_CHECKED} - StrCpy $UninstRemoveCondaRcs_System_State ${BST_UNCHECKED} - ${ElseIf} $ARGV_Uninst_RemoveCondaRcs == "system" - StrCpy $UninstRemoveCondaRcs_User_State ${BST_UNCHECKED} - StrCpy $UninstRemoveCondaRcs_System_State ${BST_CHECKED} - ${ElseIf} $ARGV_Uninst_RemoveCondaRcs == "all" - StrCpy $UninstRemoveCondaRcs_User_State ${BST_CHECKED} - StrCpy $UninstRemoveCondaRcs_System_State ${BST_CHECKED} + ${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 $UninstRemoveCondaRcs_User_State ${BST_UNCHECKED} - StrCpy $UninstRemoveCondaRcs_System_State ${BST_UNCHECKED} + StrCpy $UninstRemoveConfigFiles_User_State ${BST_UNCHECKED} + StrCpy $UninstRemoveConfigFiles_System_State ${BST_UNCHECKED} ${EndIf} ${EndIf} ClearErrors - ${GetOptions} $ARGV "/RemoveCaches=" $ARGV_Uninst_RemoveCaches + ${GetOptions} $ARGV "/RemoveUserData=" $ARGV_Uninst_RemoveUserData ${IfNot} ${Errors} - ${If} $ARGV_Uninst_RemoveCaches = "1" - StrCpy $UninstRemoveCaches_State ${BST_CHECKED} - ${ElseIf} $ARGV_Uninst_RemoveCaches = "0" - StrCpy $UninstRemoveCaches_State ${BST_UNCHECKED} + ${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 "/CondaClean=" $ARGV_Uninst_CondaClean + ${GetOptions} $ARGV "/RemoveCaches=" $ARGV_Uninst_RemoveCaches ${IfNot} ${Errors} - ${If} $ARGV_Uninst_CondaClean = "1" - StrCpy $UninstCondaClean_State ${BST_CHECKED} - ${ElseIf} $ARGV_Uninst_CondaClean = "0" - StrCpy $UninstCondaClean_State ${BST_UNCHECKED} + ${If} $ARGV_Uninst_RemoveCaches = "1" + StrCpy $UninstRemoveCaches_State ${BST_CHECKED} + ${ElseIf} $ARGV_Uninst_RemoveCaches = "0" + StrCpy $UninstRemoveCaches_State ${BST_UNCHECKED} ${EndIf} ${EndIf} #endif diff --git a/constructor/winexe.py b/constructor/winexe.py index 01f19c74d..faba7fda8 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -239,26 +239,26 @@ def uninstall_commands_conda_standalone() -> List[str]: # Parse arguments StrCpy $R0 "" - ${If} $UninstRemoveCondaRcs_User_State == ${BST_CHECKED} - ${If} $UninstRemoveCondaRcs_System_State == ${BST_CHECKED} - StrCpy $R0 "$R0 --remove-condarcs=all" + ${If} $UninstRemoveConfigFiles_User_State == ${BST_CHECKED} + ${If} $UninstRemoveConfigFiles_System_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-config-files=all" ${Else} - StrCpy $R0 "$R0 --remove-condarcs=user" + StrCpy $R0 "$R0 --remove-config-files=user" ${EndIf} - ${ElseIf} $UninstRemoveCondaRcs_System_State == ${BST_CHECKED} - StrCpy $R0 "$R0 --remove-condarcs=system" + ${ElseIf} $UninstRemoveConfigFiles_System_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-config-files=system" ${EndIf} - ${If} $UninstRemoveCaches_State == ${BST_CHECKED} - StrCpy $R0 "$R0 --remove-caches" + ${If} $UninstRemoveUserData_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-user-data" ${EndIf} - ${If} $UninstCondaClean_State == ${BST_CHECKED} - StrCpy $R0 "$R0 --conda-clean" + ${If} $UninstRemoveCaches_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-caches" ${EndIf} ${Print} "Removing files and folders..." - push '"$INSTDIR\_conda.exe" uninstall $R0 "$INSTDIR"' + push '"$INSTDIR\_conda.exe" constructor uninstall $R0 --prefix "$INSTDIR"' push 'Failed to remove files and folders. Please see the log for more information.' push 'WithLog' SetDetailsPrint listonly diff --git a/tests/test_examples.py b/tests/test_examples.py index 8dbeffa14..b46bb7f91 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -925,7 +925,7 @@ def test_ignore_condarc_files(tmp_path, monkeypatch, request): @pytest.mark.skipif(not sys.platform == "win32", reason="Windows only") @pytest.mark.skipif(not ON_CI, reason="CI only - Interacts with system files") @pytest.mark.parametrize( - "remove_caches,conda_clean,remove_condarcs", + "remove_user_data,remove_caches,remove_config_files", ( pytest.param(False, False, None, id="keep files"), pytest.param(True, True, "all", id="remove all files"), @@ -935,9 +935,9 @@ def test_ignore_condarc_files(tmp_path, monkeypatch, request): ) def test_uninstallation_standalone( monkeypatch, + remove_user_data: bool, remove_caches: bool, - conda_clean: bool, - remove_condarcs: Union[str, None], + remove_config_files: Union[str, None], tmp_path: Path, ): recipe_path = _example_path("customize_controls") @@ -976,20 +976,20 @@ def test_uninstallation_standalone( uninstall_options = [] remove_system_rcs = False remove_user_rcs = False - if remove_condarcs is not None: - uninstall_options.append(f"/RemoveCondaRcs={remove_condarcs}") - remove_system_rcs = remove_condarcs != "user" - remove_user_rcs = remove_condarcs != "system" + if remove_config_files is not None: + uninstall_options.append(f"/RemoveConfigFiles={remove_config_files}") + remove_system_rcs = remove_config_files != "user" + remove_user_rcs = remove_config_files != "system" + if remove_user_data: + uninstall_options.append("/RemoveUserData=1") if remove_caches: uninstall_options.append("/RemoveCaches=1") - if conda_clean: - uninstall_options.append("/CondaClean=1") try: _run_uninstaller_exe(install_dir, check=False, options=uninstall_options) assert not install_dir.exists() or not next(install_dir.glob("*.conda_trash"), None) - assert dot_conda_dir.exists() != remove_caches - assert pkg_cache.exists() != conda_clean + assert dot_conda_dir.exists() != remove_user_data + assert pkg_cache.exists() != remove_caches assert system_rc.exists() != remove_system_rcs assert user_rc.exists() != remove_user_rcs finally: From 014f69f796102af4abbb68dee5c542aeae31047e Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 25 Nov 2024 11:21:51 -0800 Subject: [PATCH 15/22] Update CLI options documentation --- docs/source/cli-options.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/source/cli-options.md b/docs/source/cli-options.md index a67abc936..c395bb6df 100644 --- a/docs/source/cli-options.md +++ b/docs/source/cli-options.md @@ -54,6 +54,13 @@ $ CONDA_VERBOSITY=3 bash -x my_installer.sh Windows installers have the following CLI options available: +- `/NCRC`: disables the CRC check. +- `/S` (silent): runs the installer or uninstaller in headless mode. Installers created with + `constructor 3.10` or later will report information to the active console. Note that while the + installer output will be reported in the active console, the uninstaller output will happen in + a new console. See below for different invocation examples. +- `/Q` (quiet): do not report to the console in headless mode. Only relevant when used with `/S` + (see above). Also works for the uninstallers. - `/InstallationType=[JustMe|AllUsers]`: This flag sets the installation type. The default is `JustMe`. `AllUsers` might require elevated privileges. - `/AddToPath=[0|1]`: Whether to add the installation directory to the `PATH` environment @@ -68,19 +75,14 @@ Windows installers have the following CLI options available: `0`. - `/RegisterPython=[0|1]`: Whether to register Python as default in the Windows registry. Defaults to `1`. This is preferred to `AddToPath`. -- `/Q` (quiet): do not report to the console in headless mode. Only relevant when used with `/S` - (see below). Also works for the uninstallers. - -You can also supply [standard NSIS flags](https://nsis.sourceforge.io/Docs/Chapter3.html#installerusage), but only _after_ the ones mentioned above: - -- `/NCRC`: disables the CRC check. -- `/S` (silent): runs the installer or uninstaller in headless mode. Installers created with - `constructor 3.10` or later will report information to the active console. Note that while the - installer output will be reported in the active console, the uninstaller output will happen in - a new console. See below for different invocation examples. - `/D` (directory): sets the default installation directory. Note that even if the path contains spaces, it must be the last parameter used in the command line and must not contain any quotes. - Only absolute paths are supported. The uninstaller uses `_?` instead of `/D`. + Only absolute paths are supported. + +Some of these flags are [standard NSIS flags](https://nsis.sourceforge.io/Docs/Chapter3.html#installerusage). + +> [!IMPORTANT] +> Flags without arguments should precede flags with arguments to avoid parsing errors. ### Examples @@ -105,12 +107,12 @@ Run the installer in headless mode, for all users, adding to PATH and installing `````{tab-set} ````{tab-item} CMD ```pwsh -cmd.exe /C start /wait my_installer.exe /InstallationType=AllUsers /AddToPath=1 /S /D=C:\Program Files\my_app +cmd.exe /C start /wait my_installer.exe /S /InstallationType=AllUsers /AddToPath=1 /D=C:\Program Files\my_app ``` ```` ````{tab-item} PowerShell ```pwsh -Start-Process -FilePath .\my_installer.exe -ArgumentList "/InstallationType=AllUsers /AddToPath=1 /S /D=C:\Program Files\my_app" -NoNewWindow -Wait +Start-Process -FilePath .\my_installer.exe -ArgumentList "/S /InstallationType=AllUsers /AddToPath=1 /D=C:\Program Files\my_app" -NoNewWindow -Wait ``` ```` ````` From d851c571f553a3165f7f65a1ef1dcf71e7e33484 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 25 Nov 2024 11:42:01 -0800 Subject: [PATCH 16/22] Update uninstaller documentation --- docs/source/cli-options.md | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/source/cli-options.md b/docs/source/cli-options.md index c395bb6df..b9fa759fe 100644 --- a/docs/source/cli-options.md +++ b/docs/source/cli-options.md @@ -55,12 +55,10 @@ $ CONDA_VERBOSITY=3 bash -x my_installer.sh Windows installers have the following CLI options available: - `/NCRC`: disables the CRC check. -- `/S` (silent): runs the installer or uninstaller in headless mode. Installers created with - `constructor 3.10` or later will report information to the active console. Note that while the - installer output will be reported in the active console, the uninstaller output will happen in - a new console. See below for different invocation examples. +- `/S` (silent): runs the installer in headless mode. Installers created with `constructor 3.10` or + later will report information to the active console. See below for different invocation examples. - `/Q` (quiet): do not report to the console in headless mode. Only relevant when used with `/S` - (see above). Also works for the uninstallers. + (see above). - `/InstallationType=[JustMe|AllUsers]`: This flag sets the installation type. The default is `JustMe`. `AllUsers` might require elevated privileges. - `/AddToPath=[0|1]`: Whether to add the installation directory to the `PATH` environment @@ -81,6 +79,29 @@ Windows installers have the following CLI options available: Some of these flags are [standard NSIS flags](https://nsis.sourceforge.io/Docs/Chapter3.html#installerusage). +Windows uninstallers have the following CLI options: + +- `/S` (silent): uninstaller in headless mode. Uninstallers created with `constructor 3.10` or + later will report information to the console in a new window. + See below for different invocation examples. +- `/Q` (quiet): do not report to the console in headless mode. Only relevant when used with `/S` + (see above). Also works for the uninstallers. +- `/RemoveCaches=[0|1]` (only if built with `uninstall_with_conda_exe`): + Removes caches such package caches. + For details, see the `constructor uninstall` subcommand of the `conda.exe` file. +- `/RemoveConfigFiles=[none|users|system|all]` (only if built with `uninstall_with_conda_exe`): + Removes configuration files such as `.condarc` files. `user` removes the files inside the + current user's home directory and `system` removes all files outside of that directory. + For details, see the `constructor uninstall` subcommand of the `conda.exe` file. +- `/RemoveUserData=[0|1]` (only if built with `uninstall_with_conda_exe`): + removes user data such as the `~/.conda` directory. + For details, see the `constructor uninstall` subcommand of the `conda.exe` file. +- `/Q` (quiet): do not report to the console in headless mode. Only relevant when used with `/S` +- `/D` (directory): sets the default installation directory. Note that even if the path contains + spaces, it must be the last parameter used in the command line and must not contain any quotes. + Only absolute paths are supported. +- `_?`: like `/D` but prevents the uninstaller from copying itself to the temporary directory. + > [!IMPORTANT] > Flags without arguments should precede flags with arguments to avoid parsing errors. From 837a741671ce108fa4363ebcb42813bd5a329a9f Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 25 Nov 2024 11:45:31 -0800 Subject: [PATCH 17/22] Update description of uninstallation options in the GUI --- .../nsis/StandaloneUninstallerOptions.nsh | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/constructor/nsis/StandaloneUninstallerOptions.nsh b/constructor/nsis/StandaloneUninstallerOptions.nsh index 003174aa8..c4d3cb4d1 100644 --- a/constructor/nsis/StandaloneUninstallerOptions.nsh +++ b/constructor/nsis/StandaloneUninstallerOptions.nsh @@ -30,20 +30,20 @@ Function un.UninstCustomOptions_Show !insertmacro MUI_HEADER_TEXT \ "Advanced uninstallation options" \ - "Remove configuration and cache files" + "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 .condarc files + # 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 .condarc files in the Users directory." + "This removes configuration files such as .condarc files in the Users directory." IntOp $5 $5 + 10 ${If} ${UAC_IsAdmin} @@ -53,29 +53,28 @@ Function un.UninstCustomOptions_Show ${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 .condarc files in the ProgramData directory." + "This removes configuration files such as .condarc files in the ProgramData directory." IntOp $5 $5 + 10 ${EndIf} - # Option to remove cache files - ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove cache files." + # 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 the .conda directory in the user folder and conda system cache files." + "This removes user data files such as the .conda directory inside the Users folder." IntOp $5 $5 + 10 - # Option to run conda --clean - ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Remove index and package files." + # 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% 20u \ - "This removes index and unused package files by running conda clean --all. \ - Only useful if pkgs_dirs is set in a .condarc file." + "This removes cache such as package caches and notices." IntOp $5 $5 + 20 IntOp $5 $5 + 5 From e07a541a156e0b290f117d03d4087bc09d6ad1f2 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 25 Nov 2024 11:56:34 -0800 Subject: [PATCH 18/22] Replace UNINSTALL_MENUS with UNINSTALL_COMMANDS --- constructor/winexe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constructor/winexe.py b/constructor/winexe.py index be651a1a1..72d79a1e4 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -434,7 +434,7 @@ def make_nsi( 'uninstall_name', '${NAME} ${VERSION} (Python ${PYVERSION} ${ARCH})' ) - variables['UNINSTALL_MENUS'] = '\n '.join(uninstall_menus_commands(info)) + variables['UNINSTALL_COMMANDS'] = '\n '.join(uninstall_commands(info)) variables['EXTRA_FILES'] = '\n '.join(extra_files_commands(extra_files, dir_path)) variables['SCRIPT_ENV_VARIABLES'] = '\n '.join(setup_script_env_variables(info)) variables['CUSTOM_WELCOME_FILE'] = ( From 23f913a60d39344b3423569b672c5bc58f0fcb67 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 25 Nov 2024 13:28:18 -0800 Subject: [PATCH 19/22] Fix jinja syntax --- constructor/nsis/main.nsi.tmpl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 7bfc37013..d38fb93b2 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -67,7 +67,7 @@ var /global StdOutHandleSet !include "Utils.nsh" -{%- uninstall_with_conda_exe %} +{%- if uninstall_with_conda_exe %} !include "StandaloneUninstallerOptions.nsh" {%- endif %} @@ -114,7 +114,7 @@ var /global ARGV_NoScripts var /global ARGV_NoShortcuts var /global ARGV_CheckPathLength var /global ARGV_QuietMode -{%- uninstall_with_conda_exe %} +{%- if uninstall_with_conda_exe %} var /global ARGV_Uninst_RemoveConfigFiles var /global ARGV_Uninst_RemoveUserData var /global ARGV_Uninst_RemoveCaches @@ -212,7 +212,7 @@ Page Custom mui_AnaCustomOptions_Show !insertmacro MUI_UNPAGE_WELCOME !define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.OnDirectoryLeave !insertmacro MUI_UNPAGE_CONFIRM -{%- uninstall_with_conda_exe %} +{%- if uninstall_with_conda_exe %} UninstPage Custom un.UninstCustomOptions_Show {%- endif %} !insertmacro MUI_UNPAGE_INSTFILES @@ -748,7 +748,7 @@ FunctionEnd /? (show this help message)$\n\ /S (run in CLI/headless mode)$\n\ /Q (quiet mode, do not print output to console)$\n\ -{%- uninstall_with_conda_exe %} +{%- 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\ @@ -774,7 +774,7 @@ FunctionEnd ${IfNot} ${Errors} StrCpy $QuietMode "1" ${EndIf} -{%- uninstall_with_conda_exe %} +{%- if uninstall_with_conda_exe %} ClearErrors ${GetOptions} $ARGV "/RemoveConfigFiles=" $ARGV_Uninst_RemoveConfigFiles ${IfNot} ${Errors} @@ -824,7 +824,7 @@ FunctionEnd Function un.onInit -{%- uninstall_with_conda_exe %} +{%- if uninstall_with_conda_exe %} Call un.UninstCustomOptions_InitDefaults {%- endif %} From a663b24b29ef621e668b4ef16801ebb28b1fd55a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 2 Dec 2024 08:56:53 -0800 Subject: [PATCH 20/22] Document uninstaller subcommand for Unix --- .../nsis/StandaloneUninstallerOptions.nsh | 4 +- docs/source/howto.md | 42 ++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/constructor/nsis/StandaloneUninstallerOptions.nsh b/constructor/nsis/StandaloneUninstallerOptions.nsh index c4d3cb4d1..4da385ae5 100644 --- a/constructor/nsis/StandaloneUninstallerOptions.nsh +++ b/constructor/nsis/StandaloneUninstallerOptions.nsh @@ -73,8 +73,8 @@ Function un.UninstCustomOptions_Show Pop $UninstCustomOptions.RemoveCaches ${NSD_SetState} $UninstCustomOptions.RemoveCaches $UninstRemoveCaches_State ${NSD_OnClick} $UninstCustomOptions.RemoveCaches un.UninstRemoveCaches_Onclick - ${NSD_CreateLabel} 5% "$5u" 90% 20u \ - "This removes cache such as package caches and notices." + ${NSD_CreateLabel} 5% "$5u" 90% 10u \ + "This removes cache directories such as package caches and notices." IntOp $5 $5 + 20 IntOp $5 $5 + 5 diff --git a/docs/source/howto.md b/docs/source/howto.md index c6fca5056..970cc17fd 100644 --- a/docs/source/howto.md +++ b/docs/source/howto.md @@ -160,4 +160,44 @@ If you want to perform the uninstallation steps manually, you can: ### macOS and Linux -Remove the installation directory. Usually this is a directory in your user's home directory (user installs), or under `/opt` for system-wide installations. +:::{note} +The following sections requires installers to be built using the `uninstall_with_conda_exe` option. +This is currently only implemented for `conda-standalone` 24.11.0 and higher. + +For other installers, all files need to be removed manually. +::: + +Unlike Windows, macOS and Linux installers do not ship an uninstaller executable. +However, some standalone applications (like `conda-standalone`) provide an uninstaller subcommand. +The following can be used to uninstall an existing installation: + +```bash +$ $INSTDIR/_conda constructor uninstall --prefix $INSTDIR +``` + +where `$INSTDIR` is the installation directory. This command recursively removes all environments +and removes shell initializers that point to `$INSTDIR`. + +The command supports additional options to delete files outside the installation directory: + +- `--remove-caches`: + Removes cache directories such as package caches and notices. + Not recommended with multiple conda installations when softlinks are enabled. +- `--remove-config-files {user,system,all}`: + Removes all configuration files such as `.condarc` files outside the installation directory. + `user` removes the files inside the current user's home directory + and `system` removes all files outside of that directory. +- `--remove-user-data`: + This removes user data files such as the `~/.conda` directory. + +These options are not recommended if multiple conda installations are on the same system because +they delete commonly used files. + +:::{note} +If removing these files requires superuser privileges, use `sudo -E` instead of `sudo` since +finding these files may rely on environment variables, especially `$HOME`. +::: + +For more detailed implementation notes, see the documentation of the standalone application: + +* [conda-standalone](https://github.com/conda/conda-standalone) From d3ed416272c40347c92df012e5da0b963a123cae Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 5 Dec 2024 12:01:17 -0800 Subject: [PATCH 21/22] Debug: print directory contents for standalone uninstaller --- tests/test_examples.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_examples.py b/tests/test_examples.py index b46bb7f91..08b72656b 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -987,6 +987,9 @@ def test_uninstallation_standalone( try: _run_uninstaller_exe(install_dir, check=False, options=uninstall_options) + if install_dir.exists(): + from pprint import pprint + pprint(list(install_dir.iterdir())) assert not install_dir.exists() or not next(install_dir.glob("*.conda_trash"), None) assert dot_conda_dir.exists() != remove_user_data assert pkg_cache.exists() != remove_caches From 152abb2e4eab7ae95bf2c25d7e5beb79f47b198d Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 5 Dec 2024 12:54:27 -0800 Subject: [PATCH 22/22] Always remove installation directory --- constructor/winexe.py | 27 +++++---------------------- tests/test_examples.py | 6 +----- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/constructor/winexe.py b/constructor/winexe.py index b507d36e7..ef269d922 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -264,28 +264,11 @@ def uninstall_commands_conda_standalone() -> List[str]: call un.AbortRetryNSExecWait SetDetailsPrint both - # If only install.log, the uninstaller, and .conda_trash files are left behind, - # remove the directory - IfFileExists $INSTDIR 0 skip_rmdir - StrLen $R0 ".conda_trash" - IntOp $R0 0 - $R0 - FindFirst $0 $1 $INSTDIR\* - rmdir_loop: - StrCmp "" $1 end_rmdir_loop - StrCmp "." $1 advance_rmdir_loop - StrCmp ".." $1 advance_rmdir_loop - StrCmp "install.log" $1 advance_rmdir_loop - StrCmp "Uninstall-${NAME}.exe" $1 advance_rmdir_loop - StrCpy $2 $1 "" $R0 - StrCmp $2 ".conda_trash" advance_rmdir_loop end_rmdir - advance_rmdir_loop: - FindNext $0 $1 - Goto rmdir_loop - end_rmdir_loop: - RMDir /r /REBOOTOK "$INSTDIR" - end_rmdir: - FindClose $0 - skip_rmdir: + # The uninstallation may leave the install.log, the uninstaller, + # and .conda_trash files behind, so remove those manually. + ${If} ${FileExists} "$INSTDIR" + RMDir /r /REBOOTOK "$INSTDIR" + ${EndIf} """).splitlines() diff --git a/tests/test_examples.py b/tests/test_examples.py index b1cb5efa8..0cabf2505 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -989,11 +989,7 @@ def test_uninstallation_standalone( uninstall_options.append("/RemoveCaches=1") try: - _run_uninstaller_exe(install_dir, check=False, options=uninstall_options) - if install_dir.exists(): - from pprint import pprint - pprint(list(install_dir.iterdir())) - assert not install_dir.exists() or not next(install_dir.glob("*.conda_trash"), None) + _run_uninstaller_exe(install_dir, check=True, options=uninstall_options) assert dot_conda_dir.exists() != remove_user_data assert pkg_cache.exists() != remove_caches assert system_rc.exists() != remove_system_rcs