diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 2b0a35af5..6ff4ecdc0 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -1,9 +1,11 @@ # 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 # Special logging build needed for ENABLE_LOGGING @@ -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" @@ -84,6 +109,7 @@ var /global ARGV_NoRegistry var /global ARGV_NoScripts var /global ARGV_NoShortcuts var /global ARGV_CheckPathLength +var /global ARGV_QuietMode var /global IsDomainUser var /global CheckPathLength @@ -229,35 +255,59 @@ 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\ + 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\ #if keep_pkgs is True - /KeepPkgCache=[0|1] [default: 1]$\n$\n\ + /KeepPkgCache=[0|1] [default: 1]$\n\ #endif #if keep_pkgs is False - /KeepPkgCache=[0|1] [default: 0]$\n$\n\ + /KeepPkgCache=[0|1] [default: 0]$\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\ + /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 @@ -325,6 +375,12 @@ FunctionEnd ${EndIf} ${EndIf} + ClearErrors + ${GetOptions} $ARGV "/Q" $ARGV_QuietMode + ${IfNot} ${Errors} + StrCpy $QuietMode "1" + ${EndIf} + !macroend Function OnInit_Release @@ -341,6 +397,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} @@ -676,6 +733,8 @@ Function .onInit Call OnInit_Release + ${Print} "Welcome to ${NAME} ${VERSION}$\n" + Pop $R2 Pop $R1 Pop $2 @@ -684,6 +743,48 @@ Function .onInit FunctionEnd Function un.onInit + 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\ + /_?=[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 because actually running, 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} + Push $0 Push $1 Push $2 @@ -732,6 +833,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 @@ -853,7 +955,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." \ @@ -876,7 +978,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 \ @@ -887,7 +989,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." \ @@ -928,7 +1030,7 @@ Function OnDirectoryLeave #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} @@ -947,7 +1049,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 @@ -957,7 +1059,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 @@ -975,7 +1077,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 \ @@ -990,7 +1092,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 \ @@ -1044,12 +1146,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 @@ -1070,7 +1172,6 @@ FunctionEnd # Installer sections Section "Install" ${LogSet} on - ${If} ${Silent} call OnDirectoryLeave ${EndIf} @@ -1088,6 +1189,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 @@ -1095,6 +1207,8 @@ 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__ @@ -1143,9 +1257,9 @@ Section "Install" # https://github.com/conda/conda-libmamba-solver/issues/480 System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SOLVER", "classic").r0' SetDetailsPrint TextOnly - DetailPrint "Checking virtual specs..." + ${Print} "Checking virtual specs compatibility: @VIRTUAL_SPECS_DEBUG@" push '"$INSTDIR\_conda.exe" create --dry-run --prefix "$INSTDIR\envs\_virtual_specs_checks" --offline @VIRTUAL_SPECS@' - push 'Failed to check virtual specs: @VIRTUAL_SPECS@' + push 'Failed to check virtual specs: @VIRTUAL_SPECS_DEBUG@' push 'WithLog' call AbortRetryNSExecWait SetDetailsPrint both @@ -1155,7 +1269,7 @@ Section "Install" @PKG_COMMANDS@ 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' @@ -1165,7 +1279,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 @@ -1189,7 +1303,7 @@ Section "Install" AddSize @SIZE@ #if has_conda is True - DetailPrint "Initializing conda directories..." + ${Print} "Initializing conda directories..." push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" mkdirs' push 'Failed to initialize conda directories' push 'WithLog' @@ -1197,7 +1311,7 @@ Section "Install" #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' @@ -1205,7 +1319,7 @@ Section "Install" ${EndIf} ${If} $Ana_ClearPkgCache_State = ${BST_CHECKED} - DetailPrint "Clearing package cache..." + ${Print} "Clearing package cache..." push '"$INSTDIR\_conda.exe" clean --all --force-pkgs-dirs --yes' push 'Failed to clear package cache' push 'WithLog' @@ -1213,7 +1327,7 @@ Section "Install" ${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 'WithLog' @@ -1263,7 +1377,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 +1385,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}' @@ -1342,7 +1457,7 @@ Section "Uninstall" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" - DetailPrint "Removing files and folders..." + ${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 @@ -1368,6 +1483,14 @@ 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@' != '' diff --git a/constructor/shar.py b/constructor/shar.py index 322077d87..7e5a5157e 100644 --- a/constructor/shar.py +++ b/constructor/shar.py @@ -55,7 +55,7 @@ def read_header_template(): def get_header(conda_exec, tarball, info): name = info['name'] - has_license = bool('license_file' in info) + has_license = bool(info.get('license_file')) ppd = ns_platform(info['_platform']) ppd['keep_pkgs'] = bool(info.get('keep_pkgs', False)) ppd['batch_mode'] = bool(info.get('batch_mode', False)) diff --git a/constructor/winexe.py b/constructor/winexe.py index 99fe16c02..8d6d7fd4c 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -9,7 +9,7 @@ import shutil import sys import tempfile -from os.path import abspath, dirname, isfile, join +from os.path import abspath, basename, dirname, isfile, join from pathlib import Path from subprocess import check_output, run from typing import List, Union @@ -116,7 +116,7 @@ def setup_envs_commands(info, dir_path): template = r""" # Set up {name} env SetDetailsPrint both - DetailPrint "Setting up the {name} environment ..." + ${{Print}} "Setting up the {name} environment..." SetDetailsPrint listonly # List of packages to install @@ -134,10 +134,10 @@ def setup_envs_commands(info, dir_path): # Run conda install ${{If}} $Ana_CreateShortcuts_State = ${{BST_CHECKED}} - DetailPrint "Installing packages for {name}, creating shortcuts if necessary..." + ${{Print}} "Installing packages for {name}, creating shortcuts if necessary..." push '"$INSTDIR\_conda.exe" install --offline -yp "{prefix}" --file "{env_txt}" {shortcuts}' ${{Else}} - DetailPrint "Installing packages for {name}..." + ${{Print}} "Installing packages for {name}..." push '"$INSTDIR\_conda.exe" install --offline -yp "{prefix}" --file "{env_txt}" --no-shortcuts' ${{EndIf}} push 'Failed to link extracted packages to {prefix}!' @@ -195,7 +195,7 @@ def setup_envs_commands(info, dir_path): def uninstall_menus_commands(info): tmpl = r""" SetDetailsPrint both - DetailPrint "Deleting {name} menus in {env_name}..." + ${{Print}} "Deleting {name} menus in {env_name}..." SetDetailsPrint listonly push '"$INSTDIR\_conda.exe" constructor --prefix "{path}" --rm-menus' push 'Failed to delete menus in {env_name}' @@ -346,7 +346,9 @@ def make_nsi( ppd["has_conda"] = info["_has_conda"] ppd["custom_welcome"] = info.get("welcome_file", "").endswith(".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")) + data = preprocess(data, ppd) data = fill_template(data, replace, exceptions=nsis_predefines) if info['_platform'].startswith("win") and sys.platform != 'win32': @@ -396,7 +398,9 @@ def make_nsi( ), ('@TEMP_EXTRA_FILES@', '\n '.join(insert_tempfiles_commands(temp_extra_files))), ('@VIRTUAL_SPECS@', " ".join([f'"{spec}"' for spec in info.get("virtual_specs", ())])), - + # This is the same but without quotes so we can print it fine + ('@VIRTUAL_SPECS_DEBUG@', " ".join([spec for spec in info.get("virtual_specs", ())])), + ('@LICENSEFILENAME@', basename(info.get('license_file', 'placeholder_license.txt'))), ]: data = data.replace(key, value) diff --git a/docs/source/cli-options.md b/docs/source/cli-options.md index 49b466674..a67abc936 100644 --- a/docs/source/cli-options.md +++ b/docs/source/cli-options.md @@ -68,38 +68,84 @@ 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 silent mode. +- `/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`. -### Examples: +### Examples + +Run the installer in headless mode: + +`````{tab-set} +````{tab-item} CMD +```pwsh +cmd.exe /C start /wait my_installer.exe /S +``` +```` +````{tab-item} PowerShell +```pwsh +Start-Process -FilePath .\my_installer.exe -ArgumentList "/S" -NoNewWindow -Wait +``` +```` +````` -> Note that the NSIS installers will not write any output to the terminal. You will not see any -> output, even if the installation fails. You can check the Task Manager to see if the installer is -> running, or poll the installation directory to see if the installation has finished. +Run the installer in headless mode, for all users, adding to PATH and installing to a custom path: -Run the installer in silent mode: -```batch -> cmd.exe /c start /wait my_installer.exe /S +`````{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 ``` +```` +````{tab-item} PowerShell +```pwsh +Start-Process -FilePath .\my_installer.exe -ArgumentList "/InstallationType=AllUsers /AddToPath=1 /S /D=C:\Program Files\my_app" -NoNewWindow -Wait +``` +```` +````` -Run the installer in silent mode and install to a custom path: +Redirect the console output to a file named `log.txt`: -```batch -> cmd.exe /c start /wait my_installer.exe /InstallationType=AllUsers /AddToPath=1 /S /D=C:\Program Files\my_app + +`````{tab-set} +````{tab-item} CMD +```pwsh +my_installer.exe /S > log.txt ``` +> Stream redirection only works if the installer is invoked directly. Unfortunately this means that the call won't block and the installer will run in the background. If you need to block on the call, you can poll `log.txt` until you find `Done!` or `:error:`. We strongly recommend the Powershell alternative instead. +```` +````{tab-item} PowerShell +```pwsh +Start-Process -FilePath .\my_installer.exe -ArgumentList "/S" -NoNewWindow -Wait -RedirectStandardOutput log.txt +``` +```` +````` -Run the uninstaller in silent mode from its original location: +Run the uninstaller in headless mode from its original location: -```batch -> cmd.exe /c start /wait "C:\Program Files\my_app\uninstaller.exe" /S _?=C:\Program Files\my_app +`````{tab-set} +````{tab-item} CMD +```pwsh +cmd.exe /C start /wait C:\Program Files\my_app\uninstaller.exe /S _?=C:\Program Files\my_app +``` +```` +````{tab-item} PowerShell +```pwsh +Start-Process -FilePath C:\Program Files\my_app\uninstaller.exe -ArgumentList "/S _?=C:\Program Files\my_app" -NoNewWindow -Wait ``` +```` +````` :::{admonition} EXE installers with file logging :class: tip diff --git a/docs/source/debugging.md b/docs/source/debugging.md index c9ad88481..7ac89c18f 100644 --- a/docs/source/debugging.md +++ b/docs/source/debugging.md @@ -39,7 +39,7 @@ See `man installer` for more details. ## Verbose EXE installers -Windows installers do not have a verbose mode. By default, the graphical logs are only available in the "progress bar" dialog, by clicking on "Show details". This text box is right-clickable, which will allow you to copy the contents to the clipboard (and then paste them in a text file, presumably). +By default, the graphical logs are only available in the "progress bar" dialog, by clicking on "Show details". This text box is right-clickable, which will allow you to copy the contents to the clipboard (and then paste them in a text file, presumably). If you want `conda` to print more details, then, run it from the CMD prompt like this: diff --git a/news/847-nsis-out b/news/847-nsis-out new file mode 100644 index 000000000..81893fc4c --- /dev/null +++ b/news/847-nsis-out @@ -0,0 +1,19 @@ +### Enhancements + +* Windows installers will now report progress to stdout if run in headless mode (`/S`). (#764, #812 via #847) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/test_examples.py b/tests/test_examples.py index 8a739e186..380d68f6f 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -96,7 +96,9 @@ def _check_installer_log(install_dir): except Exception as exc: error_lines.append( f"Could not read logs! {exc.__class__.__name__}: {exc}\n" - "This usually means that the destination folder could not be created.\n" + "Did you install the 'log' variant of nsis? 'conda install conda-forge::nsis=*=*log*'\n" + "Once you have installed it, set NSIS_USING_LOG_BUILD=1.\n" + "Otherwise, this usually means that the destination folder could not be created.\n" "Possible causes: permissions, non-supported characters, long paths...\n" "Consider setting 'check_path_spaces' and 'check_path_length' to 'False'." ) @@ -116,13 +118,17 @@ def _run_installer_exe(installer, install_dir, installer_input=None, timeout=420 one, since the point is to just have spaces in the installation path -- one would be enough too :) This is why we have this weird .split() thingy down there in `/D=...`. + + Note that we do print information to the console, but that's not the stdout stream + of the subprocess. We make NSIS attach itself to the parent console and write directly there. + As a result we can't capture the output, so we still have to rely on the logfiles. """ if not sys.platform.startswith("win"): raise ValueError("Can only run .exe installers on Windows") if "NSIS_USING_LOG_BUILD" not in os.environ: warnings.warn( "Windows installers are tested with NSIS in silent mode, which does " - "not report errors on exit. You should use logging-enabled NSIS builds " + "not report errors to stdout. You should use logging-enabled NSIS builds " "to generate an 'install.log' file this script will search for errors " "after completion." )