diff --git a/constructor/header.sh b/constructor/header.sh index 6b4df2d44..6c6bc4edb 100644 --- a/constructor/header.sh +++ b/constructor/header.sh @@ -71,7 +71,9 @@ fi # Export variables to make installer metadata available to pre/post install scripts # NOTE: If more vars are added, make sure to update the examples/scripts tests too -{{ script_env_variables }} +{%- for key, val in script_env_variables|items %} +export {{ key }}='{{ val }}' +{%- endfor %} export INSTALLER_NAME='{{ installer_name }}' export INSTALLER_VER='{{ installer_version }}' export INSTALLER_PLAT='{{ installer_platform }}' @@ -529,6 +531,7 @@ shortcuts="--no-shortcuts" shortcuts="" {%- endif %} +{%- set channels = final_channels|join(",") %} # shellcheck disable=SC2086 CONDA_ROOT_PREFIX="$PREFIX" \ CONDA_REGISTER_ENVS="{{ register_envs }}" \ @@ -582,7 +585,9 @@ for env_pkgs in "${PREFIX}"/pkgs/envs/*/; do done {%- endif %} -{{ install_commands }} +{%- for condarc in write_condarc %} +{{ condarc }} +{%- endfor %} POSTCONDA="$PREFIX/postconda.tar.bz2" CONDA_QUIET="$BATCH" \ diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index d38fb93b2..59f35abee 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -77,10 +77,10 @@ var /global StdOutHandleSet !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 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 }} @@ -191,9 +191,9 @@ Page Custom InstModePage_Create InstModePage_Leave Page Custom mui_AnaCustomOptions_Show !insertmacro MUI_PAGE_INSTFILES -{%- if post_install_pages %} -{{ POST_INSTALL_PAGES }} -{%- endif %} +{%- for page in POST_INSTALL_PAGES %} +{{ page }} +{%- endfor %} {%- if with_conclusion_text %} !define MUI_FINISHPAGE_TITLE {{ conclusion_title }} @@ -557,7 +557,12 @@ 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 @@ -1260,8 +1265,12 @@ Section "Install" 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" @@ -1279,7 +1288,10 @@ Section "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' @@ -1314,7 +1326,9 @@ Section "Install" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SOLVER", "").r0' ${EndIf} - {{ PKG_COMMANDS }} +{%- for dist in DISTS %} + File {{ dist }} +{%- endfor %} SetDetailsPrint TextOnly ${Print} "Setting up the package cache..." @@ -1344,9 +1358,53 @@ 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 + + # List of packages to install + SetOutPath "{{ env.env_txt_dir }}" + File "{{ env.env_txt_abspath }}" + + # A conda-meta\history file is required for a valid conda prefix + SetOutPath "{{ env.conda_meta }}" + File "{{ env.history_abspath }}" + + # 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 %} - {{ WRITE_CONDARC }} +{%- for condarc in WRITE_CONDARC %} + {{ condarc }} +{%- endfor %} AddSize {{ SIZE }} @@ -1507,7 +1565,68 @@ Section "Uninstall" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0' ${EndIf} - {{ UNINSTALL_COMMANDS }} +{%- if uninstall_with_conda_exe %} + !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" + !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" + !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" + + # 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" diff --git a/constructor/osx/checks_before_install.sh b/constructor/osx/checks_before_install.sh index 51468c52e..07ea56303 100644 --- a/constructor/osx/checks_before_install.sh +++ b/constructor/osx/checks_before_install.sh @@ -18,11 +18,11 @@ if [[ -e "$PREFIX" ]]; then # By default, osascript doesn't allow user interaction, so we have to work # around it. http://stackoverflow.com/a/11874852/161801 - logger -p "install.info" "ERROR: __PATH_EXISTS_ERROR_TEXT__" || echo "ERROR: __PATH_EXISTS_ERROR_TEXT__" + logger -p "install.info" "ERROR: {{ path_exists_error_text }}" || echo "ERROR: {{ path_exists_error_text }}" (osascript -e "try tell application (path to frontmost application as text) set theAlertText to \"Chosen path already exists!\" -set theAlertMessage to \"__PATH_EXISTS_ERROR_TEXT__\" +set theAlertMessage to \"{{ path_exists_error_text }}\" display alert theAlertText message theAlertMessage as critical buttons {\"OK\"} default button {\"OK\"} end activate app (path to frontmost application as text) diff --git a/constructor/osx/run_installation.sh b/constructor/osx/run_installation.sh index 708b04a1d..984c20807 100644 --- a/constructor/osx/run_installation.sh +++ b/constructor/osx/run_installation.sh @@ -12,12 +12,14 @@ notify() { # shellcheck disable=SC2050 {%- if progress_notifications %} osascript </dev/null || : -{{ write_condarc }} +{%- for condarc in write_condarc %} +{{ condarc }} +{%- endfor %} if ! "$PREFIX/bin/python" -V; then echo "ERROR running Python" diff --git a/constructor/osx/run_user_script.sh b/constructor/osx/run_user_script.sh index 9056b9ca1..2d00b1c75 100644 --- a/constructor/osx/run_user_script.sh +++ b/constructor/osx/run_user_script.sh @@ -10,7 +10,7 @@ notify() { # shellcheck disable=SC2050 {%- if progress_notifications %} osascript < str: return fi.read() -def pkg_commands(download_dir, dists): - for fn in dists: - yield 'File %s' % win_str_esc(join(download_dir, fn)) - - -def extra_files_commands(paths, common_parent): +def get_extra_files(paths, common_parent): paths = sorted([Path(p) for p in paths]) - lines = [] - current_output_path = "$INSTDIR" + extra_files = {} for path in paths: relative_parent = path.relative_to(common_parent).parent output_path = f"$INSTDIR\\{relative_parent}" - if output_path != current_output_path: - lines.append(f"SetOutPath {output_path}") - current_output_path = output_path - lines.append(f"File {path}") - return lines - - -def insert_tempfiles_commands(paths: os.PathLike) -> List[str]: - """Helper function that copies paths into temporary install directory. - - Args: - paths (os.PathLike): Paths to files that need to be copied - - Returns: - List[str]: Commands to be inserted into nsi template - """ - if not paths: - return [] - # Setting OutPath to PluginsDir so NSIS File command copies the path into the PluginsDir - lines = ['SetOutPath $PLUGINSDIR'] - for path in sorted([Path(p) for p in paths]): - lines.append(f"File {path}") - return lines - - -def setup_script_env_variables(info) -> List[str]: - """Helper function to insert extra environment variables into nsis template. - - Args: - info: Dictionary of information parsed from construct.yaml - - Returns: - List[str]: Commands to be inserted into nsi template - """ - lines = [] - for name, value in info.get('script_env_variables', {}).items(): - lines.append( - "System::Call 'kernel32::SetEnvironmentVariable(t,t)i" - + f"""("{name}", {win_str_esc(value)}).r0'""") - return lines + if output_path not in extra_files: + extra_files[output_path] = [] + extra_files[output_path].append(str(path)) + return extra_files def custom_nsi_insert_from_file(filepath: os.PathLike) -> str: @@ -113,65 +70,23 @@ def custom_nsi_insert_from_file(filepath: os.PathLike) -> str: def setup_envs_commands(info, dir_path): - template = r""" - # Set up {name} env - SetDetailsPrint both - ${{Print}} "Setting up the {name} environment..." - SetDetailsPrint listonly - - # List of packages to install - SetOutPath "{env_txt_dir}" - File "{env_txt_abspath}" - - # A conda-meta\history file is required for a valid conda prefix - SetOutPath "{conda_meta}" - File "{history_abspath}" - - # 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", "{register_envs}").r0' - - # Run conda install - ${{If}} $Ana_CreateShortcuts_State = ${{BST_CHECKED}} - ${{Print}} "Installing packages for {name}, creating shortcuts if necessary..." - push '"$INSTDIR\_conda.exe" install --offline -yp "{prefix}" --file "{env_txt}" {shortcuts} {no_rcs_arg}' - ${{Else}} - ${{Print}} "Installing packages for {name}..." - push '"$INSTDIR\_conda.exe" install --offline -yp "{prefix}" --file "{env_txt}" --no-shortcuts {no_rcs_arg}' - ${{EndIf}} - push 'Failed to link extracted packages to {prefix}!' - push 'WithLog' - SetDetailsPrint listonly - call AbortRetryNSExecWait - SetDetailsPrint both - - # Cleanup {name} env.txt - SetOutPath "$INSTDIR" - Delete "{env_txt}" - - # Restore shipped conda-meta\history for remapped - # channels and retain only the first transaction - SetOutPath "{conda_meta}" - File "{history_abspath}" - """ # noqa - - lines = template.format( # this one block is for the base environment - name="base", - prefix=r"$INSTDIR", - env_txt=r"$INSTDIR\pkgs\env.txt", # env.txt as seen by the running installer - env_txt_dir=r"$INSTDIR\pkgs", # env.txt location in the installer filesystem - env_txt_abspath=join(dir_path, "env.txt"), # env.txt location while building the installer - conda_meta=r"$INSTDIR\conda-meta", - history_abspath=join(dir_path, "conda-meta", "history"), - channels=','.join(get_final_channels(info)), - shortcuts=shortcuts_flags(info), - register_envs=str(info.get("register_envs", True)).lower(), - no_rcs_arg=info.get("_ignore_condarcs_arg", ""), - ).splitlines() - # now we generate one more block per extra env, if present + environments = [] + # set up the base environment + environments.append({ + "name": "base", + "prefix": r"$INSTDIR", + "env_txt": r"$INSTDIR\pkgs\env.txt", # env.txt as seen by the running installer + "env_txt_dir": r"$INSTDIR\pkgs", # env.txt location in the installer filesystem + "env_txt_abspath": join(dir_path, "env.txt"), # env.txt path while building the installer + "conda_meta": r"$INSTDIR\conda-meta", + "history_abspath": join(dir_path, "conda-meta", "history"), + "final_channels": get_final_channels(info), + "shortcuts": shortcuts_flags(info), + "register_envs": str(info.get("register_envs", True)).lower(), + "no_rcs_arg": info.get("_ignore_condarcs_arg", ""), + }) + # now we generate one item per extra env, if present for env_name in info.get("_extra_envs_info", {}): - lines += ["", ""] env_info = info["extra_envs"][env_name] # Needed for shortcuts_flags function if "_conda_exe_type" not in env_info: @@ -180,102 +95,21 @@ def setup_envs_commands(info, dir_path): "channels": env_info.get("channels", info.get("channels", ())), "channels_remap": env_info.get("channels_remap", info.get("channels_remap", ())) } - lines += template.format( - name=env_name, - prefix=join("$INSTDIR", "envs", env_name), - env_txt=join("$INSTDIR", "pkgs", "envs", env_name, "env.txt"), - env_txt_dir=join("$INSTDIR", "pkgs", "envs", env_name), - env_txt_abspath=join(dir_path, "envs", env_name, "env.txt"), - conda_meta=join("$INSTDIR", "envs", env_name, "conda-meta"), - history_abspath=join(dir_path, "envs", env_name, "conda-meta", "history"), - channels=",".join(get_final_channels(channel_info)), - shortcuts=shortcuts_flags(env_info), - register_envs=str(info.get("register_envs", True)).lower(), - no_rcs_arg=info.get("_ignore_condarcs_arg", ""), - ).splitlines() - - return [line.strip() for line in lines] - - -def uninstall_menus_commands(info: dict) -> List[str]: - tmpl = r""" - SetDetailsPrint both - ${{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}' - push 'WithLog' - call un.AbortRetryNSExecWait - SetDetailsPrint both - """ - lines = tmpl.format(name=info["name"], env_name="base", path="$INSTDIR").splitlines() - for env_name in info.get("_extra_envs_info", {}): - path = join("$INSTDIR", "envs", env_name) - lines += tmpl.format(name=info["name"], env_name=env_name, path=path).splitlines() - return [line.strip() for line in lines] - - -def uninstall_commands_default(info: dict) -> List[str]: - return uninstall_menus_commands(info) + dedent(""" - !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" - !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" - !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" - - ${Print} "Removing files and folders..." - nsExec::Exec 'cmd.exe /D /C RMDIR /Q /S "$INSTDIR"' - - # In case the last command fails, run the slow method to remove leftover - RMDir /r /REBOOTOK "$INSTDIR" - """).splitlines() - - -def uninstall_commands_conda_standalone() -> List[str]: - return dedent(r""" - !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" - !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" - !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" - - # 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} - """).splitlines() - - -def uninstall_commands(info: dict) -> List[str]: - if info.get("uninstall_with_conda_exe"): - return uninstall_commands_conda_standalone() - return uninstall_commands_default(info) + environments.append({ + "name": env_name, + "prefix": join("$INSTDIR", "envs", env_name), + "env_txt": join("$INSTDIR", "pkgs", "envs", env_name, "env.txt"), + "env_txt_dir": join("$INSTDIR", "pkgs", "envs", env_name), + "env_txt_abspath": join(dir_path, "envs", env_name, "env.txt"), + "conda_meta": join("$INSTDIR", "envs", env_name, "conda-meta"), + "history_abspath": join(dir_path, "envs", env_name, "conda-meta", "history"), + "final_channels": get_final_channels(channel_info), + "shortcuts": shortcuts_flags(env_info), + "register_envs": str(info.get("register_envs", True)).lower(), + "no_rcs_arg": info.get("_ignore_condarcs_arg", ""), + }) + + return environments def make_nsi( @@ -299,8 +133,6 @@ def make_nsi( dists += env_info["_dists"] dists = list({dist: None for dist in dists}) # de-duplicate - py_name, py_version, unused_build = filename_dist(dists[0]).rsplit('-', 2) - assert py_name == 'python' arch = int(info['_platform'].split('-')[1]) info['pre_install_desc'] = info.get('pre_install_desc', "") info['post_install_desc'] = info.get('post_install_desc', "") @@ -311,10 +143,6 @@ def make_nsi( '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())), @@ -382,6 +210,10 @@ def make_nsi( # From now on, the items added to variables will NOT be escaped + py_name, py_version, _ = filename_dist(dists[0]).rsplit('-', 2) + assert py_name == 'python' + variables['pyver_components'] = py_version.split(".") + # These are mostly booleans we use with if-checks variables.update(ns_platform(info['_platform'])) variables['initialize_conda'] = info.get('initialize_conda', True) @@ -399,7 +231,6 @@ def make_nsi( 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")) variables["uninstall_with_conda_exe"] = bool(info.get("uninstall_with_conda_exe")) approx_pkgs_size_kb = approx_size_kb(info, "pkgs") @@ -408,18 +239,19 @@ def make_nsi( variables['NAME'] = name variables['NSIS_DIR'] = NSIS_DIR variables['BITS'] = str(arch) - variables['PKG_COMMANDS'] = '\n '.join(pkg_commands(download_dir, dists)) + variables['DISTS'] = [win_str_esc(join(download_dir, dist)) for dist in 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['SETUP_ENVS'] = setup_envs_commands(info, dir_path) + variables['WRITE_CONDARC'] = list(add_condarc(info)) variables['SIZE'] = approx_pkgs_size_kb variables['UNINSTALL_NAME'] = info.get( 'uninstall_name', '${NAME} ${VERSION} (Python ${PYVERSION} ${ARCH})' ) - variables['UNINSTALL_COMMANDS'] = '\n '.join(uninstall_commands(info)) - variables['EXTRA_FILES'] = '\n '.join(extra_files_commands(extra_files, dir_path)) - variables['SCRIPT_ENV_VARIABLES'] = '\n '.join(setup_script_env_variables(info)) + variables['EXTRA_FILES'] = get_extra_files(extra_files, dir_path) + variables['SCRIPT_ENV_VARIABLES'] = { + key: win_str_esc(val) for key, val in info.get('script_env_variables', {}).items() + } variables['CUSTOM_WELCOME_FILE'] = ( custom_nsi_insert_from_file(info.get('welcome_file', '')) if variables['custom_welcome'] @@ -431,12 +263,12 @@ def make_nsi( else '' ) if isinstance(info.get("post_install_pages"), str): - variables["POST_INSTALL_PAGES"] = custom_nsi_insert_from_file(info["post_install_pages"]) + variables['POST_INSTALL_PAGES'] = [custom_nsi_insert_from_file(info["post_install_pages"])] else: - variables['POST_INSTALL_PAGES'] = '\n'.join( + variables['POST_INSTALL_PAGES'] = [ 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['TEMP_EXTRA_FILES'] = sorted(temp_extra_files, key=Path) 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", ())]) diff --git a/examples/custom_nsis_template/custom.nsi.tmpl b/examples/custom_nsis_template/custom.nsi.tmpl index 6b45e6b68..ff6526fc9 100644 --- a/examples/custom_nsis_template/custom.nsi.tmpl +++ b/examples/custom_nsis_template/custom.nsi.tmpl @@ -5,11 +5,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" @@ -114,27 +114,27 @@ BrandingText /TRIMLEFT "${COMPANY}" !define MUI_UNWELCOMEFINISHPAGE_BITMAP __WELCOMEIMAGE__ # Pages -#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__ !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipPageIfUACInnerInstance !define MUI_PAGE_CUSTOMFUNCTION_LEAVE OnDirectoryLeave !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES -#if with_conclusion_text is True +{%- if with_conclusion_text %} !define MUI_FINISHPAGE_TITLE __CONCLUSION_TITLE__ !define MUI_FINISHPAGE_TITLE_3LINES !define MUI_FINISHPAGE_TEXT __CONCLUSION_TEXT__ -#endif +{%- endif %} # Custom conclusion file(s) -@CUSTOM_CONCLUSION_FILE@ +{{ CUSTOM_CONCLUSION_FILE }} !insertmacro MUI_PAGE_FINISH @@ -248,8 +248,8 @@ Function .onInit # 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 \ @@ -259,7 +259,7 @@ Function .onInit /SD IDOK Abort ${EndIf} -#endif +{%- endif %} !insertmacro UAC_PageElevation_OnInit ${If} ${UAC_IsInnerInstance} @@ -487,7 +487,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 @@ -646,7 +646,7 @@ SectionEnd Section "Uninstall" # Remove menu items, path entries - DetailPrint "Deleting @NAME@ menus..." + DetailPrint "Deleting {{ NAME }} menus..." nsExec::ExecToLog '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR" --rm-menus' # ensure that MSVC runtime DLLs are on PATH during uninstallation @@ -705,10 +705,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/news/922-improve-jinja-integration b/news/922-improve-jinja-integration new file mode 100644 index 000000000..ce3f65851 --- /dev/null +++ b/news/922-improve-jinja-integration @@ -0,0 +1,19 @@ +### Enhancements + +* Improve use of Jinja for templating logic. (#901 via #922) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/test_examples.py b/tests/test_examples.py index 3fb7308dc..11b7def98 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -290,7 +290,7 @@ def _run_installer( ) else: raise ValueError(f"Unknown installer type: {installer.suffix}") - if check_sentinels: + if check_sentinels and not (installer.suffix == ".pkg" and ON_CI): _sentinel_file_checks(example_path, install_dir) if uninstall and installer.suffix == ".exe": _run_uninstaller_exe(install_dir, timeout=timeout, check=check_subprocess) @@ -808,6 +808,8 @@ def test_cross_osx_building(tmp_path): "micromamba", "--platform", "osx-arm64", + "-c", + "conda-forge", ], ) micromamba_arm64 = tmp_env / "bin" / "micromamba" @@ -837,6 +839,8 @@ def test_virtual_specs_failed(tmp_path, request): _check_installer_log(install_dir) continue elif installer.suffix == ".pkg": + if not ON_CI: + continue # The GUI does provide a better message with the min version and so on # but on the CLI we fail with this one instead msg = "Cannot install on volume" diff --git a/tests/test_header.py b/tests/test_header.py index 8e452387f..41d3f9359 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -57,7 +57,7 @@ def test_osxpkg_scripts_shellcheck(arch, check_path_spaces, script): installer_name="Example", installer_version="1.2.3", installer_platform="osx-64", - channels="conda-forge", + final_channels=["conda-forge"], write_condarc="", path_exists_error_text="Error", progress_notifications=True, @@ -144,7 +144,7 @@ def test_template_shellcheck( "installer_version": "1.2.3", "installer_platform": "linux-64", "installer_md5": "a0098a2c837f4cf50180cfc0a041b2af", - "script_env_variables": "", # TODO: Fill this in with actual value + "script_env_variables": {}, # TODO: Fill this in with actual value "default_prefix": "/opt/Example", "license": "Some text", "total_installation_size_kb": "1024", @@ -155,6 +155,8 @@ def test_template_shellcheck( "no_rcs_arg": "", "install_commands": "", # TODO: Fill this in with actual value "conclusion_text": "Something", + "final_channels": "", + "write_condarc": "", }, )