diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index d63ca3a..f22c5b2 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -1,15 +1,17 @@ # Installer template file for creating a Windows installer using NSIS. -# Dependencies: -# NSIS >=3.08 conda install "nsis>=3.08" (includes extra unicode plugins) +!if "${NSIS_PACKEDVERSION}" < 0x3008000 + !error "NSIS 3.08 or higher is required to build this installer!" + # conda install "nsis>=3.08" (includes extra unicode plugins) +!endif -Unicode "true" +Unicode true -#if enable_debugging is True +{%- if enable_debugging %} # Special logging build needed for ENABLE_LOGGING # See https://nsis.sourceforge.io/Special_Builds !define ENABLE_LOGGING -#endif +{%- endif %} # Comes from https://nsis.sourceforge.io/Logging:Enable_Logs_Quickly !define LogSet "!insertmacro LogSetMacro" @@ -26,6 +28,29 @@ Unicode "true" !endif !macroend +var /global QuietMode # "0" = print normally, "1" = do not print +var /global StdOutHandle +var /global StdOutHandleSet +!define Print "!insertmacro PrintMacro" +!macro PrintMacro INPUT_TEXT + DetailPrint "${INPUT_TEXT}" + ${If} ${Silent} + ${AndIf} $QuietMode != "1" + ${IfNot} $StdOutHandleSet == "1" + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + System::Call 'kernel32::AttachConsole(i -1)i.r1' + ${If} $0 = 0 + ${OrIf} $1 = 0 + System::Call 'kernel32::AllocConsole()' + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + ${EndIf} + StrCpy $StdOutHandle $0 + StrCpy $StdOutHandleSet "1" + ${EndIf} + FileWrite $StdOutHandle "${INPUT_TEXT}$\n" + ${EndIf} +!macroend + !include "WinMessages.nsh" !include "WordFunc.nsh" !include "LogicLib.nsh" @@ -42,26 +67,30 @@ Unicode "true" !include "Utils.nsh" -!define NAME __NAME__ -!define VERSION __VERSION__ -!define COMPANY __COMPANY__ -!define ARCH __ARCH__ -!define PLATFORM __PLATFORM__ -!define CONSTRUCTOR_VERSION __CONSTRUCTOR_VERSION__ -!define PY_VER __PY_VER__ -!define PYVERSION_JUSTDIGITS __PYVERSION_JUSTDIGITS__ -!define PYVERSION __PYVERSION__ -!define PYVERSION_MAJOR __PYVERSION_MAJOR__ -!define DEFAULT_PREFIX __DEFAULT_PREFIX__ -!define DEFAULT_PREFIX_DOMAIN_USER __DEFAULT_PREFIX_DOMAIN_USER__ -!define DEFAULT_PREFIX_ALL_USERS __DEFAULT_PREFIX_ALL_USERS__ -!define PRE_INSTALL_DESC __PRE_INSTALL_DESC__ -!define POST_INSTALL_DESC __POST_INSTALL_DESC__ -!define ENABLE_SHORTCUTS __ENABLE_SHORTCUTS__ -!define SHOW_REGISTER_PYTHON __SHOW_REGISTER_PYTHON__ -!define SHOW_ADD_TO_PATH __SHOW_ADD_TO_PATH__ +{%- if uninstall_with_conda_exe %} +!include "StandaloneUninstallerOptions.nsh" +{%- endif %} + +!define NAME {{ installer_name }} +!define VERSION {{ installer_version }} +!define COMPANY {{ company }} +!define ARCH {{ arch }} +!define PLATFORM {{ installer_platform }} +!define CONSTRUCTOR_VERSION {{ constructor_version }} +!define PY_VER {{ pyver_components[:2] | join(".") }} +!define PYVERSION_JUSTDIGITS {{ pyver_components | join("") }} +!define PYVERSION {{ pyver_components | join(".") }} +!define PYVERSION_MAJOR {{ pyver_components[0] }} +!define DEFAULT_PREFIX {{ default_prefix }} +!define DEFAULT_PREFIX_DOMAIN_USER {{ default_prefix_domain_user }} +!define DEFAULT_PREFIX_ALL_USERS {{ default_prefix_all_users }} +!define PRE_INSTALL_DESC {{ pre_install_desc }} +!define POST_INSTALL_DESC {{ post_install_desc }} +!define ENABLE_SHORTCUTS {{ enable_shortcuts }} +!define SHOW_REGISTER_PYTHON {{ show_register_python }} +!define SHOW_ADD_TO_PATH {{ show_add_to_path }} !define PRODUCT_NAME "${NAME} ${VERSION} (${ARCH})" -!define UNINSTALL_NAME "@UNINSTALL_NAME@" +!define UNINSTALL_NAME "{{ UNINSTALL_NAME }}" !define UNINSTREG "SOFTWARE\Microsoft\Windows\CurrentVersion\ \Uninstall\${UNINSTALL_NAME}" @@ -84,6 +113,12 @@ var /global ARGV_NoRegistry 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 @@ -104,7 +139,7 @@ CRCCheck On # Basic options Name "${PRODUCT_NAME}" -OutFile __OUTFILE__ +OutFile {{ outfile }} ShowInstDetails "hide" ShowUninstDetails "hide" # This installer contains tar.bz2 files, which are already compressed @@ -121,33 +156,33 @@ VIAddVersionKey "CompanyName" "${COMPANY}" VIAddVersionKey "LegalCopyright" "(c) ${COMPANY}" VIAddVersionKey "FileDescription" "${NAME} Installer" VIAddVersionKey "Comments" "Created by constructor ${CONSTRUCTOR_VERSION}" -VIProductVersion __VIPV__ +VIProductVersion {{ vipv }} BrandingText /TRIMLEFT "${COMPANY}" # Interface configuration -!define MUI_ICON __ICONFILE__ -!define MUI_UNICON __ICONFILE__ +!define MUI_ICON {{ iconfile }} +!define MUI_UNICON {{ iconfile }} !define MUI_HEADERIMAGE -!define MUI_HEADERIMAGE_BITMAP __HEADERIMAGE__ -!define MUI_HEADERIMAGE_UNBITMAP __HEADERIMAGE__ +!define MUI_HEADERIMAGE_BITMAP {{ headerimage }} +!define MUI_HEADERIMAGE_UNBITMAP {{ headerimage }} !define MUI_ABORTWARNING !define MUI_FINISHPAGE_NOAUTOCLOSE !define MUI_UNFINISHPAGE_NOAUTOCLOSE -!define MUI_WELCOMEFINISHPAGE_BITMAP __WELCOMEIMAGE__ -!define MUI_UNWELCOMEFINISHPAGE_BITMAP __WELCOMEIMAGE__ +!define MUI_WELCOMEFINISHPAGE_BITMAP {{ welcomeimage }} +!define MUI_UNWELCOMEFINISHPAGE_BITMAP {{ welcomeimage }} #!define MUI_CUSTOMFUNCTION_GUIINIT GuiInit # Pages #!define MUI_PAGE_CUSTOMFUNCTION_SHOW OnStartup -#if custom_welcome +{%- if custom_welcome %} # Custom welcome file(s) -@CUSTOM_WELCOME_FILE@ -#else +{{ CUSTOM_WELCOME_FILE }} +{%- else %} !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipPageIfUACInnerInstance !insertmacro MUI_PAGE_WELCOME -#endif +{%- endif %} !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipPageIfUACInnerInstance -!insertmacro MUI_PAGE_LICENSE __LICENSEFILE__ +!insertmacro MUI_PAGE_LICENSE {{ licensefile }} Page Custom InstModePage_Create InstModePage_Leave !define MUI_PAGE_CUSTOMFUNCTION_PRE DisableBackButtonIfUACInnerInstance !define MUI_PAGE_CUSTOMFUNCTION_LEAVE OnDirectoryLeave @@ -155,23 +190,31 @@ Page Custom InstModePage_Create InstModePage_Leave # Custom options now differ depending on installation mode. #Page Custom mui_AnaCustomOptions_Show !insertmacro MUI_PAGE_INSTFILES -#if with_conclusion_text is True -!define MUI_FINISHPAGE_TITLE __CONCLUSION_TITLE__ -!define MUI_FINISHPAGE_TITLE_3LINES -!define MUI_FINISHPAGE_TEXT __CONCLUSION_TEXT__ -#endif -#if custom_conclusion +{%- for page in POST_INSTALL_PAGES %} +{{ page }} +{%- endfor %} + +{%- if with_conclusion_text %} +!define MUI_FINISHPAGE_TITLE {{ conclusion_title }} +!define MUI_FINISHPAGE_TITLE_3LINES +!define MUI_FINISHPAGE_TEXT {{ conclusion_text }} +{%- endif %} + +{%- if custom_conclusion %} # Custom conclusion file(s) -@CUSTOM_CONCLUSION_FILE@ -#else +{{ CUSTOM_CONCLUSION_FILE }} +{%- else %} !insertmacro MUI_PAGE_FINISH -#endif +{%- 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 @@ -224,35 +267,54 @@ FunctionEnd ${GetParameters} $ARGV ${GetOptions} $ARGV "/?" $ARGV_Help ${IfNot} ${Errors} - MessageBox MB_OK|MB_ICONEXCLAMATION \ - "Usage: $EXEFILE [options]$\n\ - Options:$\n$\n\ - /InstallationType=AllUsers [default: JustMe]$\n$\n\ - /AddToPath=[0|1] [default: 0]$\n$\n\ -#if keep_pkgs is True - /KeepPkgCache=[0|1] [default: 1]$\n$\n\ -#endif -#if keep_pkgs is False - /KeepPkgCache=[0|1] [default: 0]$\n$\n\ -#endif - /RegisterPython=[0|1] [default: AllUsers: 1, JustMe: 0]$\n$\n\ - /NoRegistry=[0|1] [default: AllUsers: 0, JustMe: 0]$\n$\n\ - /NoScripts=[0|1] [default: 0]$\n$\n\ - /NoShortcuts=[0|1] [default: 0]$\n$\n\ - /CheckPathLength=[0|1] [default: 1]$\n$\n\ - Examples:$\n\ + SetSilent silent + ${Print} "\ + Installs ${NAME} ${VERSION}$\n\ + $\n\ + USAGE$\n\ + -----$\n\ + $\n\ + $EXEFILE [options]$\n\ + $\n\ + OPTIONS$\n\ + -------$\n\ + $\n\ + /InstallationType=AllUsers [default: JustMe]$\n\ + /AddToPath=[0|1] [default: 0]$\n\ + /KeepPkgCache=[0|1] [default: {{ 1 if keep_pkgs else 0 }}]$\n\ + /RegisterPython=[0|1] [default: AllUsers: 1, JustMe: 0]$\n\ + /NoRegistry=[0|1] [default: AllUsers: 0, JustMe: 0]$\n\ + /NoScripts=[0|1] [default: 0]$\n\ + /NoShortcuts=[0|1] [default: 0]$\n\ + /CheckPathLength=[0|1] [default: 1]$\n\ + /? (show this help message)$\n\ + /S (run in CLI/headless mode)$\n\ + /Q (quiet mode, do not print output to console)$\n\ + /D=[installation directory] (must be last parameter)$\n" + # There seems to be a limit to how many chars per ${Print} we can pass. + # The message will get truncated silently, no errors. + # That's why we split the help message in two calls. + ${Print} "\ + EXAMPLES$\n\ + --------$\n\ + $\n\ Install for all users, but don't add to PATH env var:$\n\ - $EXEFILE /InstallationType=AllUsers$\n$\n\ + > $EXEFILE /InstallationType=AllUsers$\n\ + $\n\ Install for just me, add to PATH and register as system Python:$\n\ - $EXEFILE /RegisterPython=1 /AddToPath=1$\n$\n\ + > $EXEFILE /RegisterPython=1 /AddToPath=1$\n\ + $\n\ Install for just me, with no registry modification (for CI):$\n\ - $EXEFILE /NoRegistry=1$\n$\n\ + > $EXEFILE /NoRegistry=1$\n\ + $\n\ + Install via CLI (no GUI) into C:\${NAME}$\n\ + > cmd /C START /WAIT $EXEFILE /S /D=C:\${NAME}$\n\ + $\n\ NOTE: If you install for AllUsers, then the option to AddToPath$\n\ - is disabled (i.e. if ./InstallationType=AllUsers, then$\n\ - /AddToPath=1 will be ignored).$\n" \ - /SD IDOK - Abort - ${EndIf} + is disabled (i.e. if /InstallationType=AllUsers, then$\n\ + /AddToPath=1 will be ignored)." + Abort + ${EndIf} ClearErrors ${GetOptions} $ARGV "/InstallationType=" $ARGV_InstallationType @@ -277,7 +339,7 @@ FunctionEnd ClearErrors ${GetOptions} $ARGV "/KeepPkgCache=" $ARGV_KeepPkgCache ${If} ${Errors} - StrCpy $ARGV_KeepPkgCache "@KEEP_PKGS@" + StrCpy $ARGV_KeepPkgCache "{{ 1 if keep_pkgs else 0 }}" ${EndIf} ClearErrors @@ -320,6 +382,12 @@ FunctionEnd ${EndIf} ${EndIf} + ClearErrors + ${GetOptions} $ARGV "/Q" $ARGV_QuietMode + ${IfNot} ${Errors} + StrCpy $QuietMode "1" + ${EndIf} + !macroend Function OnInit_Release @@ -336,6 +404,7 @@ Function OnInit_Release # To address CVE-2022-26526. # In AllUsers install mode, do not allow AddToPath as an option. MessageBox MB_OK|MB_ICONEXCLAMATION "/AddToPath=1 is disabled and ignored in 'All Users' installations" /SD IDOK + ${Print} "/AddToPath=1 is disabled and ignored in 'All Users' installations" StrCpy $Ana_AddToPath_State ${BST_UNCHECKED} ${Else} StrCpy $Ana_AddToPath_State ${BST_CHECKED} @@ -488,13 +557,18 @@ Function .onInit Push $R2 InitPluginsDir - @TEMP_EXTRA_FILES@ +{%- if TEMP_EXTRA_FILES | length != 0 %} + SetOutPath $PLUGINSDIR +{%- for file in TEMP_EXTRA_FILES %} + File {{ file }} +{%- endfor %} +{%- endif %} !insertmacro ParseCommandLineArgs # Select the correct registry to look at, depending # on whether it's a 32-bit or 64-bit installer - SetRegView @BITS@ -#if win64 + SetRegView {{ BITS }} +{%- if win64 %} # If we're a 64-bit installer, make sure it's 64-bit Windows ${IfNot} ${RunningX64} MessageBox MB_OK|MB_ICONEXCLAMATION \ @@ -504,7 +578,7 @@ Function .onInit /SD IDOK Abort ${EndIf} -#endif +{%- endif %} !insertmacro UAC_PageElevation_OnInit ${If} ${UAC_IsInnerInstance} @@ -607,9 +681,9 @@ Function .onInit ${IfNot} ${UAC_IsAdmin} MessageBox MB_ICONSTOP "Installation for all users requires an elevated prompt." Abort - ${EndIF} - ${EndIF} - ${EndIF} + ${EndIf} + ${EndIf} + ${EndIf} ; /D was not used, add default based on install type ${If} $InstDir == "" @@ -630,47 +704,28 @@ Function .onInit Call mui_AnaCustomOptions_InitDefaults # Override custom options with explicitly given values from contruct.yaml. # If initialize_by_default (register_python_default) is None, do nothing. -#if initialize_conda is True and initialize_by_default is True +{%- if initialize_conda %} + {%- if initialize_by_default %} ${If} $InstMode == ${JUST_ME} StrCpy $Ana_AddToPath_State ${BST_CHECKED} - ${EndIF} -#endif -#if initialize_conda is True and initialize_by_default is False + ${EndIf} + {%- else %} StrCpy $Ana_AddToPath_State ${BST_UNCHECKED} -#endif -#if register_python is True and register_python_default is True - StrCpy $Ana_RegisterSystemPython_State ${BST_CHECKED} -#endif -#if register_python is True and register_python_default is False - StrCpy $Ana_RegisterSystemPython_State ${BST_UNCHECKED} -#endif -#if check_path_length is True - StrCpy $CheckPathLength "1" -#endif -#if check_path_length is False - StrCpy $CheckPathLength "0" -#endif -#if keep_pkgs is True - StrCpy $Ana_ClearPkgCache_State ${BST_UNCHECKED} -#endif -#if keep_pkgs is False - StrCpy $Ana_ClearPkgCache_State ${BST_CHECKED} -#endif -#if pre_install_exists is True - StrCpy $Ana_PreInstall_State ${BST_CHECKED} -#endif -#if pre_install_exists is False - StrCpy $Ana_PreInstall_State ${BST_UNCHECKED} -#endif -#if post_install_exists is True - StrCpy $Ana_PostInstall_State ${BST_CHECKED} -#endif -#if post_install_exists is False - StrCpy $Ana_PostInstall_State ${BST_UNCHECKED} -#endif + {%- endif %} +{%- endif %} + +{%- if register_python %} + StrCpy $Ana_RegisterSystemPython_State {{ '${BST_CHECKED}' if register_python_default else '${BST_UNCHECKED}' }} +{%- endif %} + StrCpy $CheckPathLength "{{ 1 if check_path_length else 0 }}" + StrCpy $Ana_ClearPkgCache_State {{ '${BST_UNCHECKED}' if keep_pkgs else '${BST_CHECKED}' }} + StrCpy $Ana_PreInstall_State {{ '${BST_CHECKED}' if pre_install_exists else '${BST_UNCHECKED}' }} + StrCpy $Ana_PostInstall_State {{ '${BST_CHECKED}' if post_install_exists else '${BST_UNCHECKED}' }} Call OnInit_Release + ${Print} "Welcome to ${NAME} ${VERSION}$\n" + Pop $R2 Pop $R1 Pop $2 @@ -678,7 +733,106 @@ Function .onInit Pop $0 FunctionEnd +!macro un.ParseCommandLineArgs + ClearErrors + ${GetParameters} $ARGV + ${GetOptions} $ARGV "/?" $ARGV_Help + ${IfNot} ${Errors} + SetSilent silent + ${Print} "\ + Uninstalls ${NAME} ${VERSION}$\n\ + $\n\ + USAGE$\n\ + -----$\n\ + $\n\ + Uninstall-${NAME}.exe [options]$\n\ + $\n\ + OPTIONS$\n\ + -------$\n\ + $\n\ + /? (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\ + --------$\n\ + $\n\ + Uninstall via CLI (no GUI) from C:\${NAME}$\n\ + > cmd /C START /WAIT Uninstall-${NAME}.exe /S /_?=C:\${NAME}$\n\ + $\n\ + 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, so we can't get the parent console handle + Sleep 10000 + Abort + ${EndIf} + + ClearErrors + ${GetOptions} $ARGV "/Q" $ARGV_QuietMode + ${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 Push $2 @@ -727,6 +881,7 @@ Function un.onInit goto valid_dir invalid_dir: + ${Print} "::error:: $INSTDIR is not a valid conda directory. Please run the uninstaller from a conda directory." MessageBox MB_OK|MB_ICONSTOP \ "Error: $INSTDIR is not a valid conda directory. Please run the uninstaller from a conda directory." \ /SD IDABORT @@ -735,7 +890,7 @@ Function un.onInit # Select the correct registry to look at, depending # on whether it's a 32-bit or 64-bit installer - SetRegView @BITS@ + SetRegView {{ BITS }} # Since the switch to a dual-mode installer (All Users/Just Me), the # uninstaller will inherit the requested execution level of the main @@ -848,7 +1003,7 @@ Pop $0 Function OnDirectoryLeave ${LogSet} on ${If} ${IsNonEmptyDirectory} "$InstDir" - DetailPrint "::error:: Directory '$INSTDIR' is not empty, please choose a different location." + ${Print} "::error:: Directory '$INSTDIR' is not empty, please choose a different location." MessageBox MB_OK|MB_ICONEXCLAMATION \ "Directory '$INSTDIR' is not empty,$\n\ please choose a different location." \ @@ -871,7 +1026,7 @@ Function OnDirectoryLeave WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1 ; If we don't have admin right, we suggest a shorter path or suggest to run with admin right ${Else} - DetailPrint "::error:: The installation path should be shorter than 46 characters or \ + ${Print} "::error:: The installation path should be shorter than 46 characters or \ the installation requires administrator rights to enable long \ path on Windows." MessageBox MB_OK|MB_ICONSTOP "The installation path should be shorter than 46 characters or \ @@ -882,7 +1037,7 @@ Function OnDirectoryLeave ${EndIf} ; If we don't have admin right, we suggest a shorter path or suggest to run with admin right ${Else} - DetailPrint "::error:: The installation path should be shorter than 46 characters. \ + ${Print} "::error:: The installation path should be shorter than 46 characters. \ Please choose another location." MessageBox MB_OK|MB_ICONSTOP "The installation path should be shorter than 46 characters. \ Please choose another location." \ @@ -914,22 +1069,22 @@ Function OnDirectoryLeave StrCpy $R7 "$\n" ${EndIf} StrCpy $R8 "'Destination Folder' contains $R0 space$R1.$R7This can cause problems with several conda packages.$R7" -#if check_path_spaces is True +{%- if check_path_spaces %} StrCpy $R8 "$R8Please remove the space$R1 from the destination folder." StrCpy $R9 "Error" -#else +{%- else %} StrCpy $R8 "$R8Please consider removing the space$R1." StrCpy $R9 "Warning" -#endif +{%- endif %} # Show message box then take the user back to the Directory page. ${If} ${Silent} - DetailPrint "::$R9:: $R8" + ${Print} "::$R9:: $R8" ${Else} MessageBox MB_OK|MB_ICONINFORMATION "$R9: $R8" /SD IDOK ${EndIf} -#if check_path_spaces is True +{%- if check_path_spaces %} abort -#endif +{%- endif %} NoSpaces: Pop $R7 Pop $R8 @@ -942,7 +1097,7 @@ Function OnDirectoryLeave Pop $R0 StrCmp $R0 "" NoInvalidCharaceters - DetailPrint "::error:: 'Destination Folder' contains the following invalid character: $R0" + ${Print} "::error:: 'Destination Folder' contains the following invalid character: $R0" MessageBox MB_OK|MB_ICONEXCLAMATION \ "Error: 'Destination Folder' contains the following invalid character: $R0" \ /SD IDOK @@ -952,7 +1107,7 @@ Function OnDirectoryLeave UnicodePathTest::SpecialCharPathTest $INSTDIR Pop $R1 StrCmp $R1 "nothingspecial" nothing_special_path - DetailPrint "::error:: 'Destination Folder' contains the following invalid character$R1" + ${Print} "::error:: 'Destination Folder' contains the following invalid character$R1" MessageBox MB_OK|MB_ICONEXCLAMATION \ "Error: 'Destination Folder' contains the following invalid character$R1" \ /SD IDOK @@ -970,7 +1125,7 @@ Function OnDirectoryLeave StrCmp ${PY_VER} "2.7" not_cp_acp_capable StrCmp $R1 "ascii_cp_acp" valid_path not_cp_acp_capable: - DetailPrint "::error:: Due to incompatibility with several \ + ${Print} "::error:: Due to incompatibility with several \ Python libraries, 'Destination Folder' cannot contain non-ascii characters \ (special characters or diacritics). Please choose another location." MessageBox MB_OK|MB_ICONEXCLAMATION "Error: Due to incompatibility with several \ @@ -985,7 +1140,7 @@ Function OnDirectoryLeave ${IsWritable} $INSTDIR $R1 IntCmp $R1 0 pathgood Pop $R1 - DetailPrint "::error: Path $INSTDIR is not writable. Please check permissions or \ + ${Print} "::error: Path $INSTDIR is not writable. Please check permissions or \ try respawning the installer with elevated privileges." MessageBox MB_OK|MB_ICONEXCLAMATION \ "Error: Path $INSTDIR is not writable. Please check permissions or \ @@ -1039,12 +1194,12 @@ FunctionEnd ${ElseIf} $1 == "NoLog" nsExec::Exec $3 ${Else} - DetailPrint "::error:: AbortRetryNSExecWait: 1st argument must be 'WithLog' or 'NoLog'. You used: $1" + ${Print} "::error:: AbortRetryNSExecWait: 1st argument must be 'WithLog' or 'NoLog'. You used: $1" Abort ${EndIf} pop $0 ${If} $0 != "0" - DetailPrint "::error:: $2" + ${Print} "::error:: $2" MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON3 \ $2 /SD IDIGNORE IDABORT abort IDRETRY retry ; IDIGNORE: Continue anyway @@ -1065,14 +1220,13 @@ FunctionEnd # Installer sections Section "Install" ${LogSet} on - ${If} ${Silent} call OnDirectoryLeave ${EndIf} SetOutPath "$INSTDIR\Lib" - File "@NSIS_DIR@\_nsis.py" - File "@NSIS_DIR@\_system_path.py" + File "{{ NSIS_DIR }}\_nsis.py" + File "{{ NSIS_DIR }}\_system_path.py" # Resolve INSTDIR so that paths and registry keys do not contain '..' or similar strings. # $0 is empty if the directory doesn't exist, but the File commands should have created it already. @@ -1083,6 +1237,17 @@ Section "Install" ${EndIf} StrCpy $INSTDIR $0 +{%- if has_license %} + SetOutPath "$INSTDIR" + File {{ licensefile }} + ${Print} "By continuing this installation you are accepting this license agreement:" + ${Print} "$INSTDIR\{{ LICENSEFILENAME }}" + ${Print} "Please run the installer in GUI mode to read the details.$\n" +{%- endif %} + + ${Print} "${NAME} will now be installed into this location:" + ${Print} "$INSTDIR$\n" + ReadEnvStr $0 SystemRoot # set PATH for the installer process, so that MSVC runtimes get found OK # This is also isolating PATH to be just us and Windows core stuff, which hopefully avoids @@ -1090,16 +1255,22 @@ Section "Install" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("PATH", \ "$INSTDIR;$INSTDIR\Library\mingw-w64\bin;$INSTDIR\Library\usr\bin;$INSTDIR\Library\bin;$INSTDIR\Scripts;$INSTDIR\bin;$0;$0\system32;$0\system32\Wbem").r0' + ${Print} "Unpacking payload..." + # A conda-meta\history file is required for a valid conda prefix SetOutPath "$INSTDIR\conda-meta" - File __CONDA_HISTORY__ + File {{ conda_history }} SetOutPath "$INSTDIR" - File __CONDA_EXE__ - File __PRE_UNINSTALL__ + File {{ conda_exe }} + File {{ pre_uninstall }} - # Copy extra files (code generated on winexe.py) - @EXTRA_FILES@ +{%- for path, files in EXTRA_FILES | items %} + SetOutPath {{ path }} +{%- for file in files %} + File {{ file }} +{%- endfor %} +{%- endfor %} ${If} $InstMode = ${JUST_ME} SetOutPath "$INSTDIR" @@ -1108,20 +1279,26 @@ Section "Install" ${EndIf} SetOutPath "$INSTDIR\pkgs" - File __URLS_FILE__ - File __URLS_TXT_FILE__ -#if pre_install_exists is True - File __PRE_INSTALL__ -#endif - File __POST_INSTALL__ - File /nonfatal /r __INDEX_CACHE__ - File /r __REPODATA_RECORD__ + File {{ urls_file }} + File {{ urls_txt_file }} +{%- if pre_install_exists %} + File {{ pre_install }} +{%- endif %} + File {{ post_install }} + File /nonfatal /r {{ index_cache }} + File /r {{ repodata_record }} - @SCRIPT_ENV_VARIABLES@ + +{%- for key, escaped_val in SCRIPT_ENV_VARIABLES | items %} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("{{ key }}", {{ escaped_val }}).r0' +{%- endfor %} System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SAFETY_CHECKS", "disabled").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_EXTRA_SAFETY_CHECKS", "no").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_PKGS_DIRS", "$INSTDIR\pkgs")".r0' + # Spinners in conda write a new character with each movement of the spinner. + # For long installation times, this may cause a buffer overflow, crashing the installer. + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1")".r0' # Extra info for pre and post install scripts # NOTE: If more vars are added, make sure to update the examples/scripts tests too # There's a similar block for the pre_uninstall script, further down this file. @@ -1131,11 +1308,33 @@ Section "Install" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_VER", "${VERSION}").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_PLAT", "${PLATFORM}").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_TYPE", "EXE").r0' + ${If} ${Silent} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "1").r0' + ${Else} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0' + ${EndIf} - @PKG_COMMANDS@ + ${If} '{{ VIRTUAL_SPECS }}' != '' + # We need to specify CONDA_SOLVER=classic for conda-standalone + # to work around this bug in conda-libmamba-solver: + # https://github.com/conda/conda-libmamba-solver/issues/480 + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SOLVER", "classic").r0' + SetDetailsPrint TextOnly + ${Print} "Checking virtual specs compatibility: {{ VIRTUAL_SPECS_DEBUG }}" + push '"$INSTDIR\_conda.exe" create --dry-run --prefix "$INSTDIR\envs\_virtual_specs_checks" --offline {{ VIRTUAL_SPECS }} {{ NO_RCS_ARG }}' + push 'Failed to check virtual specs: {{ VIRTUAL_SPECS_DEBUG }}' + push 'WithLog' + call AbortRetryNSExecWait + SetDetailsPrint both + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SOLVER", "").r0' + ${EndIf} + +{%- for dist in DISTS %} + File {{ dist }} +{%- endfor %} SetDetailsPrint TextOnly - DetailPrint "Setting up the package cache..." + ${Print} "Setting up the package cache..." push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR" --extract-conda-pkgs' push 'Failed to extract packages' push 'NoLog' @@ -1145,7 +1344,7 @@ Section "Install" SetDetailsPrint both IfFileExists "$INSTDIR\pkgs\pre_install.bat" 0 NoPreInstall - DetailPrint "Running pre_install scripts..." + ${Print} "Running pre_install scripts..." ReadEnvStr $5 SystemRoot ReadEnvStr $6 windir # This 'FileExists' also returns True for directories @@ -1162,22 +1361,66 @@ Section "Install" call AbortRetryNSExecWait NoPreInstall: - @SETUP_ENVS@ +{%- for env in SETUP_ENVS %} + {%- set channels = env.final_channels|join(",") %} + # Set up {{ env.name }} env + SetDetailsPrint both + ${Print} "Setting up the {{ env.name }} environment..." + SetDetailsPrint listonly - @WRITE_CONDARC@ + # List of packages to install + SetOutPath "{{ env.env_txt_dir }}" + File "{{ env.env_txt_abspath }}" - AddSize @SIZE@ + # A conda-meta\history file is required for a valid conda prefix + SetOutPath "{{ env.conda_meta }}" + File "{{ env.history_abspath }}" -#if has_conda is True - DetailPrint "Initializing conda directories..." + # Set channels + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_CHANNELS", "{{ channels }}").r0' + # Set register_envs + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_REGISTER_ENVS", "{{ env.register_envs }}").r0' + + # Run conda install + ${If} $Ana_CreateShortcuts_State = ${BST_CHECKED} + ${Print} "Installing packages for {{ env.name }}, creating shortcuts if necessary..." + push '"$INSTDIR\_conda.exe" install --offline -yp "{{ env.prefix }}" --file "{{ env.env_txt }}" {{ env.shortcuts }} {{ env.no_rcs_arg }}' + ${Else} + ${Print} "Installing packages for {{ env.name }}..." + push '"$INSTDIR\_conda.exe" install --offline -yp "{{ env.prefix }}" --file "{{ env.env_txt }}" --no-shortcuts {{ env.no_rcs_arg }}' + ${EndIf} + push 'Failed to link extracted packages to {{ env.prefix }}!' + push 'WithLog' + SetDetailsPrint listonly + call AbortRetryNSExecWait + SetDetailsPrint both + + # Cleanup {{ env.name }} env.txt + SetOutPath "$INSTDIR" + Delete "{{ env.env_txt }}" + + # Restore shipped conda-meta\history for remapped + # channels and retain only the first transaction + SetOutPath "{{ env.conda_meta }}" + File "{{ env.history_abspath }}" +{%- endfor %} + +{%- for condarc in WRITE_CONDARC %} + {{ condarc }} +{%- endfor %} + + AddSize {{ SIZE }} + +{%- if has_conda %} + ${Print} "Initializing conda directories..." push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" mkdirs' push 'Failed to initialize conda directories' push 'WithLog' call AbortRetryNSExecWait -#endif +{%- endif %} ${If} $Ana_PostInstall_State = ${BST_CHECKED} - DetailPrint "Running post install..." + ${Print} "Running post install..." push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" post_install' push 'Failed to run post install script' push 'WithLog' @@ -1185,17 +1428,17 @@ Section "Install" ${EndIf} ${If} $Ana_ClearPkgCache_State = ${BST_CHECKED} - DetailPrint "Clearing package cache..." - push '"$INSTDIR\_conda.exe" clean --all --yes' + ${Print} "Clearing package cache..." + push '"$INSTDIR\_conda.exe" clean --all --yes {{ NO_RCS_ARG }}' push 'Failed to clear package cache' push 'WithLog' call AbortRetryNSExecWait ${EndIf} ${If} $Ana_AddToPath_State = ${BST_CHECKED} - DetailPrint "Adding to PATH..." + ${Print} "Adding to PATH..." push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" addpath ${PYVERSION} ${NAME} ${VERSION} ${ARCH}' - push 'Failed to add @NAME@ to the system PATH' + push 'Failed to add {{ NAME }} to the system PATH' push 'WithLog' call AbortRetryNSExecWait ${EndIf} @@ -1263,7 +1506,7 @@ Section "Install" # BU - built-in (local) users # DU - domain users ${If} ${UAC_IsAdmin} - DetailPrint "Setting installation directory permissions..." + ${Print} "Setting installation directory permissions..." AccessControl::DisableFileInheritance "$INSTDIR" AccessControl::RevokeOnFile "$INSTDIR" "(AU)" "GenericWrite" AccessControl::RevokeOnFile "$INSTDIR" "(DU)" "GenericWrite" @@ -1271,11 +1514,12 @@ Section "Install" AccessControl::SetOnFile "$INSTDIR" "(BU)" "GenericRead + GenericExecute" AccessControl::SetOnFile "$INSTDIR" "(DU)" "GenericRead + GenericExecute" ${EndIf} + ${Print} "Done!" SectionEnd !macro AbortRetryNSExecWaitLibNsisCmd cmd SetDetailsPrint both - DetailPrint "Running ${cmd} scripts..." + ${Print} "Running ${cmd} scripts..." SetDetailsPrint listonly ${If} ${Silent} push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" ${cmd}' @@ -1290,10 +1534,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 @@ -1305,6 +1550,10 @@ Section "Uninstall" # carefully. More info at https://docs.conda.io/projects/conda/en/latest/user-guide/troubleshooting.html#solution System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_DLL_SEARCH_MODIFICATION_ENABLE", "1").r0' + # Spinners in conda write a new character with each movement of the spinner. + # For long installation times, this may cause a buffer overflow, crashing the installer. + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1")".r0' + # Read variables the uninstaller needs from the registry StrCpy $R0 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" StrLen $R1 "Uninstall-${NAME}.exe" @@ -1337,17 +1586,75 @@ Section "Uninstall" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_VER", "$0").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_PLAT", "${PLATFORM}").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_TYPE", "EXE").r0' + ${If} ${Silent} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "1").r0' + ${Else} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0' + ${EndIf} +{%- if uninstall_with_conda_exe %} !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" - DetailPrint "Removing files and folders..." + # Parse arguments + StrCpy $R0 "" + + ${If} $UninstRemoveConfigFiles_User_State == ${BST_CHECKED} + ${If} $UninstRemoveConfigFiles_System_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-config-files=all" + ${Else} + StrCpy $R0 "$R0 --remove-config-files=user" + ${EndIf} + ${ElseIf} $UninstRemoveConfigFiles_System_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-config-files=system" + ${EndIf} + + ${If} $UninstRemoveUserData_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-user-data" + ${EndIf} + + ${If} $UninstRemoveCaches_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-caches" + ${EndIf} + + ${Print} "Removing files and folders..." + 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 + call un.AbortRetryNSExecWait + SetDetailsPrint both + + # 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} +{%- else %} +{%- for env in SETUP_ENVS | reverse %} + {%- set subdir = ("\envs\%(name)s" | format(name=env.name)) if env.name != "base" else "" %} + SetDetailsPrint both + ${Print} "Deleting ${NAME} menus in {{ env.name }}..." + SetDetailsPrint listonly + push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR{{ subdir }}" --rm-menus' + push 'Failed to delete menus in {{ env.name }}' + push 'WithLog' + call un.AbortRetryNSExecWait + SetDetailsPrint both +{%- endfor %} + !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" +{%- endif %} + # Delete user environment variables that we set during installation ${IfNot} ${UAC_IsAdmin} DeleteRegValue ${env_hkcu} "GR_PREFIX" @@ -1377,12 +1684,20 @@ Section "Uninstall" IntOp $0 $0 + 1 goto loop_py endloop_py: + + ${Print} "Done!" + ${If} ${Silent} + # give it some time so users can read the last lines + ${Print} "Closing in 3s..." + Sleep 3000 + ${EndIf} + SectionEnd -!if '@SIGNTOOL_COMMAND@' != '' +!if '{{ SIGNTOOL_COMMAND }}' != '' # Signing for installer and uninstaller; nsis 3.08 required for uninstfinalize! # "= 0" comparison required to prevent both tasks running in parallel, which would cause signtool to fail # %1 is replaced by the installer and uninstaller paths, respectively - !finalize '@SIGNTOOL_COMMAND@ "%1"' = 0 - !uninstfinalize '@SIGNTOOL_COMMAND@ "%1"' = 0 + !finalize '{{ SIGNTOOL_COMMAND }} "%1"' = 0 + !uninstfinalize '{{ SIGNTOOL_COMMAND }} "%1"' = 0 !endif diff --git a/constructor/nsis/main.nsi.tmpl.orig b/constructor/nsis/main.nsi.tmpl.orig index 664e79b..030b4cc 100644 --- a/constructor/nsis/main.nsi.tmpl.orig +++ b/constructor/nsis/main.nsi.tmpl.orig @@ -1,15 +1,17 @@ # Installer template file for creating a Windows installer using NSIS. -# Dependencies: -# NSIS >=3.08 conda install "nsis>=3.08" (includes extra unicode plugins) +!if "${NSIS_PACKEDVERSION}" < 0x3008000 + !error "NSIS 3.08 or higher is required to build this installer!" + # conda install "nsis>=3.08" (includes extra unicode plugins) +!endif -Unicode "true" +Unicode true -#if enable_debugging is True +{%- if enable_debugging %} # Special logging build needed for ENABLE_LOGGING # See https://nsis.sourceforge.io/Special_Builds !define ENABLE_LOGGING -#endif +{%- endif %} # Comes from https://nsis.sourceforge.io/Logging:Enable_Logs_Quickly !define LogSet "!insertmacro LogSetMacro" @@ -26,6 +28,29 @@ Unicode "true" !endif !macroend +var /global QuietMode # "0" = print normally, "1" = do not print +var /global StdOutHandle +var /global StdOutHandleSet +!define Print "!insertmacro PrintMacro" +!macro PrintMacro INPUT_TEXT + DetailPrint "${INPUT_TEXT}" + ${If} ${Silent} + ${AndIf} $QuietMode != "1" + ${IfNot} $StdOutHandleSet == "1" + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + System::Call 'kernel32::AttachConsole(i -1)i.r1' + ${If} $0 = 0 + ${OrIf} $1 = 0 + System::Call 'kernel32::AllocConsole()' + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + ${EndIf} + StrCpy $StdOutHandle $0 + StrCpy $StdOutHandleSet "1" + ${EndIf} + FileWrite $StdOutHandle "${INPUT_TEXT}$\n" + ${EndIf} +!macroend + !include "WinMessages.nsh" !include "WordFunc.nsh" !include "LogicLib.nsh" @@ -42,26 +67,30 @@ Unicode "true" !include "Utils.nsh" -!define NAME __NAME__ -!define VERSION __VERSION__ -!define COMPANY __COMPANY__ -!define ARCH __ARCH__ -!define PLATFORM __PLATFORM__ -!define CONSTRUCTOR_VERSION __CONSTRUCTOR_VERSION__ -!define PY_VER __PY_VER__ -!define PYVERSION_JUSTDIGITS __PYVERSION_JUSTDIGITS__ -!define PYVERSION __PYVERSION__ -!define PYVERSION_MAJOR __PYVERSION_MAJOR__ -!define DEFAULT_PREFIX __DEFAULT_PREFIX__ -!define DEFAULT_PREFIX_DOMAIN_USER __DEFAULT_PREFIX_DOMAIN_USER__ -!define DEFAULT_PREFIX_ALL_USERS __DEFAULT_PREFIX_ALL_USERS__ -!define PRE_INSTALL_DESC __PRE_INSTALL_DESC__ -!define POST_INSTALL_DESC __POST_INSTALL_DESC__ -!define ENABLE_SHORTCUTS __ENABLE_SHORTCUTS__ -!define SHOW_REGISTER_PYTHON __SHOW_REGISTER_PYTHON__ -!define SHOW_ADD_TO_PATH __SHOW_ADD_TO_PATH__ +{%- if uninstall_with_conda_exe %} +!include "StandaloneUninstallerOptions.nsh" +{%- endif %} + +!define NAME {{ installer_name }} +!define VERSION {{ installer_version }} +!define COMPANY {{ company }} +!define ARCH {{ arch }} +!define PLATFORM {{ installer_platform }} +!define CONSTRUCTOR_VERSION {{ constructor_version }} +!define PY_VER {{ pyver_components[:2] | join(".") }} +!define PYVERSION_JUSTDIGITS {{ pyver_components | join("") }} +!define PYVERSION {{ pyver_components | join(".") }} +!define PYVERSION_MAJOR {{ pyver_components[0] }} +!define DEFAULT_PREFIX {{ default_prefix }} +!define DEFAULT_PREFIX_DOMAIN_USER {{ default_prefix_domain_user }} +!define DEFAULT_PREFIX_ALL_USERS {{ default_prefix_all_users }} +!define PRE_INSTALL_DESC {{ pre_install_desc }} +!define POST_INSTALL_DESC {{ post_install_desc }} +!define ENABLE_SHORTCUTS {{ enable_shortcuts }} +!define SHOW_REGISTER_PYTHON {{ show_register_python }} +!define SHOW_ADD_TO_PATH {{ show_add_to_path }} !define PRODUCT_NAME "${NAME} ${VERSION} (${ARCH})" -!define UNINSTALL_NAME "@UNINSTALL_NAME@" +!define UNINSTALL_NAME "{{ UNINSTALL_NAME }}" !define UNINSTREG "SOFTWARE\Microsoft\Windows\CurrentVersion\ \Uninstall\${UNINSTALL_NAME}" @@ -84,6 +113,12 @@ var /global ARGV_NoRegistry 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 @@ -104,7 +139,7 @@ CRCCheck On # Basic options Name "${PRODUCT_NAME}" -OutFile __OUTFILE__ +OutFile {{ outfile }} ShowInstDetails "hide" ShowUninstDetails "hide" # This installer contains tar.bz2 files, which are already compressed @@ -121,33 +156,33 @@ VIAddVersionKey "CompanyName" "${COMPANY}" VIAddVersionKey "LegalCopyright" "(c) ${COMPANY}" VIAddVersionKey "FileDescription" "${NAME} Installer" VIAddVersionKey "Comments" "Created by constructor ${CONSTRUCTOR_VERSION}" -VIProductVersion __VIPV__ +VIProductVersion {{ vipv }} BrandingText /TRIMLEFT "${COMPANY}" # Interface configuration -!define MUI_ICON __ICONFILE__ -!define MUI_UNICON __ICONFILE__ +!define MUI_ICON {{ iconfile }} +!define MUI_UNICON {{ iconfile }} !define MUI_HEADERIMAGE -!define MUI_HEADERIMAGE_BITMAP __HEADERIMAGE__ -!define MUI_HEADERIMAGE_UNBITMAP __HEADERIMAGE__ +!define MUI_HEADERIMAGE_BITMAP {{ headerimage }} +!define MUI_HEADERIMAGE_UNBITMAP {{ headerimage }} !define MUI_ABORTWARNING !define MUI_FINISHPAGE_NOAUTOCLOSE !define MUI_UNFINISHPAGE_NOAUTOCLOSE -!define MUI_WELCOMEFINISHPAGE_BITMAP __WELCOMEIMAGE__ -!define MUI_UNWELCOMEFINISHPAGE_BITMAP __WELCOMEIMAGE__ +!define MUI_WELCOMEFINISHPAGE_BITMAP {{ welcomeimage }} +!define MUI_UNWELCOMEFINISHPAGE_BITMAP {{ welcomeimage }} #!define MUI_CUSTOMFUNCTION_GUIINIT GuiInit # Pages #!define MUI_PAGE_CUSTOMFUNCTION_SHOW OnStartup -#if custom_welcome +{%- if custom_welcome %} # Custom welcome file(s) -@CUSTOM_WELCOME_FILE@ -#else +{{ CUSTOM_WELCOME_FILE }} +{%- else %} !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipPageIfUACInnerInstance !insertmacro MUI_PAGE_WELCOME -#endif +{%- endif %} !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipPageIfUACInnerInstance -!insertmacro MUI_PAGE_LICENSE __LICENSEFILE__ +!insertmacro MUI_PAGE_LICENSE {{ licensefile }} Page Custom InstModePage_Create InstModePage_Leave !define MUI_PAGE_CUSTOMFUNCTION_PRE DisableBackButtonIfUACInnerInstance !define MUI_PAGE_CUSTOMFUNCTION_LEAVE OnDirectoryLeave @@ -155,23 +190,31 @@ Page Custom InstModePage_Create InstModePage_Leave # Custom options now differ depending on installation mode. Page Custom mui_AnaCustomOptions_Show !insertmacro MUI_PAGE_INSTFILES -#if with_conclusion_text is True -!define MUI_FINISHPAGE_TITLE __CONCLUSION_TITLE__ -!define MUI_FINISHPAGE_TITLE_3LINES -!define MUI_FINISHPAGE_TEXT __CONCLUSION_TEXT__ -#endif -#if custom_conclusion +{%- for page in POST_INSTALL_PAGES %} +{{ page }} +{%- endfor %} + +{%- if with_conclusion_text %} +!define MUI_FINISHPAGE_TITLE {{ conclusion_title }} +!define MUI_FINISHPAGE_TITLE_3LINES +!define MUI_FINISHPAGE_TEXT {{ conclusion_text }} +{%- endif %} + +{%- if custom_conclusion %} # Custom conclusion file(s) -@CUSTOM_CONCLUSION_FILE@ -#else +{{ CUSTOM_CONCLUSION_FILE }} +{%- else %} !insertmacro MUI_PAGE_FINISH -#endif +{%- 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 @@ -224,35 +267,54 @@ FunctionEnd ${GetParameters} $ARGV ${GetOptions} $ARGV "/?" $ARGV_Help ${IfNot} ${Errors} - MessageBox MB_OK|MB_ICONEXCLAMATION \ - "Usage: $EXEFILE [options]$\n\ - Options:$\n$\n\ - /InstallationType=AllUsers [default: JustMe]$\n$\n\ - /AddToPath=[0|1] [default: 0]$\n$\n\ -#if keep_pkgs is True - /KeepPkgCache=[0|1] [default: 1]$\n$\n\ -#endif -#if keep_pkgs is False - /KeepPkgCache=[0|1] [default: 0]$\n$\n\ -#endif - /RegisterPython=[0|1] [default: AllUsers: 1, JustMe: 0]$\n$\n\ - /NoRegistry=[0|1] [default: AllUsers: 0, JustMe: 0]$\n$\n\ - /NoScripts=[0|1] [default: 0]$\n$\n\ - /NoShortcuts=[0|1] [default: 0]$\n$\n\ - /CheckPathLength=[0|1] [default: 1]$\n$\n\ - Examples:$\n\ + SetSilent silent + ${Print} "\ + Installs ${NAME} ${VERSION}$\n\ + $\n\ + USAGE$\n\ + -----$\n\ + $\n\ + $EXEFILE [options]$\n\ + $\n\ + OPTIONS$\n\ + -------$\n\ + $\n\ + /InstallationType=AllUsers [default: JustMe]$\n\ + /AddToPath=[0|1] [default: 0]$\n\ + /KeepPkgCache=[0|1] [default: {{ 1 if keep_pkgs else 0 }}]$\n\ + /RegisterPython=[0|1] [default: AllUsers: 1, JustMe: 0]$\n\ + /NoRegistry=[0|1] [default: AllUsers: 0, JustMe: 0]$\n\ + /NoScripts=[0|1] [default: 0]$\n\ + /NoShortcuts=[0|1] [default: 0]$\n\ + /CheckPathLength=[0|1] [default: 1]$\n\ + /? (show this help message)$\n\ + /S (run in CLI/headless mode)$\n\ + /Q (quiet mode, do not print output to console)$\n\ + /D=[installation directory] (must be last parameter)$\n" + # There seems to be a limit to how many chars per ${Print} we can pass. + # The message will get truncated silently, no errors. + # That's why we split the help message in two calls. + ${Print} "\ + EXAMPLES$\n\ + --------$\n\ + $\n\ Install for all users, but don't add to PATH env var:$\n\ - $EXEFILE /InstallationType=AllUsers$\n$\n\ + > $EXEFILE /InstallationType=AllUsers$\n\ + $\n\ Install for just me, add to PATH and register as system Python:$\n\ - $EXEFILE /RegisterPython=1 /AddToPath=1$\n$\n\ + > $EXEFILE /RegisterPython=1 /AddToPath=1$\n\ + $\n\ Install for just me, with no registry modification (for CI):$\n\ - $EXEFILE /NoRegistry=1$\n$\n\ + > $EXEFILE /NoRegistry=1$\n\ + $\n\ + Install via CLI (no GUI) into C:\${NAME}$\n\ + > cmd /C START /WAIT $EXEFILE /S /D=C:\${NAME}$\n\ + $\n\ NOTE: If you install for AllUsers, then the option to AddToPath$\n\ - is disabled (i.e. if ./InstallationType=AllUsers, then$\n\ - /AddToPath=1 will be ignored).$\n" \ - /SD IDOK - Abort - ${EndIf} + is disabled (i.e. if /InstallationType=AllUsers, then$\n\ + /AddToPath=1 will be ignored)." + Abort + ${EndIf} ClearErrors ${GetOptions} $ARGV "/InstallationType=" $ARGV_InstallationType @@ -277,7 +339,7 @@ FunctionEnd ClearErrors ${GetOptions} $ARGV "/KeepPkgCache=" $ARGV_KeepPkgCache ${If} ${Errors} - StrCpy $ARGV_KeepPkgCache "@KEEP_PKGS@" + StrCpy $ARGV_KeepPkgCache "{{ 1 if keep_pkgs else 0 }}" ${EndIf} ClearErrors @@ -320,6 +382,12 @@ FunctionEnd ${EndIf} ${EndIf} + ClearErrors + ${GetOptions} $ARGV "/Q" $ARGV_QuietMode + ${IfNot} ${Errors} + StrCpy $QuietMode "1" + ${EndIf} + !macroend Function OnInit_Release @@ -336,6 +404,7 @@ Function OnInit_Release # To address CVE-2022-26526. # In AllUsers install mode, do not allow AddToPath as an option. MessageBox MB_OK|MB_ICONEXCLAMATION "/AddToPath=1 is disabled and ignored in 'All Users' installations" /SD IDOK + ${Print} "/AddToPath=1 is disabled and ignored in 'All Users' installations" StrCpy $Ana_AddToPath_State ${BST_UNCHECKED} ${Else} StrCpy $Ana_AddToPath_State ${BST_CHECKED} @@ -488,13 +557,18 @@ Function .onInit Push $R2 InitPluginsDir - @TEMP_EXTRA_FILES@ +{%- if TEMP_EXTRA_FILES | length != 0 %} + SetOutPath $PLUGINSDIR +{%- for file in TEMP_EXTRA_FILES %} + File {{ file }} +{%- endfor %} +{%- endif %} !insertmacro ParseCommandLineArgs # Select the correct registry to look at, depending # on whether it's a 32-bit or 64-bit installer - SetRegView @BITS@ -#if win64 + SetRegView {{ BITS }} +{%- if win64 %} # If we're a 64-bit installer, make sure it's 64-bit Windows ${IfNot} ${RunningX64} MessageBox MB_OK|MB_ICONEXCLAMATION \ @@ -504,7 +578,7 @@ Function .onInit /SD IDOK Abort ${EndIf} -#endif +{%- endif %} !insertmacro UAC_PageElevation_OnInit ${If} ${UAC_IsInnerInstance} @@ -607,9 +681,9 @@ Function .onInit ${IfNot} ${UAC_IsAdmin} MessageBox MB_ICONSTOP "Installation for all users requires an elevated prompt." Abort - ${EndIF} - ${EndIF} - ${EndIF} + ${EndIf} + ${EndIf} + ${EndIf} ; /D was not used, add default based on install type ${If} $InstDir == "" @@ -630,47 +704,28 @@ Function .onInit Call mui_AnaCustomOptions_InitDefaults # Override custom options with explicitly given values from contruct.yaml. # If initialize_by_default (register_python_default) is None, do nothing. -#if initialize_conda is True and initialize_by_default is True +{%- if initialize_conda %} + {%- if initialize_by_default %} ${If} $InstMode == ${JUST_ME} StrCpy $Ana_AddToPath_State ${BST_CHECKED} - ${EndIF} -#endif -#if initialize_conda is True and initialize_by_default is False + ${EndIf} + {%- else %} StrCpy $Ana_AddToPath_State ${BST_UNCHECKED} -#endif -#if register_python is True and register_python_default is True - StrCpy $Ana_RegisterSystemPython_State ${BST_CHECKED} -#endif -#if register_python is True and register_python_default is False - StrCpy $Ana_RegisterSystemPython_State ${BST_UNCHECKED} -#endif -#if check_path_length is True - StrCpy $CheckPathLength "1" -#endif -#if check_path_length is False - StrCpy $CheckPathLength "0" -#endif -#if keep_pkgs is True - StrCpy $Ana_ClearPkgCache_State ${BST_UNCHECKED} -#endif -#if keep_pkgs is False - StrCpy $Ana_ClearPkgCache_State ${BST_CHECKED} -#endif -#if pre_install_exists is True - StrCpy $Ana_PreInstall_State ${BST_CHECKED} -#endif -#if pre_install_exists is False - StrCpy $Ana_PreInstall_State ${BST_UNCHECKED} -#endif -#if post_install_exists is True - StrCpy $Ana_PostInstall_State ${BST_CHECKED} -#endif -#if post_install_exists is False - StrCpy $Ana_PostInstall_State ${BST_UNCHECKED} -#endif + {%- endif %} +{%- endif %} + +{%- if register_python %} + StrCpy $Ana_RegisterSystemPython_State {{ '${BST_CHECKED}' if register_python_default else '${BST_UNCHECKED}' }} +{%- endif %} + StrCpy $CheckPathLength "{{ 1 if check_path_length else 0 }}" + StrCpy $Ana_ClearPkgCache_State {{ '${BST_UNCHECKED}' if keep_pkgs else '${BST_CHECKED}' }} + StrCpy $Ana_PreInstall_State {{ '${BST_CHECKED}' if pre_install_exists else '${BST_UNCHECKED}' }} + StrCpy $Ana_PostInstall_State {{ '${BST_CHECKED}' if post_install_exists else '${BST_UNCHECKED}' }} Call OnInit_Release + ${Print} "Welcome to ${NAME} ${VERSION}$\n" + Pop $R2 Pop $R1 Pop $2 @@ -678,7 +733,106 @@ Function .onInit Pop $0 FunctionEnd +!macro un.ParseCommandLineArgs + ClearErrors + ${GetParameters} $ARGV + ${GetOptions} $ARGV "/?" $ARGV_Help + ${IfNot} ${Errors} + SetSilent silent + ${Print} "\ + Uninstalls ${NAME} ${VERSION}$\n\ + $\n\ + USAGE$\n\ + -----$\n\ + $\n\ + Uninstall-${NAME}.exe [options]$\n\ + $\n\ + OPTIONS$\n\ + -------$\n\ + $\n\ + /? (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\ + --------$\n\ + $\n\ + Uninstall via CLI (no GUI) from C:\${NAME}$\n\ + > cmd /C START /WAIT Uninstall-${NAME}.exe /S /_?=C:\${NAME}$\n\ + $\n\ + 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, so we can't get the parent console handle + Sleep 10000 + Abort + ${EndIf} + + ClearErrors + ${GetOptions} $ARGV "/Q" $ARGV_QuietMode + ${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 Push $2 @@ -727,6 +881,7 @@ Function un.onInit goto valid_dir invalid_dir: + ${Print} "::error:: $INSTDIR is not a valid conda directory. Please run the uninstaller from a conda directory." MessageBox MB_OK|MB_ICONSTOP \ "Error: $INSTDIR is not a valid conda directory. Please run the uninstaller from a conda directory." \ /SD IDABORT @@ -735,7 +890,7 @@ Function un.onInit # Select the correct registry to look at, depending # on whether it's a 32-bit or 64-bit installer - SetRegView @BITS@ + SetRegView {{ BITS }} # Since the switch to a dual-mode installer (All Users/Just Me), the # uninstaller will inherit the requested execution level of the main @@ -848,7 +1003,7 @@ Pop $0 Function OnDirectoryLeave ${LogSet} on ${If} ${IsNonEmptyDirectory} "$InstDir" - DetailPrint "::error:: Directory '$INSTDIR' is not empty, please choose a different location." + ${Print} "::error:: Directory '$INSTDIR' is not empty, please choose a different location." MessageBox MB_OK|MB_ICONEXCLAMATION \ "Directory '$INSTDIR' is not empty,$\n\ please choose a different location." \ @@ -871,7 +1026,7 @@ Function OnDirectoryLeave WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1 ; If we don't have admin right, we suggest a shorter path or suggest to run with admin right ${Else} - DetailPrint "::error:: The installation path should be shorter than 46 characters or \ + ${Print} "::error:: The installation path should be shorter than 46 characters or \ the installation requires administrator rights to enable long \ path on Windows." MessageBox MB_OK|MB_ICONSTOP "The installation path should be shorter than 46 characters or \ @@ -882,7 +1037,7 @@ Function OnDirectoryLeave ${EndIf} ; If we don't have admin right, we suggest a shorter path or suggest to run with admin right ${Else} - DetailPrint "::error:: The installation path should be shorter than 46 characters. \ + ${Print} "::error:: The installation path should be shorter than 46 characters. \ Please choose another location." MessageBox MB_OK|MB_ICONSTOP "The installation path should be shorter than 46 characters. \ Please choose another location." \ @@ -914,22 +1069,22 @@ Function OnDirectoryLeave StrCpy $R7 "$\n" ${EndIf} StrCpy $R8 "'Destination Folder' contains $R0 space$R1.$R7This can cause problems with several conda packages.$R7" -#if check_path_spaces is True +{%- if check_path_spaces %} StrCpy $R8 "$R8Please remove the space$R1 from the destination folder." StrCpy $R9 "Error" -#else +{%- else %} StrCpy $R8 "$R8Please consider removing the space$R1." StrCpy $R9 "Warning" -#endif +{%- endif %} # Show message box then take the user back to the Directory page. ${If} ${Silent} - DetailPrint "::$R9:: $R8" + ${Print} "::$R9:: $R8" ${Else} MessageBox MB_OK|MB_ICONINFORMATION "$R9: $R8" /SD IDOK ${EndIf} -#if check_path_spaces is True +{%- if check_path_spaces %} abort -#endif +{%- endif %} NoSpaces: Pop $R7 Pop $R8 @@ -942,7 +1097,7 @@ Function OnDirectoryLeave Pop $R0 StrCmp $R0 "" NoInvalidCharaceters - DetailPrint "::error:: 'Destination Folder' contains the following invalid character: $R0" + ${Print} "::error:: 'Destination Folder' contains the following invalid character: $R0" MessageBox MB_OK|MB_ICONEXCLAMATION \ "Error: 'Destination Folder' contains the following invalid character: $R0" \ /SD IDOK @@ -952,7 +1107,7 @@ Function OnDirectoryLeave UnicodePathTest::SpecialCharPathTest $INSTDIR Pop $R1 StrCmp $R1 "nothingspecial" nothing_special_path - DetailPrint "::error:: 'Destination Folder' contains the following invalid character$R1" + ${Print} "::error:: 'Destination Folder' contains the following invalid character$R1" MessageBox MB_OK|MB_ICONEXCLAMATION \ "Error: 'Destination Folder' contains the following invalid character$R1" \ /SD IDOK @@ -970,7 +1125,7 @@ Function OnDirectoryLeave StrCmp ${PY_VER} "2.7" not_cp_acp_capable StrCmp $R1 "ascii_cp_acp" valid_path not_cp_acp_capable: - DetailPrint "::error:: Due to incompatibility with several \ + ${Print} "::error:: Due to incompatibility with several \ Python libraries, 'Destination Folder' cannot contain non-ascii characters \ (special characters or diacritics). Please choose another location." MessageBox MB_OK|MB_ICONEXCLAMATION "Error: Due to incompatibility with several \ @@ -985,7 +1140,7 @@ Function OnDirectoryLeave ${IsWritable} $INSTDIR $R1 IntCmp $R1 0 pathgood Pop $R1 - DetailPrint "::error: Path $INSTDIR is not writable. Please check permissions or \ + ${Print} "::error: Path $INSTDIR is not writable. Please check permissions or \ try respawning the installer with elevated privileges." MessageBox MB_OK|MB_ICONEXCLAMATION \ "Error: Path $INSTDIR is not writable. Please check permissions or \ @@ -1039,12 +1194,12 @@ FunctionEnd ${ElseIf} $1 == "NoLog" nsExec::Exec $3 ${Else} - DetailPrint "::error:: AbortRetryNSExecWait: 1st argument must be 'WithLog' or 'NoLog'. You used: $1" + ${Print} "::error:: AbortRetryNSExecWait: 1st argument must be 'WithLog' or 'NoLog'. You used: $1" Abort ${EndIf} pop $0 ${If} $0 != "0" - DetailPrint "::error:: $2" + ${Print} "::error:: $2" MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON3 \ $2 /SD IDIGNORE IDABORT abort IDRETRY retry ; IDIGNORE: Continue anyway @@ -1065,14 +1220,13 @@ FunctionEnd # Installer sections Section "Install" ${LogSet} on - ${If} ${Silent} call OnDirectoryLeave ${EndIf} SetOutPath "$INSTDIR\Lib" - File "@NSIS_DIR@\_nsis.py" - File "@NSIS_DIR@\_system_path.py" + File "{{ NSIS_DIR }}\_nsis.py" + File "{{ NSIS_DIR }}\_system_path.py" # Resolve INSTDIR so that paths and registry keys do not contain '..' or similar strings. # $0 is empty if the directory doesn't exist, but the File commands should have created it already. @@ -1083,6 +1237,17 @@ Section "Install" ${EndIf} StrCpy $INSTDIR $0 +{%- if has_license %} + SetOutPath "$INSTDIR" + File {{ licensefile }} + ${Print} "By continuing this installation you are accepting this license agreement:" + ${Print} "$INSTDIR\{{ LICENSEFILENAME }}" + ${Print} "Please run the installer in GUI mode to read the details.$\n" +{%- endif %} + + ${Print} "${NAME} will now be installed into this location:" + ${Print} "$INSTDIR$\n" + ReadEnvStr $0 SystemRoot # set PATH for the installer process, so that MSVC runtimes get found OK # This is also isolating PATH to be just us and Windows core stuff, which hopefully avoids @@ -1090,16 +1255,22 @@ Section "Install" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("PATH", \ "$INSTDIR;$INSTDIR\Library\mingw-w64\bin;$INSTDIR\Library\usr\bin;$INSTDIR\Library\bin;$INSTDIR\Scripts;$INSTDIR\bin;$0;$0\system32;$0\system32\Wbem").r0' + ${Print} "Unpacking payload..." + # A conda-meta\history file is required for a valid conda prefix SetOutPath "$INSTDIR\conda-meta" - File __CONDA_HISTORY__ + File {{ conda_history }} SetOutPath "$INSTDIR" - File __CONDA_EXE__ - File __PRE_UNINSTALL__ + File {{ conda_exe }} + File {{ pre_uninstall }} - # Copy extra files (code generated on winexe.py) - @EXTRA_FILES@ +{%- for path, files in EXTRA_FILES | items %} + SetOutPath {{ path }} +{%- for file in files %} + File {{ file }} +{%- endfor %} +{%- endfor %} ${If} $InstMode = ${JUST_ME} SetOutPath "$INSTDIR" @@ -1108,20 +1279,26 @@ Section "Install" ${EndIf} SetOutPath "$INSTDIR\pkgs" - File __URLS_FILE__ - File __URLS_TXT_FILE__ -#if pre_install_exists is True - File __PRE_INSTALL__ -#endif - File __POST_INSTALL__ - File /nonfatal /r __INDEX_CACHE__ - File /r __REPODATA_RECORD__ + File {{ urls_file }} + File {{ urls_txt_file }} +{%- if pre_install_exists %} + File {{ pre_install }} +{%- endif %} + File {{ post_install }} + File /nonfatal /r {{ index_cache }} + File /r {{ repodata_record }} - @SCRIPT_ENV_VARIABLES@ + +{%- for key, escaped_val in SCRIPT_ENV_VARIABLES | items %} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("{{ key }}", {{ escaped_val }}).r0' +{%- endfor %} System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SAFETY_CHECKS", "disabled").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_EXTRA_SAFETY_CHECKS", "no").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_PKGS_DIRS", "$INSTDIR\pkgs")".r0' + # Spinners in conda write a new character with each movement of the spinner. + # For long installation times, this may cause a buffer overflow, crashing the installer. + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1")".r0' # Extra info for pre and post install scripts # NOTE: If more vars are added, make sure to update the examples/scripts tests too # There's a similar block for the pre_uninstall script, further down this file. @@ -1131,11 +1308,33 @@ Section "Install" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_VER", "${VERSION}").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_PLAT", "${PLATFORM}").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_TYPE", "EXE").r0' + ${If} ${Silent} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "1").r0' + ${Else} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0' + ${EndIf} - @PKG_COMMANDS@ + ${If} '{{ VIRTUAL_SPECS }}' != '' + # We need to specify CONDA_SOLVER=classic for conda-standalone + # to work around this bug in conda-libmamba-solver: + # https://github.com/conda/conda-libmamba-solver/issues/480 + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SOLVER", "classic").r0' + SetDetailsPrint TextOnly + ${Print} "Checking virtual specs compatibility: {{ VIRTUAL_SPECS_DEBUG }}" + push '"$INSTDIR\_conda.exe" create --dry-run --prefix "$INSTDIR\envs\_virtual_specs_checks" --offline {{ VIRTUAL_SPECS }} {{ NO_RCS_ARG }}' + push 'Failed to check virtual specs: {{ VIRTUAL_SPECS_DEBUG }}' + push 'WithLog' + call AbortRetryNSExecWait + SetDetailsPrint both + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SOLVER", "").r0' + ${EndIf} + +{%- for dist in DISTS %} + File {{ dist }} +{%- endfor %} SetDetailsPrint TextOnly - DetailPrint "Setting up the package cache..." + ${Print} "Setting up the package cache..." push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR" --extract-conda-pkgs' push 'Failed to extract packages' push 'NoLog' @@ -1145,7 +1344,7 @@ Section "Install" SetDetailsPrint both IfFileExists "$INSTDIR\pkgs\pre_install.bat" 0 NoPreInstall - DetailPrint "Running pre_install scripts..." + ${Print} "Running pre_install scripts..." ReadEnvStr $5 SystemRoot ReadEnvStr $6 windir # This 'FileExists' also returns True for directories @@ -1162,22 +1361,66 @@ Section "Install" call AbortRetryNSExecWait NoPreInstall: - @SETUP_ENVS@ +{%- for env in SETUP_ENVS %} + {%- set channels = env.final_channels|join(",") %} + # Set up {{ env.name }} env + SetDetailsPrint both + ${Print} "Setting up the {{ env.name }} environment..." + SetDetailsPrint listonly - @WRITE_CONDARC@ + # List of packages to install + SetOutPath "{{ env.env_txt_dir }}" + File "{{ env.env_txt_abspath }}" - AddSize @SIZE@ + # A conda-meta\history file is required for a valid conda prefix + SetOutPath "{{ env.conda_meta }}" + File "{{ env.history_abspath }}" -#if has_conda is True - DetailPrint "Initializing conda directories..." + # Set channels + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_CHANNELS", "{{ channels }}").r0' + # Set register_envs + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_REGISTER_ENVS", "{{ env.register_envs }}").r0' + + # Run conda install + ${If} $Ana_CreateShortcuts_State = ${BST_CHECKED} + ${Print} "Installing packages for {{ env.name }}, creating shortcuts if necessary..." + push '"$INSTDIR\_conda.exe" install --offline -yp "{{ env.prefix }}" --file "{{ env.env_txt }}" {{ env.shortcuts }} {{ env.no_rcs_arg }}' + ${Else} + ${Print} "Installing packages for {{ env.name }}..." + push '"$INSTDIR\_conda.exe" install --offline -yp "{{ env.prefix }}" --file "{{ env.env_txt }}" --no-shortcuts {{ env.no_rcs_arg }}' + ${EndIf} + push 'Failed to link extracted packages to {{ env.prefix }}!' + push 'WithLog' + SetDetailsPrint listonly + call AbortRetryNSExecWait + SetDetailsPrint both + + # Cleanup {{ env.name }} env.txt + SetOutPath "$INSTDIR" + Delete "{{ env.env_txt }}" + + # Restore shipped conda-meta\history for remapped + # channels and retain only the first transaction + SetOutPath "{{ env.conda_meta }}" + File "{{ env.history_abspath }}" +{%- endfor %} + +{%- for condarc in WRITE_CONDARC %} + {{ condarc }} +{%- endfor %} + + AddSize {{ SIZE }} + +{%- if has_conda %} + ${Print} "Initializing conda directories..." push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" mkdirs' push 'Failed to initialize conda directories' push 'WithLog' call AbortRetryNSExecWait -#endif +{%- endif %} ${If} $Ana_PostInstall_State = ${BST_CHECKED} - DetailPrint "Running post install..." + ${Print} "Running post install..." push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" post_install' push 'Failed to run post install script' push 'WithLog' @@ -1185,17 +1428,17 @@ Section "Install" ${EndIf} ${If} $Ana_ClearPkgCache_State = ${BST_CHECKED} - DetailPrint "Clearing package cache..." - push '"$INSTDIR\_conda.exe" clean --all --force-pkgs-dirs --yes' + ${Print} "Clearing package cache..." + push '"$INSTDIR\_conda.exe" clean --all --force-pkgs-dirs --yes {{ NO_RCS_ARG }}' push 'Failed to clear package cache' push 'WithLog' call AbortRetryNSExecWait ${EndIf} ${If} $Ana_AddToPath_State = ${BST_CHECKED} - DetailPrint "Adding to PATH..." + ${Print} "Adding to PATH..." push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" addpath ${PYVERSION} ${NAME} ${VERSION} ${ARCH}' - push 'Failed to add @NAME@ to the system PATH' + push 'Failed to add {{ NAME }} to the system PATH' push 'WithLog' call AbortRetryNSExecWait ${EndIf} @@ -1243,7 +1486,7 @@ Section "Install" # BU - built-in (local) users # DU - domain users ${If} ${UAC_IsAdmin} - DetailPrint "Setting installation directory permissions..." + ${Print} "Setting installation directory permissions..." AccessControl::DisableFileInheritance "$INSTDIR" AccessControl::RevokeOnFile "$INSTDIR" "(AU)" "GenericWrite" AccessControl::RevokeOnFile "$INSTDIR" "(DU)" "GenericWrite" @@ -1251,11 +1494,12 @@ Section "Install" AccessControl::SetOnFile "$INSTDIR" "(BU)" "GenericRead + GenericExecute" AccessControl::SetOnFile "$INSTDIR" "(DU)" "GenericRead + GenericExecute" ${EndIf} + ${Print} "Done!" SectionEnd !macro AbortRetryNSExecWaitLibNsisCmd cmd SetDetailsPrint both - DetailPrint "Running ${cmd} scripts..." + ${Print} "Running ${cmd} scripts..." SetDetailsPrint listonly ${If} ${Silent} push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" ${cmd}' @@ -1270,10 +1514,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 @@ -1285,6 +1530,10 @@ Section "Uninstall" # carefully. More info at https://docs.conda.io/projects/conda/en/latest/user-guide/troubleshooting.html#solution System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_DLL_SEARCH_MODIFICATION_ENABLE", "1").r0' + # Spinners in conda write a new character with each movement of the spinner. + # For long installation times, this may cause a buffer overflow, crashing the installer. + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1")".r0' + # Read variables the uninstaller needs from the registry StrCpy $R0 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" StrLen $R1 "Uninstall-${NAME}.exe" @@ -1317,17 +1566,75 @@ Section "Uninstall" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_VER", "$0").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_PLAT", "${PLATFORM}").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_TYPE", "EXE").r0' + ${If} ${Silent} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "1").r0' + ${Else} + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0' + ${EndIf} +{%- if uninstall_with_conda_exe %} !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" - DetailPrint "Removing files and folders..." + # Parse arguments + StrCpy $R0 "" + + ${If} $UninstRemoveConfigFiles_User_State == ${BST_CHECKED} + ${If} $UninstRemoveConfigFiles_System_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-config-files=all" + ${Else} + StrCpy $R0 "$R0 --remove-config-files=user" + ${EndIf} + ${ElseIf} $UninstRemoveConfigFiles_System_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-config-files=system" + ${EndIf} + + ${If} $UninstRemoveUserData_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-user-data" + ${EndIf} + + ${If} $UninstRemoveCaches_State == ${BST_CHECKED} + StrCpy $R0 "$R0 --remove-caches" + ${EndIf} + + ${Print} "Removing files and folders..." + 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 + call un.AbortRetryNSExecWait + SetDetailsPrint both + + # 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} +{%- else %} +{%- for env in SETUP_ENVS | reverse %} + {%- set subdir = ("\envs\%(name)s" | format(name=env.name)) if env.name != "base" else "" %} + SetDetailsPrint both + ${Print} "Deleting ${NAME} menus in {{ env.name }}..." + SetDetailsPrint listonly + push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR{{ subdir }}" --rm-menus' + push 'Failed to delete menus in {{ env.name }}' + push 'WithLog' + call un.AbortRetryNSExecWait + SetDetailsPrint both +{%- endfor %} + !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" +{%- endif %} + ${If} $INSTALLER_NAME_FULL != "" DeleteRegKey SHCTX "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$INSTALLER_NAME_FULL" ${EndIf} @@ -1348,12 +1655,20 @@ Section "Uninstall" IntOp $0 $0 + 1 goto loop_py endloop_py: + + ${Print} "Done!" + ${If} ${Silent} + # give it some time so users can read the last lines + ${Print} "Closing in 3s..." + Sleep 3000 + ${EndIf} + SectionEnd -!if '@SIGNTOOL_COMMAND@' != '' +!if '{{ SIGNTOOL_COMMAND }}' != '' # Signing for installer and uninstaller; nsis 3.08 required for uninstfinalize! # "= 0" comparison required to prevent both tasks running in parallel, which would cause signtool to fail # %1 is replaced by the installer and uninstaller paths, respectively - !finalize '@SIGNTOOL_COMMAND@ "%1"' = 0 - !uninstfinalize '@SIGNTOOL_COMMAND@ "%1"' = 0 + !finalize '{{ SIGNTOOL_COMMAND }} "%1"' = 0 + !uninstfinalize '{{ SIGNTOOL_COMMAND }} "%1"' = 0 !endif diff --git a/radioconda_installer.yaml b/radioconda_installer.yaml index b9db522..0ddf11a 100644 --- a/radioconda_installer.yaml +++ b/radioconda_installer.yaml @@ -2,6 +2,7 @@ name: radioconda_installer category: installer channels: - conda-forge + - ryanvolz dependencies: - mamba - miniforge_console_shortcut # [win]