From 59ceec74268bf62e1ee3e6b587b8ee3d56a8a94a Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 11 Nov 2024 12:20:55 +0100 Subject: [PATCH] Use Jinja in NSIS templates too --- constructor/nsis/main.nsi.tmpl | 233 ++++++++++++++++----------------- constructor/winexe.py | 227 ++++++++++++++------------------ 2 files changed, 216 insertions(+), 244 deletions(-) diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 3bb66380e..8283fe764 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -7,11 +7,11 @@ 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" @@ -67,26 +67,26 @@ var /global StdOutHandleSet !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__ +!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 {{ 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 }} !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}" @@ -130,7 +130,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 @@ -147,33 +147,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 @@ -182,22 +182,22 @@ Page Custom InstModePage_Create InstModePage_Leave Page Custom mui_AnaCustomOptions_Show !insertmacro MUI_PAGE_INSTFILES -#if post_install_pages is True -@POST_INSTALL_PAGES@ -#endif +{%- if post_install_pages %} +{{ POST_INSTALL_PAGES }} +{%- endif %} -#if with_conclusion_text is True -!define MUI_FINISHPAGE_TITLE __CONCLUSION_TITLE__ +{%- if with_conclusion_text %} +!define MUI_FINISHPAGE_TITLE {{ conclusion_title }} !define MUI_FINISHPAGE_TITLE_3LINES -!define MUI_FINISHPAGE_TEXT __CONCLUSION_TEXT__ -#endif +!define MUI_FINISHPAGE_TEXT {{ conclusion_text }} +{%- endif %} -#if custom_conclusion +{%- if custom_conclusion %} # Custom conclusion file(s) -@CUSTOM_CONCLUSION_FILE@ +{{ CUSTOM_CONCLUSION_FILE }} #else !insertmacro MUI_PAGE_FINISH -#endif +{%- endif %} !insertmacro MUI_UNPAGE_WELCOME @@ -269,12 +269,11 @@ FunctionEnd $\n\ /InstallationType=AllUsers [default: JustMe]$\n\ /AddToPath=[0|1] [default: 0]$\n\ -#if keep_pkgs is True +{%- if keep_pkgs %} /KeepPkgCache=[0|1] [default: 1]$\n\ -#endif -#if keep_pkgs is False +{%- else %} /KeepPkgCache=[0|1] [default: 0]$\n\ -#endif +{%- endif %} /RegisterPython=[0|1] [default: AllUsers: 1, JustMe: 0]$\n\ /NoRegistry=[0|1] [default: AllUsers: 0, JustMe: 0]$\n\ /NoScripts=[0|1] [default: 0]$\n\ @@ -332,7 +331,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 @@ -550,13 +549,13 @@ Function .onInit Push $R2 InitPluginsDir - @TEMP_EXTRA_FILES@ + {{ TEMP_EXTRA_FILES }} !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 \ @@ -566,7 +565,7 @@ Function .onInit /SD IDOK Abort ${EndIf} -#endif +{%- endif %} !insertmacro UAC_PageElevation_OnInit ${If} ${UAC_IsInnerInstance} @@ -692,44 +691,42 @@ 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 +{%- else %} StrCpy $Ana_AddToPath_State ${BST_UNCHECKED} -#endif -#if register_python is True and register_python_default is True +{%- endif %} +{%- endif %} +{%- if register_python %} +{%- if register_python_default %} StrCpy $Ana_RegisterSystemPython_State ${BST_CHECKED} -#endif -#if register_python is True and register_python_default is False +{%- else %} StrCpy $Ana_RegisterSystemPython_State ${BST_UNCHECKED} -#endif -#if check_path_length is True +{%- endif %} +{%- endif %} +{%- if check_path_length %} StrCpy $CheckPathLength "1" -#endif -#if check_path_length is False +{%- else %} StrCpy $CheckPathLength "0" -#endif -#if keep_pkgs is True +{%- endif %} +{%- if keep_pkgs %} StrCpy $Ana_ClearPkgCache_State ${BST_UNCHECKED} -#endif -#if keep_pkgs is False +{%- else %} StrCpy $Ana_ClearPkgCache_State ${BST_CHECKED} -#endif -#if pre_install_exists is True +{%- endif %} +{%- if pre_install_exists %} StrCpy $Ana_PreInstall_State ${BST_CHECKED} -#endif -#if pre_install_exists is False +{%- else %} StrCpy $Ana_PreInstall_State ${BST_UNCHECKED} -#endif -#if post_install_exists is True +{%- endif %} +{%- if post_install_exists %} StrCpy $Ana_PostInstall_State ${BST_CHECKED} -#endif -#if post_install_exists is False +{%- else %} StrCpy $Ana_PostInstall_State ${BST_UNCHECKED} -#endif +{%- endif %} Call OnInit_Release @@ -842,7 +839,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 @@ -1021,22 +1018,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 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} ${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 @@ -1177,8 +1174,8 @@ Section "Install" ${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. @@ -1189,13 +1186,13 @@ Section "Install" ${EndIf} StrCpy $INSTDIR $0 -#if has_license +{%- if has_license %} SetOutPath "$INSTDIR" - File __LICENSEFILE__ + File {{ licensefile }} ${Print} "By continuing this installation you are accepting this license agreement:" - ${Print} "$INSTDIR\@LICENSEFILENAME@" + ${Print} "$INSTDIR\{{ LICENSEFILENAME }}" ${Print} "Please run the installer in GUI mode to read the details.$\n" -#endif +{%- endif %} ${Print} "${NAME} will now be installed into this location:" ${Print} "$INSTDIR$\n" @@ -1211,14 +1208,14 @@ Section "Install" # 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@ + {{ EXTRA_FILES }} ${If} $InstMode = ${JUST_ME} SetOutPath "$INSTDIR" @@ -1227,16 +1224,16 @@ 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__ - - @SCRIPT_ENV_VARIABLES@ + 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 }} 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' @@ -1256,22 +1253,22 @@ Section "Install" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0' ${EndIf} - ${If} '@VIRTUAL_SPECS@' != '' + ${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@' + ${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} - @PKG_COMMANDS@ + {{ PKG_COMMANDS }} SetDetailsPrint TextOnly ${Print} "Setting up the package cache..." @@ -1301,19 +1298,19 @@ Section "Install" call AbortRetryNSExecWait NoPreInstall: - @SETUP_ENVS@ + {{ SETUP_ENVS }} - @WRITE_CONDARC@ + {{ WRITE_CONDARC }} - AddSize @SIZE@ + AddSize {{ SIZE }} -#if has_conda is True +{%- 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} ${Print} "Running post install..." @@ -1325,7 +1322,7 @@ Section "Install" ${If} $Ana_ClearPkgCache_State = ${BST_CHECKED} ${Print} "Clearing package cache..." - push '"$INSTDIR\_conda.exe" clean --all --force-pkgs-dirs --yes @NO_RCS_ARG@' + push '"$INSTDIR\_conda.exe" clean --all --force-pkgs-dirs --yes {{ NO_RCS_ARG }}' push 'Failed to clear package cache' push 'WithLog' call AbortRetryNSExecWait @@ -1334,7 +1331,7 @@ Section "Install" ${If} $Ana_AddToPath_State = ${BST_CHECKED} ${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} @@ -1413,7 +1410,7 @@ Section "Uninstall" # Remove menu items, path entries System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0' - @UNINSTALL_MENUS@ + {{ UNINSTALL_MENUS }} # ensure that MSVC runtime DLLs are on PATH during uninstallation ReadEnvStr $0 PATH @@ -1503,10 +1500,10 @@ Section "Uninstall" 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/winexe.py b/constructor/winexe.py index 7f8c9968d..2342db298 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -16,6 +16,7 @@ from .construct import ns_platform from .imaging import write_images +from .jinja import render_template from .preconda import copy_extra_files from .preconda import write_files as preconda_write_files from .signing import AzureSignTool, WindowsSignTool @@ -23,10 +24,8 @@ add_condarc, approx_size_kb, filename_dist, - fill_template, get_final_channels, make_VIProductVersion, - preprocess, shortcuts_flags, win_str_esc, ) @@ -242,70 +241,52 @@ def make_nsi( info['pre_install_desc'] = info.get('pre_install_desc', "") info['post_install_desc'] = info.get('post_install_desc', "") - replace = { - 'NAME': name, - 'VERSION': info['version'], - 'COMPANY': info.get('company', 'Unknown, Inc.'), - 'PLATFORM': info['_platform'], - 'ARCH': '%d-bit' % arch, - 'PY_VER': ".".join(py_version.split(".")[:2]), - 'PYVERSION_JUSTDIGITS': ''.join(py_version.split('.')), - 'PYVERSION': py_version, - 'PYVERSION_MAJOR': py_version.split('.')[0], - 'DEFAULT_PREFIX': info.get('default_prefix', join('%USERPROFILE%', name.lower())), - 'DEFAULT_PREFIX_DOMAIN_USER': info.get('default_prefix_domain_user', + variables = { + 'installer_name': name, + 'installer_version': info['version'], + 'company': info.get('company', 'Unknown, Inc.'), + 'installer_platform': info['_platform'], + 'arch': '%d-bit' % arch, + 'py_ver': ".".join(py_version.split(".")[:2]), + 'pyversion_justdigits': ''.join(py_version.split('.')), + 'pyversion': py_version, + 'pyversion_major': py_version.split('.')[0], + 'default_prefix': info.get('default_prefix', join('%USERPROFILE%', name.lower())), + 'default_prefix_domain_user': info.get('default_prefix_domain_user', join('%LOCALAPPDATA%', name.lower())), - 'DEFAULT_PREFIX_ALL_USERS': info.get('default_prefix_all_users', + 'default_prefix_all_users': info.get('default_prefix_all_users', join('%ALLUSERSPROFILE%', name.lower())), - 'PRE_INSTALL_DESC': info['pre_install_desc'], - 'POST_INSTALL_DESC': info['post_install_desc'], - 'ENABLE_SHORTCUTS': "yes" if info['_enable_shortcuts'] is True else "no", - 'SHOW_REGISTER_PYTHON': "yes" if info.get("register_python", True) else "no", - 'SHOW_ADD_TO_PATH': "yes" if info.get("initialize_conda", True) else "no", - 'OUTFILE': info['_outpath'], - 'VIPV': make_VIProductVersion(info['version']), - 'CONSTRUCTOR_VERSION': info['CONSTRUCTOR_VERSION'], - 'ICONFILE': '@icon.ico', - 'HEADERIMAGE': '@header.bmp', - 'WELCOMEIMAGE': '@welcome.bmp', - 'LICENSEFILE': abspath(info.get('license_file', join(NSIS_DIR, 'placeholder_license.txt'))), - 'CONDA_HISTORY': '@' + join('conda-meta', 'history'), - 'CONDA_EXE': '@_conda.exe', - 'ENV_TXT': '@env.txt', - 'URLS_FILE': '@urls', - 'URLS_TXT_FILE': '@urls.txt', - 'PRE_INSTALL': '@pre_install.bat', - 'POST_INSTALL': '@post_install.bat', - 'PRE_UNINSTALL': '@pre_uninstall.bat', - 'INDEX_CACHE': '@cache', - 'REPODATA_RECORD': '@repodata_record.json', + 'pre_install_desc': info['pre_install_desc'], + 'post_install_desc': info['post_install_desc'], + 'enable_shortcuts': "yes" if info['_enable_shortcuts'] is True else "no", + 'show_register_python': "yes" if info.get("register_python", True) else "no", + 'show_add_to_path': "yes" if info.get("initialize_conda", True) else "no", + 'outfile': info['_outpath'], + 'vipv': make_VIProductVersion(info['version']), + 'constructor_version': info['CONSTRUCTOR_VERSION'], + 'iconfile': '@icon.ico', + 'headerimage': '@header.bmp', + 'welcomeimage': '@welcome.bmp', + 'licensefile': abspath(info.get('license_file', join(NSIS_DIR, 'placeholder_license.txt'))), + 'conda_history': '@' + join('conda-meta', 'history'), + 'conda_exe': '@_conda.exe', + 'env_txt': '@env.txt', + 'urls_file': '@urls', + 'urls_txt_file': '@urls.txt', + 'pre_install': '@pre_install.bat', + 'post_install': '@post_install.bat', + 'pre_uninstall': '@pre_uninstall.bat', + 'index_cache': '@cache', + 'repodata_record': '@repodata_record.json', } - # These are NSIS predefines and must not be replaced - # https://nsis.sourceforge.io/Docs/Chapter5.html#precounter - nsis_predefines = [ - "COUNTER", - "DATE", - "FILE", - "FILEDIR", - "FUNCTION", - "GLOBAL", - "LINE", - "MACRO", - "PAGEEX", - "SECTION", - "TIME", - "TIMESTAMP", - "UNINSTALL", - ] - conclusion_text = info.get("conclusion_text", "") if conclusion_text: conclusion_lines = conclusion_text.strip().splitlines() - replace['CONCLUSION_TITLE'] = conclusion_lines[0].strip() + variables['conclusion_title'] = conclusion_lines[0].strip() # See https://nsis.sourceforge.io/Docs/Modern%20UI/Readme.html#toggle_pgf # for the newlines business - replace['CONCLUSION_TEXT'] = "\r\n".join(conclusion_lines[1:]) + variables['conclusion_text'] = "\r\n".join(conclusion_lines[1:]) for key in ['welcome_file', 'conclusion_file', 'post_install_pages']: value = info.get(key, "") @@ -330,32 +311,71 @@ def make_nsi( ) info[key] = valid_values - for key, value in replace.items(): + for key, value in variables.items(): if value.startswith('@'): value = join(dir_path, value[1:]) - replace[key] = win_str_esc(value) - - data = read_nsi_tmpl(info) - ppd = ns_platform(info['_platform']) - ppd['initialize_conda'] = info.get('initialize_conda', True) - ppd['initialize_by_default'] = info.get('initialize_by_default', None) - ppd['register_python'] = info.get('register_python', True) - ppd['register_python_default'] = info.get('register_python_default', None) - ppd['check_path_length'] = info.get('check_path_length', None) - ppd['check_path_spaces'] = info.get('check_path_spaces', True) - ppd['keep_pkgs'] = info.get('keep_pkgs') or False - ppd['pre_install_exists'] = bool(info.get('pre_install')) - ppd['post_install_exists'] = bool(info.get('post_install')) - ppd['with_conclusion_text'] = bool(conclusion_text) - ppd["enable_debugging"] = bool(os.environ.get("NSIS_USING_LOG_BUILD")) - 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) + variables[key] = win_str_esc(value) + + # From now on, the items added to variables will NOT be escaped + + # These are mostly booleans we use with if-checks + variables.update(ns_platform(info['_platform'])) + variables['initialize_conda'] = info.get('initialize_conda', True) + variables['initialize_by_default'] = info.get('initialize_by_default', None) + variables['register_python'] = info.get('register_python', True) + variables['register_python_default'] = info.get('register_python_default', None) + variables['check_path_length'] = info.get('check_path_length', None) + variables['check_path_spaces'] = info.get('check_path_spaces', True) + variables['keep_pkgs'] = info.get('keep_pkgs') or False + variables['pre_install_exists'] = bool(info.get('pre_install')) + variables['post_install_exists'] = bool(info.get('post_install')) + variables['with_conclusion_text'] = bool(conclusion_text) + variables["enable_debugging"] = bool(os.environ.get("NSIS_USING_LOG_BUILD")) + variables["has_conda"] = info["_has_conda"] + variables["custom_welcome"] = info.get("welcome_file", "").endswith(".nsi") + variables["custom_conclusion"] = info.get("conclusion_file", "").endswith(".nsi") + variables["has_license"] = bool(info.get("license_file")) + variables["post_install_pages"] = bool(info.get("post_install_pages")) + + approx_pkgs_size_kb = approx_size_kb(info, "pkgs") + + # UPPERCASE variables are unescaped (and unquoted) + variables['NAME'] = name + variables['NSIS_DIR'] = NSIS_DIR + variables['BITS'] = str(arch) + variables['PKG_COMMANDS'] = '\n '.join(pkg_commands(download_dir, dists)) + variables['SIGNTOOL_COMMAND'] = signing_tool.get_signing_command() if signing_tool else "" + variables['SETUP_ENVS'] = '\n '.join(setup_envs_commands(info, dir_path)) + variables['WRITE_CONDARC'] = '\n '.join(add_condarc(info)) + variables['SIZE'] = str(approx_pkgs_size_kb) + variables['UNINSTALL_NAME'] = info.get('uninstall_name', + '${NAME} ${VERSION} (Python ${PYVERSION} ${ARCH})' + ) + variables['UNINSTALL_MENUS'] = '\n '.join(uninstall_menus_commands(info)) + variables['EXTRA_FILES'] = '\n '.join(extra_files_commands(extra_files, dir_path)) + variables['SCRIPT_ENV_VARIABLES'] = '\n '.join(setup_script_env_variables(info)) + variables['CUSTOM_WELCOME_FILE'] = ( + custom_nsi_insert_from_file(info.get('welcome_file', '')) + if variables['custom_welcome'] + else '' + ) + variables['CUSTOM_CONCLUSION_FILE'] = ( + custom_nsi_insert_from_file(info.get('conclusion_file', '')) + if variables['custom_conclusion'] + else '' + ) + variables['POST_INSTALL_PAGES'] = '\n'.join( + custom_nsi_insert_from_file(file) for file in info.get('post_install_pages', []) + ) + variables['TEMP_EXTRA_FILES'] = '\n '.join(insert_tempfiles_commands(temp_extra_files)) + variables['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 + variables['VIRTUAL_SPECS_DEBUG'] = " ".join([spec for spec in info.get("virtual_specs", ())]) + variables['LICENSEFILENAME'] = basename(info.get('license_file', 'placeholder_license.txt')) + variables['NO_RCS_ARG'] = info.get('_ignore_condarcs_arg', '') + + + data = render_template(read_nsi_tmpl(info), **variables) if info['_platform'].startswith("win") and sys.platform != 'win32': # Branding /TRIM commannd is unsupported on non win platform data_lines = data.split("\n") @@ -365,51 +385,6 @@ def make_nsi( break data = "\n".join(data_lines) - approx_pkgs_size_kb = approx_size_kb(info, "pkgs") - - # these are unescaped (and unquoted) - for key, value in [ - ('@NAME@', name), - ('@NSIS_DIR@', NSIS_DIR), - ('@BITS@', str(arch)), - ('@PKG_COMMANDS@', '\n '.join(pkg_commands(download_dir, dists))), - ('@SIGNTOOL_COMMAND@', signing_tool.get_signing_command() if signing_tool else ""), - ('@SETUP_ENVS@', '\n '.join(setup_envs_commands(info, dir_path))), - ('@WRITE_CONDARC@', '\n '.join(add_condarc(info))), - ('@SIZE@', str(approx_pkgs_size_kb)), - ('@UNINSTALL_NAME@', info.get('uninstall_name', - '${NAME} ${VERSION} (Python ${PYVERSION} ${ARCH})' - )), - ('@UNINSTALL_MENUS@', '\n '.join(uninstall_menus_commands(info))), - ('@EXTRA_FILES@', '\n '.join(extra_files_commands(extra_files, dir_path))), - ('@SCRIPT_ENV_VARIABLES@', '\n '.join(setup_script_env_variables(info))), - ( - '@CUSTOM_WELCOME_FILE@', - custom_nsi_insert_from_file(info.get('welcome_file', '')) - if ppd['custom_welcome'] - else '' - ), - ( - '@CUSTOM_CONCLUSION_FILE@', - custom_nsi_insert_from_file(info.get('conclusion_file', '')) - if ppd['custom_conclusion'] - else '' - ), - ( - '@POST_INSTALL_PAGES@', - '\n'.join( - custom_nsi_insert_from_file(file) for file in info.get('post_install_pages', []) - ), - ), - ('@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'))), - ('@NO_RCS_ARG@', info.get('_ignore_condarcs_arg', '')), - ]: - data = data.replace(key, value) - nsi_path = join(dir_path, 'main.nsi') with open(nsi_path, 'w') as fo: fo.write(data)