From 74f6056f2991f390e9f39c2b94b192aa56e99d4b Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 14 Oct 2025 08:08:32 -0700 Subject: [PATCH 01/54] Convert script and directory `_nsis.py` subcommands into NSIS code (#1069) * Port mkdirs subcommand into NSIS * Convert cmd.exe executable into global variable * Run pre-uninstall script inside NSIS * Execute post-install script in NSIS * Add news * NSIS_SCRIPTS_RAISE_ERRORS * Convert finding cmd.exe into a macro * Remove unneeded rmreg command * Apply suggestions from code review Co-authored-by: Robin <34315751+lrandersson@users.noreply.github.com> * Replace rmreg with conda command * Add --user and --system options to init reverse command --------- Co-authored-by: jaimergp Co-authored-by: Robin <34315751+lrandersson@users.noreply.github.com> --- .github/workflows/main.yml | 1 - constructor/nsis/_nsis.py | 122 +------------------------------- constructor/nsis/main.nsi.tmpl | 88 ++++++++++++++++------- news/1069-port-nsispy-cmds-nsis | 19 +++++ scripts/run_examples.py | 4 -- 5 files changed, 83 insertions(+), 151 deletions(-) create mode 100644 news/1069-port-nsispy-cmds-nsis diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e053e5c40..afe137208 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -107,7 +107,6 @@ jobs: && files+=(--file "tests/extra-requirements-${{ runner.os }}.txt") conda install ${files[@]} -y echo "NSIS_USING_LOG_BUILD=1" >> $GITHUB_ENV - echo "NSIS_SCRIPTS_RAISE_ERRORS=1" >> $GITHUB_ENV pip install -e . --no-deps --no-build-isolation - name: Set up conda executable run: | diff --git a/constructor/nsis/_nsis.py b/constructor/nsis/_nsis.py index 82b872d21..d83008854 100644 --- a/constructor/nsis/_nsis.py +++ b/constructor/nsis/_nsis.py @@ -8,14 +8,8 @@ # be tested in an installation. import os -import re import sys -from os.path import exists, isfile, join - -try: - import winreg -except ImportError: - import _winreg as winreg +from os.path import exists, join ROOT_PREFIX = sys.prefix @@ -57,96 +51,6 @@ def err(x): OutputDebugString('_nsis.py: Error: ' + x) -class NSISReg: - def __init__(self, reg_path): - self.reg_path = reg_path - if exists(join(ROOT_PREFIX, '.nonadmin')): - self.main_key = winreg.HKEY_CURRENT_USER - else: - self.main_key = winreg.HKEY_LOCAL_MACHINE - - def set(self, name, value): - try: - winreg.CreateKey(self.main_key, self.reg_path) - registry_key = winreg.OpenKey(self.main_key, self.reg_path, 0, - winreg.KEY_WRITE) - winreg.SetValueEx(registry_key, name, 0, winreg.REG_SZ, value) - winreg.CloseKey(registry_key) - return True - except WindowsError: - return False - - def get(self, name): - try: - registry_key = winreg.OpenKey(self.main_key, self.reg_path, 0, - winreg.KEY_READ) - value, regtype = winreg.QueryValueEx(registry_key, name) - winreg.CloseKey(registry_key) - return value - except WindowsError: - return None - - -def mk_dirs(): - envs_dir = join(ROOT_PREFIX, 'envs') - if not exists(envs_dir): - os.mkdir(envs_dir) - - -def run_post_install(): - """ - call the post install script, if the file exists - """ - path = join(ROOT_PREFIX, 'pkgs', 'post_install.bat') - if not isfile(path): - return - env = os.environ.copy() - env.setdefault('PREFIX', str(ROOT_PREFIX)) - cmd_exe = os.path.join(os.environ['SystemRoot'], 'System32', 'cmd.exe') - if not os.path.isfile(cmd_exe): - cmd_exe = os.path.join(os.environ['windir'], 'System32', 'cmd.exe') - if not os.path.isfile(cmd_exe): - err("Error: running %s failed. cmd.exe could not be found. " - "Looked in SystemRoot and windir env vars.\n" % path) - if os.environ.get("NSIS_SCRIPTS_RAISE_ERRORS"): - sys.exit(1) - args = [cmd_exe, '/d', '/c', path] - import subprocess - try: - subprocess.check_call(args, env=env) - except subprocess.CalledProcessError: - err("Error: running %s failed\n" % path) - if os.environ.get("NSIS_SCRIPTS_RAISE_ERRORS"): - sys.exit(1) - - -def run_pre_uninstall(): - """ - call the pre uninstall script, if the file exists - """ - path = join(ROOT_PREFIX, 'pre_uninstall.bat') - if not isfile(path): - return - env = os.environ.copy() - env.setdefault('PREFIX', str(ROOT_PREFIX)) - cmd_exe = os.path.join(os.environ['SystemRoot'], 'System32', 'cmd.exe') - if not os.path.isfile(cmd_exe): - cmd_exe = os.path.join(os.environ['windir'], 'System32', 'cmd.exe') - if not os.path.isfile(cmd_exe): - err("Error: running %s failed. cmd.exe could not be found. " - "Looked in SystemRoot and windir env vars.\n" % path) - if os.environ.get("NSIS_SCRIPTS_RAISE_ERRORS"): - sys.exit(1) - args = [cmd_exe, '/d', '/c', path] - import subprocess - try: - subprocess.check_call(args, env=env) - except subprocess.CalledProcessError: - err("Error: running %s failed\n" % path) - if os.environ.get("NSIS_SCRIPTS_RAISE_ERRORS"): - sys.exit(1) - - allusers = (not exists(join(ROOT_PREFIX, '.nonadmin'))) # out('allusers is %s\n' % allusers) @@ -217,29 +121,9 @@ def add_condabin_to_path(): broadcast_environment_settings_change() -def rm_regkeys(): - cmdproc_reg_entry = NSISReg(r'Software\Microsoft\Command Processor') - cmdproc_autorun_val = cmdproc_reg_entry.get('AutoRun') - conda_hook_regex_pat = r'((\s+&\s+)?(if +exist)?(\s*?\"[^\"]*?conda[-_]hook\.bat\"))' - if join(ROOT_PREFIX, 'condabin') in (cmdproc_autorun_val or ''): - cmdproc_autorun_newval = re.sub(conda_hook_regex_pat, '', - cmdproc_autorun_val) - try: - cmdproc_reg_entry.set('AutoRun', cmdproc_autorun_newval) - except Exception: - # Hey, at least we made an attempt to cleanup - pass - - def main(): cmd = sys.argv[1].strip() - if cmd == 'post_install': - run_post_install() - elif cmd == 'rmreg': - rm_regkeys() - elif cmd == 'mkdirs': - mk_dirs() - elif cmd == 'addpath': + if cmd == 'addpath': # These checks are probably overkill, but could be useful # if I forget to update something that uses this code. if len(sys.argv) > 2: @@ -257,8 +141,6 @@ def main(): add_condabin_to_path() elif cmd == 'rmpath': remove_from_path() - elif cmd == 'pre_uninstall': - run_pre_uninstall() else: sys.exit("ERROR: did not expect %r" % cmd) diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 69aec1d3f..06b2a31e1 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -132,6 +132,22 @@ var /global InstMode # 0 = Just Me, 1 = All Users. !define JUST_ME 0 !define ALL_USERS 1 +var /global CMD_EXE + +!macro FindCmdExe + # Find cmd.exe + ReadEnvStr $R0 SystemRoot + ReadEnvStr $R1 windir + ${If} ${FileExists} "$R0" + StrCpy $CMD_EXE "$R0\System32\cmd.exe" + ${ElseIf} ${FileExists} "$R1" + StrCpy $CMD_EXE "$R1\System32\cmd.exe" + ${Else} + # Cross our fingers CMD is in PATH + StrCpy $CMD_EXE "cmd.exe" + ${EndIf} +!macroend + # Include this one after our defines !include "OptionsDialog.nsh" @@ -1226,6 +1242,8 @@ Section "Install" call OnDirectoryLeave ${EndIf} + !insertmacro FindCmdExe + SetOutPath "$INSTDIR\Lib" File "{{ NSIS_DIR }}\_nsis.py" File "{{ NSIS_DIR }}\_system_path.py" @@ -1353,17 +1371,7 @@ Section "Install" IfFileExists "$INSTDIR\pkgs\pre_install.bat" 0 NoPreInstall ${Print} "Running pre_install scripts..." - ReadEnvStr $5 SystemRoot - ReadEnvStr $6 windir - # This 'FileExists' also returns True for directories - ${If} ${FileExists} "$5" - push '"$5\System32\cmd.exe" /D /C "$INSTDIR\pkgs\pre_install.bat"' - ${ElseIf} ${FileExists} "$6" - push '"$6\System32\cmd.exe" /D /C "$INSTDIR\pkgs\pre_install.bat"' - ${Else} - # Cross our fingers CMD is in PATH - push 'cmd.exe /D /C "$INSTDIR\pkgs\pre_install.bat"' - ${EndIf} + push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_install.bat"' push "Failed to run pre_install" push 'WithLog' call AbortRetryNSExecWait @@ -1415,19 +1423,20 @@ Section "Install" 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 + StrCpy $R0 "$INSTDIR\envs" + ${IfNot} ${FileExists} "$R0" + CreateDirectory "$R0" + ${EndIf} {%- endif %} - ${If} $Ana_PostInstall_State = ${BST_CHECKED} - ${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' - call AbortRetryNSExecWait + ${If} ${FileExists} "$INSTDIR\pkgs\post_install.bat" + ${If} $Ana_PostInstall_State = ${BST_CHECKED} + ${Print} "Running post install..." + push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\post_install.bat"' + push "Failed to run post_install" + push 'WithLog' + call AbortRetryNSExecWait + ${EndIf} ${EndIf} ${If} $Ana_ClearPkgCache_State = ${BST_CHECKED} @@ -1544,6 +1553,8 @@ Section "Uninstall" !insertmacro un.ParseCommandLineArgs ${EndIf} + !insertmacro FindCmdExe + System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0' # ensure that MSVC runtime DLLs are on PATH during uninstallation @@ -1599,9 +1610,14 @@ Section "Uninstall" ${EndIf} {%- if uninstall_with_conda_exe %} - !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" + ${If} ${FileExists} "$INSTDIR\pkgs\pre_uninstall.bat" + ${Print} "Running pre_uninstall scripts..." + push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_uninstall.bat"' + push "Failed to run pre_uninstall" + push 'WithLog' + call un.AbortRetryNSExecWait + ${EndIf} !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" - !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" # Parse arguments StrCpy $R0 "" @@ -1649,9 +1665,29 @@ Section "Uninstall" call un.AbortRetryNSExecWait SetDetailsPrint both {%- endfor %} - !insertmacro AbortRetryNSExecWaitLibNsisCmd "pre_uninstall" + ${If} ${FileExists} "$INSTDIR\pkgs\pre_uninstall.bat" + ${Print} "Running pre_uninstall scripts..." + push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_uninstall.bat"' + push "Failed to run pre_uninstall" + push 'WithLog' + call un.AbortRetryNSExecWait + ${EndIf} !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" - !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmreg" +{%- if has_conda %} + ${If} ${FileExists} "$INSTDIR\.nonadmin" + StrCpy $R0 "user" + ${Else} + StrCpy $R0 "system" + ${EndIf} + # When running conda.bat directly, there is a non-fatal error + # that DOSKEY (called by conda_hook.bat) is not a valid command. + # While the operation still succeeds, this error is confusing. + # Calling via cmd.exe fixes that. + push '"$CMD_EXE" /D /C "$INSTDIR\condabin\conda.bat" init cmd.exe --reverse --$R0' + push 'Failed to clean AutoRun' + push 'WithLog' + call un.AbortRetryNSExecWait +{%- endif %} ${Print} "Removing files and folders..." nsExec::Exec 'cmd.exe /D /C RMDIR /Q /S "$INSTDIR"' diff --git a/news/1069-port-nsispy-cmds-nsis b/news/1069-port-nsispy-cmds-nsis new file mode 100644 index 000000000..c73866e27 --- /dev/null +++ b/news/1069-port-nsispy-cmds-nsis @@ -0,0 +1,19 @@ +### Enhancements + +* Port script execution, AutoRun manipulation, and directory creation functions from `_nsis.py` to NSIS. (#1069) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/scripts/run_examples.py b/scripts/run_examples.py index d0555ccff..eea2c63b3 100644 --- a/scripts/run_examples.py +++ b/scripts/run_examples.py @@ -118,10 +118,6 @@ def run_examples(keep_artifacts=None, conda_exe=None, debug=False): if os.path.exists(os.path.join(fpath, "construct.yaml")): example_paths.append(fpath) - # NSIS won't error out when running scripts unless - # we set this custom environment variable - os.environ["NSIS_SCRIPTS_RAISE_ERRORS"] = "1" - parent_output = tempfile.mkdtemp() tested_files = set() which_errored = {} From 49a66c421ffe67aad450ed3231df589e85fc80bc Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 20 Oct 2025 10:44:21 +0200 Subject: [PATCH 02/54] Use macos-15-intel (#1091) --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index afe137208..2e8a3edf5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,10 +53,10 @@ jobs: conda-standalone: conda-standalone-onedir check-docs-schema: true # MACOS - - os: macos-13 + - os: macos-15-intel python-version: "3.9" conda-standalone: conda-standalone-nightly - - os: macos-13 + - os: macos-15-intel python-version: "3.10" conda-standalone: conda-standalone-onedir - os: macos-latest @@ -208,7 +208,7 @@ jobs: include: - runner: ubuntu-latest subdir: linux-64 - - runner: macos-13 + - runner: macos-15-intel subdir: osx-64 - runner: macos-latest subdir: osx-arm64 From 15f2521b258da874b72cffdd1dab72fe5bfb9a53 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:43:57 -0700 Subject: [PATCH 03/54] [pre-commit.ci] pre-commit autoupdate (#1093) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.0 → v0.14.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.0...v0.14.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a8add2e8..932a0edf2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.0 + rev: v0.14.1 hooks: # Run the linter. - id: ruff From c5925bd99305834976f06e74d12a5f9342a664c8 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:30:00 -0400 Subject: [PATCH 04/54] Update schema default value and readme (#1088) * Clarify text in readme header * Update default enable_currentUserHome to reflect the documentation * Add news * Apply review suggestions --- constructor/_schema.py | 2 +- constructor/data/construct.schema.json | 2 +- constructor/osx/readme_header.rtf | 2 +- news/1088-update-schema-default-value | 19 +++++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 news/1088-update-schema-default-value diff --git a/constructor/_schema.py b/constructor/_schema.py index c77a00cf2..2e1785b13 100644 --- a/constructor/_schema.py +++ b/constructor/_schema.py @@ -610,7 +610,7 @@ class ConstructorConfiguration(BaseModel): Internally, this is passed to `pkgbuild --install-location`. macOS only. """ - pkg_domains: dict[PkgDomains, bool] = {"enable_anywhere": True, "enable_currentUserHome": False} + pkg_domains: dict[PkgDomains, bool] = {"enable_anywhere": True, "enable_currentUserHome": True} """ The domains the package can be installed into. For a detailed explanation, see: https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html diff --git a/constructor/data/construct.schema.json b/constructor/data/construct.schema.json index 5914be40e..78f371941 100644 --- a/constructor/data/construct.schema.json +++ b/constructor/data/construct.schema.json @@ -921,7 +921,7 @@ }, "default": { "enable_anywhere": true, - "enable_currentUserHome": false + "enable_currentUserHome": true }, "description": "The domains the package can be installed into. For a detailed explanation, see: https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html constructor defaults to `enable_anywhere=true` and `enable_currentUserHome=true`. `enable_localSystem` should not be set to true unless `default_location_pkg` is set as well. macOS only.", "propertyNames": { diff --git a/constructor/osx/readme_header.rtf b/constructor/osx/readme_header.rtf index c77b114c6..0a4faa668 100644 --- a/constructor/osx/readme_header.rtf +++ b/constructor/osx/readme_header.rtf @@ -10,7 +10,7 @@ \f0\fs30 \cf0 Anaconda is the most popular Python data science platform. See {\field{\*\fldinst{HYPERLINK "https://www.anaconda.com/downloads"}}{\fldrslt https://www.anaconda.com/downloads}}/.\ \ -By default, this installer modifies your bash profile to activate the base environment of __NAME__ when your shell starts up. To disable this, choose "Customize" at the "Installation Type" phase, and disable the "Modify PATH" option. If you decline this option, the executables installed by this installer will not be available on PATH. You will need to use the full executable path to run commands, or otherwise initialize the base environment of __NAME__ on your own. \ +By default, this installer modifies all available shells to activate the base environment of __NAME__ when the shell starts up. To disable this, choose "Customize" at the "Installation Type" phase, and disable the "Modify PATH" option. If you decline this option, the executables installed by this installer will not be available on PATH. You will need to use the full executable path to run commands, or otherwise initialize the base environment of __NAME__ on your own. \ \ To install to a different location, select "Change Install Location..." at the "Installation Type" phase, then choose "Install on a specific disk...", choose the disk you wish to install on, and click "Choose Folder...". The "Install for me only" option will install __NAME__ to the default location, ~/__NAME_LOWER__.\ \ diff --git a/news/1088-update-schema-default-value b/news/1088-update-schema-default-value new file mode 100644 index 000000000..1b1d4a79d --- /dev/null +++ b/news/1088-update-schema-default-value @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* PKG: Restore the default value of `enable_currentUserHome` to the old default value (`true`). (#1070 via #1088) + +### Deprecations + +* + +### Docs + +* PKG: Clarify that the profile of all available shells will be modified by default. (#1070 via #1088) + +### Other + +* From 99e6cef14a65f0051b77a90ba8f78ebd6e2b31a1 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:37:15 -0400 Subject: [PATCH 05/54] Add license file to .pkg installers (#1085) * Add license file to .pkg installers * Add test * Add news * Rename file, test difference between .sh/.pkg * Updated test to point at which file that is missing * improve error for file copy operation, fix issue in windows test * Update from pre-commit * Fix issue with string being passed instead of a list * add fixes from review --- constructor/osxpkg.py | 4 +++ constructor/preconda.py | 14 ++++++++--- examples/extra_files/TEST_LICENSE.txt | 1 + examples/extra_files/construct.yaml | 1 + examples/extra_files/test_install.bat | 1 + examples/extra_files/test_install.sh | 32 ++++++++++++++++++++++-- news/1085-include-installer-license-file | 19 ++++++++++++++ 7 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 examples/extra_files/TEST_LICENSE.txt create mode 100644 news/1085-include-installer-license-file diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index 3785ac617..ab400cf19 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -565,6 +565,10 @@ def create(info, verbose=False): os.makedirs(pkgs_dir) preconda.write_files(info, prefix) preconda.copy_extra_files(info.get("extra_files", []), prefix) + + # Add potential license file + if license_file := info.get("license_file"): + preconda.copy_extra_files([license_file], prefix) # These are the user-provided scripts, maybe patched to have a shebang # They will be called by a wrapping script added later, if present if info.get("pre_install"): diff --git a/constructor/preconda.py b/constructor/preconda.py index 2beeb3dc6..12201ee77 100644 --- a/constructor/preconda.py +++ b/constructor/preconda.py @@ -328,18 +328,24 @@ def copy_extra_files( Returns: list[os.PathLike]: List of normalized paths of copied locations. """ + + def validate_file_path(file_path: str) -> Path: + fpath = Path(file_path) + if not fpath.exists(): + raise FileNotFoundError(f"File {file_path} does not exist.") + return fpath + if not extra_files: return [] copied = [] for path in extra_files: if isinstance(path, str): - copied.append(shutil.copy(path, workdir)) + orig_path = validate_file_path(path) + copied.append(shutil.copy(orig_path, workdir)) elif isinstance(path, dict): assert len(path) == 1 origin, destination = next(iter(path.items())) - orig_path = Path(origin) - if not orig_path.exists(): - raise FileNotFoundError(f"File {origin} does not exist.") + orig_path = validate_file_path(origin) dest_path = Path(workdir) / destination dest_path.parent.mkdir(parents=True, exist_ok=True) copied.append(shutil.copy(orig_path, dest_path)) diff --git a/examples/extra_files/TEST_LICENSE.txt b/examples/extra_files/TEST_LICENSE.txt new file mode 100644 index 000000000..85588b6a7 --- /dev/null +++ b/examples/extra_files/TEST_LICENSE.txt @@ -0,0 +1 @@ +This file only exists for testing the .sh and .pkg installers. diff --git a/examples/extra_files/construct.yaml b/examples/extra_files/construct.yaml index 0bcbd2b6d..64ca06a9a 100644 --- a/examples/extra_files/construct.yaml +++ b/examples/extra_files/construct.yaml @@ -4,6 +4,7 @@ name: ExtraFiles version: X installer_type: all +license_file: TEST_LICENSE.txt check_path_spaces: False check_path_length: False channels: diff --git a/examples/extra_files/test_install.bat b/examples/extra_files/test_install.bat index 186b63724..d294cf364 100644 --- a/examples/extra_files/test_install.bat +++ b/examples/extra_files/test_install.bat @@ -2,3 +2,4 @@ echo Added by test-install script > "%PREFIX%\test_install_sentinel.txt" if not exist "%PREFIX%\more_data\README.md" exit 1 if not exist "%PREFIX%\something2.txt" exit 1 +if not exist "%PREFIX%\TEST_LICENSE.txt" exit 1 diff --git a/examples/extra_files/test_install.sh b/examples/extra_files/test_install.sh index e44f6fdb2..b2e99e55e 100644 --- a/examples/extra_files/test_install.sh +++ b/examples/extra_files/test_install.sh @@ -2,5 +2,33 @@ set -euxo pipefail echo "Added by test-install script" > "$PREFIX/test_install_sentinel.txt" -test -f "$PREFIX/more_data/README.md" -test -f "$PREFIX/something2.txt" +missing=false + +if [ ! -f "$PREFIX/more_data/README.md" ]; then + echo "Missing: $PREFIX/more_data/README.md" + missing=true +fi + +if [ ! -f "$PREFIX/something2.txt" ]; then + echo "Missing: $PREFIX/something2.txt" + missing=true +fi + +# Ideally we should test the .pkg and .sh installers separately since +# the current behavior for .sh-installers is to include but also rename the license file to LICENSE.txt, +# but for .pkg the name of the provided license file remains unchanged. +if [ "$INSTALLER_TYPE" = "SH" ]; then + if [ ! -f "$PREFIX/LICENSE.txt" ]; then + echo "Missing: $PREFIX/LICENSE.txt" + missing=true + fi +else # .pkg + if [ ! -f "$PREFIX/TEST_LICENSE.txt" ]; then + echo "Missing: $PREFIX/TEST_LICENSE.txt" + missing=true + fi +fi + +if [ "$missing" = true ]; then + exit 1 +fi diff --git a/news/1085-include-installer-license-file b/news/1085-include-installer-license-file new file mode 100644 index 000000000..e63afd76a --- /dev/null +++ b/news/1085-include-installer-license-file @@ -0,0 +1,19 @@ +### Enhancements + +* Include the license file in PKG installers. (#1074 via #1085) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* From c4bd5369707747124bc74f8a8dd844519a053596 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:08:44 -0400 Subject: [PATCH 06/54] Add unset of environment variables (#1082) * Add news * Add unset of environment variables * Verify expected environment variables are unset * Unset env vars also for pkg installers * Fix typo with OR instead of AND * Fix formatting * Remove uses of OLD_LD_LIBRARY_PATH * Add suggestion to news --- constructor/header.sh | 5 ++--- constructor/osx/run_installation.sh | 2 +- examples/grin/hello.sh | 1 - examples/scripts/post_install.sh | 33 ++++++++++++++++++++++++++++- news/1082-unset-variables | 19 +++++++++++++++++ 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 news/1082-unset-variables diff --git a/constructor/header.sh b/constructor/header.sh index 81cf0bf2e..be5012b48 100644 --- a/constructor/header.sh +++ b/constructor/header.sh @@ -10,10 +10,9 @@ set -eu {%- if osx %} -unset DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH +unset DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH DYLD_INSERT_LIBRARIES DYLD_FRAMEWORK_PATH {%- else %} -export OLD_LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" -unset LD_LIBRARY_PATH +unset LD_LIBRARY_PATH LD_PRELOAD LD_AUDIT {%- endif %} if ! echo "$0" | grep '\.sh$' > /dev/null; then diff --git a/constructor/osx/run_installation.sh b/constructor/osx/run_installation.sh index 00afff654..0905f6851 100644 --- a/constructor/osx/run_installation.sh +++ b/constructor/osx/run_installation.sh @@ -20,7 +20,7 @@ logger -p "install.info" "$1" || echo "$1" {%- set channels = final_channels|join(",") %} -unset DYLD_LIBRARY_PATH +unset DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH DYLD_INSERT_LIBRARIES DYLD_FRAMEWORK_PATH PREFIX="$2/{{ pkg_name_lower }}" PREFIX=$(cd "$PREFIX"; pwd) diff --git a/examples/grin/hello.sh b/examples/grin/hello.sh index f34e57aaf..e7c60eb9a 100644 --- a/examples/grin/hello.sh +++ b/examples/grin/hello.sh @@ -3,4 +3,3 @@ set -euxo pipefail echo "Hello: PREFIX='$PREFIX'" echo "LD_LIBRARY_PATH: ${LD_LIBRARY_PATH:-}" -echo "OLD_LD_LIBRARY_PATH: ${OLD_LD_LIBRARY_PATH:-}" diff --git a/examples/scripts/post_install.sh b/examples/scripts/post_install.sh index 18d632a63..06bcafb78 100644 --- a/examples/scripts/post_install.sh +++ b/examples/scripts/post_install.sh @@ -23,12 +23,43 @@ test "${CUSTOM_VARIABLE_2}" = '$ECOND-CUSTOM_'\''STRING'\'' WITH SPACES AND @*! test "${INSTALLER_UNATTENDED}" = "1" -if [[ $(uname -s) == Linux ]]; then +# Print to stderr if any of the input variables are set, and returns 1 - otherwise 0. +# Note that variables that are set but are empty strings will also trigger an error. +# All input variables are checked before exit. +verify_var_is_unset() { + local failed=0 + for var in "$@"; do + if [[ -n "${!var+x}" ]]; then + echo "Error: environment variable $var must be unset." >&2 + failed=1 + fi + done + return $failed +} + +if [[ $(uname -s) == "Linux" ]]; then if [[ ${INSTALLER_PLAT} != linux-* ]]; then + echo "Error: INSTALLER_PLAT must match 'linux-*' on Linux systems." + exit 1 + fi + + if ! verify_var_is_unset LD_LIBRARY_PATH LD_PRELOAD LD_AUDIT; then + echo "Error: One or more of LD_LIBRARY_PATH, LD_PRELOAD, or LD_AUDIT are set." exit 1 fi + else # macOS if [[ ${INSTALLER_PLAT} != osx-* ]]; then + echo "Error: INSTALLER_PLAT must match 'osx-*' on macOS systems." + exit 1 + fi + + if ! verify_var_is_unset \ + DYLD_LIBRARY_PATH \ + DYLD_FALLBACK_LIBRARY_PATH \ + DYLD_INSERT_LIBRARIES \ + DYLD_FRAMEWORK_PATH; then + echo "Error: One or more DYLD_* environment variables are set." exit 1 fi fi diff --git a/news/1082-unset-variables b/news/1082-unset-variables new file mode 100644 index 000000000..bb8a1c18f --- /dev/null +++ b/news/1082-unset-variables @@ -0,0 +1,19 @@ +### Enhancements + +* Unset additional environment variables in shell-based installers to avoid accidental loading of external libraries. (#1082) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 7af598347b747c419e7c9918a420c47dc75ed605 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 23 Oct 2025 08:39:49 -0700 Subject: [PATCH 07/54] Add guard to macOS and glibc version checks (#1094) --- constructor/header.sh | 86 ++++++++++++++++++---------------- news/1094-add-guards-os-checks | 19 ++++++++ 2 files changed, 64 insertions(+), 41 deletions(-) create mode 100644 news/1094-add-guards-os-checks diff --git a/constructor/header.sh b/constructor/header.sh index be5012b48..1438f5b1f 100644 --- a/constructor/header.sh +++ b/constructor/header.sh @@ -21,49 +21,53 @@ if ! echo "$0" | grep '\.sh$' > /dev/null; then fi {%- if osx and min_osx_version %} -min_osx_version="{{ min_osx_version }}" -system_osx_version="${CONDA_OVERRIDE_OSX:-$(SYSTEM_VERSION_COMPAT=0 sw_vers -productVersion)}" -# shellcheck disable=SC2183 disable=SC2046 -int_min_osx_version="$(printf "%02d%02d%02d" $(echo "$min_osx_version" | sed 's/\./ /g'))" -# shellcheck disable=SC2183 disable=SC2046 -int_system_osx_version="$(printf "%02d%02d%02d" $(echo "$system_osx_version" | sed 's/\./ /g'))" -if [ "$int_system_osx_version" -lt "$int_min_osx_version" ]; then - echo "Installer requires macOS >=${min_osx_version}, but system has ${system_osx_version}." - exit 1 +if [ "$(uname)" = "Darwin" ]; then + min_osx_version="{{ min_osx_version }}" + system_osx_version="${CONDA_OVERRIDE_OSX:-$(SYSTEM_VERSION_COMPAT=0 sw_vers -productVersion)}" + # shellcheck disable=SC2183 disable=SC2046 + int_min_osx_version="$(printf "%02d%02d%02d" $(echo "$min_osx_version" | sed 's/\./ /g'))" + # shellcheck disable=SC2183 disable=SC2046 + int_system_osx_version="$(printf "%02d%02d%02d" $(echo "$system_osx_version" | sed 's/\./ /g'))" + if [ "$int_system_osx_version" -lt "$int_min_osx_version" ]; then + echo "Installer requires macOS >=${min_osx_version}, but system has ${system_osx_version}." + exit 1 + fi fi {%- elif linux and min_glibc_version %} -min_glibc_version="{{ min_glibc_version }}" -system_glibc_version="${CONDA_OVERRIDE_GLIBC:-}" -if [ "${system_glibc_version}" = "" ]; then - case "$(ldd --version 2>&1)" in - *musl*) - # musl ldd will report musl version; call libc.so directly - # see https://github.com/conda/constructor/issues/850#issuecomment-2343756454 - libc_so="$(find /lib /usr/local/lib /usr/lib -name 'libc.so.*' -print -quit 2>/dev/null)" - if [ -z "${libc_so}" ]; then - libc_so="$(strings /etc/ld.so.cache | grep '^/.*/libc\.so.*' | head -1)" - fi - if [ -z "${libc_so}" ]; then - echo "Warning: Couldn't find libc.so; won't be able to determine GLIBC version!" >&2 - echo "Override by setting CONDA_OVERRIDE_GLIBC" >&2 - system_glibc_version="0.0" - else - system_glibc_version=$("${libc_so}" --version | awk 'NR==1{ sub(/\.$/, ""); print $NF}') - fi - ;; - *) - # ldd reports glibc in the last field of the first line - system_glibc_version=$(ldd --version | awk 'NR==1{print $NF}') - ;; - esac -fi -# shellcheck disable=SC2183 disable=SC2046 -int_min_glibc_version="$(printf "%02d%02d%02d" $(echo "$min_glibc_version" | sed 's/\./ /g'))" -# shellcheck disable=SC2183 disable=SC2046 -int_system_glibc_version="$(printf "%02d%02d%02d" $(echo "$system_glibc_version" | sed 's/\./ /g'))" -if [ "$int_system_glibc_version" -lt "$int_min_glibc_version" ]; then - echo "Installer requires GLIBC >=${min_glibc_version}, but system has ${system_glibc_version}." - exit 1 +if [ "$(uname)" = "Linux" ]; then + min_glibc_version="{{ min_glibc_version }}" + system_glibc_version="${CONDA_OVERRIDE_GLIBC:-}" + if [ "${system_glibc_version}" = "" ]; then + case "$(ldd --version 2>&1)" in + *musl*) + # musl ldd will report musl version; call libc.so directly + # see https://github.com/conda/constructor/issues/850#issuecomment-2343756454 + libc_so="$(find /lib /usr/local/lib /usr/lib -name 'libc.so.*' -print -quit 2>/dev/null)" + if [ -z "${libc_so}" ]; then + libc_so="$(strings /etc/ld.so.cache | grep '^/.*/libc\.so.*' | head -1)" + fi + if [ -z "${libc_so}" ]; then + echo "Warning: Couldn't find libc.so; won't be able to determine GLIBC version!" >&2 + echo "Override by setting CONDA_OVERRIDE_GLIBC" >&2 + system_glibc_version="0.0" + else + system_glibc_version=$("${libc_so}" --version | awk 'NR==1{ sub(/\.$/, ""); print $NF}') + fi + ;; + *) + # ldd reports glibc in the last field of the first line + system_glibc_version=$(ldd --version | awk 'NR==1{print $NF}') + ;; + esac + fi + # shellcheck disable=SC2183 disable=SC2046 + int_min_glibc_version="$(printf "%02d%02d%02d" $(echo "$min_glibc_version" | sed 's/\./ /g'))" + # shellcheck disable=SC2183 disable=SC2046 + int_system_glibc_version="$(printf "%02d%02d%02d" $(echo "$system_glibc_version" | sed 's/\./ /g'))" + if [ "$int_system_glibc_version" -lt "$int_min_glibc_version" ]; then + echo "Installer requires GLIBC >=${min_glibc_version}, but system has ${system_glibc_version}." + exit 1 + fi fi {%- endif %} diff --git a/news/1094-add-guards-os-checks b/news/1094-add-guards-os-checks new file mode 100644 index 000000000..cdcece6bb --- /dev/null +++ b/news/1094-add-guards-os-checks @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* Add guards to macOS and `glibc` version checks. (#1094) + +### Deprecations + +* + +### Docs + +* + +### Other + +* From e5240000af6a7b70584ae201769ec89bcfc7cbea Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:17:17 -0400 Subject: [PATCH 08/54] Preserve name of conda exe (#1090) * Fix issue with hardcoded conda exe * Fix formatting * Add quotes and test adjustments * Add missing quotes * Adjust osxpkg handling of _conda * Update macOS test, conda.exe instead of _conda * add news * Update test to account for CONSTRUCTOR_CONDA_EXE * Fix formatting and update the news * adjust setting of conda_exe_name and unify sh and pkg installers * Rename news file * Update news * Apply suggestion from @marcoesters --------- Co-authored-by: Marco Esters --- constructor/header.sh | 8 +++++++- constructor/osx/prepare_installation.sh | 7 ++++++- constructor/osx/run_installation.sh | 2 +- constructor/osx/run_user_script.sh | 2 +- constructor/osxpkg.py | 9 ++++++--- constructor/shar.py | 2 ++ constructor/utils.py | 25 +++++++++++++++++++++++++ news/1090-mamba-standalone-fix | 19 +++++++++++++++++++ tests/test_examples.py | 10 ++++++++-- tests/test_header.py | 2 ++ 10 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 news/1090-mamba-standalone-fix diff --git a/constructor/header.sh b/constructor/header.sh index 1438f5b1f..784a17a0e 100644 --- a/constructor/header.sh +++ b/constructor/header.sh @@ -495,9 +495,15 @@ unset PYTHON_SYSCONFIGDATA_NAME _CONDA_PYTHON_SYSCONFIGDATA_NAME # the first binary payload: the standalone conda executable printf "Unpacking bootstrapper...\n" -CONDA_EXEC="$PREFIX/_conda" +CONDA_EXEC="$PREFIX/{{ conda_exe_name }}" extract_range "${boundary0}" "${boundary1}" > "$CONDA_EXEC" chmod +x "$CONDA_EXEC" + +{%- if conda_exe_name != "_conda" %} +# In case there are packages that depend on _conda +ln -s "$CONDA_EXEC" "$PREFIX"/_conda +{%- endif %} + {%- for filename, (start, end, executable) in conda_exe_payloads|items %} mkdir -p "$(dirname "$PREFIX/{{ filename }}")" {%- if start == end %} diff --git a/constructor/osx/prepare_installation.sh b/constructor/osx/prepare_installation.sh index cf14cbf65..eb83f37e5 100644 --- a/constructor/osx/prepare_installation.sh +++ b/constructor/osx/prepare_installation.sh @@ -22,7 +22,7 @@ PREFIX="$2/{{ pkg_name_lower }}" PREFIX=$(cd "$PREFIX"; pwd) export PREFIX echo "PREFIX=$PREFIX" -CONDA_EXEC="$PREFIX/_conda" +CONDA_EXEC="$PREFIX/{{ conda_exe_name }}" # Installers should ignore pre-existing configuration files. unset CONDARC unset MAMBARC @@ -30,6 +30,11 @@ unset MAMBARC chmod +x "$CONDA_EXEC" +{%- if conda_exe_name != "_conda" %} +# In case there are packages that depend on _conda +ln -s "$CONDA_EXEC" "$PREFIX"/_conda +{%- endif %} + # Create a blank history file so conda thinks this is an existing env mkdir -p "$PREFIX/conda-meta" touch "$PREFIX/conda-meta/history" diff --git a/constructor/osx/run_installation.sh b/constructor/osx/run_installation.sh index 0905f6851..3047b5517 100644 --- a/constructor/osx/run_installation.sh +++ b/constructor/osx/run_installation.sh @@ -26,7 +26,7 @@ PREFIX="$2/{{ pkg_name_lower }}" PREFIX=$(cd "$PREFIX"; pwd) export PREFIX echo "PREFIX=$PREFIX" -CONDA_EXEC="$PREFIX/_conda" +CONDA_EXEC="$PREFIX/{{ conda_exe_name }}" # Installers should ignore pre-existing configuration files. unset CONDARC unset MAMBARC diff --git a/constructor/osx/run_user_script.sh b/constructor/osx/run_user_script.sh index 2d00b1c75..6209a5b0f 100644 --- a/constructor/osx/run_user_script.sh +++ b/constructor/osx/run_user_script.sh @@ -22,7 +22,7 @@ PREFIX="$2/{{ pkg_name_lower }}" PREFIX=$(cd "$PREFIX"; pwd) export PREFIX echo "PREFIX=$PREFIX" -CONDA_EXEC="$PREFIX/_conda" +CONDA_EXEC="$PREFIX/{{ conda_exe_name }}" # /COMMON UTILS # Expose these to user scripts as well diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index ab400cf19..5bdcece4d 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -25,6 +25,7 @@ approx_size_kb, copy_conda_exe, explained_check_call, + format_conda_exe_name, get_final_channels, parse_virtual_specs, rm_rf, @@ -364,6 +365,7 @@ def move_script(src, dst, info, ensure_shebang=False, user_script_type=None): variables["no_rcs_arg"] = info.get("_ignore_condarcs_arg", "") variables["script_env_variables"] = info.get("script_env_variables", {}) variables["initialize_conda"] = info.get("initialize_conda", "classic") + variables["conda_exe_name"] = format_conda_exe_name(info["_conda_exe"]) data = render_template(data, **variables) @@ -556,7 +558,7 @@ def create(info, verbose=False): # 1. Prepare installation # The 'prepare_installation' package contains the prepopulated package cache, the modified - # conda-meta metadata staged into pkgs/conda-meta, _conda (conda-standalone), + # conda-meta metadata staged into pkgs/conda-meta, _conda (conda-standalone, [--conda-exe]), # Optionally, extra files and the user-provided scripts. # We first populate PACKAGE_ROOT with everything needed, and then run pkg build on that dir fresh_dir(PACKAGE_ROOT) @@ -593,7 +595,8 @@ def create(info, verbose=False): for dist in all_dists: os.link(join(CACHE_DIR, dist), join(pkgs_dir, dist)) - copy_conda_exe(prefix, "_conda", info["_conda_exe"]) + exe_name = format_conda_exe_name(info["_conda_exe"]) + copy_conda_exe(prefix, exe_name, info["_conda_exe"]) # Sign conda-standalone so it can pass notarization codesigner = None @@ -608,7 +611,7 @@ def create(info, verbose=False): "com.apple.security.cs.disable-library-validation": True, "com.apple.security.cs.allow-dyld-environment-variables": True, } - codesigner.sign_bundle(join(prefix, "_conda"), entitlements=entitlements) + codesigner.sign_bundle(join(prefix, exe_name), entitlements=entitlements) # This script checks to see if the install location already exists and/or contains spaces # Not to be confused with the user-provided pre_install! diff --git a/constructor/shar.py b/constructor/shar.py index f6361a0f9..8475fca38 100644 --- a/constructor/shar.py +++ b/constructor/shar.py @@ -30,6 +30,7 @@ approx_size_kb, copy_conda_exe, filename_dist, + format_conda_exe_name, get_final_channels, hash_files, parse_virtual_specs, @@ -110,6 +111,7 @@ def get_header(conda_exec, tarball, info): virtual_specs = parse_virtual_specs(info) min_osx_version = virtual_specs.get("__osx", {}).get("min") or "" variables["min_osx_version"] = min_osx_version + variables["conda_exe_name"] = format_conda_exe_name(info["_conda_exe"]) min_glibc_version = virtual_specs.get("__glibc", {}).get("min") or "" variables["min_glibc_version"] = min_glibc_version diff --git a/constructor/utils.py b/constructor/utils.py index 705a42bd7..c63329c16 100644 --- a/constructor/utils.py +++ b/constructor/utils.py @@ -344,6 +344,31 @@ def identify_conda_exe(conda_exe: str | Path | None = None) -> tuple[StandaloneE return None, None +def format_conda_exe_name(conda_exe: str | Path) -> str: + """Return a formatted alias for given stand-alone executable. + + - If given executable cannot be identified, returns the basename of given executable. + - If stand-alone conda is identified, returns '_conda'. + - If stand-alone mamba/micromamba is identified, returns 'micromamba'. + + Parameters:: + - conda_exe: str | Path + Path to the conda executable to be accounted for. + """ + conda_exe_name, _ = identify_conda_exe(conda_exe) + if conda_exe_name is None: + # This implies that identify_conda_exe failed + return Path(conda_exe).name + if conda_exe_name == StandaloneExe.CONDA: + return "_conda" + elif conda_exe_name == StandaloneExe.MAMBA: + return "micromamba" + else: + # This should never happen, but as a safe-guard in case `identify_conda_exe` is changed without + # accounting for this function. + raise RuntimeError("Unable to format conda exe name") + + def check_version( exe_version: str | VersionOrder | None = None, min_version: str | None = None, diff --git a/news/1090-mamba-standalone-fix b/news/1090-mamba-standalone-fix new file mode 100644 index 000000000..0074a9f69 --- /dev/null +++ b/news/1090-mamba-standalone-fix @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* Rename mamba-based standalone binaries to `micromamba` and create a symbolic link to `_conda` for backwards compatibility. (#1033 via #1090) + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/test_examples.py b/tests/test_examples.py index 7d8913e2f..04241cffb 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -21,7 +21,12 @@ from conda.models.version import VersionOrder as Version from ruamel.yaml import YAML -from constructor.utils import StandaloneExe, check_version, identify_conda_exe +from constructor.utils import ( + StandaloneExe, + check_version, + format_conda_exe_name, + identify_conda_exe, +) if TYPE_CHECKING: from collections.abc import Generator, Iterable @@ -663,8 +668,9 @@ def test_macos_signing(tmp_path, self_signed_application_certificate_macos): # including binary archives like the PlugIns file cmd = ["pkgutil", "--expand-full", installer, expanded_path] _execute(cmd) + conda_exe_name = format_conda_exe_name(CONSTRUCTOR_CONDA_EXE) components = [ - Path(expanded_path, "prepare_installation.pkg", "Payload", "osx-pkg-test", "_conda"), + Path(expanded_path, "prepare_installation.pkg", "Payload", "osx-pkg-test", conda_exe_name), Path(expanded_path, "Plugins", "ExtraPage.bundle"), ] validated_signatures = [] diff --git a/tests/test_header.py b/tests/test_header.py index 94e5c906e..03c5b8a2f 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -70,6 +70,7 @@ def test_osxpkg_scripts_shellcheck(arch, check_path_spaces, script): no_rcs_arg="", script_env_variables={}, initialize_conda="condabin", + conda_exe_name="_conda", ) findings, returncode = run_shellcheck(processed) @@ -162,6 +163,7 @@ def test_template_shellcheck( "write_condarc": "", "conda_exe_payloads": conda_exe_payloads_and_size[0], "conda_exe_payloads_size": conda_exe_payloads_and_size[1], + "conda_exe_name": "_conda", }, ) From 1ae9387fe4e9d1202d784f9734e302195cde748f Mon Sep 17 00:00:00 2001 From: conda-bot <18747875+conda-bot@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:54:54 -0500 Subject: [PATCH 09/54] =?UTF-8?q?=F0=9F=A4=96=20updated=20file(s)=20(#1096?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HOW_WE_USE_GITHUB.md | 1 - 1 file changed, 1 deletion(-) diff --git a/HOW_WE_USE_GITHUB.md b/HOW_WE_USE_GITHUB.md index 88aced25a..a52f7e877 100644 --- a/HOW_WE_USE_GITHUB.md +++ b/HOW_WE_USE_GITHUB.md @@ -250,7 +250,6 @@ support: Unfortunately, this issue is outside the scope of support we offer via GitHub or is not directly related to this project. Community support can be found elsewhere, though, and we encourage you to explore the following options: -- [Conda discourse forum](https://conda.discourse.group/) - [Community chat channels](https://conda.org/community#chat) - [Stack Overflow posts tagged "conda"](https://stackoverflow.com/questions/tagged/conda) From 508853a70192320a88ab5e62e200fd3662780498 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 07:53:01 -0700 Subject: [PATCH 10/54] [pre-commit.ci] pre-commit autoupdate (#1098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.1 → v0.14.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.1...v0.14.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 932a0edf2..251470744 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.1 + rev: v0.14.2 hooks: # Run the linter. - id: ruff From 0cb0f5f25572ea3cee308cfd203990a710952834 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:56:59 -0700 Subject: [PATCH 11/54] Bump actions/upload-artifact from 4.6.2 to 5.0.0 in /.github/workflows (#1097) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...330a01c490aca151604b8cf639adc76d48f6c5d4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e8a3edf5..8dcc72b94 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -169,7 +169,7 @@ jobs: git diff --exit-code - name: Upload the example installers as artifacts if: github.event_name == 'pull_request' && matrix.python-version == '3.9' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: installers-${{ runner.os }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} path: "${{ runner.temp }}/examples_artifacts" From c368383710a7c2b81ad1b0ecb9724b38d3577447 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 3 Nov 2025 10:55:04 -0800 Subject: [PATCH 12/54] Merge commit from fork * Detect icacls along with cmd.exe * Set permissions at the beginning of the installation process * Add comments explaining permission restrictions * Add news file * Add explanation for why users with deny directives are skipped * Abort if icacls command fails * Add user domain to grant full access rights to installing user --- constructor/nsis/main.nsi.tmpl | 138 +++++++++++++----- .../ghsa-vvpr-2qg4-2mrq-excessive-permissions | 20 +++ 2 files changed, 120 insertions(+), 38 deletions(-) create mode 100644 news/ghsa-vvpr-2qg4-2mrq-excessive-permissions diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 06b2a31e1..9f0b328e0 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -59,6 +59,8 @@ var /global StdOutHandleSet !include "x64.nsh" !include "FileFunc.nsh" +!include "StrFunc.nsh" +${Using:StrFunc} StrStr !insertmacro GetParameters !insertmacro GetOptions @@ -133,18 +135,22 @@ var /global InstMode # 0 = Just Me, 1 = All Users. !define ALL_USERS 1 var /global CMD_EXE +var /global ICACLS_EXE -!macro FindCmdExe +!macro FindWindowsBinaries # Find cmd.exe ReadEnvStr $R0 SystemRoot ReadEnvStr $R1 windir ${If} ${FileExists} "$R0" StrCpy $CMD_EXE "$R0\System32\cmd.exe" + StrCpy $ICACLS_EXE "$R0\System32\icacls.exe" ${ElseIf} ${FileExists} "$R1" StrCpy $CMD_EXE "$R1\System32\cmd.exe" + StrCpy $ICACLS_EXE "$R1\System32\icacls.exe" ${Else} - # Cross our fingers CMD is in PATH + # Cross our fingers binaries are in PATH StrCpy $CMD_EXE "cmd.exe" + StrCpy $ICACLS_EXE "icacls.exe" ${EndIf} !macroend @@ -1235,6 +1241,78 @@ FunctionEnd !insertmacro AbortRetryNSExecWaitMacro "" !insertmacro AbortRetryNSExecWaitMacro "un." +!macro setInstdirPermissions + # To address CVE-2022-26526. + # Revoke the write permission on directory "$INSTDIR" for Users. Users are: + # AU - authenticated users (NT AUTHORITY\Authenticated Users) + # BU - built-in (local) users (BUILTIN\Users) + # DU - domain users (\DOMAIN USERS) + # This also applies for single-user installations to avoid giving other users + # full access on shared drives. + ${If} ${UAC_IsAdmin} + StrCpy $0 "(AU) (BU) (DU)" + ${Else} + # Not every directory grants write access to Users (e.g., %USERPROFILE%), + # so test whether user groups have the necessary rights. + nsExec::ExecToStack '"$ICACLS_EXE" "$INSTDIR"' + Pop $R0 + Pop $R1 + ${If} $R0 != "0" + StrCpy $R1 \ + "Unable to determine the defaults permissions of the installation directory. "\ + "Ensure that you have read access to $INSTDIR and icacls.exe is in your PATH." + ${Print} $R1 + Abort + ${EndIf} + StrCpy $0 "" + StrCpy $R2 "NT AUTHORITY\Authenticated Users|BUILTIN\Users|\Domain Users" + StrCpy $R3 "(AU)|(BU)|(DU)" + StrCpy $R4 1 + loop_single_user_default_access: + ${WordFind} $R2 "|" "E+$R4" $R5 + ${WordFind} $R3 "|" "E+$R4" $R6 + IfErrors endloop_single_user_default_access + ${StrStr} $R7 $R1 $R5 + ${If} $R7 == "" + goto increment_loop_single_user_default_access + ${EndIf} + # If the user group has a deny permission directive, do not change permissions. + # Granting (RX) permissions may increase the permissions that are inherited and + # it is very unlikely that a user is granted write permissions but denied others. + ${StrStr} $R7 $R1 "$R5:(D)" + ${If} $R7 != "" + goto increment_loop_single_user_default_access + ${EndIf} + StrCpy $0 "$0 $R6" + increment_loop_single_user_default_access: + IntOp $R4 $R4 + 1 + goto loop_single_user_default_access + endloop_single_user_default_access: + ${EndIf} + AccessControl::DisableFileInheritance "$INSTDIR" + ${If} $0 != "" + StrCpy $1 1 + loop_access: + ${WordFind} $0 " " "E+$1" $2 + IfErrors endloop_access + AccessControl::RevokeOnFile "$INSTDIR" "$2" "GenericWrite" + AccessControl::SetOnFile "$INSTDIR" "$2" "GenericRead + GenericExecute" + IntOp $1 $1 + 1 + goto loop_access + endloop_access: + ${EndIf} + ${IfNot} ${UAC_IsAdmin} + # Ensure that creator has full access + ReadEnvStr $R0 USERDOMAIN + ReadEnvStr $R1 USERNAME + ${If} $R0 == "" + AccessControl::SetOnFile "$INSTDIR" "$R1" "FullAccess" + ${Else} + AccessControl::SetOnFile "$INSTDIR" "$R0\$R1" "FullAccess" + ${EndIf} + ${EndIf} +!macroend + # Installer sections Section "Install" ${LogSet} on @@ -1242,11 +1320,9 @@ Section "Install" call OnDirectoryLeave ${EndIf} - !insertmacro FindCmdExe + !insertmacro FindWindowsBinaries - SetOutPath "$INSTDIR\Lib" - File "{{ NSIS_DIR }}\_nsis.py" - File "{{ NSIS_DIR }}\_system_path.py" + SetOutPath "$INSTDIR" # 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. @@ -1257,6 +1333,15 @@ Section "Install" ${EndIf} StrCpy $INSTDIR $0 + # Restrict permissions immediately after creating $INSTDIR + # If not, the installation directory may inherit write-permissions + # for users even during an all-users installation. + !insertmacro setInstdirPermissions + + SetOutPath "$INSTDIR\Lib" + File "{{ NSIS_DIR }}\_nsis.py" + File "{{ NSIS_DIR }}\_system_path.py" + {%- if has_license %} SetOutPath "$INSTDIR" File {{ licensefile }} @@ -1498,37 +1583,14 @@ Section "Install" WriteUninstaller "$INSTDIR\Uninstall-${NAME}.exe" - # To address CVE-2022-26526. - # Revoke the write permission on directory "$INSTDIR" for Users if this is - # being run with administrative privileges. Users are: - # AU - authenticated users - # BU - built-in (local) users - # DU - domain users - ${If} ${UAC_IsAdmin} - ${Print} "Setting installation directory permissions..." - AccessControl::DisableFileInheritance "$INSTDIR" - # Enable inheritance on all files inside $INSTDIR. - # Use icacls because it is much faster than custom NSIS solutions. - # We continue on error because icacls fails on broken links. - ReadEnvStr $0 SystemRoot - ReadEnvStr $1 windir - ${If} ${FileExists} "$0" - push '"$0\System32\icacls.exe" "$INSTDIR\*" /inheritance:e /T /C /Q' - ${ElseIf} ${FileExists} "$1" - push '"$1\System32\icacls.exe" "$INSTDIR\*" /inheritance:e /T /C /Q' - ${Else} - # Cross our fingers icacls is in PATH - push 'icacls.exe "$INSTDIR\*" /inheritance:e /T /C /Q' - ${EndIf} - push 'Failed to enable inheritance for all files in the installation directory.' - push 'NoLog' - call AbortRetryNSExecWait - AccessControl::RevokeOnFile "$INSTDIR" "(AU)" "GenericWrite" - AccessControl::RevokeOnFile "$INSTDIR" "(DU)" "GenericWrite" - AccessControl::RevokeOnFile "$INSTDIR" "(BU)" "GenericWrite" - AccessControl::SetOnFile "$INSTDIR" "(BU)" "GenericRead + GenericExecute" - AccessControl::SetOnFile "$INSTDIR" "(DU)" "GenericRead + GenericExecute" - ${EndIf} + ${Print} "Setting installation directory permissions..." + # Enable inheritance on all files inside $INSTDIR. + # Use icacls because it is much faster than custom NSIS solutions. + # We continue on error because icacls fails on broken links. + push '"$ICACLS_EXE" "$INSTDIR\*" /inheritance:e /T /C /Q' + push 'Failed to enable inheritance for all files in the installation directory.' + push 'NoLog' + call AbortRetryNSExecWait ${Print} "Done!" SectionEnd @@ -1553,7 +1615,7 @@ Section "Uninstall" !insertmacro un.ParseCommandLineArgs ${EndIf} - !insertmacro FindCmdExe + !insertmacro FindWindowsBinaries System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0' diff --git a/news/ghsa-vvpr-2qg4-2mrq-excessive-permissions b/news/ghsa-vvpr-2qg4-2mrq-excessive-permissions new file mode 100644 index 000000000..6eb57b4b0 --- /dev/null +++ b/news/ghsa-vvpr-2qg4-2mrq-excessive-permissions @@ -0,0 +1,20 @@ +### Enhancements + +* + +### Bug fixes + +* EXE: Remove write access for users during the installation process. +* EXE: Remove write access for users except for the installing user from single-user installations. + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 4cc113ef4f0f3d3390e9ac5fb05d26d8d5406ba1 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 4 Nov 2025 01:28:19 -0800 Subject: [PATCH 13/54] Prepare 3.13.0 (#1100) Co-authored-by: jaimergp --- .authors.yml | 24 ++++++++--- .mailmap | 2 + AUTHORS.md | 2 + CHANGELOG.md | 41 +++++++++++++++++++ news/1036-check-path-length-docs | 20 --------- news/1040-misleading-conda-init-prompt | 19 --------- news/1053-fix-typo-sh | 19 --------- news/1058-add-support-for-frozen-envs | 19 --------- news/1059-lockfiles | 19 --------- news/1068-remove-obsolete-nsispy-functions | 19 --------- news/1069-port-nsispy-cmds-nsis | 19 --------- news/1073-update-win-sdk | 18 -------- news/1077-use-windows-2022 | 19 --------- news/1082-unset-variables | 19 --------- news/1085-include-installer-license-file | 19 --------- news/1088-update-schema-default-value | 19 --------- news/1090-mamba-standalone-fix | 19 --------- news/1094-add-guards-os-checks | 19 --------- .../ghsa-vvpr-2qg4-2mrq-excessive-permissions | 20 --------- 19 files changed, 63 insertions(+), 292 deletions(-) delete mode 100644 news/1036-check-path-length-docs delete mode 100644 news/1040-misleading-conda-init-prompt delete mode 100644 news/1053-fix-typo-sh delete mode 100644 news/1058-add-support-for-frozen-envs delete mode 100644 news/1059-lockfiles delete mode 100644 news/1068-remove-obsolete-nsispy-functions delete mode 100644 news/1069-port-nsispy-cmds-nsis delete mode 100644 news/1073-update-win-sdk delete mode 100644 news/1077-use-windows-2022 delete mode 100644 news/1082-unset-variables delete mode 100644 news/1085-include-installer-license-file delete mode 100644 news/1088-update-schema-default-value delete mode 100644 news/1090-mamba-standalone-fix delete mode 100644 news/1094-add-guards-os-checks delete mode 100644 news/ghsa-vvpr-2qg4-2mrq-excessive-permissions diff --git a/.authors.yml b/.authors.yml index 42f8efbac..bac4b5778 100644 --- a/.authors.yml +++ b/.authors.yml @@ -264,7 +264,7 @@ github: chenghlee - name: conda-bot email: ad-team+condabot@anaconda.com - num_commits: 52 + num_commits: 56 first_commit: 2022-01-25 21:38:28 alternate_emails: - 18747875+conda-bot@users.noreply.github.com @@ -277,7 +277,7 @@ aliases: - Jaime RGP - jaimergp - num_commits: 108 + num_commits: 110 first_commit: 2022-01-08 14:56:53 github: jaimergp - name: Tom Hören @@ -357,7 +357,7 @@ github: RahulARanger - name: Marco Esters email: mesters@anaconda.com - num_commits: 49 + num_commits: 57 first_commit: 2023-05-12 11:44:12 github: marcoesters - name: Darryl Miles @@ -372,7 +372,7 @@ github: deepeshaburse - name: pre-commit-ci[bot] email: 66853113+pre-commit-ci[bot]@users.noreply.github.com - num_commits: 53 + num_commits: 65 first_commit: 2023-05-02 12:01:43 github: pre-commit-ci[bot] - name: Matthias Kuhn @@ -382,7 +382,7 @@ github: m-kuhn - name: dependabot[bot] email: 49699333+dependabot[bot]@users.noreply.github.com - num_commits: 43 + num_commits: 53 github: dependabot[bot] first_commit: 2024-05-07 10:16:05 - name: Julien Jerphanion @@ -405,5 +405,17 @@ github: Jrice1317 alternate_emails: - 100002667+Jrice1317@users.noreply.github.com - num_commits: 2 + num_commits: 4 first_commit: 2025-07-30 14:27:00 +- name: dionizijefa + github: dionizijefa + email: dionizije.fa@hotmail.com + num_commits: 1 + first_commit: 2025-08-27 17:15:44 +- name: Robin Andersson + aliases: + - Robin + github: lrandersson + email: 34315751+lrandersson@users.noreply.github.com + num_commits: 4 + first_commit: 2025-10-21 08:30:00 diff --git a/.mailmap b/.mailmap index dbd21b652..d6e9fb67b 100644 --- a/.mailmap +++ b/.mailmap @@ -63,6 +63,7 @@ Pradipta Ghosh Rachel Rigdon rrigdon <45607889+rrigdon@users.noreply.github.com> Ray Donnelly Richard Höchenberger +Robin Andersson <34315751+lrandersson@users.noreply.github.com> Robin <34315751+lrandersson@users.noreply.github.com> Ryan Sai Hanuma Rahul Sophia Castellarin soapy1 @@ -83,6 +84,7 @@ bkreider conda-bot Conda Bot <18747875+conda-bot@users.noreply.github.com> conda-bot conda bot <18747875+conda-bot@users.noreply.github.com> dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +dionizijefa guimondmm pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> y2kbugger diff --git a/AUTHORS.md b/AUTHORS.md index 219cf91ba..2f475da16 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -52,6 +52,7 @@ Authors are sorted alphabetically. * Rachel Rigdon * Ray Donnelly * Richard Höchenberger +* Robin Andersson * Ryan * Sai Hanuma Rahul * Sophia Castellarin @@ -71,6 +72,7 @@ Authors are sorted alphabetically. * bkreider * conda-bot * dependabot[bot] +* dionizijefa * guimondmm * pre-commit-ci[bot] * y2kbugger diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8f99055..23626ac79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ [//]: # (current developments) +## 2025-11-03 3.13.0: +### Enhancements + +* Add support for installing [protected (frozen) conda environments](https://conda.org/learn/ceps/cep-0022#specification). (#1058) +* Ship `conda-meta/initial-state.explicit.txt` as a copy of the lockfile that provisions the initial state of each environment. (#1052 via #1059) +* Remove unused functions from `_nsis.py`. (#1068) +* Port script execution, AutoRun manipulation, and directory creation functions from `_nsis.py` to NSIS. (#1069) +* Unset additional environment variables in shell-based installers to avoid accidental loading of external libraries. (#1082) +* Include the license file in PKG installers. (#1074 via #1085) + +### Bug fixes + +* SH: Fixed misleading wording for shell initialization in installation prompt. (#1039 via #1340) +* PKG: Restore the default value of `enable_currentUserHome` to the old default value (`true`). (#1070 via #1088) +* Rename mamba-based standalone binaries to `micromamba` and create a symbolic link to `_conda` for backwards compatibility. (#1033 via #1090) +* Add guards to macOS and `glibc` version checks. (#1094) +* EXE: Remove write access for users during the installation process. (`c368383710a7c2b81ad1b0ecb9724b38d3577447`) +* EXE: Remove write access for users except for the installing user from single-user installations. (`c368383710a7c2b81ad1b0ecb9724b38d3577447`) + +### Docs + +* Document that `check_path_length` defaults to `False` in line with prior behavior and declare + it as `bool` only in the schema. (#1036) +* PKG: Clarify that the profile of all available shells will be modified by default. (#1070 via #1088) + +### Other + +* Fix typo in license prompt message for SH installers. (#1035 via $1053) +* CI: Update signtool.exe path for Windows 2025 runner images (SDK 10.0.26100.0) (#1073) +* CI: Use `windows-2022` for integration tests. (#1077) + +### Contributors + +* @Jrice1317 +* @jaimergp +* @marcoesters +* @lrandersson +* @dionizijefa + + + ## 2025-08-06 3.12.2: ### Bug fixes diff --git a/news/1036-check-path-length-docs b/news/1036-check-path-length-docs deleted file mode 100644 index 097b13c83..000000000 --- a/news/1036-check-path-length-docs +++ /dev/null @@ -1,20 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* Document that `check_path_length` defaults to `False` in line with prior behavior and declare - it as `bool` only in the schema. (#1036) - -### Other - -* diff --git a/news/1040-misleading-conda-init-prompt b/news/1040-misleading-conda-init-prompt deleted file mode 100644 index 8ca55535d..000000000 --- a/news/1040-misleading-conda-init-prompt +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* SH: Fixed misleading wording for shell initialization in installation prompt. (#1039 via #1340) - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1053-fix-typo-sh b/news/1053-fix-typo-sh deleted file mode 100644 index aba0dd64d..000000000 --- a/news/1053-fix-typo-sh +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* Fix typo in license prompt message for SH installers. (#1035 via $1053) diff --git a/news/1058-add-support-for-frozen-envs b/news/1058-add-support-for-frozen-envs deleted file mode 100644 index 1c8e37ef6..000000000 --- a/news/1058-add-support-for-frozen-envs +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* Add support for installing [protected conda environments](https://conda.org/learn/ceps/cep-0022#specification). (#1058) - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1059-lockfiles b/news/1059-lockfiles deleted file mode 100644 index e4586e8ad..000000000 --- a/news/1059-lockfiles +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* Ship `conda-meta/initial-state.explicit.txt` as a copy of the lockfile that provisions the initial state of each environment. (#1052 via #1059) - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1068-remove-obsolete-nsispy-functions b/news/1068-remove-obsolete-nsispy-functions deleted file mode 100644 index e1b9d3f77..000000000 --- a/news/1068-remove-obsolete-nsispy-functions +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* Remove unused functions from `_nsis.py`. (#1068) - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1069-port-nsispy-cmds-nsis b/news/1069-port-nsispy-cmds-nsis deleted file mode 100644 index c73866e27..000000000 --- a/news/1069-port-nsispy-cmds-nsis +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* Port script execution, AutoRun manipulation, and directory creation functions from `_nsis.py` to NSIS. (#1069) - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1073-update-win-sdk b/news/1073-update-win-sdk deleted file mode 100644 index c0bc46d5f..000000000 --- a/news/1073-update-win-sdk +++ /dev/null @@ -1,18 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other -* Update signtool.exe path for Windows 2025 runner images (SDK 10.0.26100.0) (#1073) diff --git a/news/1077-use-windows-2022 b/news/1077-use-windows-2022 deleted file mode 100644 index 6e0eca5a7..000000000 --- a/news/1077-use-windows-2022 +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* Use `windows-2022` for integration tests. (#1077) diff --git a/news/1082-unset-variables b/news/1082-unset-variables deleted file mode 100644 index bb8a1c18f..000000000 --- a/news/1082-unset-variables +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* Unset additional environment variables in shell-based installers to avoid accidental loading of external libraries. (#1082) - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1085-include-installer-license-file b/news/1085-include-installer-license-file deleted file mode 100644 index e63afd76a..000000000 --- a/news/1085-include-installer-license-file +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* Include the license file in PKG installers. (#1074 via #1085) - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1088-update-schema-default-value b/news/1088-update-schema-default-value deleted file mode 100644 index 1b1d4a79d..000000000 --- a/news/1088-update-schema-default-value +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* PKG: Restore the default value of `enable_currentUserHome` to the old default value (`true`). (#1070 via #1088) - -### Deprecations - -* - -### Docs - -* PKG: Clarify that the profile of all available shells will be modified by default. (#1070 via #1088) - -### Other - -* diff --git a/news/1090-mamba-standalone-fix b/news/1090-mamba-standalone-fix deleted file mode 100644 index 0074a9f69..000000000 --- a/news/1090-mamba-standalone-fix +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* Rename mamba-based standalone binaries to `micromamba` and create a symbolic link to `_conda` for backwards compatibility. (#1033 via #1090) - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1094-add-guards-os-checks b/news/1094-add-guards-os-checks deleted file mode 100644 index cdcece6bb..000000000 --- a/news/1094-add-guards-os-checks +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* Add guards to macOS and `glibc` version checks. (#1094) - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/ghsa-vvpr-2qg4-2mrq-excessive-permissions b/news/ghsa-vvpr-2qg4-2mrq-excessive-permissions deleted file mode 100644 index 6eb57b4b0..000000000 --- a/news/ghsa-vvpr-2qg4-2mrq-excessive-permissions +++ /dev/null @@ -1,20 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* EXE: Remove write access for users during the installation process. -* EXE: Remove write access for users except for the installing user from single-user installations. - -### Deprecations - -* - -### Docs - -* - -### Other - -* From f32e2db91384cd237db5940f166bd61e6b3ec5d6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 06:54:51 -0800 Subject: [PATCH 14/54] [pre-commit.ci] pre-commit autoupdate (#1099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.2 → v0.14.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.2...v0.14.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 251470744..c74a81c68 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.2 + rev: v0.14.3 hooks: # Run the linter. - id: ruff From cb24eb042d838e724125cfb6608597ab10852d1b Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:21:03 -0500 Subject: [PATCH 15/54] Remove double comma (#1104) * Remove double comma * add news * Update news/1104-fix-text-in-header Co-authored-by: Marco Esters --------- Co-authored-by: Marco Esters --- constructor/header.sh | 2 +- news/1104-fix-text-in-header | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 news/1104-fix-text-in-header diff --git a/constructor/header.sh b/constructor/header.sh index 784a17a0e..c8f7609a3 100644 --- a/constructor/header.sh +++ b/constructor/header.sh @@ -778,7 +778,7 @@ if [ "$BATCH" = "0" ]; then printf "Note: You can undo this later by running \`conda init --reverse \$SHELL\`\\n" printf "\\n" printf "Proceed with initialization? [yes|no]\\n" - printf "[%s] >>> " "$DEFAULT"„ + printf "[%s] >>> " "$DEFAULT" read -r ans if [ "$ans" = "" ]; then ans=$DEFAULT diff --git a/news/1104-fix-text-in-header b/news/1104-fix-text-in-header new file mode 100644 index 000000000..c5e0ee7bc --- /dev/null +++ b/news/1104-fix-text-in-header @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* SH: Resolve malformed text displayed during installation. (#1104) + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 329cf6634adb591b468e0113734b61d0f43002b9 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:32:22 -0500 Subject: [PATCH 16/54] Prepare 3.13.1 (#1106) * Updated authorship for 3.13.1 * Updated CHANGELOG for 3.13.1 * Remove bot from CHANGELOG --- .authors.yml | 4 ++-- CHANGELOG.md | 11 +++++++++++ news/1104-fix-text-in-header | 19 ------------------- 3 files changed, 13 insertions(+), 21 deletions(-) delete mode 100644 news/1104-fix-text-in-header diff --git a/.authors.yml b/.authors.yml index bac4b5778..ef2e2a617 100644 --- a/.authors.yml +++ b/.authors.yml @@ -372,7 +372,7 @@ github: deepeshaburse - name: pre-commit-ci[bot] email: 66853113+pre-commit-ci[bot]@users.noreply.github.com - num_commits: 65 + num_commits: 66 first_commit: 2023-05-02 12:01:43 github: pre-commit-ci[bot] - name: Matthias Kuhn @@ -417,5 +417,5 @@ - Robin github: lrandersson email: 34315751+lrandersson@users.noreply.github.com - num_commits: 4 + num_commits: 5 first_commit: 2025-10-21 08:30:00 diff --git a/CHANGELOG.md b/CHANGELOG.md index 23626ac79..12670b048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ [//]: # (current developments) +## 2025-11-10 3.13.1: +### Bug fixes + +* SH: Resolve malformed text displayed during installation. (#1104) + +### Contributors + +* @lrandersson + + + ## 2025-11-03 3.13.0: ### Enhancements diff --git a/news/1104-fix-text-in-header b/news/1104-fix-text-in-header deleted file mode 100644 index c5e0ee7bc..000000000 --- a/news/1104-fix-text-in-header +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* SH: Resolve malformed text displayed during installation. (#1104) - -### Deprecations - -* - -### Docs - -* - -### Other - -* From c2aa22095ed783bc706bcff94152326b85d1aab1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 08:51:46 -0800 Subject: [PATCH 17/54] [pre-commit.ci] pre-commit autoupdate (#1107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.3 → v0.14.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.3...v0.14.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c74a81c68..c09a26f00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.3 + rev: v0.14.4 hooks: # Run the linter. - id: ruff From 170417a1800c0dd12a5d29f4882091b9d818dbaf Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 11 Nov 2025 07:06:06 -0800 Subject: [PATCH 18/54] Remove custom python commands from EXE installers (#1089) * Remove python from base environment where possible * Fix NSIS template * Add news file * Only exit with error if base needs python * Remove Python logic from NSIS if Python is not in the base environment * Explain why return code 2 is used in _base_needs_python * Always set register_python_default * Consolidate different path removal options * Remove duplicate code * Unify uninstallation workflow * Always show /AddToPath example in help text * Add post-solve check for mamba * Do not check for mamba 1 because mamba 1 depends on Python * Add clarifying comments * Remove unused installer_type parameter * Restore base environment python check * Fix syntax error --- constructor/fcp.py | 14 ++-- constructor/main.py | 16 +++++ constructor/nsis/main.nsi.tmpl | 107 ++++++++++++++++++---------- constructor/osx/run_installation.sh | 5 -- constructor/winexe.py | 28 +++++--- news/1089-remove-python-commands | 20 ++++++ tests/test_examples.py | 2 +- 7 files changed, 134 insertions(+), 58 deletions(-) create mode 100644 news/1089-remove-python-commands diff --git a/constructor/fcp.py b/constructor/fcp.py index 45b93bcd1..4ad7f53ca 100644 --- a/constructor/fcp.py +++ b/constructor/fcp.py @@ -234,11 +234,9 @@ def _solve_precs( conda_exe="conda.exe", extra_env=False, input_dir="", + base_needs_python=True, ): - # Add python to specs, since all installers need a python interpreter. In the future we'll - # probably want to add conda too. - # JRG: This only applies to the `base` environment; `extra_envs` are exempt - if not extra_env: + if not extra_env and base_needs_python: specs = (*specs, "python") if environment: logger.debug("specs: ", environment) @@ -312,8 +310,8 @@ def _solve_precs( if python_prec: precs.remove(python_prec) precs.insert(0, python_prec) - elif not extra_env: - # the base environment must always have python; this has been addressed + elif not extra_env and base_needs_python: + # the base environment may require python; this has been addressed # at the beginning of _main() but we can still get here through the # environment_file option sys.exit("python MUST be part of the base environment") @@ -392,6 +390,7 @@ def _main( extra_envs=None, check_path_spaces=True, input_dir="", + base_needs_python=True, ): precs = _solve_precs( name, @@ -408,6 +407,7 @@ def _main( verbose=verbose, conda_exe=conda_exe, input_dir=input_dir, + base_needs_python=base_needs_python, ) extra_envs = extra_envs or {} conda_in_base: PackageCacheRecord = next((prec for prec in precs if prec.name == "conda"), None) @@ -496,6 +496,7 @@ def main(info, verbose=True, dry_run=False, conda_exe="conda.exe"): transmute_file_type = info.get("transmute_file_type", "") extra_envs = info.get("extra_envs", {}) check_path_spaces = info.get("check_path_spaces", True) + base_needs_python = info.get("_win_install_needs_python_exe", False) if not channel_urls and not channels_remap and not (environment or environment_file): sys.exit("Error: at least one entry in 'channels' or 'channels_remap' is required") @@ -548,6 +549,7 @@ def main(info, verbose=True, dry_run=False, conda_exe="conda.exe"): extra_envs, check_path_spaces, input_dir, + base_needs_python, ) info["_all_pkg_records"] = pkg_records # full PackageRecord objects diff --git a/constructor/main.py b/constructor/main.py index d7b02e7ce..ee8104ad3 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -15,6 +15,7 @@ import json import logging import os +import subprocess import sys from os.path import abspath, expanduser, isdir, join from pathlib import Path @@ -76,6 +77,18 @@ def get_output_filename(info): ) +def _win_install_needs_python_exe(conda_exe: str) -> bool: + results = subprocess.run( + [conda_exe, "constructor", "windows", "--help"], + capture_output=True, + check=False, + ) + # Argparse uses return code 2 if a subcommand does not exist + # If the windows subcommand does not exist, python.exe is still + # required in the base environment. + return results.returncode == 2 + + def main_build( dir_path, output_dir=".", @@ -275,6 +288,9 @@ def is_conda_meta_frozen(path_str: str) -> bool: "enable_currentUserHome": "true", } + if osname == "win": + info["_win_install_needs_python_exe"] = _win_install_needs_python_exe(info["_conda_exe"]) + info["installer_type"] = itypes[0] fcp_main(info, verbose=verbose, dry_run=dry_run, conda_exe=conda_exe) if dry_run: diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 9f0b328e0..371eff0f7 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -79,10 +79,12 @@ ${Using:StrFunc} StrStr !define ARCH {{ arch }} !define PLATFORM {{ installer_platform }} !define CONSTRUCTOR_VERSION {{ constructor_version }} +{%- if has_python %} !define PY_VER {{ pyver_components[:2] | join(".") }} !define PYVERSION_JUSTDIGITS {{ pyver_components | join("") }} !define PYVERSION {{ pyver_components | join(".") }} !define PYVERSION_MAJOR {{ pyver_components[0] }} +{%- endif %} !define DEFAULT_PREFIX {{ default_prefix }} !define DEFAULT_PREFIX_DOMAIN_USER {{ default_prefix_domain_user }} !define DEFAULT_PREFIX_ALL_USERS {{ default_prefix_all_users }} @@ -304,7 +306,9 @@ FunctionEnd /InstallationType=AllUsers [default: JustMe]$\n\ /AddToPath=[0|1] [default: 0]$\n\ /KeepPkgCache=[0|1] [default: {{ 1 if keep_pkgs else 0 }}]$\n\ +{%- if has_python %} /RegisterPython=[0|1] [default: AllUsers: 1, JustMe: 0]$\n\ +{%- endif %} /NoRegistry=[0|1] [default: AllUsers: 0, JustMe: 0]$\n\ /NoScripts=[0|1] [default: 0]$\n\ /NoShortcuts=[0|1] [default: 0]$\n\ @@ -323,9 +327,14 @@ FunctionEnd Install for all users, but don't add to PATH env var:$\n\ > $EXEFILE /InstallationType=AllUsers$\n\ $\n\ +{%- if has_python %} Install for just me, add to PATH and register as system Python:$\n\ > $EXEFILE /RegisterPython=1 /AddToPath=1$\n\ $\n\ +{%- endif %} + Install for just me and add to PATH:$\n\ + > $EXEFILE /AddToPath=1$\n\ + $\n\ Install for just me, with no registry modification (for CI):$\n\ > $EXEFILE /NoRegistry=1$\n\ $\n\ @@ -349,6 +358,7 @@ FunctionEnd ${EndIf} ClearErrors +{%- if has_python %} ${GetOptions} $ARGV "/RegisterPython=" $ARGV_RegisterPython ${IfNot} ${Errors} ${If} $ARGV_RegisterPython = "1" @@ -357,6 +367,7 @@ FunctionEnd StrCpy $Ana_RegisterSystemPython_State ${BST_UNCHECKED} ${EndIf} ${EndIf} +{%- endif %} ClearErrors ${GetOptions} $ARGV "/KeepPkgCache=" $ARGV_KeepPkgCache @@ -1142,6 +1153,7 @@ Function OnDirectoryLeave UnicodePathTest::UnicodePathTest $INSTDIR Pop $R1 +{%- if has_python %} # Python 3 can be installed in a CP_ACP path until MKL is Unicode capable. # (mkl_rt.dll calls LoadLibraryA() to load mkl_intel_thread.dll) # Python 2 can only be installed to an ASCII path. @@ -1159,6 +1171,7 @@ Function OnDirectoryLeave abort valid_path: +{%- endif %} Push $R1 ${IsWritable} $INSTDIR $R1 @@ -1241,6 +1254,50 @@ FunctionEnd !insertmacro AbortRetryNSExecWaitMacro "" !insertmacro AbortRetryNSExecWaitMacro "un." +{%- set pathname = "$INSTDIR\\condabin" if initialize_conda == "condabin" else "$INSTDIR\\Scripts & Library\\bin" %} +!macro AddRemovePath add_remove un +{# python.exe is required if conda-standalone does not support the windows subcommand (<25.11.x) #} +{%- if needs_python_exe %} + ${If} ${add_remove} == "add" +{%- if initialize_conda == 'condabin' %} + ${Print} "Adding {{ pathname }} PATH..." + StrCpy $R0 "addcondabinpath" +{%- else %} + ${Print} "Adding {{ pathname }} to PATH..." + StrCpy $R0 "addpath ${PYVERSION} ${NAME} ${VERSION} ${ARCH}" +{%- endif %} + StrCpy $R1 "Failed to add {{ NAME }} to PATH" + ${Else} + ${Print} "Running rmpath script..." + StrCpy $R0 "rmpath" + StrCpy $R1 "Failed to remove {{ NAME }} from PATH" + ${EndIf} + ${If} ${Silent} + push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" $R0' + ${Else} + push '"$INSTDIR\python.exe" -E -s "$INSTDIR\Lib\_nsis.py" $R0' + ${EndIf} + push $R1 + push 'WithLog' + call ${un}AbortRetryNSExecWait +{%- else %} +{%- set pathflag = "--condabin" if initialize_conda == "condabin" else "--classic" %} + ${If} ${add_remove} == "add" + ${Print} "Adding {{ pathname }} to PATH..." + StrCpy $R0 "prepend" + StrCpy $R1 'Failed to add {{ NAME }} to PATH' + ${Else} + ${Print} "Removing {{ pathname }} from PATH..." + StrCpy $R0 "remove" + StrCpy $R1 'Failed to remove {{ NAME }} from PATH' + ${EndIf} + push '"$INSTDIR\_conda.exe" constructor windows path --$R0=user --prefix "$INSTDIR" {{ pathflag }}' + push $R1 + push 'WithLog' + call ${un}AbortRetryNSExecWait +{%- endif %} +!macroend + !macro setInstdirPermissions # To address CVE-2022-26526. # Revoke the write permission on directory "$INSTDIR" for Users. Users are: @@ -1338,9 +1395,11 @@ Section "Install" # for users even during an all-users installation. !insertmacro setInstdirPermissions +{% if needs_python_exe %} SetOutPath "$INSTDIR\Lib" File "{{ NSIS_DIR }}\_nsis.py" File "{{ NSIS_DIR }}\_system_path.py" +{% endif %} {%- if has_license %} SetOutPath "$INSTDIR" @@ -1533,20 +1592,14 @@ Section "Install" ${EndIf} {% if initialize_conda %} - ${If} $Ana_AddToPath_State = ${BST_CHECKED} -{%- if initialize_conda == 'condabin' %} - ${Print} "Adding $INSTDIR\condabin to PATH..." - push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" addcondabinpath' -{%- else %} - ${Print} "Adding $INSTDIR\Scripts & Library\bin to PATH..." - push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" addpath ${PYVERSION} ${NAME} ${VERSION} ${ARCH}' -{%- endif %} - push 'Failed to add {{ NAME }} to PATH' - push 'WithLog' - call AbortRetryNSExecWait + ${If} ${FileExists} "$INSTDIR\.nonadmin" + ${If} $Ana_AddToPath_State = ${BST_CHECKED} + !insertmacro AddRemovePath "add" "" + ${EndIf} ${EndIf} {%- endif %} +{%- if has_python %} # Create registry entries saying this is the system Python # (for this version) !define PYREG "Software\Python\PythonCore\${PY_VER}" @@ -1564,6 +1617,7 @@ Section "Install" WriteRegStr SHCTX "${PYREG}\PythonPath" \ "" "$INSTDIR\Lib;$INSTDIR\DLLs" ${EndIf} +{%- endif %} ${If} $ARGV_NoRegistry == "0" # Registry uninstall info @@ -1594,21 +1648,6 @@ Section "Install" ${Print} "Done!" SectionEnd -!macro AbortRetryNSExecWaitLibNsisCmd cmd - SetDetailsPrint both - ${Print} "Running ${cmd} scripts..." - SetDetailsPrint listonly - ${If} ${Silent} - push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" ${cmd}' - ${Else} - push '"$INSTDIR\python.exe" -E -s "$INSTDIR\Lib\_nsis.py" ${cmd}' - ${EndIf} - push "Failed to run ${cmd}" - push 'WithLog' - call un.AbortRetryNSExecWait - SetDetailsPrint both -!macroend - Section "Uninstall" ${LogSet} on ${If} ${Silent} @@ -1660,7 +1699,7 @@ Section "Uninstall" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_NAME", "${NAME}").r0' StrCpy $0 ${VERSION} ${If} $INSTALLER_VERSION != "" - StrCpy $0 $INSTALLER_VERSION + StrCpy $0 $INSTALLER_VERSION ${EndIf} System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_VER", "$0").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_PLAT", "${PLATFORM}").r0' @@ -1671,7 +1710,6 @@ Section "Uninstall" System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0' ${EndIf} -{%- if uninstall_with_conda_exe %} ${If} ${FileExists} "$INSTDIR\pkgs\pre_uninstall.bat" ${Print} "Running pre_uninstall scripts..." push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_uninstall.bat"' @@ -1679,8 +1717,11 @@ Section "Uninstall" push 'WithLog' call un.AbortRetryNSExecWait ${EndIf} - !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" + ${If} ${FileExists} "$INSTDIR\.nonadmin" + !insertmacro AddRemovePath "remove" "un." + ${EndIf} +{%- if uninstall_with_conda_exe %} # Parse arguments StrCpy $R0 "" @@ -1727,14 +1768,6 @@ Section "Uninstall" call un.AbortRetryNSExecWait SetDetailsPrint both {%- endfor %} - ${If} ${FileExists} "$INSTDIR\pkgs\pre_uninstall.bat" - ${Print} "Running pre_uninstall scripts..." - push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_uninstall.bat"' - push "Failed to run pre_uninstall" - push 'WithLog' - call un.AbortRetryNSExecWait - ${EndIf} - !insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath" {%- if has_conda %} ${If} ${FileExists} "$INSTDIR\.nonadmin" StrCpy $R0 "user" diff --git a/constructor/osx/run_installation.sh b/constructor/osx/run_installation.sh index 3047b5517..80975cd5e 100644 --- a/constructor/osx/run_installation.sh +++ b/constructor/osx/run_installation.sh @@ -119,11 +119,6 @@ find "$PREFIX/pkgs" -type d -empty -exec rmdir {} \; 2>/dev/null || : {{ condarc }} {%- endfor %} -if ! "$PREFIX/bin/python" -V; then - echo "ERROR running Python" - exit 1 -fi - # This is not needed for the default install to ~, but if the user changes the # install location, the permissions will default to root unless this is done. chown -R "${USER}" "$PREFIX" diff --git a/constructor/winexe.py b/constructor/winexe.py index 3c137f5c1..1af81dfc3 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -225,16 +225,27 @@ 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 + default_uninstall_name = "${NAME} ${VERSION}" + variables["has_python"] = False + for dist in dists: + py_name, py_version, _ = filename_dist(dist).rsplit("-", 2) + if py_name == "python": + variables["has_python"] = True + variables["pyver_components"] = py_version.split(".") + break + + if variables["has_python"]: + variables["register_python"] = info.get("register_python", True) + variables["register_python_default"] = info.get("register_python_default", None) + default_uninstall_name += " (Python ${PYVERSION} ${ARCH})" + else: + variables["register_python"] = False + variables["register_python_default"] = None + variables.update(ns_platform(info["_platform"])) variables["initialize_conda"] = info.get("initialize_conda", "classic") 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", False) variables["check_path_spaces"] = info.get("check_path_spaces", True) variables["keep_pkgs"] = info.get("keep_pkgs") or False @@ -247,6 +258,7 @@ def make_nsi( variables["custom_conclusion"] = info.get("conclusion_file", "").endswith(".nsi") variables["has_license"] = bool(info.get("license_file")) variables["uninstall_with_conda_exe"] = bool(info.get("uninstall_with_conda_exe")) + variables["needs_python_exe"] = info.get("_win_install_needs_python_exe", True) approx_pkgs_size_kb = approx_size_kb(info, "pkgs") @@ -259,9 +271,7 @@ def make_nsi( 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_NAME"] = info.get("uninstall_name", default_uninstall_name) 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() diff --git a/news/1089-remove-python-commands b/news/1089-remove-python-commands new file mode 100644 index 000000000..66079a75f --- /dev/null +++ b/news/1089-remove-python-commands @@ -0,0 +1,20 @@ +### Enhancements + +* **Breaking change**: Replace custom Python script with `conda-standalone` calls. + This removes Python as an implicit dependency from installers. (#549 via #1089) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/test_examples.py b/tests/test_examples.py index 04241cffb..a2ae8ad85 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1012,7 +1012,7 @@ def test_initialization(tmp_path, request, monkeypatch, method): request.addfinalizer( lambda: subprocess.run([sys.executable, "-m", "conda", "init", "--reverse"]) ) - monkeypatch.setenv("initialization_method", method) + monkeypatch.setenv("initialization_method", str(method).lower()) input_path = _example_path("initialization") initialize = method is not False for installer, install_dir in create_installer(input_path, tmp_path): From c5b5849900d89764694b9ed28ade4fd39df96aa9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 09:27:07 -0800 Subject: [PATCH 19/54] [pre-commit.ci] pre-commit autoupdate (#1112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.4 → v0.14.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.4...v0.14.5) - [github.com/python-jsonschema/check-jsonschema: 0.34.1 → 0.35.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.1...0.35.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c09a26f00..75b65ece0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.4 + rev: v0.14.5 hooks: # Run the linter. - id: ruff @@ -31,6 +31,6 @@ repos: - id: shellcheck exclude: ^(constructor/header.sh|constructor/osx/.*sh) - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.1 + rev: 0.35.0 hooks: - id: check-github-workflows From 227bb6511026059de247aa35b63ecc93f0be4738 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:39:42 -0500 Subject: [PATCH 20/54] Improve handling of initialize_conda / register_python including its default values (#1105) * Fix inconsistency in options * Add missing % * Ensure potential empty rows are trimmed * Fixed another merge conflict * Add clarity regarding default of REG_PY * Account for has_python * Update release notes * Additional review fixes * Docs updates * Adjust setting of default values * Changed back the default value * More fixes and lock dependency in test * Review fixes * Doc updates (_schema.py and make_docs) --- CONSTRUCT.md | 14 +- constructor/_schema.py | 14 +- constructor/data/construct.schema.json | 4 +- constructor/nsis/OptionsDialog.nsh | 197 ++++++++++++------ constructor/nsis/main.nsi.tmpl | 188 ++++++++--------- constructor/winexe.py | 2 - docs/source/cli-options.md | 2 +- docs/source/construct-yaml.md | 14 +- examples/custom_nsis_template/custom.nsi.tmpl | 2 - examples/extra_envs/construct.yaml | 2 +- news/1105-improve-handling-of-certain-options | 22 ++ 11 files changed, 276 insertions(+), 185 deletions(-) create mode 100644 news/1105-improve-handling-of-certain-options diff --git a/CONSTRUCT.md b/CONSTRUCT.md index 77155d3e9..210cc88e9 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -525,17 +525,19 @@ See also `initialize_by_default`. ### `initialize_by_default` -Default value for the option added by `initialize_conda`. The default -is true for GUI installers (EXE, PKG) and false for shell installers. The user -is able to change the default during interactive installation. NOTE: For Windows, -`AddToPath` is disabled when `InstallationType=AllUsers`. +Default value for the option added by `initialize_conda`. The default is +true for PKG installers, and false for EXE and SH shell installers. +The user is able to change the default during interactive installations. +Non-interactive installations are not affected by this value: users must explicitly request +to add to `PATH` via CLI options. +NOTE: For Windows, `/AddToPath` is disabled when `/InstallationType=AllUsers`. Only applies if `initialize_conda` is not false. ### `register_python` -Whether to offer the user an option to register the installed Python instance as the -system's default Python. (Windows only) +If the installer installs a Python instance, offer the user an option to register the installed Python instance as the +system's default Python. Defaults to `true` for GUI and `false` for CLI installations. (Windows only) ### `register_python_default` diff --git a/constructor/_schema.py b/constructor/_schema.py index 2e1785b13..d50cac46a 100644 --- a/constructor/_schema.py +++ b/constructor/_schema.py @@ -692,17 +692,19 @@ class ConstructorConfiguration(BaseModel): """ initialize_by_default: bool | None = None """ - Default value for the option added by `initialize_conda`. The default - is true for GUI installers (EXE, PKG) and false for shell installers. The user - is able to change the default during interactive installation. NOTE: For Windows, - `AddToPath` is disabled when `InstallationType=AllUsers`. + Default value for the option added by `initialize_conda`. The default is + true for PKG installers, and false for EXE and SH shell installers. + The user is able to change the default during interactive installations. + Non-interactive installations are not affected by this value: users must explicitly request + to add to `PATH` via CLI options. + NOTE: For Windows, `/AddToPath` is disabled when `/InstallationType=AllUsers`. Only applies if `initialize_conda` is not false. """ register_python: bool = True """ - Whether to offer the user an option to register the installed Python instance as the - system's default Python. (Windows only) + If the installer installs a Python instance, offer the user an option to register the installed Python instance as the + system's default Python. Defaults to `true` for GUI and `false` for CLI installations. (Windows only) """ register_python_default: bool | None = False """ diff --git a/constructor/data/construct.schema.json b/constructor/data/construct.schema.json index 78f371941..228b82946 100644 --- a/constructor/data/construct.schema.json +++ b/constructor/data/construct.schema.json @@ -750,7 +750,7 @@ } ], "default": null, - "description": "Default value for the option added by `initialize_conda`. The default is true for GUI installers (EXE, PKG) and false for shell installers. The user is able to change the default during interactive installation. NOTE: For Windows, `AddToPath` is disabled when `InstallationType=AllUsers`.\nOnly applies if `initialize_conda` is not false.", + "description": "Default value for the option added by `initialize_conda`. The default is true for PKG installers, and false for EXE and SH shell installers. The user is able to change the default during interactive installations. Non-interactive installations are not affected by this value: users must explicitly request to add to `PATH` via CLI options. NOTE: For Windows, `/AddToPath` is disabled when `/InstallationType=AllUsers`.\nOnly applies if `initialize_conda` is not false.", "title": "Initialize By Default" }, "initialize_conda": { @@ -1076,7 +1076,7 @@ }, "register_python": { "default": true, - "description": "Whether to offer the user an option to register the installed Python instance as the system's default Python. (Windows only)", + "description": "If the installer installs a Python instance, offer the user an option to register the installed Python instance as the system's default Python. Defaults to `true` for GUI and `false` for CLI installations. (Windows only)", "title": "Register Python", "type": "boolean" }, diff --git a/constructor/nsis/OptionsDialog.nsh b/constructor/nsis/OptionsDialog.nsh index 0b5f15c6d..11c94e5a3 100644 --- a/constructor/nsis/OptionsDialog.nsh +++ b/constructor/nsis/OptionsDialog.nsh @@ -7,30 +7,90 @@ Var mui_AnaCustomOptions Var mui_AnaCustomOptions.AddToPath -Var mui_AnaCustomOptions.RegisterSystemPython Var mui_AnaCustomOptions.PostInstall Var mui_AnaCustomOptions.PreInstall Var mui_AnaCustomOptions.ClearPkgCache Var mui_AnaCustomOptions.CreateShortcuts # These are the checkbox states, to be used by the installer -Var Ana_AddToPath_State -Var Ana_RegisterSystemPython_State Var Ana_PostInstall_State Var Ana_PreInstall_State Var Ana_ClearPkgCache_State Var Ana_CreateShortcuts_State Var Ana_AddToPath_Label -Var Ana_RegisterSystemPython_Label Var Ana_ClearPkgCache_Label Var Ana_PostInstall_Label Var Ana_PreInstall_Label + +!if ${REGISTER_PYTHON_OPTION} == 1 + Var mui_AnaCustomOptions.RegisterSystemPython + Var Ana_RegisterSystemPython_Label + + Function RegisterSystemPython_OnClick + Pop $0 + + # Sync UI with variable + ${NSD_GetState} $0 $1 + ${If} $1 == ${BST_CHECKED} + StrCpy $REG_PY 1 + ${Else} + StrCpy $REG_PY 0 + ${EndIf} + + ShowWindow $Ana_RegisterSystemPython_Label ${SW_HIDE} + ${If} $REG_PY == 1 + SetCtlColors $Ana_RegisterSystemPython_Label ff0000 transparent + ${Else} + SetCtlColors $Ana_RegisterSystemPython_Label 000000 transparent + ${EndIf} + ShowWindow $Ana_RegisterSystemPython_Label ${SW_SHOW} + + # If the button was checked, make sure we're not conflicting + # with another system installed Python + ${If} $REG_PY == 1 + # Check if a Python of the version we're installing + # already exists, in which case warn the user before + # proceeding. + ReadRegStr $2 SHCTX "Software\Python\PythonCore\${PY_VER}\InstallPath" "" + ${If} "$2" != "" + ${AndIf} ${FileExists} "$2\Python.exe" + MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \ + "A version of Python ${PY_VER} (${ARCH}) is already at$\n\ + $2$\n\ + We recommend that if you want ${NAME} registered as your $\n\ + system Python, you unregister this Python first. If you really$\n\ + know this is what you want, click OK, otherwise$\n\ + click cancel to continue.$\n$\n\ + NOTE: Anaconda 1.3 and earlier lacked an uninstall, if$\n\ + you are upgrading an old Anaconda, please delete the$\n\ + directory manually." \ + IDOK KeepSettingLabel + # If they don't click OK, uncheck it + StrCpy $REG_PY 0 + ${NSD_Uncheck} $0 + + KeepSettingLabel: + + ${EndIf} + ${EndIf} + FunctionEnd +!endif + Function mui_AnaCustomOptions_InitDefaults - # Initialize defaults - ${If} $Ana_AddToPath_State == "" - StrCpy $Ana_AddToPath_State ${BST_UNCHECKED} + # AddToPath / conda init default + !if ${INIT_CONDA_OPTION} == 1 + # Ensure we initialize from compile-time default value + StrCpy $INIT_CONDA ${INIT_CONDA_DEFAULT_VALUE} + !else + StrCpy $INIT_CONDA 0 + !endif + + # Register Python default while accounting for existing installations + !if ${REGISTER_PYTHON_OPTION} == 1 + # Ensure we initialize from compile-time default value + StrCpy $REG_PY ${REGISTER_PYTHON_DEFAULT_VALUE} # Default whether to register as system python as: # Enabled - if no system python is registered, OR # a system python which does not exist is registered. @@ -38,11 +98,13 @@ Function mui_AnaCustomOptions_InitDefaults ReadRegStr $2 SHCTX "Software\Python\PythonCore\${PY_VER}\InstallPath" "" ${If} "$2" != "" ${AndIf} ${FileExists} "$2\Python.exe" - StrCpy $Ana_RegisterSystemPython_State ${BST_UNCHECKED} - ${Else} - StrCpy $Ana_RegisterSystemPython_State ${BST_CHECKED} + StrCpy $REG_PY 0 ${EndIf} - ${EndIf} + !else + StrCpy $REG_PY 0 + !endif + + # Shortcuts defaults ${If} $Ana_CreateShortcuts_State == "" ${If} "${ENABLE_SHORTCUTS}" == "yes" StrCpy $Ana_CreateShortcuts_State ${BST_CHECKED} @@ -57,7 +119,7 @@ FunctionEnd Function mui_AnaCustomOptions_Show ; Enforce that the defaults were initialized - ${If} $Ana_AddToPath_State == "" + ${If} $INIT_CONDA == "" Abort ${EndIf} @@ -84,7 +146,7 @@ Function mui_AnaCustomOptions_Show ${NSD_OnClick} $mui_AnaCustomOptions.CreateShortcuts CreateShortcuts_OnClick ${EndIf} - ${If} "${SHOW_ADD_TO_PATH}" != "no" + !if ${INIT_CONDA_OPTION} == 1 # AddToPath is only an option for JustMe installations; it is disabled for AllUsers # installations. (Addresses CVE-2022-26526) ${If} $InstMode = ${JUST_ME} @@ -92,9 +154,18 @@ Function mui_AnaCustomOptions_Show environment variable" IntOp $5 $5 + 11 Pop $mui_AnaCustomOptions.AddToPath - ${NSD_SetState} $mui_AnaCustomOptions.AddToPath $Ana_AddToPath_State + + # Set state of check-box + ${If} $INIT_CONDA == 1 + ${NSD_Check} $mui_AnaCustomOptions.AddToPath + ${Else} + ${NSD_Uncheck} $mui_AnaCustomOptions.AddToPath + ${EndIf} + ${NSD_OnClick} $mui_AnaCustomOptions.AddToPath AddToPath_OnClick - ${If} "${SHOW_ADD_TO_PATH}" == "condabin" + + # Account for the conda mode + ${If} "${INIT_CONDA_MODE}" == "condabin" ${NSD_CreateLabel} 5% "$5u" 90% 20u \ "Adds condabin/, which only contains the 'conda' executables, to PATH. \ Does not require special shortcuts but activation needs \ @@ -107,26 +178,54 @@ Function mui_AnaCustomOptions_Show ${EndIf} IntOp $5 $5 + 20 Pop $Ana_AddToPath_Label + + # Color the label if needed; even if the user has not interacted with the checkbox yet + ${If} $INIT_CONDA = 1 + ${If} "${INIT_CONDA_MODE}" == "classic" + SetCtlColors $Ana_AddToPath_Label ff0000 transparent + ${Else} + # Here INIT_CONDA_MODE equals condabin + SetCtlColors $Ana_AddToPath_Label 000000 transparent + ${EndIf} + ${Else} + SetCtlColors $Ana_AddToPath_Label 000000 transparent + ${EndIf} ${EndIf} - ${EndIf} + !endif - ${If} "${SHOW_REGISTER_PYTHON}" == "yes" + !if ${REGISTER_PYTHON_OPTION} == 1 ${If} $InstMode = ${JUST_ME} StrCpy $1 "my default" ${Else} StrCpy $1 "the system" ${EndIf} + ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "&Register ${NAME} as $1 Python ${PY_VER}" IntOp $5 $5 + 11 Pop $mui_AnaCustomOptions.RegisterSystemPython - ${NSD_SetState} $mui_AnaCustomOptions.RegisterSystemPython $Ana_RegisterSystemPython_State + + # Set state of check-box + ${If} $REG_PY == 1 + ${NSD_Check} $mui_AnaCustomOptions.RegisterSystemPython + ${Else} + ${NSD_Uncheck} $mui_AnaCustomOptions.RegisterSystemPython + ${EndIf} + ${NSD_OnClick} $mui_AnaCustomOptions.RegisterSystemPython RegisterSystemPython_OnClick + ${NSD_CreateLabel} 5% "$5u" 90% 20u \ "Allows other programs, such as VSCode, PyCharm, etc. to automatically \ detect ${NAME} as the primary Python ${PY_VER} on the system." IntOp $5 $5 + 20 Pop $Ana_RegisterSystemPython_Label - ${EndIf} + + # Color the label if needed; even if the user has not interacted with the checkbox yet + ${If} $REG_PY = 1 + SetCtlColors $Ana_RegisterSystemPython_Label ff0000 transparent + ${Else} + SetCtlColors $Ana_RegisterSystemPython_Label 000000 transparent + ${EndIf} + !endif ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Clear the package cache upon completion" @@ -167,58 +266,24 @@ FunctionEnd Function AddToPath_OnClick Pop $0 - ShowWindow $Ana_AddToPath_Label ${SW_HIDE} - ${NSD_GetState} $0 $Ana_AddToPath_State - ${If} $Ana_AddToPath_State == ${BST_UNCHECKED} - ${Else} - ${If} "${SHOW_ADD_TO_PATH}" == "condabin" - SetCtlColors $Ana_AddToPath_Label 000000 transparent - ${Else} - SetCtlColors $Ana_AddToPath_Label ff0000 transparent - ${EndIf} - ${EndIf} - ShowWindow $Ana_AddToPath_Label ${SW_SHOW} -FunctionEnd - -Function RegisterSystemPython_OnClick - Pop $0 - - ShowWindow $Ana_RegisterSystemPython_Label ${SW_HIDE} - ${NSD_GetState} $0 $Ana_RegisterSystemPython_State - ${If} $Ana_RegisterSystemPython_State == ${BST_UNCHECKED} - SetCtlColors $Ana_RegisterSystemPython_Label ff0000 transparent + # Sync UI with variable + ${NSD_GetState} $0 $1 + ${If} $1 == ${BST_CHECKED} + StrCpy $INIT_CONDA 1 ${Else} - SetCtlColors $Ana_RegisterSystemPython_Label 000000 transparent + StrCpy $INIT_CONDA 0 ${EndIf} - ShowWindow $Ana_RegisterSystemPython_Label ${SW_SHOW} - - # If the button was checked, make sure we're not conflicting - # with another system installed Python - ${If} $Ana_RegisterSystemPython_State == ${BST_CHECKED} - # Check if a Python of the version we're installing - # already exists, in which case warn the user before - # proceeding. - ReadRegStr $2 SHCTX "Software\Python\PythonCore\${PY_VER}\InstallPath" "" - ${If} "$2" != "" - ${AndIf} ${FileExists} "$2\Python.exe" - MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \ - "A version of Python ${PY_VER} (${ARCH}) is already at$\n\ - $2$\n\ - We recommend that if you want ${NAME} registered as your $\n\ - system Python, you unregister this Python first. If you really$\n\ - know this is what you want, click OK, otherwise$\n\ - click cancel to continue.$\n$\n\ - NOTE: Anaconda 1.3 and earlier lacked an uninstall, if$\n\ - you are upgrading an old Anaconda, please delete the$\n\ - directory manually." \ - IDOK KeepSettingLabel - # If they don't click OK, uncheck it - StrCpy $Ana_RegisterSystemPython_State ${BST_UNCHECKED} - ${NSD_SetState} $0 $Ana_RegisterSystemPython_State -KeepSettingLabel: + ShowWindow $Ana_AddToPath_Label ${SW_HIDE} + # Only color it red if it's classic + ${If} $INIT_CONDA == 1 + ${If} "${INIT_CONDA_MODE}" == "classic" + SetCtlColors $Ana_AddToPath_Label ff0000 transparent ${EndIf} + ${Else} + SetCtlColors $Ana_AddToPath_Label 000000 transparent ${EndIf} + ShowWindow $Ana_AddToPath_Label ${SW_SHOW} FunctionEnd Function PostInstall_OnClick diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 371eff0f7..fb93badb6 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -73,30 +73,36 @@ ${Using:StrFunc} StrStr !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 NAME {{ installer_name }} +!define VERSION {{ installer_version }} +!define COMPANY {{ company }} +!define ARCH {{ arch }} +!define PLATFORM {{ installer_platform }} +!define CONSTRUCTOR_VERSION {{ constructor_version }} {%- if has_python %} -!define PY_VER {{ pyver_components[:2] | join(".") }} -!define PYVERSION_JUSTDIGITS {{ pyver_components | join("") }} -!define PYVERSION {{ pyver_components | join(".") }} -!define PYVERSION_MAJOR {{ pyver_components[0] }} -{%- endif %} -!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 UNINSTREG "SOFTWARE\Microsoft\Windows\CurrentVersion\ - \Uninstall\${UNINSTALL_NAME}" +!define PY_VER {{ pyver_components[:2] | join(".") }} +!define PYVERSION_JUSTDIGITS {{ pyver_components | join("") }} +!define PYVERSION {{ pyver_components | join(".") }} +!define PYVERSION_MAJOR {{ pyver_components[0] }} +{% endif %} +!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 REGISTER_PYTHON_OPTION {{ '1' if register_python and has_python else '0' }} +!define REGISTER_PYTHON_DEFAULT_VALUE {{ '1' if register_python_default else '0' }} +!define INIT_CONDA_OPTION {{ '1' if initialize_conda else '0' }} +!define INIT_CONDA_MODE "{{ 'condabin' if initialize_conda == 'condabin' else 'classic' }}" +!define INIT_CONDA_DEFAULT_VALUE {{ '1' if initialize_by_default else '0' }} +!define PRODUCT_NAME "${NAME} ${VERSION} (${ARCH})" +!define UNINSTALL_NAME "{{ UNINSTALL_NAME }}" +!define UNINSTREG "SOFTWARE\Microsoft\Windows\CurrentVersion\ + \Uninstall\${UNINSTALL_NAME}" + +var /global INIT_CONDA +var /global REG_PY var /global INSTDIR_JUSTME var /global INSTALLER_VERSION @@ -112,7 +118,9 @@ var /global ARGV_Help var /global ARGV_InstallationType var /global ARGV_AddToPath var /global ARGV_KeepPkgCache +{%- if has_python %} var /global ARGV_RegisterPython +{%- endif %} var /global ARGV_NoRegistry var /global ARGV_NoScripts var /global ARGV_NoShortcuts @@ -303,11 +311,13 @@ FunctionEnd OPTIONS$\n\ -------$\n\ $\n\ - /InstallationType=AllUsers [default: JustMe]$\n\ + /InstallationType=[AllUsers|JustMe] [default: JustMe]$\n\ +{%- if initialize_conda %} /AddToPath=[0|1] [default: 0]$\n\ +{%- endif %} /KeepPkgCache=[0|1] [default: {{ 1 if keep_pkgs else 0 }}]$\n\ -{%- if has_python %} - /RegisterPython=[0|1] [default: AllUsers: 1, JustMe: 0]$\n\ +{%- if has_python and register_python %} + /RegisterPython=[0|1] [default: 0]$\n\ {%- endif %} /NoRegistry=[0|1] [default: AllUsers: 0, JustMe: 0]$\n\ /NoScripts=[0|1] [default: 0]$\n\ @@ -327,14 +337,16 @@ FunctionEnd Install for all users, but don't add to PATH env var:$\n\ > $EXEFILE /InstallationType=AllUsers$\n\ $\n\ -{%- if has_python %} - Install for just me, add to PATH and register as system Python:$\n\ - > $EXEFILE /RegisterPython=1 /AddToPath=1$\n\ +{%- if has_python and register_python %} + Install for just me, and register as system Python:$\n\ + > $EXEFILE /RegisterPython=1$\n\ $\n\ {%- endif %} +{%- if initialize_conda %} Install for just me and add to PATH:$\n\ > $EXEFILE /AddToPath=1$\n\ $\n\ +{%- endif %} Install for just me, with no registry modification (for CI):$\n\ > $EXEFILE /NoRegistry=1$\n\ $\n\ @@ -357,17 +369,46 @@ FunctionEnd ${EndIf} ${EndIf} - ClearErrors -{%- if has_python %} - ${GetOptions} $ARGV "/RegisterPython=" $ARGV_RegisterPython - ${IfNot} ${Errors} - ${If} $ARGV_RegisterPython = "1" - StrCpy $Ana_RegisterSystemPython_State ${BST_CHECKED} - ${ElseIf} $ARGV_RegisterPython = "0" - StrCpy $Ana_RegisterSystemPython_State ${BST_UNCHECKED} + + !if ${REGISTER_PYTHON_OPTION} == 1 + ClearErrors + ${GetOptions} $ARGV "/RegisterPython=" $ARGV_RegisterPython + ${IfNot} ${Errors} + ${If} $ARGV_RegisterPython == "1" + StrCpy $REG_PY 1 + ${ElseIf} $ARGV_RegisterPython == "0" + StrCpy $REG_PY 0 + ${EndIf} + ${Else} + # If we have Errors, the option is not explicitly set by the user + StrCpy $REG_PY 0 ${EndIf} - ${EndIf} -{%- endif %} + + !endif + + !if ${INIT_CONDA_OPTION} == 1 + ClearErrors + ${GetOptions} $ARGV "/AddToPath=" $ARGV_AddToPath + ${IfNot} ${Errors} + ${If} $ARGV_AddToPath = "1" + # To address CVE-2022-26526. + # In AllUsers install mode, do not allow AddToPath as an option. + ${If} $InstMode == ${ALL_USERS} + 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 $INIT_CONDA 0 + ${Else} + StrCpy $INIT_CONDA 1 + ${EndIf} + ${ElseIf} $ARGV_AddToPath = "0" + StrCpy $INIT_CONDA 0 + ${EndIf} + ${Else} + # If we have Errors, the option is not explicitly set by the user + StrCpy $INIT_CONDA 0 + ${EndIf} + !endif ClearErrors ${GetOptions} $ARGV "/KeepPkgCache=" $ARGV_KeepPkgCache @@ -423,31 +464,6 @@ FunctionEnd !macroend -Function OnInit_Release - ${LogSet} on - !insertmacro ParseCommandLineArgs - - # Parsing the AddToPath option here (and not in ParseCommandLineArgs) to prevent the MessageBox from showing twice. - # For more context, see https://github.com/conda/constructor/pull/584#issuecomment-1347688020 - ClearErrors - ${GetOptions} $ARGV "/AddToPath=" $ARGV_AddToPath - ${IfNot} ${Errors} - ${If} $ARGV_AddToPath = "1" - ${If} $InstMode == ${ALL_USERS} - # 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} - ${EndIf} - ${ElseIf} $ARGV_AddToPath = "0" - StrCpy $Ana_AddToPath_State ${BST_UNCHECKED} - ${EndIf} - ${EndIf} -FunctionEnd - Function InstModePage_RadioButton_OnClick ${LogSet} on Exch $0 @@ -589,6 +605,14 @@ Function .onInit Push $R1 Push $R2 + # 1. Initialize core options default values + Call mui_AnaCustomOptions_InitDefaults + + # 2. Account finally for CLI to potentially override core default values + ${If} ${Silent} + !insertmacro ParseCommandLineArgs + ${EndIf} + InitPluginsDir {%- if TEMP_EXTRA_FILES | length != 0 %} SetOutPath $PLUGINSDIR @@ -596,7 +620,6 @@ Function .onInit 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 @@ -733,32 +756,11 @@ Function .onInit StrCpy $CheckPathLength "1" ${EndIf} - # Initialize the default settings for the anaconda custom options - Call mui_AnaCustomOptions_InitDefaults - # Override custom options with explicitly given values from construct.yaml. - # If initialize_by_default / register_python_default - # are None, do nothing. Note that these variables exist even when the construct.yaml - # settings are disabled, and the installer will respect them later! -{%- if initialize_conda %} - {%- if initialize_by_default %} - ${If} $InstMode == ${JUST_ME} - StrCpy $Ana_AddToPath_State ${BST_CHECKED} - ${EndIf} - {%- else %} - StrCpy $Ana_AddToPath_State ${BST_UNCHECKED} - {%- 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 @@ -1591,19 +1593,19 @@ Section "Install" call AbortRetryNSExecWait ${EndIf} -{% if initialize_conda %} - ${If} ${FileExists} "$INSTDIR\.nonadmin" - ${If} $Ana_AddToPath_State = ${BST_CHECKED} - !insertmacro AddRemovePath "add" "" + !if ${INIT_CONDA_OPTION} == 1 + ${If} ${FileExists} "$INSTDIR\.nonadmin" + ${If} $INIT_CONDA = 1 + !insertmacro AddRemovePath "add" "" + ${EndIf} ${EndIf} - ${EndIf} -{%- endif %} + !endif {%- if has_python %} # Create registry entries saying this is the system Python # (for this version) !define PYREG "Software\Python\PythonCore\${PY_VER}" - ${If} $Ana_RegisterSystemPython_State == ${BST_CHECKED} + ${If} $REG_PY == 1 WriteRegStr SHCTX "${PYREG}\Help\Main Python Documentation" \ "Main Python Documentation" \ "$INSTDIR\Doc\python${PYVERSION_JUSTDIGITS}.chm" diff --git a/constructor/winexe.py b/constructor/winexe.py index 1af81dfc3..d225182af 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -166,8 +166,6 @@ def make_nsi( "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": info.get("initialize_conda", "classic") or "no", "outfile": info["_outpath"], "vipv": make_VIProductVersion(info["version"]), "constructor_version": info["CONSTRUCTOR_VERSION"], diff --git a/docs/source/cli-options.md b/docs/source/cli-options.md index 97bd1ea8b..cad946d87 100644 --- a/docs/source/cli-options.md +++ b/docs/source/cli-options.md @@ -72,7 +72,7 @@ Windows installers have the following CLI options available: - `/NoShortcuts=[0|1]`: If set to `1`, the installer will not create any shortcuts. Defaults to `0`. - `/RegisterPython=[0|1]`: Whether to register Python as default in the Windows registry. Defaults - to `1`. This is preferred to `AddToPath`. + to `0`. This is preferred to `AddToPath`. - `/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. diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index 77155d3e9..210cc88e9 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -525,17 +525,19 @@ See also `initialize_by_default`. ### `initialize_by_default` -Default value for the option added by `initialize_conda`. The default -is true for GUI installers (EXE, PKG) and false for shell installers. The user -is able to change the default during interactive installation. NOTE: For Windows, -`AddToPath` is disabled when `InstallationType=AllUsers`. +Default value for the option added by `initialize_conda`. The default is +true for PKG installers, and false for EXE and SH shell installers. +The user is able to change the default during interactive installations. +Non-interactive installations are not affected by this value: users must explicitly request +to add to `PATH` via CLI options. +NOTE: For Windows, `/AddToPath` is disabled when `/InstallationType=AllUsers`. Only applies if `initialize_conda` is not false. ### `register_python` -Whether to offer the user an option to register the installed Python instance as the -system's default Python. (Windows only) +If the installer installs a Python instance, offer the user an option to register the installed Python instance as the +system's default Python. Defaults to `true` for GUI and `false` for CLI installations. (Windows only) ### `register_python_default` diff --git a/examples/custom_nsis_template/custom.nsi.tmpl b/examples/custom_nsis_template/custom.nsi.tmpl index ff6526fc9..1ed5b41f3 100644 --- a/examples/custom_nsis_template/custom.nsi.tmpl +++ b/examples/custom_nsis_template/custom.nsi.tmpl @@ -55,8 +55,6 @@ Unicode "true" # OptionsDialog.nsh plug-in constructor uses !define PRE_INSTALL_DESC __PRE_INSTALL_DESC__ !define POST_INSTALL_DESC __POST_INSTALL_DESC__ -!define SHOW_REGISTER_PYTHON __SHOW_REGISTER_PYTHON__ -!define SHOW_ADD_TO_PATH __SHOW_ADD_TO_PATH__ !define PRODUCT_NAME "${NAME} Uninstaller Patch" !define UNINSTREG "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" diff --git a/examples/extra_envs/construct.yaml b/examples/extra_envs/construct.yaml index aedaf28ff..ef09adfcd 100644 --- a/examples/extra_envs/construct.yaml +++ b/examples/extra_envs/construct.yaml @@ -9,7 +9,7 @@ channels: specs: - python=3.9 - conda # conda is required for extra_envs - - miniforge_console_shortcut # [win] + - miniforge_console_shortcut 1.* # [win] exclude: # [unix] - tk # [unix] extra_envs: diff --git a/news/1105-improve-handling-of-certain-options b/news/1105-improve-handling-of-certain-options new file mode 100644 index 000000000..171008d30 --- /dev/null +++ b/news/1105-improve-handling-of-certain-options @@ -0,0 +1,22 @@ +### Enhancements + +* EXE: Improve handling of options `initialize_conda`, `register_python` with their corresponding default values. The behavior of these options + with respect to `initialize_by_default` and `register_python_default` is now consistent with `.sh` and `.pkg` installers. + Windows CLI installations now don't add `conda` to `PATH` or register Python by default, and command-line arguments are + only parsed when installing in silent mode (enabled with the flag `/S`). (#1003, #1004 via #1105) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 16750ed9658745b9e8c0ae1e503720e2906eb904 Mon Sep 17 00:00:00 2001 From: conda-bot <18747875+conda-bot@users.noreply.github.com> Date: Tue, 25 Nov 2025 01:04:46 +0100 Subject: [PATCH 21/54] =?UTF-8?q?=F0=9F=A4=96=20updated=20file(s)=20(#1113?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HOW_WE_USE_GITHUB.md | 84 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/HOW_WE_USE_GITHUB.md b/HOW_WE_USE_GITHUB.md index a52f7e877..af3f0f113 100644 --- a/HOW_WE_USE_GITHUB.md +++ b/HOW_WE_USE_GITHUB.md @@ -39,6 +39,8 @@ This document seeks to outline how we as a community use GitHub Issues to track - [What is "Issue Sorting"?](#what-is-issue-sorting) - [Issue Sorting Procedures](#issue-sorting-procedures) + - [Development Processes](#development-processes) + - [Code Review and Merging](#code-review-and-merging) - [Commit Signing](#commit-signing) - [Types of Issues](#types-of-issues) - [Standard Issue](#standard-issue) @@ -250,6 +252,7 @@ support: Unfortunately, this issue is outside the scope of support we offer via GitHub or is not directly related to this project. Community support can be found elsewhere, though, and we encourage you to explore the following options: +- [Conda discourse forum](https://conda.discourse.group/) - [Community chat channels](https://conda.org/community#chat) - [Stack Overflow posts tagged "conda"](https://stackoverflow.com/questions/tagged/conda) @@ -264,6 +267,79 @@ Community support can be found elsewhere, though, and we encourage you to explor In order to not have to manually type or copy/paste the above repeatedly, note that it's possible to add text for the most commonly-used responses via [GitHub's "Add Saved Reply" option][docs-saved-reply]. +## Development Processes + +The following are practices the conda organization encourages for feature +development. While we recommend projects under the conda organization adopt +these practices, they are not strictly required. + +### How should we approach feature development? + +For new features, first open an issue if one doesn’t exist. Once the feature request +has been accepted (indicated by the issue's status transitioning from "Sorting" to +"Refinement"), create a specification to gather early feedback. This can include +mockups, API/command references, a written plan in the issue, and sample CLI +arguments (without functionality). + +### What is our change process? + +For larger features, break down the work into smaller, manageable issues +that are added to the backlog. As long as a feature remains on the roadmap +or backlog, do not create long-lived feature branches that span multiple +pull requests. Instead, you should integrate small slices of an overall +feature directly into the main branch to avoid complex integration challenges. + +### Should we make unrelated changes at the same time? + +When making changes, try to follow the Campsite Rule to leave things better +than when you found them. You should enhance the code you encounter, even if +primary goal is unrelated. This could involve refactoring small sections, +improving readability, or fixing minor bugs. + +## Code Review and Merging + +### What are the review requirements? + +#### Standard Review + +Most code changes require one reviewer from someone on the maintainer team for +the repository. Instead of waiting for someone on the team to review it, +directly requesting a review from the person you previously identified to work +with is preferred to optimize teamwork. If you paired with them during +development, continuous review counts as this requirement. + +#### Second Review + +Required only when the code author or the first reviewer feels like it is +necessary to get another set of eyes on a proposed change. In this case, they +add someone specific through GitHub's Request Review feature with a comment on +what they want the person to look for. + +### What are the code review best practices? + +If you are conducting a review, adhere to these best practices: + +- Provide comprehensive feedback in the first review to minimize review rounds +- Reserve Request Changes for blocking issues (bugs or other major problems) — + Select Comment for suggestions and improvements +- Follow-up reviews should focus on whether requested changes resolve original + comments +- Code should be production-ready and maintainable when merged, but doesn't + need to be perfect +- If providing feedback outside the core review focus (nitpicks, tips, + suggestions), clearly mark these as non-blocking comments that don't need to + be addressed before merging. + +### How do we merge code? + +If you are the approving reviewer (typically the first reviewer, or the second +reviewer when needed) and you have completed your review and approved the +changes, you should merge the code immediately to maintain development +velocity. + +Normally, we use squash and merge to keep a clean git history. If you are +merging a pull request, help ensure that the pull request title is updated. + ## Commit Signing For all maintainers, we require commit signing and strongly recommend it for all others wishing to contribute. More information about how to set this up within GitHub can be found here: @@ -274,11 +350,15 @@ For all maintainers, we require commit signing and strongly recommend it for all ### Standard Issue -TODO +Standard issues represent typical bug reports, feature requests, or other work +items that have a clear definition and expected outcome. ### Epics -TODO +Epics are large work items that can be broken down into smaller, more +manageable issues. They typically represent major features or significant +changes that span multiple iterations or releases. Relate the smaller +issues to the epic using the sub-issues feature in GitHub. ### Spikes From b029e9512acda269cd9c3f2413359c7b8ff82334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:05:08 -0800 Subject: [PATCH 22/54] Bump peter-evans/create-pull-request in /.github/workflows (#1114) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.8 to 7.0.9. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/271a8d0340265f705b14b6d32b9829c1cb33d45e...84ae59a2cdc2258d6fa0732dd66352dddae2a412) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 7.0.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index e238f8ef5..27bf24d7a 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -80,7 +80,7 @@ jobs: - if: github.event.comment.body != '@conda-bot render' id: create # no-op if no commits were made - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: push-to-fork: ${{ env.FORK }} token: ${{ secrets.SYNC_TOKEN }} From 072e20b70363daf8c3b2f492049cd3655b29599e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:05:22 -0800 Subject: [PATCH 23/54] [pre-commit.ci] pre-commit autoupdate (#1116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.5 → v0.14.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.5...v0.14.6) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75b65ece0..1c69e4bf3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.5 + rev: v0.14.6 hooks: # Run the linter. - id: ruff From 48bb5a1b4eba264d17227b25d610a6143e266774 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:10:29 -0800 Subject: [PATCH 24/54] Bump actions/checkout from 5.0.0 to 6.0.0 in /.github/workflows (#1115) Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...1af3b93b6815bc44a9784bd300feb67ff0d1eeb3) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/main.yml | 6 +++--- .github/workflows/update.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index faa269f02..92d9fd1cc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,7 +26,7 @@ jobs: run: shell: bash -el {0} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 with: diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 3fd3be3a0..2aaff319f 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -23,7 +23,7 @@ jobs: GLOBAL: https://raw.githubusercontent.com/conda/infra/main/.github/global.yml LOCAL: .github/labels.yml steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - id: has_local uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8dcc72b94..93eccf6e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -85,7 +85,7 @@ jobs: PYTHONUNBUFFERED: "1" steps: - name: Retrieve the source code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 @@ -182,7 +182,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Retrieve the source code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Report failures uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 env: @@ -218,7 +218,7 @@ jobs: steps: # Clean checkout of specific git ref needed for package metadata version # which needs env vars GIT_DESCRIBE_TAG and GIT_BUILD_STR: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: ref: ${{ github.ref }} clean: true diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 27bf24d7a..48c59b056 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -44,7 +44,7 @@ jobs: echo REPOSITORY=$(curl --silent ${{ github.event.issue.pull_request.url }} | jq --raw-output '.head.repo.full_name') >> $GITHUB_ENV echo REF=$(curl --silent ${{ github.event.issue.pull_request.url }} | jq --raw-output '.head.ref') >> $GITHUB_ENV - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: repository: ${{ env.REPOSITORY || github.repository }} ref: ${{ env.REF || '' }} From fb7d83a7963ef2302ef223d3b4dd4efc70622766 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:10:45 -0500 Subject: [PATCH 25/54] Update URL in examples (#1117) * Update URL * Update construct.yaml that somehow got updated incorrectly --- examples/azure_signtool/construct.yaml | 2 +- examples/customize_controls/construct.yaml | 2 +- examples/customized_welcome_conclusion/construct.yaml | 2 +- examples/exe_extra_pages/construct.yaml | 2 +- examples/extra_files/construct.yaml | 2 +- examples/from_existing_env/construct.yaml | 2 +- examples/grin/construct.yaml | 2 +- examples/miniconda/construct.yaml | 2 +- examples/newchan/construct.yaml | 2 +- examples/noconda/constructor_input.yaml | 2 +- examples/osxpkg/construct.yaml | 2 +- examples/osxpkg_extra_pages/construct.yaml | 2 +- examples/register_envs/construct.yaml | 2 +- examples/scripts/construct.yaml | 2 +- examples/shortcuts/construct.yaml | 2 +- examples/signing/construct.yaml | 2 +- examples/use_channel_remap/construct.yaml | 4 ++-- tests/test_main.py | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/examples/azure_signtool/construct.yaml b/examples/azure_signtool/construct.yaml index f40c2efa3..86a6fb983 100644 --- a/examples/azure_signtool/construct.yaml +++ b/examples/azure_signtool/construct.yaml @@ -5,7 +5,7 @@ name: Signed_AzureSignTool version: X installer_type: exe channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python windows_signing_tool: azuresigntool # [win] diff --git a/examples/customize_controls/construct.yaml b/examples/customize_controls/construct.yaml index 074c6e8de..907ba11c9 100644 --- a/examples/customize_controls/construct.yaml +++ b/examples/customize_controls/construct.yaml @@ -6,7 +6,7 @@ version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python diff --git a/examples/customized_welcome_conclusion/construct.yaml b/examples/customized_welcome_conclusion/construct.yaml index 79f55f943..751e1305c 100644 --- a/examples/customized_welcome_conclusion/construct.yaml +++ b/examples/customized_welcome_conclusion/construct.yaml @@ -5,7 +5,7 @@ name: CustomizedWelcomeConclusion version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python conclusion_file: custom_conclusion.nsi # [win] diff --git a/examples/exe_extra_pages/construct.yaml b/examples/exe_extra_pages/construct.yaml index 862cb1d9b..5452ce126 100644 --- a/examples/exe_extra_pages/construct.yaml +++ b/examples/exe_extra_pages/construct.yaml @@ -10,7 +10,7 @@ name: {{ name }} version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python {% if os.environ.get("POST_INSTALL_PAGES_LIST") %} diff --git a/examples/extra_files/construct.yaml b/examples/extra_files/construct.yaml index 64ca06a9a..7b8c9a600 100644 --- a/examples/extra_files/construct.yaml +++ b/examples/extra_files/construct.yaml @@ -8,7 +8,7 @@ license_file: TEST_LICENSE.txt check_path_spaces: False check_path_length: False channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python extra_files: diff --git a/examples/from_existing_env/construct.yaml b/examples/from_existing_env/construct.yaml index e60d00945..89df7b411 100644 --- a/examples/from_existing_env/construct.yaml +++ b/examples/from_existing_env/construct.yaml @@ -5,6 +5,6 @@ version: X installer_type: all environment: {{ os.environ.get("CONSTRUCTOR_TEST_EXISTING_ENV", os.environ["CONDA_PREFIX"]) }} channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ initialize_by_default: false register_python: False diff --git a/examples/grin/construct.yaml b/examples/grin/construct.yaml index 553ffa45e..6e3823bc8 100644 --- a/examples/grin/construct.yaml +++ b/examples/grin/construct.yaml @@ -7,7 +7,7 @@ version: 3 # channels to pull packages from channels: &id1 - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ - https://conda.anaconda.org/ilan # specifications diff --git a/examples/miniconda/construct.yaml b/examples/miniconda/construct.yaml index 7c5dad48b..589b4223a 100644 --- a/examples/miniconda/construct.yaml +++ b/examples/miniconda/construct.yaml @@ -6,7 +6,7 @@ version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python diff --git a/examples/newchan/construct.yaml b/examples/newchan/construct.yaml index bd32546ed..147d94424 100644 --- a/examples/newchan/construct.yaml +++ b/examples/newchan/construct.yaml @@ -5,7 +5,7 @@ name: Funnychan version: 2.5.5 channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ - https://conda.anaconda.org/ilan # specifications diff --git a/examples/noconda/constructor_input.yaml b/examples/noconda/constructor_input.yaml index 5e3fa6fd3..0a17e6cb2 100644 --- a/examples/noconda/constructor_input.yaml +++ b/examples/noconda/constructor_input.yaml @@ -5,7 +5,7 @@ name: NoConda version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python exclude: # [unix] diff --git a/examples/osxpkg/construct.yaml b/examples/osxpkg/construct.yaml index d9ddae385..7c786e0b0 100644 --- a/examples/osxpkg/construct.yaml +++ b/examples/osxpkg/construct.yaml @@ -10,7 +10,7 @@ default_location_pkg: Library pkg_name: "osx-pkg-test" channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ attempt_hardlinks: True diff --git a/examples/osxpkg_extra_pages/construct.yaml b/examples/osxpkg_extra_pages/construct.yaml index 201fe8969..2b6023892 100644 --- a/examples/osxpkg_extra_pages/construct.yaml +++ b/examples/osxpkg_extra_pages/construct.yaml @@ -10,7 +10,7 @@ default_location_pkg: Library pkg_name: "osx-pkg-test" channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ attempt_hardlinks: True diff --git a/examples/register_envs/construct.yaml b/examples/register_envs/construct.yaml index b55eae9ea..86d621561 100644 --- a/examples/register_envs/construct.yaml +++ b/examples/register_envs/construct.yaml @@ -5,7 +5,7 @@ name: RegisterEnvs version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python register_envs: false diff --git a/examples/scripts/construct.yaml b/examples/scripts/construct.yaml index 935b1f40b..33dcfee91 100644 --- a/examples/scripts/construct.yaml +++ b/examples/scripts/construct.yaml @@ -5,7 +5,7 @@ name: Scripts version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python diff --git a/examples/shortcuts/construct.yaml b/examples/shortcuts/construct.yaml index b237e83c2..a17be497c 100644 --- a/examples/shortcuts/construct.yaml +++ b/examples/shortcuts/construct.yaml @@ -7,7 +7,7 @@ installer_type: all channels: - conda-test/label/menuinst-tests - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python diff --git a/examples/signing/construct.yaml b/examples/signing/construct.yaml index 06ce44d00..07bf9685f 100644 --- a/examples/signing/construct.yaml +++ b/examples/signing/construct.yaml @@ -5,7 +5,7 @@ name: Signed version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - python # This certificate is generated and copied over on the spot during CI diff --git a/examples/use_channel_remap/construct.yaml b/examples/use_channel_remap/construct.yaml index bf4aa7f2b..667f6658a 100644 --- a/examples/use_channel_remap/construct.yaml +++ b/examples/use_channel_remap/construct.yaml @@ -10,10 +10,10 @@ keep_pkgs: True # we just remap the main conda channel, to show the idea of remap channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ channels_remap: - - src: http://repo.anaconda.com/pkgs/main/ + - src: https://repo.anaconda.com/pkgs/main/ dest: file:///usr/local/share/private_repo/ specs: diff --git a/tests/test_main.py b/tests/test_main.py index 71aa79fdb..896c7d8a6 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -10,7 +10,7 @@ def test_dry_run(tmp_path): version: X installer_type: all channels: - - http://repo.anaconda.com/pkgs/main/ + - https://repo.anaconda.com/pkgs/main/ specs: - ca-certificates """ From eb7a28cbdba47e9cac339864d764f155378df720 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 1 Dec 2025 23:04:53 +0100 Subject: [PATCH 26/54] Fix cache/ path which prevented repodata files from being written (#1121) * Fix cache/ path which prevented repodata files from being written * add news --- constructor/shar.py | 2 +- news/1121-repodata-cache | 19 +++++++++++++++++++ tests/test_examples.py | 3 +++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 news/1121-repodata-cache diff --git a/constructor/shar.py b/constructor/shar.py index 8475fca38..745f95c61 100644 --- a/constructor/shar.py +++ b/constructor/shar.py @@ -150,7 +150,7 @@ def create(info, verbose=False): "pkgs/%s.sh" % key, filter=make_executable if has_shebang(info[key]) else None, ) - cache_dir = join(tmp_dir, "cache") + cache_dir = join(tmp_dir, "pkgs", "cache") if isdir(cache_dir): for cf in os.listdir(cache_dir): if cf.endswith(".json"): diff --git a/news/1121-repodata-cache b/news/1121-repodata-cache new file mode 100644 index 000000000..a835cea27 --- /dev/null +++ b/news/1121-repodata-cache @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* Ensure cached repodata files are shipped in SH installers. (#1119 via #1121). + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/test_examples.py b/tests/test_examples.py index a2ae8ad85..3fc38104f 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -547,6 +547,9 @@ def test_example_miniforge(tmp_path, request, example): check_sentinels=installer.suffix != ".pkg", uninstall=False, ) + # Check that key metadata files are in place + assert install_dir.glob("conda-meta/*.json") + assert install_dir.glob("pkgs/cache/*.json") # enables offline installs if installer.suffix == ".pkg" and ON_CI: basename = "Miniforge3" if example == "miniforge" else "Miniforge3-mamba2" _sentinel_file_checks(input_path, Path(os.environ["HOME"]) / basename) From 96d5ea96d62285e427017a7d2ed242447b9f8cb6 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 3 Dec 2025 04:34:53 -0600 Subject: [PATCH 27/54] Prepare 3.14 (#1123) --- .authors.yml | 12 +++++----- CHANGELOG.md | 22 +++++++++++++++++++ news/1089-remove-python-commands | 20 ----------------- news/1105-improve-handling-of-certain-options | 22 ------------------- news/1121-repodata-cache | 19 ---------------- 5 files changed, 28 insertions(+), 67 deletions(-) delete mode 100644 news/1089-remove-python-commands delete mode 100644 news/1105-improve-handling-of-certain-options delete mode 100644 news/1121-repodata-cache diff --git a/.authors.yml b/.authors.yml index ef2e2a617..ffabdc373 100644 --- a/.authors.yml +++ b/.authors.yml @@ -264,7 +264,7 @@ github: chenghlee - name: conda-bot email: ad-team+condabot@anaconda.com - num_commits: 56 + num_commits: 57 first_commit: 2022-01-25 21:38:28 alternate_emails: - 18747875+conda-bot@users.noreply.github.com @@ -277,7 +277,7 @@ aliases: - Jaime RGP - jaimergp - num_commits: 110 + num_commits: 111 first_commit: 2022-01-08 14:56:53 github: jaimergp - name: Tom Hören @@ -357,7 +357,7 @@ github: RahulARanger - name: Marco Esters email: mesters@anaconda.com - num_commits: 57 + num_commits: 58 first_commit: 2023-05-12 11:44:12 github: marcoesters - name: Darryl Miles @@ -372,7 +372,7 @@ github: deepeshaburse - name: pre-commit-ci[bot] email: 66853113+pre-commit-ci[bot]@users.noreply.github.com - num_commits: 66 + num_commits: 69 first_commit: 2023-05-02 12:01:43 github: pre-commit-ci[bot] - name: Matthias Kuhn @@ -382,7 +382,7 @@ github: m-kuhn - name: dependabot[bot] email: 49699333+dependabot[bot]@users.noreply.github.com - num_commits: 53 + num_commits: 55 github: dependabot[bot] first_commit: 2024-05-07 10:16:05 - name: Julien Jerphanion @@ -417,5 +417,5 @@ - Robin github: lrandersson email: 34315751+lrandersson@users.noreply.github.com - num_commits: 5 + num_commits: 8 first_commit: 2025-10-21 08:30:00 diff --git a/CHANGELOG.md b/CHANGELOG.md index 12670b048..30443dd96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ [//]: # (current developments) +## 2025-12-02 3.14.0: +### Enhancements + +* Replace custom Python script with `conda-standalone` calls. + This removes Python as an implicit dependency from installers. (#549 via #1089) +* EXE: Improve handling of options `initialize_conda`, `register_python` with their corresponding default values. The behavior of these options + with respect to `initialize_by_default` and `register_python_default` is now consistent with `.sh` and `.pkg` installers. + Windows CLI installations now don't add `conda` to `PATH` or register Python by default, and command-line arguments are + only parsed when installing in silent mode (enabled with the flag `/S`). (#1003, #1004 via #1105) + +### Bug fixes + +* Ensure cached repodata files are shipped in SH installers. (#1119 via #1121). + +### Contributors + +* @jaimergp +* @marcoesters +* @lrandersson + + + ## 2025-11-10 3.13.1: ### Bug fixes diff --git a/news/1089-remove-python-commands b/news/1089-remove-python-commands deleted file mode 100644 index 66079a75f..000000000 --- a/news/1089-remove-python-commands +++ /dev/null @@ -1,20 +0,0 @@ -### Enhancements - -* **Breaking change**: Replace custom Python script with `conda-standalone` calls. - This removes Python as an implicit dependency from installers. (#549 via #1089) - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1105-improve-handling-of-certain-options b/news/1105-improve-handling-of-certain-options deleted file mode 100644 index 171008d30..000000000 --- a/news/1105-improve-handling-of-certain-options +++ /dev/null @@ -1,22 +0,0 @@ -### Enhancements - -* EXE: Improve handling of options `initialize_conda`, `register_python` with their corresponding default values. The behavior of these options - with respect to `initialize_by_default` and `register_python_default` is now consistent with `.sh` and `.pkg` installers. - Windows CLI installations now don't add `conda` to `PATH` or register Python by default, and command-line arguments are - only parsed when installing in silent mode (enabled with the flag `/S`). (#1003, #1004 via #1105) - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1121-repodata-cache b/news/1121-repodata-cache deleted file mode 100644 index a835cea27..000000000 --- a/news/1121-repodata-cache +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* Ensure cached repodata files are shipped in SH installers. (#1119 via #1121). - -### Deprecations - -* - -### Docs - -* - -### Other - -* From 974e94429bca42ae0c1a6f2ce303a4c7b7421aba Mon Sep 17 00:00:00 2001 From: conda-bot <18747875+conda-bot@users.noreply.github.com> Date: Thu, 4 Dec 2025 22:26:10 +0100 Subject: [PATCH 28/54] =?UTF-8?q?=F0=9F=A4=96=20updated=20file(s)=20(#1118?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HOW_WE_USE_GITHUB.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/HOW_WE_USE_GITHUB.md b/HOW_WE_USE_GITHUB.md index af3f0f113..51289afdd 100644 --- a/HOW_WE_USE_GITHUB.md +++ b/HOW_WE_USE_GITHUB.md @@ -340,12 +340,6 @@ velocity. Normally, we use squash and merge to keep a clean git history. If you are merging a pull request, help ensure that the pull request title is updated. -## Commit Signing - -For all maintainers, we require commit signing and strongly recommend it for all others wishing to contribute. More information about how to set this up within GitHub can be found here: - -- [GitHub's signing commits docs][docs-commit-signing] - ## Types of Issues ### Standard Issue From 42573112c857c5e93b56ab25221f1c4656d89ec8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:26:24 -0600 Subject: [PATCH 29/54] [pre-commit.ci] pre-commit autoupdate (#1120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.6 → v0.14.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.6...v0.14.7) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c69e4bf3..56576060d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.6 + rev: v0.14.7 hooks: # Run the linter. - id: ruff From 9470590e68335e2952b9d873d47d668b82b2caa5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 07:41:46 -0600 Subject: [PATCH 30/54] Bump actions/setup-python from 6.0.0 to 6.1.0 in /.github/workflows (#1122) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/e797f83bcb11b83ae66e0230d6156d7c80228e7c...83679a892e2d95755f2dac6acb0bfd1e9ac5d548) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93eccf6e4..ee65769cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -224,7 +224,7 @@ jobs: clean: true fetch-depth: 0 - - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - name: Build Python sdist and wheel run: | python -m pip install build From 481203646339efffbc715b5dc5ecc7c082e6e0e1 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:12:23 -0500 Subject: [PATCH 31/54] Assign default values to uninitialized variables (#1124) * Assign default values to uninitialized variables * Create 1124-assign-uninitialized-variables * Ensure other variables affected by CLI parsing are initialized * Reformulate the news * Apply suggestions from code review --------- Co-authored-by: Marco Esters --- constructor/nsis/main.nsi.tmpl | 34 +++++++++++------------- news/1124-assign-uninitialized-variables | 19 +++++++++++++ 2 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 news/1124-assign-uninitialized-variables diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index fb93badb6..7bbfd7565 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -260,6 +260,19 @@ Function SkipPageIfUACInnerInstance ${EndIf} FunctionEnd +Function InitializeVariables + StrCpy $CheckPathLength "{{ 1 if check_path_length else 0 }}" + StrCpy $ARGV_NoRegistry "0" + StrCpy $ARGV_KeepPkgCache "{{ 1 if keep_pkgs else 0 }}" + + # Package cache option + StrCpy $Ana_ClearPkgCache_State {{ '${BST_UNCHECKED}' if keep_pkgs else '${BST_CHECKED}' }} + + # Pre/post install + 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}' }} +FunctionEnd + !macro DoElevation GetDlgItem $1 $HWNDParent 1 System::Call user32::GetFocus()i.s @@ -322,7 +335,7 @@ FunctionEnd /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\ + /CheckPathLength=[0|1] [default: {{ 1 if check_path_length else 0 }}]$\n\ /? (show this help message)$\n\ /S (run in CLI/headless mode)$\n\ /Q (quiet mode, do not print output to console)$\n\ @@ -412,15 +425,9 @@ FunctionEnd ClearErrors ${GetOptions} $ARGV "/KeepPkgCache=" $ARGV_KeepPkgCache - ${If} ${Errors} - StrCpy $ARGV_KeepPkgCache "{{ 1 if keep_pkgs else 0 }}" - ${EndIf} ClearErrors ${GetOptions} $ARGV "/NoRegistry=" $ARGV_NoRegistry - ${If} ${Errors} - StrCpy $ARGV_NoRegistry "0" - ${EndIf} ClearErrors ${GetOptions} $ARGV "/NoScripts=" $ARGV_NoScripts @@ -605,8 +612,9 @@ Function .onInit Push $R1 Push $R2 - # 1. Initialize core options default values + # 1. Initialize core options default values and other variables Call mui_AnaCustomOptions_InitDefaults + Call InitializeVariables # 2. Account finally for CLI to potentially override core default values ${If} ${Silent} @@ -751,16 +759,6 @@ Function .onInit ${EndIf} ${EndIf} - ; Set default value - ${If} $CheckPathLength == "" - StrCpy $CheckPathLength "1" - ${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}' }} - ${Print} "Welcome to ${NAME} ${VERSION}$\n" Pop $R2 diff --git a/news/1124-assign-uninitialized-variables b/news/1124-assign-uninitialized-variables new file mode 100644 index 000000000..0a3ee5411 --- /dev/null +++ b/news/1124-assign-uninitialized-variables @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* EXE: Fix a regression with uninitialized variables that prevented installations from being added to the "Add/Remove Programs" list. (#1124) + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 7a837bd9d3c94813cc4734ebc6f9879557ddd488 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:00:54 -0500 Subject: [PATCH 32/54] Prepare 3.14.1 (#1131) * Updated authorship for 3.14.1 * Updated CHANGELOG for 3.14.1 * Remove bots from changelog * Apply suggestions from code review --------- Co-authored-by: Marco Esters --- .authors.yml | 10 +++++----- CHANGELOG.md | 12 ++++++++++++ news/1124-assign-uninitialized-variables | 19 ------------------- 3 files changed, 17 insertions(+), 24 deletions(-) delete mode 100644 news/1124-assign-uninitialized-variables diff --git a/.authors.yml b/.authors.yml index ffabdc373..077e14206 100644 --- a/.authors.yml +++ b/.authors.yml @@ -264,7 +264,7 @@ github: chenghlee - name: conda-bot email: ad-team+condabot@anaconda.com - num_commits: 57 + num_commits: 58 first_commit: 2022-01-25 21:38:28 alternate_emails: - 18747875+conda-bot@users.noreply.github.com @@ -357,7 +357,7 @@ github: RahulARanger - name: Marco Esters email: mesters@anaconda.com - num_commits: 58 + num_commits: 59 first_commit: 2023-05-12 11:44:12 github: marcoesters - name: Darryl Miles @@ -372,7 +372,7 @@ github: deepeshaburse - name: pre-commit-ci[bot] email: 66853113+pre-commit-ci[bot]@users.noreply.github.com - num_commits: 69 + num_commits: 70 first_commit: 2023-05-02 12:01:43 github: pre-commit-ci[bot] - name: Matthias Kuhn @@ -382,7 +382,7 @@ github: m-kuhn - name: dependabot[bot] email: 49699333+dependabot[bot]@users.noreply.github.com - num_commits: 55 + num_commits: 56 github: dependabot[bot] first_commit: 2024-05-07 10:16:05 - name: Julien Jerphanion @@ -417,5 +417,5 @@ - Robin github: lrandersson email: 34315751+lrandersson@users.noreply.github.com - num_commits: 8 + num_commits: 9 first_commit: 2025-10-21 08:30:00 diff --git a/CHANGELOG.md b/CHANGELOG.md index 30443dd96..404568bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ [//]: # (current developments) +## 2025-12-08 3.14.1: +### Bug fixes + +* EXE: Fix a regression with uninitialized variables that prevented installations from being added to the "Add/Remove Programs" list. (#1124) + +### Contributors + +* @marcoesters +* @lranderssons + + + ## 2025-12-02 3.14.0: ### Enhancements diff --git a/news/1124-assign-uninitialized-variables b/news/1124-assign-uninitialized-variables deleted file mode 100644 index 0a3ee5411..000000000 --- a/news/1124-assign-uninitialized-variables +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* EXE: Fix a regression with uninitialized variables that prevented installations from being added to the "Add/Remove Programs" list. (#1124) - -### Deprecations - -* - -### Docs - -* - -### Other - -* From 4e914b8c97f66eaf1a0115c0e19bf494e292c5d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 08:09:17 -0800 Subject: [PATCH 33/54] Bump peter-evans/create-pull-request in /.github/workflows (#1126) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.9 to 7.0.11. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/84ae59a2cdc2258d6fa0732dd66352dddae2a412...22a9089034f40e5a961c8808d113e2c98fb63676) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 7.0.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 48c59b056..da7733d5a 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -80,7 +80,7 @@ jobs: - if: github.event.comment.body != '@conda-bot render' id: create # no-op if no commits were made - uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: push-to-fork: ${{ env.FORK }} token: ${{ secrets.SYNC_TOKEN }} From f1c024678313b557b8727e21f00d3b7dae79401b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:02:38 -0800 Subject: [PATCH 34/54] [pre-commit.ci] pre-commit autoupdate (#1129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.7 → v0.14.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.7...v0.14.8) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56576060d..2ace2f202 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.7 + rev: v0.14.8 hooks: # Run the linter. - id: ruff From 45ebb933b8b3862b4da799f24b099e61ef984076 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:02:54 -0800 Subject: [PATCH 35/54] Bump actions/checkout from 6.0.0 to 6.0.1 in /.github/workflows (#1128) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/1af3b93b6815bc44a9784bd300feb67ff0d1eeb3...8e8c483db84b4bee98b60c0593521ed34d9990e8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/main.yml | 6 +++--- .github/workflows/update.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 92d9fd1cc..49a93859e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,7 +26,7 @@ jobs: run: shell: bash -el {0} steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 with: diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 2aaff319f..b34a5dad7 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -23,7 +23,7 @@ jobs: GLOBAL: https://raw.githubusercontent.com/conda/infra/main/.github/global.yml LOCAL: .github/labels.yml steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - id: has_local uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee65769cd..0290f4474 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -85,7 +85,7 @@ jobs: PYTHONUNBUFFERED: "1" steps: - name: Retrieve the source code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 @@ -182,7 +182,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Retrieve the source code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Report failures uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 env: @@ -218,7 +218,7 @@ jobs: steps: # Clean checkout of specific git ref needed for package metadata version # which needs env vars GIT_DESCRIBE_TAG and GIT_BUILD_STR: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.ref }} clean: true diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index da7733d5a..d1407eb58 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -44,7 +44,7 @@ jobs: echo REPOSITORY=$(curl --silent ${{ github.event.issue.pull_request.url }} | jq --raw-output '.head.repo.full_name') >> $GITHUB_ENV echo REF=$(curl --silent ${{ github.event.issue.pull_request.url }} | jq --raw-output '.head.ref') >> $GITHUB_ENV - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: ${{ env.REPOSITORY || github.repository }} ref: ${{ env.REF || '' }} From 434c009bff305dd6486ad845e2b3c72cda7fa021 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:03:18 -0800 Subject: [PATCH 36/54] Bump actions/stale from 10.1.0 to 10.1.1 in /.github/workflows (#1127) Bumps [actions/stale](https://github.com/actions/stale) from 10.1.0 to 10.1.1. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/5f858e3efba33a5ca4407a664cc011ad407f2008...997185467fa4f803885201cee163a9f38240193d) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ed159bfeb..28c857650 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -38,7 +38,7 @@ jobs: with: path: https://raw.githubusercontent.com/conda/infra/main/.github/messages.yml - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 id: stale with: # Only issues with these labels are checked whether they are stale From 86e67b87cd3bd10981934d2f7e69a47c59d18628 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 10 Dec 2025 09:03:33 -0800 Subject: [PATCH 37/54] Improve logging for EXE installers (#1108) * Remove unused utility functions * Ensure that log file is UTF-16 * Only start logging after INSTDIR has been created * Stop logging before INSTDIR is removed to allow removal without reboot * Move registry clean-up to the beginning of the uninstallation * Prevent variable overwrite with Print macro * Use R* registers when using Print macro * Only add newline to print when input string does not end in a linefeed * Implement step log parsing * Wrap commands into cmd.exe call for better error output * Add STEP_LOG defintion * Enable logging for conda-standalone * Require conda-standalone 24.1.2 for logged package extraction * Add comment to explain usage of subshells for command execution * Add message box to icacls failure * Do not use pythonw.exe anymore * Remove misleading warning about old paths * Add news * Account for unknown conda.exe types * Make abort default for silent installations * Do not assume that install.log exists after uninstallation * Re-add DetailPrint to Print macro to output to log * Use f-strings in print statements * Do not use UTF-16 LE * Use type for tee-like output * Explicit exit with error codes --- constructor/main.py | 28 +++- constructor/nsis/Utils.nsh | 123 -------------- constructor/nsis/_nsis.py | 19 +-- constructor/nsis/_system_path.py | 23 +-- constructor/nsis/main.nsi.tmpl | 267 ++++++++++++++++++++----------- constructor/winexe.py | 3 + news/1108-improve-logging | 23 +++ recipe/meta.yaml | 2 +- tests/test_examples.py | 2 +- 9 files changed, 226 insertions(+), 264 deletions(-) create mode 100644 news/1108-improve-logging diff --git a/constructor/main.py b/constructor/main.py index ee8104ad3..abf03ad2b 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -19,6 +19,7 @@ import sys from os.path import abspath, expanduser, isdir, join from pathlib import Path +from tempfile import TemporaryDirectory from textwrap import dedent from . import __version__ @@ -77,7 +78,22 @@ def get_output_filename(info): ) -def _win_install_needs_python_exe(conda_exe: str) -> bool: +def _conda_exe_supports_logging(conda_exe: str, conda_exe_type: StandaloneExe | None) -> bool: + """Test if the standalone binary supports the the --log-file argument. + + Only available for conda-standalone. + """ + if not conda_exe_type: + return False + with TemporaryDirectory() as tmpdir: + logfile = Path(tmpdir, "conda.log") + subprocess.run([conda_exe, "--version", f"--log-file={logfile}"]) + return logfile.exists() + + +def _win_install_needs_python_exe(conda_exe: str, conda_exe_type: StandaloneExe | None) -> bool: + if not conda_exe_type: + return True results = subprocess.run( [conda_exe, "constructor", "windows", "--help"], capture_output=True, @@ -270,6 +286,11 @@ def is_conda_meta_frozen(path_str: str) -> bool: else: info["_ignore_condarcs_arg"] = "" + info["_conda_exe_supports_logging"] = _conda_exe_supports_logging( + info["_conda_exe"], + info["_conda_exe_type"], + ) + if "pkg" in itypes: if (domains := info.get("pkg_domains")) is not None: domains = {key: str(val).lower() for key, val in domains.items()} @@ -289,7 +310,10 @@ def is_conda_meta_frozen(path_str: str) -> bool: } if osname == "win": - info["_win_install_needs_python_exe"] = _win_install_needs_python_exe(info["_conda_exe"]) + info["_win_install_needs_python_exe"] = _win_install_needs_python_exe( + info["_conda_exe"], + info["_conda_exe_type"], + ) info["installer_type"] = itypes[0] fcp_main(info, verbose=verbose, dry_run=dry_run, conda_exe=conda_exe) diff --git a/constructor/nsis/Utils.nsh b/constructor/nsis/Utils.nsh index 986bf4344..d7ecd1ea0 100644 --- a/constructor/nsis/Utils.nsh +++ b/constructor/nsis/Utils.nsh @@ -1,128 +1,5 @@ # Miscellaneous helpers. -# We're not using RIndexOf at the moment, so ifdef it out for now (which -# prevents the compiler warnings about an unused function). -!ifdef INDEXOF -Function IndexOf - Exch $R0 - Exch - Exch $R1 - Push $R2 - Push $R3 - - StrCpy $R3 $R0 - StrCpy $R0 -1 - IntOp $R0 $R0 + 1 - - StrCpy $R2 $R3 1 $R0 - StrCmp $R2 "" +2 - StrCmp $R2 $R1 +2 -3 - - StrCpy $R0 -1 - - Pop $R3 - Pop $R2 - Pop $R1 - Exch $R0 -FunctionEnd - -!macro IndexOf Var Str Char - Push "${Char}" - Push "${Str}" - Call IndexOf - Pop "${Var}" - !macroend -!define IndexOf "!insertmacro IndexOf" - -Function RIndexOf - Exch $R0 - Exch - Exch $R1 - Push $R2 - Push $R3 - - StrCpy $R3 $R0 - StrCpy $R0 0 - IntOp $R0 $R0 + 1 - StrCpy $R2 $R3 1 -$R0 - StrCmp $R2 "" +2 - StrCmp $R2 $R1 +2 -3 - - StrCpy $R0 -1 - - Pop $R3 - Pop $R2 - Pop $R1 - Exch $R0 -FunctionEnd - -!macro RIndexOf Var Str Char - Push "${Char}" - Push "${Str}" - Call RIndexOf - Pop "${Var}" -!macroend - -!define RIndexOf "!insertmacro RIndexOf" -!endif - -!macro StrStr - Exch $R1 ; st=haystack,old$R1, $R1=needle - Exch ; st=old$R1,haystack - Exch $R2 ; st=old$R1,old$R2, $R2=haystack - Push $R3 - Push $R4 - Push $R5 - StrLen $R3 $R1 - StrCpy $R4 0 - ; $R1=needle - ; $R2=haystack - ; $R3=len(needle) - ; $R4=cnt - ; $R5=tmp - loop: - StrCpy $R5 $R2 $R3 $R4 - StrCmp $R5 $R1 done - StrCmp $R5 "" done - IntOp $R4 $R4 + 1 - Goto loop - done: - StrCpy $R1 $R2 "" $R4 - Pop $R5 - Pop $R4 - Pop $R3 - Pop $R2 - Exch $R1 -!macroend - -!macro GetShortPathName - Pop $0 - # Return the 8.3 short path name for $0. We ensure $0 exists by calling - # SetOutPath first (kernel32::GetShortPathName() fails otherwise). - SetOutPath $0 - Push $0 - Push ' ' - Call StrStr - Pop $1 - ${If} $1 != "" - # Our installation directory has a space, so use the short name from - # here in. (This ensures no directories with spaces are written to - # registry values or configuration files.) After GetShortPathName(), - # $0 will have the new name and $1 will have the length (if it's 0, - # assume an error occurred and leave $INSTDIR as it is). - System::Call "kernel32::GetShortPathName(\ - t'$RootDir', \ - t.R0, \ - i${NSIS_MAX_STRLEN}) i.R1" - - ${If} $R1 > 0 - Push $R0 - ${EndIf} - ${Else} - Push $0 - ${EndIf} -!macroend - ; Slightly modified version of http://nsis.sourceforge.net/IsWritable Function IsWritable !define IsWritable `!insertmacro IsWritableCall` diff --git a/constructor/nsis/_nsis.py b/constructor/nsis/_nsis.py index d83008854..a7e260308 100644 --- a/constructor/nsis/_nsis.py +++ b/constructor/nsis/_nsis.py @@ -35,24 +35,7 @@ def gui_excepthook(exctype, value, tb): sys.excepthook = gui_excepthook -# If pythonw is being run, there may be no write function -if sys.stdout and sys.stdout.write: - out = sys.stdout.write - err = sys.stderr.write -else: - import ctypes - OutputDebugString = ctypes.windll.kernel32.OutputDebugStringW - OutputDebugString.argtypes = [ctypes.c_wchar_p] - - def out(x): - OutputDebugString('_nsis.py: ' + x) - - def err(x): - OutputDebugString('_nsis.py: Error: ' + x) - - allusers = (not exists(join(ROOT_PREFIX, '.nonadmin'))) -# out('allusers is %s\n' % allusers) # This must be the same as conda's binpath_from_arg() in conda/cli/activate.py PATH_SUFFIXES = ('', @@ -96,7 +79,7 @@ def add_to_path(pyversion, arch): except IOError: old_prefixes = [] for prefix in old_prefixes: - out('Removing old installation at %s from PATH (if any entries get found)\n' % (prefix)) + print(f"Removing old installation at {prefix} from PATH (if any entries get found)") remove_from_path(prefix) # add Anaconda to the path diff --git a/constructor/nsis/_system_path.py b/constructor/nsis/_system_path.py index ad391be86..ff35fe20e 100644 --- a/constructor/nsis/_system_path.py +++ b/constructor/nsis/_system_path.py @@ -11,28 +11,10 @@ import ctypes import os import re -import sys from ctypes import wintypes from os import path -if sys.version_info[0] >= 3: - import winreg as reg -else: - import _winreg as reg - -# If pythonw is being run, there may be no write function -if sys.stdout and sys.stdout.write: - out = sys.stdout.write - err = sys.stderr.write -else: - OutputDebugString = ctypes.windll.kernel32.OutputDebugStringW - OutputDebugString.argtypes = [ctypes.c_wchar_p] - - def out(x): - OutputDebugString('_nsis.py: ' + x) - - def err(x): - OutputDebugString('_nsis.py: Error: ' + x) +import winreg as reg HWND_BROADCAST = 0xffff WM_SETTINGCHANGE = 0x001A @@ -159,9 +141,6 @@ def add_to_system_path(paths, allusers=True, path_env_var='PATH'): final_value = final_value.replace('"', '') # Warn about directories that do not exist. directories = final_value.split(';') - for directory in directories: - if '%' not in directory and not os.path.exists(directory): - out("WARNING: Old PATH entry '%s' does not exist\n" % (directory)) reg.SetValueEx(key, path_env_var, 0, reg_type, final_value) finally: diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 7bbfd7565..2df9313e4 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -17,6 +17,16 @@ Unicode true !define LogSet "!insertmacro LogSetMacro" !macro LogSetMacro SETTING !ifdef ENABLE_LOGGING + ${If} ${SETTING} == "on" + ${IfNot} ${FileExists} "$INSTDIR\install.log" + # Enforce UTF-16 encoding in the log file + # NSIS doesn't write the correct BOM to log files, + # so each character will be followed by NUL. + FileOpen $R0 "$INSTDIR\install.log" w + FileWrite $R0 "" + FileClose $R0 + ${EndIf} + ${EndIf} LogSet ${SETTING} !endif !macroend @@ -31,9 +41,12 @@ Unicode true 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}" +# Print and PrintToConsole are macros because it makes them easier to call. +# However, that also means that registers must be handled with caution because +# they will be overwritten in the macro. It is best to use $R* registers if +# temporary variables need to be used. +!define PrintToConsole "!insertmacro PrintToConsoleMacro" +!macro PrintToConsoleMacro INPUT_TEXT ${If} ${Silent} ${AndIf} $QuietMode != "1" ${IfNot} $StdOutHandleSet == "1" @@ -47,10 +60,24 @@ var /global StdOutHandleSet StrCpy $StdOutHandle $0 StrCpy $StdOutHandleSet "1" ${EndIf} - FileWrite $StdOutHandle "${INPUT_TEXT}$\n" + # Only add newline if input text doesn't have it already + StrLen $2 "${INPUT_TEXT}" + IntOp $2 $2 - 1 + StrCpy $2 "${INPUT_TEXT}" 1 $2 + ${If} $2 == "$\n" + FileWrite $StdOutHandle "${INPUT_TEXT}" + ${Else} + FileWrite $StdOutHandle "${INPUT_TEXT}$\n" + ${EndIf} ${EndIf} !macroend +!define Print "!insertmacro PrintMacro" +!macro PrintMacro INPUT_TEXT + DetailPrint "${INPUT_TEXT}" + ${PrintToConsole} "${INPUT_TEXT}" +!macroend + !include "WinMessages.nsh" !include "WordFunc.nsh" !include "LogicLib.nsh" @@ -98,8 +125,11 @@ ${Using:StrFunc} StrStr !define INIT_CONDA_DEFAULT_VALUE {{ '1' if initialize_by_default else '0' }} !define PRODUCT_NAME "${NAME} ${VERSION} (${ARCH})" !define UNINSTALL_NAME "{{ UNINSTALL_NAME }}" -!define UNINSTREG "SOFTWARE\Microsoft\Windows\CurrentVersion\ - \Uninstall\${UNINSTALL_NAME}" +!define UNINSTREG "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_NAME}" +# Silent installations do not output to the console and outputs to stdout +# are not written into install.log. STEP_LOG creates an intermittent file +# that is output into these streams after the commands finish. +!define STEP_LOG "$INSTDIR\.step.log" var /global INIT_CONDA var /global REG_PY @@ -254,7 +284,6 @@ UninstPage Custom un.UninstCustomOptions_Show !insertmacro MUI_LANGUAGE "English" Function SkipPageIfUACInnerInstance - ${LogSet} on ${If} ${UAC_IsInnerInstance} Abort ${EndIf} @@ -472,7 +501,6 @@ FunctionEnd !macroend Function InstModePage_RadioButton_OnClick - ${LogSet} on Exch $0 Push $1 Push $2 @@ -488,7 +516,6 @@ Function InstModePage_RadioButton_OnClick FunctionEnd Function InstModePage_Create - ${LogSet} on Push $0 Push $1 Push $2 @@ -535,7 +562,6 @@ Function InstModePage_Create FunctionEnd Function DisableBackButtonIfUACInnerInstance - ${LogSet} on Push $0 ${If} ${UAC_IsInnerInstance} GetDlgItem $0 $HWNDParent 3 @@ -545,7 +571,6 @@ Function DisableBackButtonIfUACInnerInstance FunctionEnd Function RemoveNextBtnShield - ${LogSet} on Push $0 GetDlgItem $0 $HWNDParent 1 SendMessage $0 ${BCM_SETSHIELD} 0 0 @@ -553,7 +578,6 @@ Function RemoveNextBtnShield FunctionEnd Function InstModeChanged - ${LogSet} on # When using the installer with /S (silent mode), the /D option sets $INSTDIR, # and it is therefore important not to overwrite $INSTDIR here, but it is also # important that we do call SetShellVarContext with the appropriate value. @@ -584,7 +608,6 @@ FunctionEnd !macroend Function InstModePage_Leave - ${LogSet} on Push $0 Push $1 Push $2 @@ -605,7 +628,6 @@ Function InstModePage_Leave FunctionEnd Function .onInit - ${LogSet} on Push $0 Push $1 Push $2 @@ -952,7 +974,6 @@ FunctionEnd # http://nsis.sourceforge.net/Check_for_spaces_in_a_directory_path Function CheckForSpaces - ${LogSet} on Exch $R0 Push $R1 Push $R2 @@ -976,7 +997,6 @@ FunctionEnd # http://nsis.sourceforge.net/StrCSpn,_StrCSpnReverse:_Scan_strings_for_characters Function StrCSpn - ${LogSet} on Exch $R0 ; string to check Exch Exch $R1 ; string of chars @@ -1036,7 +1056,6 @@ Pop $0 Function OnDirectoryLeave - ${LogSet} on ${If} ${IsNonEmptyDirectory} "$InstDir" ${Print} "::error:: Directory '$INSTDIR' is not empty, please choose a different location." MessageBox MB_OK|MB_ICONEXCLAMATION \ @@ -1191,7 +1210,6 @@ Function OnDirectoryLeave FunctionEnd Function .onVerifyInstDir - ${LogSet} on StrLen $0 $Desktop StrCpy $0 $INSTDIR $0 StrCmp $0 $Desktop 0 PathGood @@ -1210,37 +1228,85 @@ Function un.OnDirectoryLeave confirmed_yes: FunctionEnd -# Make function available for both installer and uninstaller +# Make functions available for both installer and uninstaller # Uninstaller functions need an `un.` prefix, so we use a macro to do both # see https://nsis.sourceforge.io/Sharing_functions_between_Installer_and_Uninstaller -!macro AbortRetryNSExecWaitMacro un +!macro FunctionTemplates un + Function ${un}PrintFromStepLog + Exch $R0 + Push $R1 + Push $R2 + ClearErrors + FileOpen $R1 "${STEP_LOG}" r + IfErrors close_file + read_line: + FileRead $R1 $R2 + IfErrors close_file + ${If} $R0 == "ToConsole" + ${OrIf} $R0 == "both" + ${PrintToConsole} "$R2" + ${EndIf} + ${If} $R0 == "ToLog" + ${OrIf} $R0 == "both" + ${LogText} "$R2" + ${EndIf} + goto read_line + close_file: + FileClose $R1 + Pop $R2 + Pop $R1 + Pop $R0 + FunctionEnd + Function ${un}AbortRetryNSExecWait # This function expects three arguments in the stack - # $1: 'WithLog' or 'NoLog': Use ExecToLog or just Exec, respectively - # $2: The message to show if an error occurred - # $3: The command to run, quoted + # $R1: 'WithLog' or 'NoLog': Use ExecToLog or just Exec, respectively + # $R2: The message to show if an error occurred + # $R3: The command to run, quoted # Note that the args need to be pushed to the stack in reverse order! # Search 'AbortRetryNSExecWait' in this script to see examples - ${LogSet} on - Pop $1 - Pop $2 - Pop $3 + Pop $R1 + Pop $R2 + Pop $R3 ${Do} - ${If} $1 == "WithLog" - nsExec::ExecToLog $3 - ${ElseIf} $1 == "NoLog" - nsExec::Exec $3 + # Execute command inside a subshell to catch issues with command execution. + # When binaries are executed with Exec or ExectToLog, only the output of these + # commands are logged. If they do not start successfully (e.g., if they don't exist), + # no error messages are returned. Using a subshell reveals these kinds of issues. + # Enable delayed expansion so that error levels are parsed at runtime instead of + # parse time. + StrCpy $R3 '"$CMD_EXE" /V:ON /D /C "$R3"' + ${If} $R1 == "WithLog" + nsExec::ExecToLog $R3 + ${ElseIf} $R1 == "NoLog" + nsExec::Exec $R3 ${Else} - ${Print} "::error:: AbortRetryNSExecWait: 1st argument must be 'WithLog' or 'NoLog'. You used: $1" + ${Print} "::error:: AbortRetryNSExecWait: 1st argument must be 'WithLog' or 'NoLog'. You used: $R1" Abort ${EndIf} - pop $0 - ${If} $0 != "0" - ${Print} "::error:: $2" - MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON3 \ - $2 /SD IDIGNORE IDABORT abort IDRETRY retry + pop $R0 + ${If} $R1 == "WithLog" + ${AndIf} ${FileExists} "${STEP_LOG}" + ${If} ${Silent} + push "both" + Call ${un}PrintFromStepLog + ${Else} + push "ToLog" + Call ${un}PrintFromStepLog + ${EndIf} + ${EndIf} + ${If} $R0 != "0" + # Always print on error + ${If} $R1 == "NoLog" + ${AndIf} ${FileExists} "${STEP_LOG}" + Push "both" + Call ${un}PrintFromStepLog + ${EndIf} + ${Print} "::error:: $R2" + MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \ + $R2 /SD IDABORT IDABORT abort IDRETRY retry ; IDIGNORE: Continue anyway - StrCpy $0 "0" + StrCpy $R0 "0" goto retry abort: ; Abort installation @@ -1248,11 +1314,14 @@ FunctionEnd retry: ; Retry the nsExec command ${EndIf} - ${LoopWhile} $0 != "0" + ${If} ${FileExists} "${STEP_LOG}" + Delete "${STEP_LOG}" + ${EndIf} + ${LoopWhile} $R0 != "0" FunctionEnd !macroend -!insertmacro AbortRetryNSExecWaitMacro "" -!insertmacro AbortRetryNSExecWaitMacro "un." +!insertmacro FunctionTemplates "" +!insertmacro FunctionTemplates "un." {%- set pathname = "$INSTDIR\\condabin" if initialize_conda == "condabin" else "$INSTDIR\\Scripts & Library\\bin" %} !macro AddRemovePath add_remove un @@ -1272,11 +1341,8 @@ FunctionEnd StrCpy $R0 "rmpath" StrCpy $R1 "Failed to remove {{ NAME }} from PATH" ${EndIf} - ${If} ${Silent} - push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" $R0' - ${Else} - push '"$INSTDIR\python.exe" -E -s "$INSTDIR\Lib\_nsis.py" $R0' - ${EndIf} + # `type` is used to simulate a `tee`-like output in cmd.exe + push '"$INSTDIR\python.exe" -E -s "$INSTDIR\Lib\_nsis.py" $R0 > "${STEP_LOG}" 2>&1 & SET ERR=!ERRORLEVEL! & type "${STEP_LOG}" & EXIT /B !ERR!' push $R1 push 'WithLog' call ${un}AbortRetryNSExecWait @@ -1291,7 +1357,7 @@ FunctionEnd StrCpy $R0 "remove" StrCpy $R1 'Failed to remove {{ NAME }} from PATH' ${EndIf} - push '"$INSTDIR\_conda.exe" constructor windows path --$R0=user --prefix "$INSTDIR" {{ pathflag }}' + push '"$INSTDIR\_conda.exe" constructor windows path --$R0=user --prefix "$INSTDIR" {{ pathflag }} {{ CONDA_LOG_ARG }}' push $R1 push 'WithLog' call ${un}AbortRetryNSExecWait @@ -1319,6 +1385,7 @@ FunctionEnd "Unable to determine the defaults permissions of the installation directory. "\ "Ensure that you have read access to $INSTDIR and icacls.exe is in your PATH." ${Print} $R1 + MessageBox MB_ICONSTOP $R1 Abort ${EndIf} StrCpy $0 "" @@ -1372,7 +1439,6 @@ FunctionEnd # Installer sections Section "Install" - ${LogSet} on ${If} ${Silent} call OnDirectoryLeave ${EndIf} @@ -1380,6 +1446,7 @@ Section "Install" !insertmacro FindWindowsBinaries SetOutPath "$INSTDIR" + ${LogSet} on # 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. @@ -1491,7 +1558,7 @@ Section "Install" 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 '"$INSTDIR\_conda.exe" create --dry-run --prefix "$INSTDIR\envs\_virtual_specs_checks" --offline {{ VIRTUAL_SPECS }} {{ NO_RCS_ARG }} {{ CONDA_LOG_ARG }}' push 'Failed to check virtual specs: {{ VIRTUAL_SPECS_DEBUG }}' push 'WithLog' call AbortRetryNSExecWait @@ -1503,20 +1570,17 @@ Section "Install" File {{ dist }} {%- endfor %} - SetDetailsPrint TextOnly ${Print} "Setting up the package cache..." - push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR" --extract-conda-pkgs' + push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR" --extract-conda-pkgs {{ CONDA_LOG_ARG }}' push 'Failed to extract packages' - push 'NoLog' - # We use NoLog here because TQDM progress bars are parsed as a single line in NSIS 3.08 - # These can crash the installer if they get too long (a few packages is enough!) + push 'WithLog' call AbortRetryNSExecWait - SetDetailsPrint both IfFileExists "$INSTDIR\pkgs\pre_install.bat" 0 NoPreInstall - ${Print} "Running pre_install scripts..." - push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_install.bat"' - push "Failed to run pre_install" + ${Print} "Running pre-install script..." + # `type` is used to simulate a `tee`-like output in cmd.exe + push '"$INSTDIR\pkgs\pre_install.bat" > "${STEP_LOG}" 2>&1 & SET ERR=!ERRORLEVEL! & type "${STEP_LOG}" & EXIT /B !ERR!' + push "Failed to run pre-install script." push 'WithLog' call AbortRetryNSExecWait NoPreInstall: @@ -1543,10 +1607,10 @@ Section "Install" # 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.lockfile_txt }}" {{ env.shortcuts }} {{ env.no_rcs_arg }}' + push '"$INSTDIR\_conda.exe" install --offline -yp "{{ env.prefix }}" --file "{{ env.lockfile_txt }}" {{ env.shortcuts }} {{ env.no_rcs_arg }} {{ CONDA_LOG_ARG }}' ${Else} ${Print} "Installing packages for {{ env.name }}..." - push '"$INSTDIR\_conda.exe" install --offline -yp "{{ env.prefix }}" --file "{{ env.lockfile_txt }}" --no-shortcuts {{ env.no_rcs_arg }}' + push '"$INSTDIR\_conda.exe" install --offline -yp "{{ env.prefix }}" --file "{{ env.lockfile_txt }}" --no-shortcuts {{ env.no_rcs_arg }} {{ CONDA_LOG_ARG }}' ${EndIf} push 'Failed to link extracted packages to {{ env.prefix }}!' push 'WithLog' @@ -1575,9 +1639,10 @@ Section "Install" ${If} ${FileExists} "$INSTDIR\pkgs\post_install.bat" ${If} $Ana_PostInstall_State = ${BST_CHECKED} - ${Print} "Running post install..." - push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\post_install.bat"' - push "Failed to run post_install" + ${Print} "Running post-install..." + # `type` is used to simulate a `tee`-like output in cmd.exe + push '"$INSTDIR\pkgs\post_install.bat" > "${STEP_LOG}" 2>&1 & SET ERR=!ERRORLEVEL! & type "${STEP_LOG}" & call exit !ERR!' + push "Failed to run post-install script." push 'WithLog' call AbortRetryNSExecWait ${EndIf} @@ -1585,7 +1650,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 }} {{ CONDA_LOG_ARG }}' push 'Failed to clear package cache' push 'WithLog' call AbortRetryNSExecWait @@ -1641,9 +1706,10 @@ Section "Install" # Enable inheritance on all files inside $INSTDIR. # Use icacls because it is much faster than custom NSIS solutions. # We continue on error because icacls fails on broken links. - push '"$ICACLS_EXE" "$INSTDIR\*" /inheritance:e /T /C /Q' + # `type` is used to simulate a `tee`-like output in cmd.exe + push '"$ICACLS_EXE" "$INSTDIR\*" /inheritance:e /T /C /Q > "${STEP_LOG}" 2>&1 & SET ERR=!ERRORLEVEL! & type "${STEP_LOG}" & EXIT /B !ERR!' push 'Failed to enable inheritance for all files in the installation directory.' - push 'NoLog' + push 'WithLog' call AbortRetryNSExecWait ${Print} "Done!" SectionEnd @@ -1672,6 +1738,9 @@ Section "Uninstall" # For long installation times, this may cause a buffer overflow, crashing the installer. System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1")".r0' + # Remove registry entries first because they are difficult to clean + # up manually if the uninstallation irrecoverably fails. + # Read variables the uninstaller needs from the registry StrCpy $R0 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" StrLen $R1 "Uninstall-${NAME}.exe" @@ -1694,6 +1763,27 @@ Section "Uninstall" goto loop_path endloop_path: + ${If} $INSTALLER_NAME_FULL != "" + DeleteRegKey SHCTX "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$INSTALLER_NAME_FULL" + ${EndIf} + + # If Anaconda was registered as the official Python for this version, + # remove it from the registry + StrCpy $R0 "SOFTWARE\Python\PythonCore" + StrCpy $0 0 + loop_py: + EnumRegKey $1 SHCTX $R0 $0 + StrCmp $1 "" endloop_py + ReadRegStr $2 SHCTX "$R0\$1\InstallPath" "" + ${If} $2 == $INSTDIR + StrCpy $R1 $1 + DeleteRegKey SHCTX "$R0\$1" + goto endloop_py + ${EndIf} + IntOp $0 $0 + 1 + goto loop_py + endloop_py: + # Extra info for pre_uninstall scripts System::Call 'kernel32::SetEnvironmentVariable(t,t)i("PREFIX", "$INSTDIR").r0' System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_NAME", "${NAME}").r0' @@ -1711,9 +1801,10 @@ Section "Uninstall" ${EndIf} ${If} ${FileExists} "$INSTDIR\pkgs\pre_uninstall.bat" - ${Print} "Running pre_uninstall scripts..." - push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_uninstall.bat"' - push "Failed to run pre_uninstall" + ${Print} "Running pre-uninstall script..." + # `type` is used to simulate a `tee`-like output in cmd.exe + push '"$INSTDIR\pkgs\pre_uninstall.bat" > "${STEP_LOG}" 2>&1 & SET ERR=!ERRORLEVEL! & type "${STEP_LOG}" & EXIT /B !ERR!' + push "Failed to run pre-uninstall scripts." push 'WithLog' call un.AbortRetryNSExecWait ${EndIf} @@ -1744,7 +1835,7 @@ Section "Uninstall" ${EndIf} ${Print} "Removing files and folders..." - push '"$INSTDIR\_conda.exe" constructor uninstall $R0 --prefix "$INSTDIR"' + push '"$INSTDIR\_conda.exe" constructor uninstall $R0 --prefix "$INSTDIR" {{ CONDA_LOG_ARG }}' push 'Failed to remove files and folders. Please see the log for more information.' push 'WithLog' SetDetailsPrint listonly @@ -1754,6 +1845,9 @@ Section "Uninstall" # The uninstallation may leave the install.log, the uninstaller, # and .conda_trash files behind, so remove those manually. ${If} ${FileExists} "$INSTDIR" + # Stop logging or the uninstaller will not remove the install.log file + # without requiring a reboot + LogSet Off RMDir /r /REBOOTOK "$INSTDIR" ${EndIf} {%- else %} @@ -1762,7 +1856,7 @@ Section "Uninstall" SetDetailsPrint both ${Print} "Deleting ${NAME} menus in {{ env.name }}..." SetDetailsPrint listonly - push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR{{ subdir }}" --rm-menus' + push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR{{ subdir }}" --rm-menus {{ CONDA_LOG_ARG }}' push 'Failed to delete menus in {{ env.name }}' push 'WithLog' call un.AbortRetryNSExecWait @@ -1774,11 +1868,8 @@ Section "Uninstall" ${Else} StrCpy $R0 "system" ${EndIf} - # When running conda.bat directly, there is a non-fatal error - # that DOSKEY (called by conda_hook.bat) is not a valid command. - # While the operation still succeeds, this error is confusing. - # Calling via cmd.exe fixes that. - push '"$CMD_EXE" /D /C "$INSTDIR\condabin\conda.bat" init cmd.exe --reverse --$R0' + # `type` is used to simulate a `tee`-like output in cmd.exe + push '"$INSTDIR\condabin\conda.bat" init cmd.exe --reverse --$R0 > "${STEP_LOG}" 2>&1 & SET ERR=!ERRORLEVEL! & type "${STEP_LOG}" & EXIT /B !ERR!' push 'Failed to clean AutoRun' push 'WithLog' call un.AbortRetryNSExecWait @@ -1787,32 +1878,14 @@ Section "Uninstall" ${Print} "Removing files and folders..." nsExec::Exec 'cmd.exe /D /C RMDIR /Q /S "$INSTDIR"' + # Stop logging or the uninstaller will not remove the install.log file + # without requiring a reboot + LogSet Off # 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} - - # If Anaconda was registered as the official Python for this version, - # remove it from the registry - StrCpy $R0 "SOFTWARE\Python\PythonCore" - StrCpy $0 0 - loop_py: - EnumRegKey $1 SHCTX $R0 $0 - StrCmp $1 "" endloop_py - ReadRegStr $2 SHCTX "$R0\$1\InstallPath" "" - ${If} $2 == $INSTDIR - StrCpy $R1 $1 - DeleteRegKey SHCTX "$R0\$1" - goto endloop_py - ${EndIf} - IntOp $0 $0 + 1 - goto loop_py - endloop_py: - ${Print} "Done!" ${If} ${Silent} # give it some time so users can read the last lines diff --git a/constructor/winexe.py b/constructor/winexe.py index d225182af..cd1cf05aa 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -261,6 +261,9 @@ def make_nsi( approx_pkgs_size_kb = approx_size_kb(info, "pkgs") # UPPERCASE variables are unescaped (and unquoted) + variables["CONDA_LOG_ARG"] = ( + '--log-file "${STEP_LOG}"' if info.get("_conda_exe_supports_logging") else "" + ) variables["NAME"] = name variables["NSIS_DIR"] = NSIS_DIR variables["BITS"] = str(arch) diff --git a/news/1108-improve-logging b/news/1108-improve-logging new file mode 100644 index 000000000..265f2a1ce --- /dev/null +++ b/news/1108-improve-logging @@ -0,0 +1,23 @@ +### Enhancements + +* Improve logging experience for EXE installers: (#1108) + - Use `cmd.exe` to run commands so that outputs are captured. + - Output command output in CLI installations. + - Prevent log builds from writing to log before installation directory exists. + - Remove registry entries while installation directory still exists so that errors are logged. + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/recipe/meta.yaml b/recipe/meta.yaml index c5876d0cd..f9334ccb7 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -25,7 +25,7 @@ requirements: - conda >=4.6 - python # >=3.8 - ruamel.yaml >=0.11.14,<0.19 - - conda-standalone + - conda-standalone >=24.1.2 - jinja2 - jsonschema >=4 - pillow >=3.1 # [win or osx] diff --git a/tests/test_examples.py b/tests/test_examples.py index 3fc38104f..b0d976def 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -210,7 +210,7 @@ def _run_uninstaller_exe( f"_?={install_dir}", ] process = _execute(cmd, timeout=timeout, check=check) - if check: + if check and Path(install_dir, "install.log").exists(): _check_installer_log(install_dir) remaining_files = list(install_dir.iterdir()) if len(remaining_files) > 3: From 6f7d526775aa67c66089cc18d346fa1be489c0d0 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:05:03 -0500 Subject: [PATCH 38/54] Fix issue with GetOptions resetting variable and account for KeepPkgCache (#1132) * Fix unset variable and introduce an additional variable * docstring * Try adding tests for NoRegistry * Update how data is extracted for tests * Adjust test * Parametrize test * Update helper function to return bools as intended * Add news * Add PR number and comments * Update the test to use installer instead of manual yaml parsing * pre-commit fix * Fix wrong index after update * Update constructor/nsis/main.nsi.tmpl Co-authored-by: Marco Esters --------- Co-authored-by: Marco Esters --- constructor/nsis/main.nsi.tmpl | 23 ++++++++-- news/1132-fix-cli-args | 19 ++++++++ tests/test_examples.py | 83 +++++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 news/1132-fix-cli-args diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 2df9313e4..762ffe667 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -134,6 +134,8 @@ ${Using:StrFunc} StrStr var /global INIT_CONDA var /global REG_PY +var /global NO_REGISTRY + var /global INSTDIR_JUSTME var /global INSTALLER_VERSION var /global INSTALLER_NAME_FULL @@ -291,8 +293,7 @@ FunctionEnd Function InitializeVariables StrCpy $CheckPathLength "{{ 1 if check_path_length else 0 }}" - StrCpy $ARGV_NoRegistry "0" - StrCpy $ARGV_KeepPkgCache "{{ 1 if keep_pkgs else 0 }}" + StrCpy $NO_REGISTRY "0" # Package cache option StrCpy $Ana_ClearPkgCache_State {{ '${BST_UNCHECKED}' if keep_pkgs else '${BST_CHECKED}' }} @@ -454,9 +455,23 @@ FunctionEnd ClearErrors ${GetOptions} $ARGV "/KeepPkgCache=" $ARGV_KeepPkgCache + ${IfNot} ${Errors} + ${If} $ARGV_KeepPkgCache = "1" + StrCpy $Ana_ClearPkgCache_State ${BST_UNCHECKED} + ${ElseIf} $ARGV_KeepPkgCache = "0" + StrCpy $Ana_ClearPkgCache_State ${BST_CHECKED} + ${EndIf} + ${EndIf} ClearErrors ${GetOptions} $ARGV "/NoRegistry=" $ARGV_NoRegistry + ${IfNot} ${Errors} + ${If} $ARGV_NoRegistry = "1" + StrCpy $NO_REGISTRY "1" + ${ElseIf} $ARGV_NoRegistry = "0" + StrCpy $NO_REGISTRY "0" + ${EndIf} + ${EndIf} ClearErrors ${GetOptions} $ARGV "/NoScripts=" $ARGV_NoScripts @@ -1074,7 +1089,7 @@ Function OnDirectoryLeave ; With windows 10, we can enable support for long path, for earlier ; version, suggest user to use shorter installation path ${If} ${AtLeastWin10} - ${AndIfNot} $ARGV_NoRegistry = "1" + ${AndIfNot} $NO_REGISTRY = "1" ; If we have admin right, we enable long path on windows ${If} ${UAC_IsAdmin} WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1 @@ -1684,7 +1699,7 @@ Section "Install" ${EndIf} {%- endif %} - ${If} $ARGV_NoRegistry == "0" + ${If} $NO_REGISTRY == "0" # Registry uninstall info WriteRegStr SHCTX "${UNINSTREG}" "DisplayName" "${UNINSTALL_NAME}" WriteRegStr SHCTX "${UNINSTREG}" "DisplayVersion" "${VERSION}" diff --git a/news/1132-fix-cli-args b/news/1132-fix-cli-args new file mode 100644 index 000000000..9aae82d89 --- /dev/null +++ b/news/1132-fix-cli-args @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* EXE: Fixed an issue for silent installers where command-line argument `/KeepPkgCache` was ignored and `/NoRegistry` would reset the default value. (#1132) + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/test_examples.py b/tests/test_examples.py index b0d976def..2561d9c44 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -34,6 +34,8 @@ if sys.platform == "darwin": from constructor.osxpkg import calculate_install_dir elif sys.platform.startswith("win"): + import winreg + import ntsecuritycon as con import win32security @@ -60,6 +62,49 @@ KEEP_ARTIFACTS_PATH = None +def _is_program_installed(partial_name: str) -> bool: + """ + Checks if a program is listed in the Windows 'Installed apps' menu. + We search by looking for a partial name to avoid having to account for Python version and arch. + Returns True if a match is found, otherwise False. + """ + + if not sys.platform.startswith("win"): + return False + + # For its current purpose HKEY_CURRENT_USER is sufficient, + # but additional registry locations could be added later. + UNINSTALL_PATHS = [ + (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), + ] + + partial_name = partial_name.lower() + + for hive, path in UNINSTALL_PATHS: + try: + reg_key = winreg.OpenKey(hive, path) + except FileNotFoundError: + continue + + subkey_count = winreg.QueryInfoKey(reg_key)[0] + + for i in range(subkey_count): + try: + subkey_name = winreg.EnumKey(reg_key, i) + subkey = winreg.OpenKey(reg_key, subkey_name) + + display_name, _ = winreg.QueryValueEx(subkey, "DisplayName") + + if partial_name in display_name.lower(): + return True + + except (FileNotFoundError, OSError, TypeError): + # Some keys may lack DisplayName or have unexpected value types + continue + + return False + + def _execute( cmd: Iterable[str], installer_input=None, check=True, timeout=420, **env_vars ) -> subprocess.CompletedProcess: @@ -1038,8 +1083,6 @@ def test_initialization(tmp_path, request, monkeypatch, method): ) if installer.suffix == ".exe": try: - import winreg - paths = [] for root, keyname in ( (winreg.HKEY_CURRENT_USER, r"Environment"), @@ -1423,3 +1466,39 @@ def test_regressions(tmp_path, request): check_subprocess=True, uninstall=True, ) + + +@pytest.mark.parametrize("no_registry", (0, 1)) +@pytest.mark.skipif(not ON_CI, reason="CI only") +@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") +def test_not_in_installed_menu_list_(tmp_path, request, no_registry): + """Verify the app is in the Installed Apps Menu (or not), based on the CLI arg '/NoRegistry'. + If NoRegistry=0, we expect to find the installer in the Menu, otherwise not. + """ + input_path = _example_path("extra_files") # The specific example we use here is not important + options = ["/InstallationType=JustMe", f"/NoRegistry={no_registry}"] + for installer, install_dir in create_installer(input_path, tmp_path): + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=False, + options=options, + ) + + # Use the installer file name for the registry search + installer_file_name_parts = Path(installer).name.split("-") + name = installer_file_name_parts[0] + version = installer_file_name_parts[1] + partial_name = f"{name} {version}" + + is_in_installed_apps_menu = _is_program_installed(partial_name) + _run_uninstaller_exe(install_dir) + + # If no_registry=0 we expect is_in_installed_apps_menu=True + # If no_registry=1 we expect is_in_installed_apps_menu=False + assert is_in_installed_apps_menu == (no_registry == 0), ( + f"Unable to find program '{partial_name}' in the 'Installed apps' menu" + ) From 576212e70e69f8c5f3c51207239c9ebd12a11347 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:40:10 -0500 Subject: [PATCH 39/54] Prepare 3.14.2 (#1134) * Updated authorship for 3.14.2 * Updated CHANGELOG for 3.14.2 * Remove bots --- .authors.yml | 8 ++++---- CHANGELOG.md | 20 ++++++++++++++++++++ news/1108-improve-logging | 23 ----------------------- news/1132-fix-cli-args | 19 ------------------- 4 files changed, 24 insertions(+), 46 deletions(-) delete mode 100644 news/1108-improve-logging delete mode 100644 news/1132-fix-cli-args diff --git a/.authors.yml b/.authors.yml index 077e14206..b1a8d3aef 100644 --- a/.authors.yml +++ b/.authors.yml @@ -357,7 +357,7 @@ github: RahulARanger - name: Marco Esters email: mesters@anaconda.com - num_commits: 59 + num_commits: 60 first_commit: 2023-05-12 11:44:12 github: marcoesters - name: Darryl Miles @@ -372,7 +372,7 @@ github: deepeshaburse - name: pre-commit-ci[bot] email: 66853113+pre-commit-ci[bot]@users.noreply.github.com - num_commits: 70 + num_commits: 71 first_commit: 2023-05-02 12:01:43 github: pre-commit-ci[bot] - name: Matthias Kuhn @@ -382,7 +382,7 @@ github: m-kuhn - name: dependabot[bot] email: 49699333+dependabot[bot]@users.noreply.github.com - num_commits: 56 + num_commits: 59 github: dependabot[bot] first_commit: 2024-05-07 10:16:05 - name: Julien Jerphanion @@ -417,5 +417,5 @@ - Robin github: lrandersson email: 34315751+lrandersson@users.noreply.github.com - num_commits: 9 + num_commits: 11 first_commit: 2025-10-21 08:30:00 diff --git a/CHANGELOG.md b/CHANGELOG.md index 404568bfd..65bb2bd58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ [//]: # (current developments) +## 2025-12-10 3.14.2: +### Enhancements + +* Improve logging experience for EXE installers: (#1108) + - Use `cmd.exe` to run commands so that outputs are captured. + - Output command output in CLI installations. + - Prevent log builds from writing to log before installation directory exists. + - Remove registry entries while installation directory still exists so that errors are logged. + +### Bug fixes + +* EXE: Fixed an issue for silent installers where command-line argument `/KeepPkgCache` was ignored and `/NoRegistry` would reset the default value. (#1132) + +### Contributors + +* @marcoesters +* @lrandersson + + + ## 2025-12-08 3.14.1: ### Bug fixes diff --git a/news/1108-improve-logging b/news/1108-improve-logging deleted file mode 100644 index 265f2a1ce..000000000 --- a/news/1108-improve-logging +++ /dev/null @@ -1,23 +0,0 @@ -### Enhancements - -* Improve logging experience for EXE installers: (#1108) - - Use `cmd.exe` to run commands so that outputs are captured. - - Output command output in CLI installations. - - Prevent log builds from writing to log before installation directory exists. - - Remove registry entries while installation directory still exists so that errors are logged. - -### Bug fixes - -* - -### Deprecations - -* - -### Docs - -* - -### Other - -* diff --git a/news/1132-fix-cli-args b/news/1132-fix-cli-args deleted file mode 100644 index 9aae82d89..000000000 --- a/news/1132-fix-cli-args +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* EXE: Fixed an issue for silent installers where command-line argument `/KeepPkgCache` was ignored and `/NoRegistry` would reset the default value. (#1132) - -### Deprecations - -* - -### Docs - -* - -### Other - -* From a707971caae9801c917646df67e4fac2ad2a92fd Mon Sep 17 00:00:00 2001 From: David Laehnemann <1379875+dlaehnemann@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:37:06 +0100 Subject: [PATCH 40/54] fix: symbolic linking (`ln -s`) with pre-existing installation (#1135) * fix: symbolic linking (`ln -s`) failure with pre-existing installation * fix: symbolic linking (`ln -s`) failure with pre-existing installation, also for osx prepare_installation.sh script * docs: add news file * Rename fix-symbolic-linking to 1135-fix-symbolic-linking * Apply suggestion from @marcoesters --------- Co-authored-by: Marco Esters --- constructor/header.sh | 2 +- constructor/osx/prepare_installation.sh | 2 +- news/1135-fix-symbolic-linking | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 news/1135-fix-symbolic-linking diff --git a/constructor/header.sh b/constructor/header.sh index c8f7609a3..e560b5be4 100644 --- a/constructor/header.sh +++ b/constructor/header.sh @@ -501,7 +501,7 @@ chmod +x "$CONDA_EXEC" {%- if conda_exe_name != "_conda" %} # In case there are packages that depend on _conda -ln -s "$CONDA_EXEC" "$PREFIX"/_conda +ln -s -f "$CONDA_EXEC" "$PREFIX"/_conda {%- endif %} {%- for filename, (start, end, executable) in conda_exe_payloads|items %} diff --git a/constructor/osx/prepare_installation.sh b/constructor/osx/prepare_installation.sh index eb83f37e5..0e974c83f 100644 --- a/constructor/osx/prepare_installation.sh +++ b/constructor/osx/prepare_installation.sh @@ -32,7 +32,7 @@ chmod +x "$CONDA_EXEC" {%- if conda_exe_name != "_conda" %} # In case there are packages that depend on _conda -ln -s "$CONDA_EXEC" "$PREFIX"/_conda +ln -s -f "$CONDA_EXEC" "$PREFIX"/_conda {%- endif %} # Create a blank history file so conda thinks this is an existing env diff --git a/news/1135-fix-symbolic-linking b/news/1135-fix-symbolic-linking new file mode 100644 index 000000000..d771b3103 --- /dev/null +++ b/news/1135-fix-symbolic-linking @@ -0,0 +1,3 @@ +### Bug fixes + +* Force symbolic linking of `_conda` in SH and PKG installers. This fixes a regression introduced by #1090 that made installations fail if `_conda` already exists in the target location. (#1135) From dc91fd3a6882b85497844428d4496cdd0a66f57e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:37:22 -0800 Subject: [PATCH 41/54] [pre-commit.ci] pre-commit autoupdate (#1140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.8 → v0.14.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.8...v0.14.9) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ace2f202..43fda497f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.8 + rev: v0.14.9 hooks: # Run the linter. - id: ruff From 89d26ddc5afddd2453069429e147edd98e1b282f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:37:48 -0800 Subject: [PATCH 42/54] Bump actions/upload-artifact from 5.0.0 to 6.0.0 in /.github/workflows (#1139) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5.0.0 to 6.0.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/330a01c490aca151604b8cf639adc76d48f6c5d4...b7c566a772e6b6bfb58ed0dc250532a479d7789f) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0290f4474..f06e48e51 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -169,7 +169,7 @@ jobs: git diff --exit-code - name: Upload the example installers as artifacts if: github.event_name == 'pull_request' && matrix.python-version == '3.9' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: installers-${{ runner.os }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} path: "${{ runner.temp }}/examples_artifacts" From b0f8736025cbf5ef1f3bdfc81c1b719e24fe839f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:38:30 -0800 Subject: [PATCH 43/54] Bump dessant/lock-threads from 5.0.1 to 6.0.0 in /.github/workflows (#1138) Bumps [dessant/lock-threads](https://github.com/dessant/lock-threads) from 5.0.1 to 6.0.0. - [Release notes](https://github.com/dessant/lock-threads/releases) - [Changelog](https://github.com/dessant/lock-threads/blob/main/CHANGELOG.md) - [Commits](https://github.com/dessant/lock-threads/compare/1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771...7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7) --- updated-dependencies: - dependency-name: dessant/lock-threads dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 65f8f40b3..a5bbc9331 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -17,7 +17,7 @@ jobs: if: '!github.event.repository.fork' runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 + - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 with: # Number of days of inactivity before a closed issue is locked issue-inactive-days: 180 From 3cc759aa18118faf9908461d1994cafb0c1bdb47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:38:45 -0800 Subject: [PATCH 44/54] Bump codecov/codecov-action from 5.5.1 to 5.5.2 in /.github/workflows (#1137) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.1 to 5.5.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/5a1091511ad55cbe89839c7260b706298ca349f7...671740ac38dd9b0130fbe1cec585b89eea48d3de) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 5.5.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f06e48e51..2b1f0570c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -139,7 +139,7 @@ jobs: pytest -vv --cov=constructor --cov-branch tests/ -m "not examples" coverage run --branch --append -m constructor -V coverage json - - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} flags: unit @@ -157,7 +157,7 @@ jobs: pytest -vv --cov=constructor --cov-branch tests/test_examples.py coverage run --branch --append -m constructor -V coverage json - - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} flags: integration From 4ce7589e6c2027a7d7185b71a5708c398607f234 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:39:48 -0800 Subject: [PATCH 45/54] Bump peter-evans/create-pull-request in /.github/workflows (#1136) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.11 to 8.0.0. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/22a9089034f40e5a961c8808d113e2c98fb63676...98357b18bf14b5342f975ff684046ec3b2a07725) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index d1407eb58..3ee42ebc0 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -80,7 +80,7 @@ jobs: - if: github.event.comment.body != '@conda-bot render' id: create # no-op if no commits were made - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 with: push-to-fork: ${{ env.FORK }} token: ${{ secrets.SYNC_TOKEN }} From e4adc7bc6ee151f65d14538ea9eaef120f006e53 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Mon, 15 Dec 2025 16:27:47 -0500 Subject: [PATCH 46/54] Change LogSet to macro call instead (#1141) * Change LogSet to macro call instead * Add news --- constructor/nsis/main.nsi.tmpl | 4 ++-- news/1141-logset-off | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 news/1141-logset-off diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 762ffe667..3ebfe1a07 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -1862,7 +1862,7 @@ Section "Uninstall" ${If} ${FileExists} "$INSTDIR" # Stop logging or the uninstaller will not remove the install.log file # without requiring a reboot - LogSet Off + ${LogSet} off RMDir /r /REBOOTOK "$INSTDIR" ${EndIf} {%- else %} @@ -1895,7 +1895,7 @@ Section "Uninstall" # Stop logging or the uninstaller will not remove the install.log file # without requiring a reboot - LogSet Off + ${LogSet} off # In case the last command fails, run the slow method to remove leftover RMDir /r /REBOOTOK "$INSTDIR" diff --git a/news/1141-logset-off b/news/1141-logset-off new file mode 100644 index 000000000..d3bfea1b6 --- /dev/null +++ b/news/1141-logset-off @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* EXE: Update calls to built-in function `LogSet` to instead call the intended macro definition `${LogSet}$`. (#1141) + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 97dbba0fc910ee103e84ffc626c63c66884dd79b Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:33:38 -0500 Subject: [PATCH 47/54] Prepare 3.14.3 (#1142) * Updated authorship for 3.14.3 * Updated CHANGELOG for 3.14.3 * Remove bots * Apply suggestions from code review --------- Co-authored-by: Marco Esters --- .authors.yml | 11 ++++++++--- .mailmap | 1 + AUTHORS.md | 1 + CHANGELOG.md | 13 +++++++++++++ news/1135-fix-symbolic-linking | 3 --- news/1141-logset-off | 19 ------------------- 6 files changed, 23 insertions(+), 25 deletions(-) delete mode 100644 news/1135-fix-symbolic-linking delete mode 100644 news/1141-logset-off diff --git a/.authors.yml b/.authors.yml index b1a8d3aef..dba9e7267 100644 --- a/.authors.yml +++ b/.authors.yml @@ -372,7 +372,7 @@ github: deepeshaburse - name: pre-commit-ci[bot] email: 66853113+pre-commit-ci[bot]@users.noreply.github.com - num_commits: 71 + num_commits: 72 first_commit: 2023-05-02 12:01:43 github: pre-commit-ci[bot] - name: Matthias Kuhn @@ -382,7 +382,7 @@ github: m-kuhn - name: dependabot[bot] email: 49699333+dependabot[bot]@users.noreply.github.com - num_commits: 59 + num_commits: 63 github: dependabot[bot] first_commit: 2024-05-07 10:16:05 - name: Julien Jerphanion @@ -417,5 +417,10 @@ - Robin github: lrandersson email: 34315751+lrandersson@users.noreply.github.com - num_commits: 11 + num_commits: 13 first_commit: 2025-10-21 08:30:00 +- name: David Laehnemann + github: dlaehnemann + email: 1379875+dlaehnemann@users.noreply.github.com + num_commits: 1 + first_commit: 2025-12-15 15:37:06 diff --git a/.mailmap b/.mailmap index d6e9fb67b..bccc50941 100644 --- a/.mailmap +++ b/.mailmap @@ -19,6 +19,7 @@ Chris Burr Chris Burr Daniel Bast <2790401+dbast@users.noreply.github.com> Darryl Miles +David Laehnemann <1379875+dlaehnemann@users.noreply.github.com> Deepesha Burse <87636253+deepeshaburse@users.noreply.github.com> Eric Dill Eric Prestat diff --git a/AUTHORS.md b/AUTHORS.md index 2f475da16..77d6c831d 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -11,6 +11,7 @@ Authors are sorted alphabetically. * Connor Martin * Daniel Bast * Darryl Miles +* David Laehnemann * Deepesha Burse * Eric Dill * Eric Prestat diff --git a/CHANGELOG.md b/CHANGELOG.md index 65bb2bd58..b960d91fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ [//]: # (current developments) +## 2025-12-15 3.14.3: +### Bug fixes + +* Force symbolic linking of `_conda` in SH and PKG installers. This fixes a regression introduced by #1090 that made installations fail if `_conda` already exists in the target location. (#1135) +* EXE: Update calls to built-in function `LogSet` to instead call the intended macro definition `${LogSet}`. (#1141) + +### Contributors + +* @dlaehnemann +* @lrandersson + + + ## 2025-12-10 3.14.2: ### Enhancements diff --git a/news/1135-fix-symbolic-linking b/news/1135-fix-symbolic-linking deleted file mode 100644 index d771b3103..000000000 --- a/news/1135-fix-symbolic-linking +++ /dev/null @@ -1,3 +0,0 @@ -### Bug fixes - -* Force symbolic linking of `_conda` in SH and PKG installers. This fixes a regression introduced by #1090 that made installations fail if `_conda` already exists in the target location. (#1135) diff --git a/news/1141-logset-off b/news/1141-logset-off deleted file mode 100644 index d3bfea1b6..000000000 --- a/news/1141-logset-off +++ /dev/null @@ -1,19 +0,0 @@ -### Enhancements - -* - -### Bug fixes - -* EXE: Update calls to built-in function `LogSet` to instead call the intended macro definition `${LogSet}$`. (#1141) - -### Deprecations - -* - -### Docs - -* - -### Other - -* From 0c45f572b9c643d3fb25721cf0039ed547436648 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:56:48 -0500 Subject: [PATCH 48/54] Remove use of Python 3.9, include 3.13 in testing (#1145) * Remove use of Python 3.9, testing includes 3.13 * Create 1145-python-test-deps * More Python bumps * Removed a lot of stuff but not bird.png * pre-commit * Apply suggestion from @marcoesters --------- Co-authored-by: Marco Esters --- .github/workflows/main.yml | 26 +- constructor/_schema.py | 14 +- examples/extra_envs/construct.yaml | 10 +- examples/extra_envs/test_install.bat | 10 +- examples/extra_envs/test_install.sh | 14 +- examples/from_env_yaml/env.yaml | 2 +- examples/from_explicit/explicit_osx-arm64.txt | 19 - examples/grin/construct.yaml | 66 ---- examples/grin/eula.txt | 24 -- examples/grin/goodbye.sh | 4 - examples/grin/hello.sh | 5 - examples/grin/pkgs.txt | 17 - examples/grin/test-post.bat | 1 - examples/jetsonconda/EULA.txt | 24 -- examples/jetsonconda/README.md | 33 -- examples/jetsonconda/bird.png | Bin 12203 -> 0 bytes examples/jetsonconda/construct.yaml | 23 -- examples/miniconda/EULA.txt | 24 -- examples/miniconda/README.md | 34 -- examples/miniconda/construct.yaml | 13 - examples/newchan/construct.yaml | 20 -- examples/newchan/eula.txt | 24 -- news/1145-python-test-deps | 19 + pyproject.toml | 4 +- recipe/meta.yaml | 4 +- scripts/run_examples.py | 340 ------------------ 26 files changed, 61 insertions(+), 713 deletions(-) delete mode 100644 examples/from_explicit/explicit_osx-arm64.txt delete mode 100644 examples/grin/construct.yaml delete mode 100644 examples/grin/eula.txt delete mode 100644 examples/grin/goodbye.sh delete mode 100644 examples/grin/hello.sh delete mode 100644 examples/grin/pkgs.txt delete mode 100644 examples/grin/test-post.bat delete mode 100644 examples/jetsonconda/EULA.txt delete mode 100644 examples/jetsonconda/README.md delete mode 100644 examples/jetsonconda/bird.png delete mode 100644 examples/jetsonconda/construct.yaml delete mode 100644 examples/miniconda/EULA.txt delete mode 100644 examples/miniconda/README.md delete mode 100644 examples/miniconda/construct.yaml delete mode 100644 examples/newchan/construct.yaml delete mode 100644 examples/newchan/eula.txt create mode 100644 news/1145-python-test-deps delete mode 100644 scripts/run_examples.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2b1f0570c..31bbdef95 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,44 +40,44 @@ jobs: include: # UBUNTU - os: ubuntu-latest - python-version: "3.9" + python-version: "3.10" conda-standalone: conda-standalone - os: ubuntu-latest - python-version: "3.10" + python-version: "3.11" conda-standalone: conda-standalone-nightly - os: ubuntu-latest - python-version: "3.11" + python-version: "3.12" conda-standalone: micromamba - os: ubuntu-latest - python-version: "3.12" + python-version: "3.13" conda-standalone: conda-standalone-onedir check-docs-schema: true # MACOS - os: macos-15-intel - python-version: "3.9" + python-version: "3.10" conda-standalone: conda-standalone-nightly - os: macos-15-intel - python-version: "3.10" + python-version: "3.11" conda-standalone: conda-standalone-onedir - os: macos-latest - python-version: "3.11" + python-version: "3.12" conda-standalone: conda-standalone - os: macos-latest - python-version: "3.12" + python-version: "3.13" conda-standalone: micromamba # WINDOWS - os: windows-2022 - python-version: "3.9" + python-version: "3.10" conda-standalone: conda-standalone-nightly - os: windows-2022 - python-version: "3.10" + python-version: "3.11" conda-standalone: conda-standalone - os: windows-2022 - python-version: "3.11" + python-version: "3.12" # conda-standalone: micromamba conda-standalone: conda-standalone-nightly - os: windows-2022 - python-version: "3.12" + python-version: "3.13" # conda-standalone: micromamba conda-standalone: conda-standalone-onedir @@ -168,7 +168,7 @@ jobs: python constructor/_schema.py git diff --exit-code - name: Upload the example installers as artifacts - if: github.event_name == 'pull_request' && matrix.python-version == '3.9' + if: github.event_name == 'pull_request' && matrix.python-version == '3.10' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: installers-${{ runner.os }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} diff --git a/constructor/_schema.py b/constructor/_schema.py index d50cac46a..a8ae97514 100644 --- a/constructor/_schema.py +++ b/constructor/_schema.py @@ -188,13 +188,13 @@ class LicensesBuildOutput(BaseModel): licenses: _LicensesBuildOutputOptions -BuildOutputConfigs: TypeAlias = Union[ - HashBuildOutput, - InfoJsonBuildOutput, - PkgsListBuildOutput, - LockfileBuildOutput, - LicensesBuildOutput, -] +BuildOutputConfigs: TypeAlias = ( + HashBuildOutput + | InfoJsonBuildOutput + | PkgsListBuildOutput + | LockfileBuildOutput + | LicensesBuildOutput +) class ConstructorConfiguration(BaseModel): diff --git a/examples/extra_envs/construct.yaml b/examples/extra_envs/construct.yaml index ef09adfcd..e7370e72c 100644 --- a/examples/extra_envs/construct.yaml +++ b/examples/extra_envs/construct.yaml @@ -7,15 +7,15 @@ installer_type: all channels: - https://conda.anaconda.org/conda-forge specs: - - python=3.9 + - python=3.10 - conda # conda is required for extra_envs - miniforge_console_shortcut 1.* # [win] exclude: # [unix] - tk # [unix] extra_envs: - py310: + py311: specs: - - python=3.10 + - python=3.11 - pip channels: - conda-forge @@ -33,10 +33,10 @@ build_outputs: - info.json - pkgs_list - pkgs_list: - env: py310 + env: py311 - lockfile - lockfile: - env: py310 + env: py311 - licenses: include_text: True text_errors: replace diff --git a/examples/extra_envs/test_install.bat b/examples/extra_envs/test_install.bat index ecf8eeed7..d2d4336a5 100644 --- a/examples/extra_envs/test_install.bat +++ b/examples/extra_envs/test_install.bat @@ -1,12 +1,12 @@ echo Added by test-install script > "%PREFIX%\test_install_sentinel.txt" -:: base env has python 3.9 +:: base env has python 3.10 if not exist "%PREFIX%\conda-meta\history" exit 1 -"%PREFIX%\python.exe" -c "from sys import version_info; assert version_info[:2] == (3, 9)" || goto :error +"%PREFIX%\python.exe" -c "from sys import version_info; assert version_info[:2] == (3, 10)" || goto :error -:: extra env named 'py310' has python 3.10 -if not exist "%PREFIX%\envs\py310\conda-meta\history" exit 1 -"%PREFIX%\envs\py310\python.exe" -c "from sys import version_info; assert version_info[:2] == (3, 10)" || goto :error +:: extra env named 'py311' has python 3.11 +if not exist "%PREFIX%\envs\py311\conda-meta\history" exit 1 +"%PREFIX%\envs\py311\python.exe" -c "from sys import version_info; assert version_info[:2] == (3, 11)" || goto :error :: extra env named 'dav1d' only contains dav1d, no python if not exist "%PREFIX%\envs\dav1d\conda-meta\history" exit 1 diff --git a/examples/extra_envs/test_install.sh b/examples/extra_envs/test_install.sh index 7cfef8278..a3c84d744 100644 --- a/examples/extra_envs/test_install.sh +++ b/examples/extra_envs/test_install.sh @@ -4,9 +4,9 @@ set -euxo pipefail echo "Added by test-install script" > "$PREFIX/test_install_sentinel.txt" # tests -# base environment uses python 3.9 and excludes tk +# base environment uses python 3.10 and excludes tk test -f "$PREFIX/conda-meta/history" -"$PREFIX/bin/python" -c "from sys import version_info; assert version_info[:2] == (3, 9)" +"$PREFIX/bin/python" -c "from sys import version_info; assert version_info[:2] == (3, 10)" # we use python -m pip instead of the pip entry point # because the spaces break the shebang - this will be fixed # with a new conda release, but for now this is the workaround @@ -16,12 +16,12 @@ test -f "$PREFIX/conda-meta/history" "$PREFIX/bin/python" -m conda list -p "$PREFIX" | jq -e '.[] | select(.name == "tk")' && exit 1 echo "Previous test failed as expected" -# extra env named 'py310' uses python 3.10, has tk, but we removed setuptools -test -f "$PREFIX/envs/py310/conda-meta/history" -"$PREFIX/envs/py310/bin/python" -c "from sys import version_info; assert version_info[:2] == (3, 10)" +# extra env named 'py311' uses python 3.11, has tk, but we removed setuptools +test -f "$PREFIX/envs/py311/conda-meta/history" +"$PREFIX/envs/py311/bin/python" -c "from sys import version_info; assert version_info[:2] == (3, 11)" # setuptools shouldn't be listed by conda! -"$PREFIX/bin/python" -m conda list -p "$PREFIX/envs/py310" | jq -e '.[] | select(.name == "setuptools")' && exit 1 -"$PREFIX/envs/py310/bin/python" -c "import setuptools" && exit 1 +"$PREFIX/bin/python" -m conda list -p "$PREFIX/envs/py311" | jq -e '.[] | select(.name == "setuptools")' && exit 1 +"$PREFIX/envs/py311/bin/python" -c "import setuptools" && exit 1 echo "Previous test failed as expected" # this env only contains dav1d, no python; it should have been created with no errors, diff --git a/examples/from_env_yaml/env.yaml b/examples/from_env_yaml/env.yaml index fc6253531..d919343da 100644 --- a/examples/from_env_yaml/env.yaml +++ b/examples/from_env_yaml/env.yaml @@ -2,5 +2,5 @@ name: testenv channels: - defaults dependencies: - - python=3.9 + - python=3.10 - conda=23.3 diff --git a/examples/from_explicit/explicit_osx-arm64.txt b/examples/from_explicit/explicit_osx-arm64.txt deleted file mode 100644 index 891d899c9..000000000 --- a/examples/from_explicit/explicit_osx-arm64.txt +++ /dev/null @@ -1,19 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: osx-arm64 -@EXPLICIT -https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2021.5.30-h4653dfc_0.tar.bz2#21b35f488f8ccf40c7d3ae05303e24e5 -https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.2-h9aa5885_4.tar.bz2#8a4aa25a5741e65f2338204f8a45d8d8 -https://conda.anaconda.org/conda-forge/noarch/tzdata-2021a-he74cb21_0.tar.bz2#6f36861f102249fc54861ff9343c3fdd -https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.5-h642e427_1.tar.bz2#9ab2316785cb81c464ab9a99512dae71 -https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.2.11-h31e879b_1009.tar.bz2#96796f31644a5e13e12dc194284f7681 -https://conda.anaconda.org/conda-forge/osx-arm64/openssl-1.1.1k-h27ca646_0.tar.bz2#e056be01e85706eeb056d7b979b4a30d -https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.1-hfbdcbf2_0.tar.bz2#c9b0df52c6942e7c7066667fb4ec7404 -https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.10-hf7e6567_1.tar.bz2#42c39bbf010ef4fa5839c61a19535980 -https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.35.5-hc49ca36_0.tar.bz2#f7357f92c4a3799f14763cad1d605e64 -https://conda.anaconda.org/conda-forge/osx-arm64/python-3.9.4-h5b20da3_0_cpython.tar.bz2#eee24d5270bca46d9f003f7be5f07de6 -https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.9-1_cp39.tar.bz2#06e5b68ca789e3b7910c3f09cdffda7c -https://conda.anaconda.org/conda-forge/noarch/wheel-0.36.2-pyhd3deb0d_0.tar.bz2#768bfbe026426d0e76b377997d1f2b98 -https://conda.anaconda.org/conda-forge/osx-arm64/certifi-2021.5.30-py39h2804cbe_0.tar.bz2#402804274647d0b524c3de7e46b6162a -https://conda.anaconda.org/conda-forge/osx-arm64/setuptools-49.6.0-py39h2804cbe_3.tar.bz2#b2268b9c73391e6199dc6936da31cddf -https://conda.anaconda.org/conda-forge/noarch/pip-21.1.2-pyhd8ed1ab_0.tar.bz2#dbd830edaffe5fc9ae6c1d425db2b5f2 diff --git a/examples/grin/construct.yaml b/examples/grin/construct.yaml deleted file mode 100644 index 6e3823bc8..000000000 --- a/examples/grin/construct.yaml +++ /dev/null @@ -1,66 +0,0 @@ -# yaml-language-server: $schema=../../constructor/data/construct.schema.json -"$schema": "../../constructor/data/construct.schema.json" - -# name and version (required) -name: test -version: 3 - -# channels to pull packages from -channels: &id1 - - https://repo.anaconda.com/pkgs/main/ - - https://conda.anaconda.org/ilan - -# specifications -specs: - - python - - grin - - sample # [osx] - -# exclude these packages (list of names) -exclude: - - openssl # [unix] - - readline # [unix] - - tk # [unix] - - python - -# explicitly listed packages -# pkgs.txt -packages: - - python-2.7.9-0.tar.bz2 - -keep_pkgs: True - -pre_install: hello.sh # [unix] -post_install: goodbye.sh # [unix] -post_install: test-post.bat # [win] - -# The conda default channels which are used when running a conda which -# was installed be the constructor created (requires conda in the -# specifications) installer -conda_default_channels: *id1 - -# type of the installer being created. Possible values are "sh", "pkg", -# and "exe". By default, the type is "sh" on Unix, and "exe" on Windows. -installer_type: pkg # [osx] - -# installer filename (a reasonable default filename will determined by -# the `name`, (optional) `version`, OS and installer type) -#installer_filename: grin.sh - -# a file with a license text, which is shown during the install process -license_file: eula.txt - -# default install prefix -#default_prefix: /opt/example - -# If `welcome_image` or `header_image` are not provided, their texts -# default to `name`, which may be overridden by the following keys -#welcome_image_text: |- -# multi-line -# welcome-text -#header_image_text: |- -# multi-line -# header-text - -check_path_spaces: False -check_path_length: False diff --git a/examples/grin/eula.txt b/examples/grin/eula.txt deleted file mode 100644 index aa06ac8a7..000000000 --- a/examples/grin/eula.txt +++ /dev/null @@ -1,24 +0,0 @@ -Copyright (c) 2016, ... -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of ... nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL ... BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/grin/goodbye.sh b/examples/grin/goodbye.sh deleted file mode 100644 index 1afa24798..000000000 --- a/examples/grin/goodbye.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -echo "Goodbye: PREFIX='$PREFIX'" diff --git a/examples/grin/hello.sh b/examples/grin/hello.sh deleted file mode 100644 index e7c60eb9a..000000000 --- a/examples/grin/hello.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -echo "Hello: PREFIX='$PREFIX'" -echo "LD_LIBRARY_PATH: ${LD_LIBRARY_PATH:-}" diff --git a/examples/grin/pkgs.txt b/examples/grin/pkgs.txt deleted file mode 100644 index 51ad9c750..000000000 --- a/examples/grin/pkgs.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: osx-64 - -#conda=3.17.0=py27_1 - -conda=3.17.0=py27_0 -conda-build=1.17.0=py27_0 - -#https://repo.anaconda.com/pkgs/main/osx-64/openssl-1.0.2o-h26aff7b_0.tar.bz2 -https://repo.anaconda.com/pkgs/main/osx-64/pip-10.0.1-py27_0.tar.bz2 - -pycosat-0.6.1-py27_0.tar.bz2 - -#readline-6.2-2.tar.bz2#0801e644bd0c1cd7f0923b56c52eb7f7 - -https://repo.anaconda.com/pkgs/main/osx-64/yaml-0.1.7-hc338f04_2.tar.bz2#dab654341f57e56b615a678800262b0e diff --git a/examples/grin/test-post.bat b/examples/grin/test-post.bat deleted file mode 100644 index 8a667b30c..000000000 --- a/examples/grin/test-post.bat +++ /dev/null @@ -1 +0,0 @@ -echo "Hello World!" > %PREFIX%\HELLO.txt diff --git a/examples/jetsonconda/EULA.txt b/examples/jetsonconda/EULA.txt deleted file mode 100644 index a31699711..000000000 --- a/examples/jetsonconda/EULA.txt +++ /dev/null @@ -1,24 +0,0 @@ -Copyright (c) 2016, Anaconda, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Anaconda, Inc. nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL ANACONDA, INC. BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/jetsonconda/README.md b/examples/jetsonconda/README.md deleted file mode 100644 index bf403ae53..000000000 --- a/examples/jetsonconda/README.md +++ /dev/null @@ -1,33 +0,0 @@ -Jetsonconda example -================= - -In this example, we want to demonstrate how to build Jetsonconda installers for -Linux, Mac and Windows. - -We only want to construct installers which include: - - Python 3.5 (because Python 3 is the way of the future) - - `conda`, so people can install additional packages - - `numpy` (but not the MKL linked version, to save space) - - `scipy`, `pandas` - - the Jupyter `notebook` - - `matplotlib` (but not Qt and PyQt, again to save space) - - `lighttpd`, the web server, but only on Unix systems - -We also want to include our license file `EULA.txt`, which located in -this directory. -Also, we want to have a our own welcome image for the Windows installer. -This image `bird.png` is also located in this directory, and is re-sized -by constructor as well. - -Finally, to create a Jetsonconda installer, you simply run (in this directory): - - $ constructor . - ... - $ ls -lh Jetson* - -rwxr-xr-x 1 ilan staff 59M Feb 27 18:02 Jetsonconda-2.5.5-MacOSX-x86_64.sh - -This was done on Mac OS X. -A 60MB installer is not bad for all these packages, I would say. -Note that `constructor` will be default create an installer for the platform -which it is executed on. However, it is also possible to build installers -for other platforms, see the platform key. diff --git a/examples/jetsonconda/bird.png b/examples/jetsonconda/bird.png deleted file mode 100644 index 2efe0f30aa3e3de5d76f1ca9165ae8c30bbf0866..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12203 zcmbVyXH-*N)aE5bB?tnEC{>DtCM7^<(whPaNDI;lgchoRAWD&rC{0R;fKmjc_l|T7 z(xij57nSzXReF<&-#4>nX3bjPnweiWxp$v?_T6Wn=j`%C>*?O7rDmZ90Dx9YQ{4an zCA?6(W9fZ9SZzETb&b z8QdM~zErFBd4L*H7M`FR{5Z+pG2t!cNbEIe1>3aOH(`(HM;n@KqOY-T9FW&P>Go&| zF8TTS)hNAQPfeWkvfLMzdy#fHim%+SbwW!c!z37Pm z0Omwc0949Y5I`T?4hA@Z5D1_QfM7r{@P8o~d=tf?T-;PPd;ZSFk1{yGhUY;@)9<}bCbe1!U@(#>+)jr&+-WMijigZKG^^@7`?HQQs{uIm z7#SU$8k`E}=A6g!mX9d1?yZBo`Prh(G7K*sqJB0E^~ZUFfIhvO#J~lPh?SHNH&wD_ zjT@&=Q*_P0Q7Y?Y6F8+-+j4aPg6rjOmLc7Lvsm1TD{Gw1U zErOtDi)c&5dm1jRo&Elh=NHrgK9>~P!vh!Y(*qP#BR36AXLx+P75$S+*;YFoUOu@$+u9skL1he!+R(I zlQ?Sc+X6`*JeU?h@4Yjn#6iroK%y!PNVr!U@+LiZIQbD9pvMZ5;O?Xb)-|yA>Liin z@-5h#8~}a6di#KE+!_n@phdNB&n*t1LT4hxG?CRzwhIa*Tn-V?+zfnFC{DroZv7x} zVmm#3fdv=S=oR#;=sE!ALMV7+{z;_2W4S!t<9LM=_!`&#_-jQC=AUnFUp%&gIH2#$ zi@NVT5G@d=P^Kaw*tx#%4GwznQA*x8l@Su70GO!;DEM?4AwaO$CD52Vz2d>01DvoM zey0CPM7YDi`cB{2tB$@DK^S27;%+1})X$q@<)5gfUcrUAGbuAT3-m>dBS_g)A2b$e z5d&-T=is8q5Hx!Je7}gS;+=lKJh3rYY+bm7ggpgk++fHXubb%!r#j4+WmP3==1<6~ zNS6-sAStlv6F6d`h_S`G$>!0INiou}vX$IBtvfe?x>@8}k8XlM@2xO51=6)IjS%HV zv%2j?F$fj#+nkYTAPsvGCh?(`OG--O2Eh5Cb_0=D^NIrSF_B-p@9eXd7!*M2w(MdK zovs2Z26Lzl)l3LANHD&KTS_bGSMUt@3a^dq;QXni&whh_oep4{s^=oL_LtO?zG;;zn}h`+Wg5%N5$~r@Rjnjrc;IQjn8SR zbg?bp+GF;=S7pztCeWvOHgFy1J;je)oDZDc^Se8@TE47$CHT62xv_Xp5dV3d+g8XA z#i`LPlP5pomy(tU36C2u4kdPH^KnJA-yuR{`w>d)EDj6W^RHL!)b{U6UbB3;;64vc zR&&MHS=oQ_yyJ!6VaUoYVO(J<+y7c-l4AElbP&~iLpWR3z2tWdm3~c;ANC+{Vcd?> zqDpCZoNMhiGL`vREV5=VqTtTU>h{0yFBtEAh8gEN@0|Vds6lM z%d<$cdN7|eFE_%?+*ik`t_H0^sl%mZE_JJZod>CZham(%P*;g={%3?+evIn)Sd&ic zRpkIO=xEiNO2-I=#+L1$7C#(J*)_jXYRLM|JT%F76Rw4euM{J^xttwshVi6WBX8el z72g|ao7f&+D>R+*SwZcNc9hQq;H~bOJb5AG>g3fZHm+_F!R+!oo^~*4*ZkE^_5OO* zcp}|+gqBJ{Y(e{+c%1Bbnk1=E>UIs@Wy)D^Mlh*z;PF`=KbQ|$lmlnwuF)wjZHgK3 zs&lJ0n=MONB>Tj2mgQ9^?xIe#@IiOh=!%H3!iR~MUQ`$dE2E5IO^b->!}Il`-pn%z z4MRp*$F&W6j|Uo0V<5^;^*vQ#o*{3>q@~+1N=d%>pUpPWY9D!*8#*AG6OXYh-tu*( zhRbWWg}B%!czIy5k$A`c6&CAprbJYa2n6Nyt4ry^mh4dvrIdnYxG7;>X9+atswyUt zxZm+K;k-OWB8Z+zoU?D0O{PUB)Qll5A-}1;WP)%|lU$K8TaW4URJ~Rf3exJn?6B>L z#@CgU*N`EMAF7J$kn!om*qn^)eBh$@lDfLWWHE)5TPk=nYI%2d@@Hx0WXY`^dqvF?eg!h-|X1=Y>T82dG1*H_c=&jH@%tQ7mQY@2POtHl{a9) zdq2c&Uud1JY?zB2!Z9Vac>-NtPi&{nEFDU84UY!S>il&Y_EN0^rd4PrjESVrMdCON zfs|`1C4Sd|IV`ktV7P9BFuas&Nti!V{(VYcg<&V=(FoH;x8U?&LKF|w6aD9^kf&$X zr`uhGOYr`y-}n^Q>bY=0=~x$&(EE|mtj zP}H9`TD?#Sjv};cHa8-#k0H^x&vWj5Qjf{eF&DFG^nPmQn+2&W-^~s@AzwaH>6B<- z%jmu3z2aDtcRn5_J@wl$OV%^}Xk;rQbaCS7gK_AsczoQkc3X21C-J021uOEH4A1_O z$6x%FG+Hj+zp0>2gc8as7d^w0f5t>@xY-I|LY^+KX^eOdu-V{0UDJA1k<3gm+^qaV z{*IC>u`y&3h-Apqid4Kqo>@h!$(D&O&E1xR`KadM4Am@=o(5jk0?2@Y7bZoD-w;C> zT|N@O-V~r-m5?VYr3d+9Po*=U&q$O_Ot#$9Y0U%nU;x3of3dclU#_*e)i-)Uaw^e_Zols>3@@qR>cDlk>5OXY*j^Xuj%vS!sq8&%v#_m}SBFT*b{@@Si{`On0??+Y)EZXZy% zhetQQ9IamoyR|EB{U;Jucn4K-zZQaa;YOg0l6AZVQLF@6l0czI{regDdnu5=gR*Dd zJ|omPw7~fqrFgO!Svfi&Y_}1&6Gnr4J~`=c3#zk*U!AFLbZCVV@FV#x>M0N$)xaQ6 zy$vp)fsQ0WSx4$`R1dEh2y=!tsAJtZE31&agJ5FzPvP2kiFvo}8#<<)oeb>jbS$we zGXa5d;}tb^0R?L1YzDSn6rEM_&EHjAvhkuavA%`W1Cln(au#_Hwwa4XQmqtGKepv+ zu**)8$Pp<_h-unh#beq_x!v?M8YX>ePC1g!plbCylbOTsx%efcelT?!lufE8O;@>X zJcBD8XM_mUCJy*Si)t}dkAF{%pLwHl+Ht21N{oG$y+@(xwY>JpT|B;~!j;)ZU?J$T zaFVT_s36)fC}|IFRwR!9v~J{Is}(UVB8ejwF1fZTetaT4i^JbHS;w5-yAp$|E1qsg zvnNZjS)Q+Y5Vv1tO3&}e?VQ^w7Juwp>bvbzpdd5yZUrRab2@R^{^FSR{>5bAFNuj| z8s6jasBh}RX^;W7#bml2OIDh`ug>M-kp8<28N+4d1rgJg9S{8^9%EkDL8ptOdhoB_ zzR>0s`AU0biIY@*H&E|VL2v^(BzR&5jU?Iq+Mwj5b$O^_*qOT+cC^z3=F{<|r zF~?}r}FiC3_l+S+z16s+!cRv=8GT!k1`J7oNC; z8)@XXI`wcjCT908WlFu^=z6^D+(p1rHl1>RQLH|IVhQ7(Is%)WM*`s1fYfYLUo>nV z+B_DoG|e{!Z{k63M+au(FHp+cKa(vin^a`?^-X5olTJRm9QNP0S512Rd_BLYbLkmj z@;%@oY9U2ddioj ztxNuLoM3tX++jKKx7mgQBtuw3JG$j)ioH3Bg+O>7aBw%R(Df~w9p6#I zVUoW%=HHTsYowVv#ZV+$c66z_?#Wt{h!z~)m_w8DOgv%e@;?@ju0`aXUo`5j&j|K0 zIW3B$;<)LPp0D#%b@B-HG?vb=b>KN}+rh%2i-cOEWS7`lPsosG-6%4W{aHl}v3)?& z-8AfLGbrMxC?cvN5jx=k(at0}M%@jq3Jp}X`+A@oIl5pSxR zN*b%!etgMEp?@baXMUTbcz@foc?5GV2DjJh#oNiH0R`)E%k#M7No2f>4t0 zOxDwARLrE1@bq2Aa>Q%G=_jsL@nnGv&2?#`Kxc^VB;I!5D*RURdX-K|x~t*BVg8Q) zZvXv-_&5v-*L5EDFi1FnCn21=nhQ?Ia*|cyR&aZ0Z{tmQR26SyVA=Q0-S6j5t2ERbRFWY0l443R%ypg)&Gia(+FufIJ1b8lQ2yzq4AgC&spPm4!W z*`h+Q&Sp2)X+Kj^AarDT({ws1c+)X<$bx0stg6P=zFK(Zy3&WS=EGRAQ&HW3U6OI0 zhzqZ$H|62+;q!~JKRXL&@4LSn2|vZI91+yyjHAe+gVS5Ad32O1z3lJcYTo|_tVb?w z`+EH8xH$$+yv&;v@U2?dOmRT7!?G#SD|2=(yK&HWEyu2&7X*_yZU0c?d zrH>&E6*|h)DPC(AqU94U7K7*hkj%=vL^U{Eq5e$Ep%+TH`{|vqTo0WtXP%LStK`5t z4K|H~#8~)hFFXG0Fxbm8^+PxH7h?9B$kptoWQ~0$UR)|rmr&8>_V2*d^r;QAmRAua z%%?)aQJ92$ovx(iq~j#QJhZ1KZVc~r{ZRhFPgOYSp&=U zo5BbPswFof?-Q6)b2TcTBg0i$?`CaM5PfP*MUB&z>Rr~f^1#FIDsq#T;WnmplQ!If zB+(J>W?z`kZ3Ne?sp0U;?oI}n$!LO5j+UvVID|?W|DT1M0aE1w^u;S**A}XQNG(fU z@yI%%;KUp-uM1*7nwemf3YcZpb@F7@j~%fX*sBUk@fT#yv?F={IWl+gn+LG66B`J! zEAf$0XmBsHP5(r%8^xGdgQKWX^}%vyoaW`6{%|CTg!Jt_zksm@@Z>kaUoAU3^@zto zViv1kUkjP$EJ9`JqVH;>0>6XA zpWWKo{gIdgw}E`VXD;>D8I&}+i6jY=D*%FKRZREnAvLe+Tmvwd^4a^5B&5>2UaLyw z=99LI-p3w)^iL2)hB~Op(FP@F!OBi(u%hp6%5FnTEK8T4!+?tg3$EKZRY;g3!7#af z^VG@$K5FaKYUwXSB|X# z+tx3mq-+Z1L|ziYw8S~;7OcG7Of6Af7#fPrAuNmA~SHNYcvj5RSZGI4Sn_< z=|RA2aZzN0dtN87kYo+n-yc+fwKtk{&%WmEEsk+Nix7|$+VY$(-3!EeI@H*{4p^7Ha}TS;CmWNmqqdEk5#*D*-7V=vph9!k{Ctq zJ?yogU47*;hKj}_Myc(-195Jsy>|H)|AmZs!aTvUD1X(KW8w}L_v+nVm@8Ay zT>kpophqPurj@QqSHi!0SLr7H9g!(`FDtDJ`dQ2Sn`T*X9*(A(0t~0mJor|b{lH(! zV(NZC?IvCOJK~W0LV^3)gY+9+zc&wPbwQdA?(acsi$sx&D(e9Ee2r4X*5yt9A-CxI z>jY(O7T!5DULZwhZytp=uU1=h3IF%5un04S*9?@9Mo1l~oX8h* z6IF=5MfAn@4r>K$cI)mpvXPw@gEF67en90cCZiE!Gn1rPEw|cOjfJ2%Y2|$DYgJ2x zk+F?5x=#YC^E`FM%on$wT*;YXQ_}+-kB{o=*gK7dg*`8?0O>%9hHlfc;8jE?^z-c% z9u9wz5`}>C)rT8pD%@sOYE+R5onCK^XVhPOhPq(V$!8>bg3GUGHigMH zri&&HFS38>4tB@b=>E<3yh}|(p_)SqAs?twvxHLon_h!v=DIH?A4tBX|5A?;>l}xn z1m(NUu)|$Jet7KngwVj!D2Hk10!54waa%oWl3X5LTPiu4>*uI5!#9z5shN{@u98N3 z3>k>Vcg!~x$P2Iz)hrlay5(={;5AYiW-sVoEU)i3WBL&#XpRZN(@gLNyYbZgF*9|q znl$lafk`W;iUjK4BpSy~@WKLvALrJp;YT86o#*eJbygz-CNAfT`M7y0n}F7|uJSqz zi`>LDch+Kxj!dh#YKC&*LLoIg^-8C$Z8Tq`FPwZ^@^2K~L8p=`b)>)jH8=!EP7@(2 zq|e2XE){uAND{H^0rFDspJ5cZgM4_An5Koziu(4xxG!MO&AYU(3=xvN*Kp+{lNCr_ zn~288P44K#Vx1ywPKB)ywOZzfCB{!Ag{Qd;97mza%6BCrXp%zfs%@Cx&l-Ko%@o)U z&q_=R-CTr`Z73O$x2#L8DkB%&*BT^dWNBk|dSffr(rKkchPfO(S}Z1HSu#s=v%2W9 zBmhtoOpB>Ro7|wZ15ach-2aPRfqH*y)2ADp*Ma+VyrkP2;B3^>a z$t1?K!aI!?ZC&!Fb|Cc9@1z?nvaPxLZdTgomp*A=pUu6gElZ#;tu3nAFsm=vk_@3q zeDbrmqg*QX_2DvCN`^|K|9}zeO(sL%-B6+`rP}Fk&DrYd-)if^YVUEWZMw;DVrl$% zFux~M>F(w1avh;<*Y9)X-lNpjODmVeCZOSla|tmF3EuHGXx8#OKav!EAD&!IJ3W$& z_z*67yT*_rta{eq2g33|Y! z`^AG%5$dF8m}m5|?w2lvF)zNo#N=h==dZ1e$EC83fnkEMuo@!`XmP)o`=f1Dt>KM3 zV*J9OYA6nt#1m^$o-5%HwN^x+`im?xxaGlX&#>osUenC4^$R5XIQ8s{Kdd zR%U{|Vg?U>W}ht%v1g5jjdCMW3M&(Hg=eJ(Z3+t+o4YN7Z_W8G4(XswSk$mDC2J21 zy=CIKkh8h6LgwO(4(6%XoT;>5wO@a6B#~4-D@{dWj=@b9M+%F_atWE=$MTZ=cphnaWAHRK5Ri8g zJhjEEKKQo$NN8G-Hjb$p9MVXP+*%1vbbd3w$Sq_;6CzvOouUAgB1IWJ@$=i)@C-y4 zv4C=*xHKpC(kn4=zGH6#&v4O<>L@u~qY61@t2PZmSWaP>6G;fzjWH^;+6E()et*f7 zSU7wI$w=I+p%i-9ul`OT9IlFFAl8_N_&B?_nGL+qC@7*#$^uWi2nZF5FmU6szLpPN zt@TLo5S!X~jYw{xN~07;UYqDNw^ppjG6qN033V~LNeqWOPpYOPCCUF<^iBXcf~cCD9e>^}S7j61SArlIam>e3k`E<~WFDgFB7tA1 z1fmG*P;YHvH|@fm)BbO{3UaTes45G}$RQGdo6E{=Qm0wJgU<3X((9RnD2Bsr^)1P9 zb&ih|jY;Oy=r_?mA5=!9dUW-1RP?_=H zC+L}q&l+1yyo;&r*^`rw(B#iLmMB6VPkX~WPgdV-bvRsG&)f#YWQ_BlyZ|Zd<$yu3 zg7J>dz&v3yK`l*HEQs+p<-NtbI-Sr_dZJS&8C7&L{qi1Av9Nf%9j{(`Lxs*_fc8jD z?=YqnP@^ZUZDS{kQK__pPz*1Ir^b+xNUHb){VM3=6eo=5V#|Kp)DEt&+!WI>D|QM= zYq#qHm8i)1X3!1__dg)^z9bYt(7-QRVWsJ39LlBS=AB$P2SWFgRaFhH@&*OKd4n+o zQqm#L1BfyF7d<`OjEn_w@|L;ZDC&DeLC^YMwEFbf9X^9JL;j17U;(v+9Yt;qH{!w0 zKhk`w_1YYlfh-&HvI4=Q|FgTO2P4MdK!COZjI39(IyQ$zNPr>%`gi7FyO8O_7TM;& z+v|bkm=WNQp#I;xjs;iI+=zMLg9XXd@$&S@2mOqB z3G<6qS_{ItVjbz zg;tgk;F9hBkK)kyt9Er?qf{GLDy^%%T)4#y5`9)d)JeM?K3XJvSzbp)wp+=4rmi63 zq#Yb1wn7e<_WA6#RAeK6>5hlCgQ<%p`2%B~sLFw0{JAMvdzED{GYkXS*z4^_%T~oU zYq%~SPB<)*>XyfBUx>3s(U_eh^Z4M&eQhs;ld&Low4I|YRjQIs-ph2OH*HQ z^n~48N`7^x_A$(XIw2Y4UME-TxUXx`WPvr8Y5202g~BBRdZMPTnaRpm*(fYJ3@*rD zSk!L!J6_(PbF}<`3OP@uo2-YyvQ*RQ;AlEbiQyG?QqJ>p8)SQEmNvB`9h4|ud~*$Z zYeXX2q!&oNMol^TJ@K)8STZVe8~N(9zU4`EIpjW*x9s;mrcl@~jHyl2oO9AJdSmHq za{ExWjgukyk-jH)$~xtkt*YKF+0fBaAqXnQGc0LXou*e>Bw>l{8exJ|o@7_=lj?}7 z=r)MwkjITts*j)}qK`IH<1MzP$e=v1Hh=oprk&7NUx#x?yKf9$;w_!t^11@2rzBTP z(ypR$CdJIoC4rXW{DmTwm$IXUtAy@|^Q2^>_V z_0DkeXqX(>CN`gP?BU|kN}_4UXjtMpRIJhphLW65WPktk38xZg?Z*|v&7rUgb-@}Z zhR}Al*LRS(@~abjEK!_7uajv1Ru0K^T*nN!>xj66OzaIIZ7vxx9&;?qdpTrmw4obe z#skCJ2{jlli_pMc_Danvq1A;8Sb<;c#S@#;l0oKd#EC)+R~J=dgoNI82#!M zrd-WL^>%2rf)NqYklu`Av4Lzg==sP|4X8Vp=HPz!Jf|2WUN=$#!8G(Ob9jT&Cz(2{ zc}0^IftqMzA?Jgm4vBvj7zLrknjbUwOhCi@{;0_>$r|Q!qFUi_AeO01-N3>PER5sU zvn2l(uiMu)$+7=)k^>x6!dj8;CmUW_=WrJ+IvlAp4& zrWvK(y?zzlYP$9*k%9A8+bECxGFim~)|9{`J^Uwa1~QI*p5r4A6n@dBG$zi6X;E!@l+oJ5*KSVB@o-$Y!}$Tj^q=KlcU2l zvfynH>8o^9bHuG5w}mJyiA>3NoZPIk$qm;*G+d#MOzXQo!>cfUh?bR;HzPa_;wmj zPnzU}a3fTgp0;0EBe7+2=+@S>PvRAamxI*WuODscnw2HEx*WBTL3ny$)_kTr&3qty z#mf|AxR{RDjV?;6P`_#rrT0=x(=06MU{hR+vU;-Ov9Q}>39U&dv{XyqEi%BWUfv;^ z`sr1W_Z#xLqH0pcVD{#kTSNOs^rR;9f`zJ~(vMFx3a)*icl&rDDUBwZE{Ui0^#=79X^q1ecw7{Velvc*BIh3WL#)9eCj zxw3F+JA{KG6=V<*V?*85z*`X$S=pJG3~y`e1k@OeXToB zvSl@X`|F*q@=Oh}Y-n5^NGsFkPFYC`K?;zP2v&W9>R*x^KI39z-L(9yMV`9BW1d12 zPSjbu>^8*}KoaPGdl3f^!N)o3ZhWNdRm&fN&QD4p$Fj*M%m`MDv{}_lClA?WJ+Jg~ z;|IZNL$kDzb2|@vm9F&mL_P)Qi~q?D$xhNQ(i5H_d1dy`i9J$H?6A&nPn91JCY0F1 zK_%*($HG#o`k8F2SELy3I~x&S#=Z@9mh&9nvoIqy**e{XBML<*(7()UZmiNesYXCJ z?~tUkC-PhDroHLgjiHn3ATFtg!~i*P1);5reif!kn$QNWGd|Ox%|en86feHlI~K}t zK|&zP;I=-yAaHIr{OW&Dnu1LRvLblM#j)1}G??4uHDw4Si+Z3&!x`+5M?{1e>sfNA zPK5MAOhxj4yq7de{cK|x(HqUItR1*usG>?^R`tKIrRZbpgmV86>Xd0=58b;;;~M}s z_kU+q>1~ugv35x1uv5bt$JGe|-M?(tD3$N}ZgiGy9NoA!andSnImPbUL#}_$75tAe zQR`6naN?z`P`7?io}LXz@9zzIA}tn1Nv81-gaF9`A^p*H>Lk?mzp5z^V3sWWm$@~~ zRD=dhqS{?wG^3~i>>HhsUKw5>i_z#mV?x$0SJS_b$t8nDn{u0{K~pR5Ln(l9?p`L|6tX5#=o50Vsyy0m2cqZfjaX~i{ynuXmq&Lm zcYb4W@D|6Ze+}FeJ5wi=ZTfyU45m3{`QXH9RM&X7$D3=LD5@Y32qaxwFytyvzferx zGkbqe>kgVEzA^5zRzPR7iRaEitCFnsugr%6H_^wLE3TjpK{;_e&k$JI=^Y-al3^)&xW7HHL@;SSjSmyyQQ2E_%XL!(OvDwpl z5@fB_l3By8@sH=in`ePbff_#1&;mrI=HW@pDLFrDbbw?zWk`TR%1xD@Ho&nOeL!AZkIS!Lw8ta8Di?u1o{mbrLieU@HeT#*roL^bSl)ebaOi#Dk?Afk zn5!HSjopr3@7;w>-2Nc!e{wPkTNrUpyUfC`AKT|t9@nf{%tHCTh^%DIQRTq3lbCVy;=DCziQ=Db6CY@2(2DEDB$-RykQ8gJ|q*SMWb zzjKNy-1WG4k{|YD|M^+}LD5(C7==i`Sj*px>gFSm5zVl0ooFxo4W_%ymP$QUp+&0!b zweCt8nz{EwzyI)jJS94YusZSJa7ThwMm%nF^b_5M$3;~9#Zw$<{vLpshbaZXSuV)ugt$en%qv>vIcnUpfo_oy+4} zx9~CFOl$>VuQBrGTz$*P)IY3c{-esb9)Hs-U^_KAEcnDTKtRK7ZgpJ*Z#EOVzY4i< z49{pfS-Y6N_`b0iZMDrK`~2gCTU>_R&gGfPDn+8A-$UKHYQT;w$HaE24^RH zU&Z3*r#}vzD3{Hmj#I{p=Bq}!-aa3cHtmjG;dY_)D6^LO-S)OVteK+o?S^IUn$%d~ zmTuy1k2!~|_0NBvIOvYWrNmOhs>|err$*@ZCiXW-e+D6UKU6)Z?^w`y#xo=*2IKHm^H}ZEr>Ht{9y$lW>ZGkNxEQOT-@Lu?1xF zAEj}|+mg3mVNcp_f1-2u+c}2Ciz7ZGE%pKUVqjGL`LpNH1ILED2J%zKIID-#SCm@o z-}!1u`)+==jcpEMe58bnUw)$Q)YCV$e@Rqe)aUKfvu~z_QBjQ_&(es+eHL@mv4+C3 zysSLl7d`)MFLIoI!eIp(FrTNBf<8V|y0DXeCBEmQwQoT;Wl#Nd0ZbJb@9OEfdQ#TU zaMI)ws9>&vmC`lBf^=k+W{x3MjO<;x!sa(@Ajyl7_Ww2r_1TWn0oQr4;Q+!e+{SbW2$Km9^H^sXzL$=xNHtIx?6mbPjhw-`tXu^Fn&fIjtiYHrJOcm-Y3()ns?{l;vgVlj-vJcLAaa>m zG)m&}a@-_ou`Zlf?g~Y_R}pBeitlvtPRS(6gCdJ3g-kEBsl}t4-c+%u7Ve8)0}{e! z+H2;D2}N9_;KT?O_!;2Dz(VkiHa1}%TP&#F*`Kru4?>|p9X=nv;m!9`2#?XG z?G@XeumFG!Nme3>EwXp?o1GUeP?J|k*d%7($j>Xy0Ry=z*k@-4%fsXzLwf5DA0Gun z9iT@C@+Qq4EsVFtM+m4;11I!ENNfat%7?uA{A$=6-|D>}4}gU74O4GDqmo$qBQcP& znGrF9;W3bthe platform key. diff --git a/examples/miniconda/construct.yaml b/examples/miniconda/construct.yaml deleted file mode 100644 index 589b4223a..000000000 --- a/examples/miniconda/construct.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=../../constructor/data/construct.schema.json -"$schema": "../../constructor/data/construct.schema.json" - -name: MinicondaX -version: X -installer_type: all - -channels: - - https://repo.anaconda.com/pkgs/main/ - -specs: - - python - - conda diff --git a/examples/newchan/construct.yaml b/examples/newchan/construct.yaml deleted file mode 100644 index 147d94424..000000000 --- a/examples/newchan/construct.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# yaml-language-server: $schema=../../constructor/data/construct.schema.json -"$schema": "../../constructor/data/construct.schema.json" - -name: Funnychan -version: 2.5.5 - -channels: - - https://repo.anaconda.com/pkgs/main/ - - https://conda.anaconda.org/ilan - -# specifications -specs: - - python - - grin - - sample - -license_file: eula.txt - -check_path_spaces: False -check_path_length: False diff --git a/examples/newchan/eula.txt b/examples/newchan/eula.txt deleted file mode 100644 index aa06ac8a7..000000000 --- a/examples/newchan/eula.txt +++ /dev/null @@ -1,24 +0,0 @@ -Copyright (c) 2016, ... -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of ... nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL ... BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/news/1145-python-test-deps b/news/1145-python-test-deps new file mode 100644 index 000000000..f3179bf63 --- /dev/null +++ b/news/1145-python-test-deps @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* Remove Python `3.9` from the testing suite, include Python `3.13`. (#1145) diff --git a/pyproject.toml b/pyproject.toml index 8f48009ac..f54eaac55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "constructor" description = "create installer from conda packages" readme = "README.md" license = {text = "BSD-3-Clause"} -requires-python = ">=3.8" +requires-python = ">=3.10" dynamic = [ "version", ] @@ -46,7 +46,7 @@ constructor = [ [tool.ruff] line-length = 100 -target-version = "py39" +target-version = "py310" exclude = [ "constructor/nsis/*.py", ] diff --git a/recipe/meta.yaml b/recipe/meta.yaml index f9334ccb7..d68af36a1 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -17,13 +17,13 @@ build: requirements: host: - - python # >=3.8 + - python # >=3.10 - pip - setuptools >=70.1 - setuptools_scm >=6.2 run: - conda >=4.6 - - python # >=3.8 + - python # >=3.10 - ruamel.yaml >=0.11.14,<0.19 - conda-standalone >=24.1.2 - jinja2 diff --git a/scripts/run_examples.py b/scripts/run_examples.py deleted file mode 100644 index eea2c63b3..000000000 --- a/scripts/run_examples.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/env python -"""Run examples bundled with this repo.""" - -import argparse -import os -import platform -import shutil -import subprocess -import sys -import tempfile -import time -import warnings -from datetime import timedelta -from pathlib import Path - -from constructor.utils import rm_rf - -try: - import coverage # noqa - - COV_CMD = ["coverage", "run", "--branch", "--append", "-m"] -except ImportError: - COV_CMD = [] - - -warnings.warn( - "This script is now deprecated and will be removed soon. " - "Please use tests/test_examples.py with pytest.", - DeprecationWarning, -) - - -HERE = os.path.abspath(os.path.dirname(__file__)) -REPO_DIR = os.path.dirname(HERE) -EXAMPLES_DIR = os.path.join(REPO_DIR, "examples") -PY3 = sys.version_info[0] == 3 -WHITELIST = ["grin", "jetsonconda", "miniconda", "newchan"] -BLACKLIST = [] -WITH_SPACES = {"extra_files", "noconda", "signing", "scripts"} - -# .sh installers to also test in interactiv mode -# (require all to a have License = have same interactive input steps) -INTERACTIVE_TESTS = ["miniforge"] -# Test runs with even Python version are done in interactive mode, odd in batch mode -INTERACTIVE_TESTING = (sys.version_info.minor % 2) == 0 - - -def _execute(cmd, installer_input=None, **env_vars): - print(" ".join(cmd)) - t0 = time.time() - if env_vars: - env = os.environ.copy() - env.update(env_vars) - else: - env = None - p = subprocess.Popen( - cmd, - stdin=subprocess.PIPE if installer_input else None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - env=env, - ) - try: - stdout, stderr = p.communicate(input=installer_input, timeout=420) - errored = p.returncode != 0 - except subprocess.TimeoutExpired: - p.kill() - stdout, stderr = p.communicate() - print("--- TEST TIMEOUT ---") - errored = True - t1 = time.time() - if errored or "CONDA_VERBOSITY" in env_vars: - print(f"--- RETURNCODE: {p.returncode} ---") - if stdout: - print("--- STDOUT ---") - print(stdout) - if stderr: - print("--- STDERR ---") - print(stderr) - print("--- Done in", timedelta(seconds=t1 - t0)) - return errored - - -def run_examples(keep_artifacts=None, conda_exe=None, debug=False): - """Run examples bundled with the repository. - - Parameters - ---------- - keep_artifacts: str, optional=None - Path where the generated installers will be moved to. - Will be created if it doesn't exist. - - Returns - ------- - int - Number of failed examples - """ - if sys.platform.startswith("win") and "NSIS_USING_LOG_BUILD" not in os.environ: - print( - "! Warning !" - " Windows installers are tested with NSIS in silent mode, which does" - " not report errors on exit. You should use logging-enabled NSIS builds" - " to generate an 'install.log' file this script will search for errors" - " after completion." - ) - example_paths = [] - errored = 0 - if platform.system() != "Darwin": - BLACKLIST.append(os.path.join(EXAMPLES_DIR, "osxpkg")) - if keep_artifacts: - os.makedirs(keep_artifacts, exist_ok=True) - - whitelist = [os.path.join(EXAMPLES_DIR, p) for p in WHITELIST] - for fname in os.listdir(EXAMPLES_DIR): - fpath = os.path.join(EXAMPLES_DIR, fname) - if os.path.isdir(fpath) and fpath not in whitelist and fpath not in BLACKLIST: - if os.path.exists(os.path.join(fpath, "construct.yaml")): - example_paths.append(fpath) - - parent_output = tempfile.mkdtemp() - tested_files = set() - which_errored = {} - for example_path in sorted(example_paths): - example_name = Path(example_path).name - test_with_spaces = example_name in WITH_SPACES - print(example_name) - print("-" * len(example_name)) - if ( - sys.platform.startswith("win") - and conda_exe - and "micromamba" in os.path.basename(conda_exe).lower() - ): - print( - f"! Skipping {example_name}... Shortcut creation on Windows is " - "not supported with micromamba." - ) - continue - output_dir = tempfile.mkdtemp(prefix=f"{example_name}-", dir=parent_output) - # resolve path to avoid some issues with TEMPDIR on Windows - output_dir = str(Path(output_dir).resolve()) - is_fromenv = os.path.basename(example_path) == "fromenv" - if is_fromenv: - env_file = os.path.join(example_path, "environment.txt") - test_prefix = os.path.join(example_path, "tmp_prefix_fromenv") - cmd = ["conda", "create", "--prefix", test_prefix, "--file", env_file, "--yes"] - errored += _execute(cmd) - cmd = COV_CMD + ["constructor", "-v", example_path, "--output-dir", output_dir] - if conda_exe: - cmd += ["--conda-exe", conda_exe] - if debug: - cmd.append("--debug") - creation_errored = _execute(cmd) - if is_fromenv: - rm_rf(test_prefix) - errored += creation_errored - for fpath in os.listdir(output_dir): - ext = fpath.rsplit(".", 1)[-1] - if fpath in tested_files or ext not in ("sh", "exe", "pkg"): - continue - tested_files.add(fpath) - test_suffix = "s p a c e s" if test_with_spaces else None - env_dir = tempfile.mkdtemp(suffix=test_suffix, dir=output_dir) - rm_rf(env_dir) - fpath = os.path.join(output_dir, fpath) - print("--- Testing", os.path.basename(fpath)) - installer_input = None - if ext == "sh": - if INTERACTIVE_TESTING and example_name in INTERACTIVE_TESTS: - cmd = ["/bin/sh", fpath] - # Input: Enter, yes to the license, installation folder, no to initialize shells - installer_input = f"\nyes\n{env_dir}\nno\n" - else: - cmd = ["/bin/sh", fpath, "-b", "-p", env_dir] - elif ext == "pkg": - if os.environ.get("CI"): - # We want to run it in an arbitrary directory, but the options - # are limited here... We can only install to $HOME :shrug: - # but that will pollute ~, so we only do it if we are running on CI - cmd = [ - "installer", - "-pkg", - fpath, - "-dumplog", - "-target", - "CurrentUserHomeDirectory", - ] - else: - # This command only expands the PKG, but does not install - cmd = ["pkgutil", "--expand", fpath, env_dir] - elif ext == "exe": - # NSIS manual: - # > /D sets the default installation directory ($INSTDIR), overriding InstallDir - # > and InstallDirRegKey. It must be the last parameter used in the command line - # > and must not contain any quotes, even if the path contains spaces. Only - # > absolute paths are supported. - # Since subprocess.Popen WILL escape the spaces with quotes, we need to provide - # them as separate arguments. We don't care about multiple spaces collapsing into - # 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 here: - cmd = ["cmd.exe", "/c", "start", "/wait", fpath, "/S", *f"/D={env_dir}".split()] - env = {"CONDA_VERBOSITY": "3"} if debug else {} - test_errored = _execute(cmd, installer_input=installer_input, **env) - # Windows EXEs never throw a non-0 exit code, so we need to check the logs, - # which are only written if a special NSIS build is used - win_error_lines = [] - if ext == "exe" and os.environ.get("NSIS_USING_LOG_BUILD"): - test_errored = 0 - try: - log_is_empty = True - with open(os.path.join(env_dir, "install.log"), encoding="utf-16-le") as f: - for line in f: - log_is_empty = False - if ":error:" in line.lower(): - win_error_lines.append(line) - test_errored = 1 - if log_is_empty: - test_errored = 1 - win_error_lines.append("Logfile was unexpectedly empty!") - except Exception as exc: - test_errored = 1 - win_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" - "Possible causes: permissions, non-supported characters, long paths...\n" - "Consider setting 'check_path_spaces' and 'check_path_length' to 'False'." - ) - for script_prefix in "pre", "post", "test": - install_location = Path(env_dir) - if ext == "exe": - script_ext = "bat" - elif ext == "sh": - script_ext = "sh" - elif example_name == "osxpkg": # we only test one pkg example - script_ext = "sh" - install_location = Path("~/Library/osx-pkg-test").expanduser() - else: - continue - if (Path(example_path) / f"{script_prefix}_install.{script_ext}").exists() and not ( - install_location / f"{script_prefix}_install_sentinel.txt" - ).exists(): - # All pre/post scripts need to write a sentinel file so we can tell they did run - test_errored += 1 - which_errored.setdefault(example_path, []).append( - f"Did not find {script_prefix}_install.{script_ext} sentinel!" - ) - errored += test_errored - if test_errored: - which_errored.setdefault(example_path, []).append(fpath) - if win_error_lines: - print("--- LOGS ---") - for line in win_error_lines: - print(line.rstrip()) - if ext == "pkg" and os.environ.get("CI"): - # more complete logs are available under /var/log/install.log - print("--- LOGS ---") - print("Tip: Debug locally and check the full logs in the Installer UI") - print(" or check /var/log/install.log if run from the CLI.") - elif ext == "exe" and not test_with_spaces: - # The installer succeeded, test the uninstaller on Windows - # The un-installers are only tested when testing without spaces, as they hang during - # testing but work in UI mode. - uninstaller = next( - (p for p in os.listdir(env_dir) if p.startswith("Uninstall-")), None - ) - if uninstaller: - cmd = [ - "cmd.exe", - "/c", - "start", - "/wait", - os.path.join(env_dir, uninstaller), - # We need silent mode + "uninstaller location" (_?=...) so the command can - # be waited; otherwise, since the uninstaller copies itself to a different - # location so it can be auto-deleted, it returns immediately and it gives - # us problems with the tempdir cleanup later - f"/S _?={env_dir}", - ] - test_errored = _execute(cmd) - errored += test_errored - if test_errored: - which_errored.setdefault(example_path, []).append( - "Wrong uninstall exit code or timeout." - ) - paths_after_uninstall = os.listdir(env_dir) - if len(paths_after_uninstall) > 2: - # The debug installer writes to install.log too, which will only - # be deleted _after_ a reboot. Finding some files is ok, but more - # than two usually means a problem with the uninstaller. - # Note this is is not exhaustive, because we are not checking - # whether the registry was restored, menu items were deleted, etc. - # TODO :) - which_errored.setdefault(example_path, []).append( - "Uninstaller left too many files behind!\n - \n - ".join( - paths_after_uninstall - ) - ) - else: - which_errored.setdefault(example_path, []).append("Could not find uninstaller!") - - if keep_artifacts: - dest = os.path.join(keep_artifacts, os.path.basename(fpath)) - if os.path.isfile(dest): - os.unlink(dest) - shutil.move(fpath, keep_artifacts) - if creation_errored: - which_errored.setdefault(example_path, []).append("Could not create installer!") - print() - - print("-------------------------------") - if errored: - print("Some examples failed:") - for installer, reasons in which_errored.items(): - print(f"+ {os.path.basename(installer)}") - for reason in reasons: - print(f"---> {reason}") - print("Assets saved in:", keep_artifacts or parent_output) - else: - print("All examples ran successfully!") - shutil.rmtree(parent_output) - return errored - - -def cli(): - p = argparse.ArgumentParser() - p.add_argument("--keep-artifacts") - p.add_argument("--conda-exe") - p.add_argument("--debug", action="store_true", default=False) - return p.parse_args() - - -if __name__ == "__main__": - args = cli() - if args.conda_exe: - assert os.path.isfile(args.conda_exe) - n_errors = run_examples( - keep_artifacts=args.keep_artifacts, conda_exe=args.conda_exe, debug=args.debug - ) - sys.exit(n_errors) From a2caeec7ae7905a23ee595974b18f9dfe4829147 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Wed, 17 Dec 2025 13:57:24 +0000 Subject: [PATCH 49/54] Initial MSI implementation, based on Briefcase (#1084) * Initial prototype as shown in demo * Switch to install_launcher option * Update schema properly * Move MSI file rather than copying it * Add fallbacks for invalid versions and app names * Use absolute paths in install script * Check that briefcase.exe exists * Add briefcase to dependencies, and make it and tomli-w Windows-only * Move Windows-specific dependencies from environment.yml to extra-requirements-windows.txt --------- Co-authored-by: Marco Esters --- .gitignore | 5 + CONSTRUCT.md | 8 +- constructor/_schema.py | 9 +- constructor/briefcase.py | 171 +++++++++++++++++++++ constructor/briefcase/run_installation.bat | 10 ++ constructor/data/construct.schema.json | 5 +- constructor/main.py | 6 +- constructor/osxpkg.py | 3 +- constructor/utils.py | 2 + dev/extra-requirements-windows.txt | 2 + docs/source/construct-yaml.md | 8 +- docs/source/howto.md | 10 +- pyproject.toml | 4 +- recipe/meta.yaml | 2 + tests/test_briefcase.py | 134 ++++++++++++++++ 15 files changed, 364 insertions(+), 15 deletions(-) create mode 100644 constructor/briefcase.py create mode 100644 constructor/briefcase/run_installation.bat create mode 100644 tests/test_briefcase.py diff --git a/.gitignore b/.gitignore index 22609e17e..9733f24b1 100644 --- a/.gitignore +++ b/.gitignore @@ -150,8 +150,13 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# VS Code .vscode/ +# macOS +.DS_Store + # Rever rever/ diff --git a/CONSTRUCT.md b/CONSTRUCT.md index 210cc88e9..04943a353 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -235,6 +235,7 @@ The type of the installer being created. Possible values are: - `sh`: shell-based installer for Linux or macOS - `pkg`: macOS GUI installer built with Apple's `pkgbuild` - `exe`: Windows GUI installer built with NSIS +- `msi`: Windows GUI installer built with Briefcase and WiX The default type is `sh` on Linux and macOS, and `exe` on Windows. A special value of `all` builds _both_ `sh` and `pkg` installers on macOS, as well @@ -317,8 +318,11 @@ Name of the company/entity responsible for the installer. ### `reverse_domain_identifier` Unique identifier for this package, formatted with reverse domain notation. This is -used internally in the PKG installers to handle future updates and others. If not -provided, it will default to `io.continuum`. (MacOS only) +used internally in the MSI and PKG installers to handle future updates and others. +If not provided, it will default to: + +* In MSI installers: `io.continuum` followed by an ID derived from the `name`. +* In PKG installers: `io.continuum`. ### `uninstall_name` diff --git a/constructor/_schema.py b/constructor/_schema.py index a8ae97514..4ea916ad7 100644 --- a/constructor/_schema.py +++ b/constructor/_schema.py @@ -40,6 +40,7 @@ class WinSignTools(StrEnum): class InstallerTypes(StrEnum): ALL = "all" EXE = "exe" + MSI = "msi" PKG = "pkg" SH = "sh" @@ -401,6 +402,7 @@ class ConstructorConfiguration(BaseModel): - `sh`: shell-based installer for Linux or macOS - `pkg`: macOS GUI installer built with Apple's `pkgbuild` - `exe`: Windows GUI installer built with NSIS + - `msi`: Windows GUI installer built with Briefcase and WiX The default type is `sh` on Linux and macOS, and `exe` on Windows. A special value of `all` builds _both_ `sh` and `pkg` installers on macOS, as well @@ -484,8 +486,11 @@ class ConstructorConfiguration(BaseModel): reverse_domain_identifier: NonEmptyStr | None = None """ Unique identifier for this package, formatted with reverse domain notation. This is - used internally in the PKG installers to handle future updates and others. If not - provided, it will default to `io.continuum`. (MacOS only) + used internally in the MSI and PKG installers to handle future updates and others. + If not provided, it will default to: + + * In MSI installers: `io.continuum` followed by an ID derived from the `name`. + * In PKG installers: `io.continuum`. """ uninstall_name: NonEmptyStr | None = None """ diff --git a/constructor/briefcase.py b/constructor/briefcase.py new file mode 100644 index 000000000..70569c794 --- /dev/null +++ b/constructor/briefcase.py @@ -0,0 +1,171 @@ +""" +Logic to build installers using Briefcase. +""" + +import logging +import re +import shutil +import sysconfig +import tempfile +from pathlib import Path +from subprocess import run + +import tomli_w + +from . import preconda +from .utils import DEFAULT_REVERSE_DOMAIN_ID, copy_conda_exe, filename_dist + +BRIEFCASE_DIR = Path(__file__).parent / "briefcase" +EXTERNAL_PACKAGE_PATH = "external" + +# Default to a low version, so that if a valid version is provided in the future, it'll +# be treated as an upgrade. +DEFAULT_VERSION = "0.0.1" + +logger = logging.getLogger(__name__) + + +def get_name_version(info): + if not (name := info.get("name")): + raise ValueError("Name is empty") + if not (version := info.get("version")): + raise ValueError("Version is empty") + + # Briefcase requires version numbers to be in the canonical Python format, and some + # installer types use the version to distinguish between upgrades, downgrades and + # reinstalls. So try to produce a consistent ordering by extracting the last valid + # version from the Constructor version string. + # + # Hyphens aren't allowed in this format, but for compatibility with Miniconda's + # version format, we treat them as dots. + matches = list( + re.finditer( + r"(\d+!)?\d+(\.\d+)*((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?", + version.lower().replace("-", "."), + ) + ) + if not matches: + logger.warning( + f"Version {version!r} contains no valid version numbers; " + f"defaulting to {DEFAULT_VERSION}" + ) + return f"{name} {version}", DEFAULT_VERSION + + match = matches[-1] + version = match.group() + + # Treat anything else in the version string as part of the name. + start, end = match.span() + strip_chars = " .-_" + before = info["version"][:start].strip(strip_chars) + after = info["version"][end:].strip(strip_chars) + name = " ".join(s for s in [name, before, after] if s) + + return name, version + + +# Takes an arbitrary string with at least one alphanumeric character, and makes it into +# a valid Python package name. +def make_app_name(name, source): + app_name = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") + if not app_name: + raise ValueError(f"{source} contains no alphanumeric characters") + return app_name + + +# Some installer types use the reverse domain ID to detect when the product is already +# installed, so it should be both unique between different products, and stable between +# different versions of a product. +def get_bundle_app_name(info, name): + # If reverse_domain_identifier is provided, use it as-is, + if (rdi := info.get("reverse_domain_identifier")) is not None: + if "." not in rdi: + raise ValueError(f"reverse_domain_identifier {rdi!r} contains no dots") + bundle, app_name = rdi.rsplit(".", 1) + + # Ensure that the last component is a valid Python package name, as Briefcase + # requires. + if not re.fullmatch( + r"[A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9]", app_name, flags=re.IGNORECASE + ): + app_name = make_app_name( + app_name, f"Last component of reverse_domain_identifier {rdi!r}" + ) + + # If reverse_domain_identifier isn't provided, generate it from the name. + else: + bundle = DEFAULT_REVERSE_DOMAIN_ID + app_name = make_app_name(name, f"Name {name!r}") + + return bundle, app_name + + +# Create a Briefcase configuration file. Using a full TOML writer rather than a Jinja +# template allows us to avoid escaping strings everywhere. +def write_pyproject_toml(tmp_dir, info): + name, version = get_name_version(info) + bundle, app_name = get_bundle_app_name(info, name) + + config = { + "project_name": name, + "bundle": bundle, + "version": version, + "license": ({"file": info["license_file"]} if "license_file" in info else {"text": ""}), + "app": { + app_name: { + "formal_name": f"{info['name']} {info['version']}", + "description": "", # Required, but not used in the installer. + "external_package_path": EXTERNAL_PACKAGE_PATH, + "use_full_install_path": False, + "install_launcher": False, + "post_install_script": str(BRIEFCASE_DIR / "run_installation.bat"), + } + }, + } + + if "company" in info: + config["author"] = info["company"] + + (tmp_dir / "pyproject.toml").write_text(tomli_w.dumps({"tool": {"briefcase": config}})) + + +def create(info, verbose=False): + tmp_dir = Path(tempfile.mkdtemp()) + write_pyproject_toml(tmp_dir, info) + + external_dir = tmp_dir / EXTERNAL_PACKAGE_PATH + external_dir.mkdir() + preconda.write_files(info, external_dir) + preconda.copy_extra_files(info.get("extra_files", []), external_dir) + + download_dir = Path(info["_download_dir"]) + pkgs_dir = external_dir / "pkgs" + for dist in info["_dists"]: + shutil.copy(download_dir / filename_dist(dist), pkgs_dir) + + copy_conda_exe(external_dir, "_conda.exe", info["_conda_exe"]) + + briefcase = Path(sysconfig.get_path("scripts")) / "briefcase.exe" + if not briefcase.exists(): + raise FileNotFoundError( + f"Dependency 'briefcase' does not seem to be installed.\nTried: {briefcase}" + ) + + logger.info("Building installer") + run( + [briefcase, "package"] + (["-v"] if verbose else []), + cwd=tmp_dir, + check=True, + ) + + dist_dir = tmp_dir / "dist" + msi_paths = list(dist_dir.glob("*.msi")) + if len(msi_paths) != 1: + raise RuntimeError(f"Found {len(msi_paths)} MSI files in {dist_dir}") + + outpath = Path(info["_outpath"]) + outpath.unlink(missing_ok=True) + shutil.move(msi_paths[0], outpath) + + if not info.get("_debug"): + shutil.rmtree(tmp_dir) diff --git a/constructor/briefcase/run_installation.bat b/constructor/briefcase/run_installation.bat new file mode 100644 index 000000000..190a6d9f7 --- /dev/null +++ b/constructor/briefcase/run_installation.bat @@ -0,0 +1,10 @@ +set PREFIX=%cd% +_conda constructor --prefix %PREFIX% --extract-conda-pkgs + +set CONDA_PROTECT_FROZEN_ENVS=0 +set CONDA_ROOT_PREFIX=%PREFIX% +set CONDA_SAFETY_CHECKS=disabled +set CONDA_EXTRA_SAFETY_CHECKS=no +set CONDA_PKGS_DIRS=%PREFIX%\pkgs + +_conda install --offline --file %PREFIX%\conda-meta\initial-state.explicit.txt -yp %PREFIX% diff --git a/constructor/data/construct.schema.json b/constructor/data/construct.schema.json index 228b82946..09e860f6e 100644 --- a/constructor/data/construct.schema.json +++ b/constructor/data/construct.schema.json @@ -224,6 +224,7 @@ "enum": [ "all", "exe", + "msi", "pkg", "sh" ], @@ -824,7 +825,7 @@ } ], "default": null, - "description": "The type of the installer being created. Possible values are:\n- `sh`: shell-based installer for Linux or macOS\n- `pkg`: macOS GUI installer built with Apple's `pkgbuild`\n- `exe`: Windows GUI installer built with NSIS\nThe default type is `sh` on Linux and macOS, and `exe` on Windows. A special value of `all` builds _both_ `sh` and `pkg` installers on macOS, as well as `sh` on Linux and `exe` on Windows.", + "description": "The type of the installer being created. Possible values are:\n- `sh`: shell-based installer for Linux or macOS\n- `pkg`: macOS GUI installer built with Apple's `pkgbuild`\n- `exe`: Windows GUI installer built with NSIS\n- `msi`: Windows GUI installer built with Briefcase and WiX\nThe default type is `sh` on Linux and macOS, and `exe` on Windows. A special value of `all` builds _both_ `sh` and `pkg` installers on macOS, as well as `sh` on Linux and `exe` on Windows.", "title": "Installer Type" }, "keep_pkgs": { @@ -1104,7 +1105,7 @@ } ], "default": null, - "description": "Unique identifier for this package, formatted with reverse domain notation. This is used internally in the PKG installers to handle future updates and others. If not provided, it will default to `io.continuum`. (MacOS only)", + "description": "Unique identifier for this package, formatted with reverse domain notation. This is used internally in the MSI and PKG installers to handle future updates and others. If not provided, it will default to:\n* In MSI installers: `io.continuum` followed by an ID derived from the `name`. * In PKG installers: `io.continuum`.", "title": "Reverse Domain Identifier" }, "script_env_variables": { diff --git a/constructor/main.py b/constructor/main.py index abf03ad2b..a1e3ae1c6 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -40,7 +40,7 @@ def get_installer_type(info): osname, unused_arch = info["_platform"].split("-") - os_allowed = {"linux": ("sh",), "osx": ("sh", "pkg"), "win": ("exe",)} + os_allowed = {"linux": ("sh",), "osx": ("sh", "pkg"), "win": ("exe", "msi")} all_allowed = set(sum(os_allowed.values(), ("all",))) itype = info.get("installer_type") @@ -357,6 +357,10 @@ def is_conda_meta_frozen(path_str: str) -> bool: from .winexe import create as winexe_create create = winexe_create + elif itype == "msi": + from .briefcase import create as briefcase_create + + create = briefcase_create info["installer_type"] = itype info["_outpath"] = abspath(join(output_dir, get_output_filename(info))) create(info, verbose=verbose) diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index 5bdcece4d..2afaddbc1 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -21,6 +21,7 @@ from .jinja import render_template from .signing import CodeSign from .utils import ( + DEFAULT_REVERSE_DOMAIN_ID, add_condarc, approx_size_kb, copy_conda_exe, @@ -392,7 +393,7 @@ def fresh_dir(dir_path): def pkgbuild(name, identifier=None, version=None, install_location=None): "see `man pkgbuild` for the meaning of optional arguments" if identifier is None: - identifier = "io.continuum" + identifier = DEFAULT_REVERSE_DOMAIN_ID args = [ "pkgbuild", "--root", diff --git a/constructor/utils.py b/constructor/utils.py index c63329c16..7766264bb 100644 --- a/constructor/utils.py +++ b/constructor/utils.py @@ -26,6 +26,8 @@ from conda.models.version import VersionOrder from ruamel.yaml import YAML +DEFAULT_REVERSE_DOMAIN_ID = "io.continuum" + logger = logging.getLogger(__name__) yaml = YAML(typ="rt") yaml.default_flow_style = False diff --git a/dev/extra-requirements-windows.txt b/dev/extra-requirements-windows.txt index 1f405685d..d382e69cb 100644 --- a/dev/extra-requirements-windows.txt +++ b/dev/extra-requirements-windows.txt @@ -1 +1,3 @@ +conda-forge::briefcase>=0.3.26 conda-forge::nsis>=3.08=*_log_* +conda-forge::tomli-w>=1.2.0 diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index 210cc88e9..04943a353 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -235,6 +235,7 @@ The type of the installer being created. Possible values are: - `sh`: shell-based installer for Linux or macOS - `pkg`: macOS GUI installer built with Apple's `pkgbuild` - `exe`: Windows GUI installer built with NSIS +- `msi`: Windows GUI installer built with Briefcase and WiX The default type is `sh` on Linux and macOS, and `exe` on Windows. A special value of `all` builds _both_ `sh` and `pkg` installers on macOS, as well @@ -317,8 +318,11 @@ Name of the company/entity responsible for the installer. ### `reverse_domain_identifier` Unique identifier for this package, formatted with reverse domain notation. This is -used internally in the PKG installers to handle future updates and others. If not -provided, it will default to `io.continuum`. (MacOS only) +used internally in the MSI and PKG installers to handle future updates and others. +If not provided, it will default to: + +* In MSI installers: `io.continuum` followed by an ID derived from the `name`. +* In PKG installers: `io.continuum`. ### `uninstall_name` diff --git a/docs/source/howto.md b/docs/source/howto.md index 8087a2803..c255b6e84 100644 --- a/docs/source/howto.md +++ b/docs/source/howto.md @@ -7,10 +7,12 @@ which it is running. In other words, if you run constructor on a Windows computer, you can only generate Windows installers. This is largely because OS-native tools are needed to generate the Windows `.exe` files and macOS `.pkg` files. There is a key in `construct.yaml`, `installer_type`, which dictates -the type of installer that gets generated. This is primarily only useful for -macOS, where you can generate either `.pkg` or `.sh` installers. When not set in -`construct.yaml`, this value defaults to `.sh` on Unix platforms, and `.exe` on -Windows. Using this key is generally done with selectors. For example, to +the type of installer that gets generated. This is useful for macOS, where you can +generate either `.pkg` or `.sh` installers, and Windows, where you can generate +either `.exe` or `.msi` installers. + +When not set in`construct.yaml`, this value defaults to `.sh` on Unix platforms, and +`.exe` on Windows. Using this key is generally done with selectors. For example, to build a `.pkg` installer on MacOS, but fall back to default behavior on other platforms: diff --git a/pyproject.toml b/pyproject.toml index f54eaac55..2c457356d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,9 @@ dependencies = [ "ruamel.yaml >=0.11.14,<0.19", "pillow >=3.1 ; platform_system=='Windows' or platform_system=='Darwin'", "jinja2", - "jsonschema >=4" + "jsonschema >=4", + "briefcase >=0.3.26 ; platform_system=='Windows'", + "tomli-w >=1.2.0 ; platform_system=='Windows'", ] [project.optional-dependencies] diff --git a/recipe/meta.yaml b/recipe/meta.yaml index d68af36a1..595ece483 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -30,6 +30,8 @@ requirements: - jsonschema >=4 - pillow >=3.1 # [win or osx] - nsis >=3.08 # [win] + - briefcase >=0.3.26 # [win] + - tomli-w >=1.2.0 # [win] run_constrained: # [unix] - nsis >=3.08 # [unix] - conda-libmamba-solver !=24.11.0 diff --git a/tests/test_briefcase.py b/tests/test_briefcase.py new file mode 100644 index 000000000..a858b6ae4 --- /dev/null +++ b/tests/test_briefcase.py @@ -0,0 +1,134 @@ +import pytest + +from constructor.briefcase import get_bundle_app_name, get_name_version + + +@pytest.mark.parametrize( + "name_in, version_in, name_expected, version_expected", + [ + # Valid versions + ("Miniconda", "1", "Miniconda", "1"), + ("Miniconda", "1.2", "Miniconda", "1.2"), + ("Miniconda", "1.2.3", "Miniconda", "1.2.3"), + ("Miniconda", "1.2a1", "Miniconda", "1.2a1"), + ("Miniconda", "1.2b2", "Miniconda", "1.2b2"), + ("Miniconda", "1.2rc3", "Miniconda", "1.2rc3"), + ("Miniconda", "1.2.post4", "Miniconda", "1.2.post4"), + ("Miniconda", "1.2.dev5", "Miniconda", "1.2.dev5"), + ("Miniconda", "1.2rc3.post4.dev5", "Miniconda", "1.2rc3.post4.dev5"), + # Hyphens are treated as dots + ("Miniconda", "1.2-3", "Miniconda", "1.2.3"), + ("Miniconda", "1.2-3.4-5.6", "Miniconda", "1.2.3.4.5.6"), + # Additional text before and after the last valid version should be treated as + # part of the name. + ("Miniconda", "1.2 3.4 5.6", "Miniconda 1.2 3.4", "5.6"), + ("Miniconda", "1.2_3.4_5.6", "Miniconda 1.2_3.4", "5.6"), + ("Miniconda", "1.2c3", "Miniconda 1.2c", "3"), + ("Miniconda", "1.2rc3.dev5.post4", "Miniconda 1.2rc3.dev5.post", "4"), + ("Miniconda", "py313", "Miniconda py", "313"), + ("Miniconda", "py.313", "Miniconda py", "313"), + ("Miniconda", "py3.13", "Miniconda py", "3.13"), + ("Miniconda", "py313_1.2", "Miniconda py313", "1.2"), + ("Miniconda", "1.2 and more", "Miniconda and more", "1.2"), + ("Miniconda", "1.2! and more", "Miniconda ! and more", "1.2"), + ("Miniconda", "py313 1.2 and more", "Miniconda py313 and more", "1.2"), + # Numbers in the name are not added to the version. + ("Miniconda3", "1", "Miniconda3", "1"), + ], +) +def test_name_version(name_in, version_in, name_expected, version_expected): + name_actual, version_actual = get_name_version( + {"name": name_in, "version": version_in}, + ) + assert (name_actual, version_actual) == (name_expected, version_expected) + + +@pytest.mark.parametrize( + "info", + [ + {}, + {"name": ""}, + ], +) +def test_name_empty(info): + with pytest.raises(ValueError, match="Name is empty"): + get_name_version(info) + + +@pytest.mark.parametrize( + "info", + [ + {"name": "Miniconda"}, + {"name": "Miniconda", "version": ""}, + ], +) +def test_version_empty(info): + with pytest.raises(ValueError, match="Version is empty"): + get_name_version(info) + + +@pytest.mark.parametrize("version_in", ["x", ".", " ", "hello"]) +def test_version_invalid(version_in, caplog): + name_actual, version_actual = get_name_version( + {"name": "Miniconda3", "version": version_in}, + ) + assert name_actual == f"Miniconda3 {version_in}" + assert version_actual == "0.0.1" + assert caplog.messages == [ + f"Version {version_in!r} contains no valid version numbers; defaulting to 0.0.1" + ] + + +@pytest.mark.parametrize( + "rdi, name, bundle_expected, app_name_expected", + [ + # Valid rdi + ("org.conda", "ignored", "org", "conda"), + ("org.Conda", "ignored", "org", "Conda"), + ("org.conda-miniconda", "ignored", "org", "conda-miniconda"), + ("org.conda_miniconda", "ignored", "org", "conda_miniconda"), + ("org-conda.miniconda", "ignored", "org-conda", "miniconda"), + ("org.conda.miniconda", "ignored", "org.conda", "miniconda"), + ("org.conda.1", "ignored", "org.conda", "1"), + # Invalid rdi + ("org.hello-", "Miniconda", "org", "hello"), + ("org.-hello", "Miniconda", "org", "hello"), + ("org.hello world", "Miniconda", "org", "hello-world"), + ("org.hello!world", "Miniconda", "org", "hello-world"), + # Missing rdi + (None, "x", "io.continuum", "x"), + (None, "X", "io.continuum", "x"), + (None, "1", "io.continuum", "1"), + (None, "Miniconda", "io.continuum", "miniconda"), + (None, "Miniconda3", "io.continuum", "miniconda3"), + (None, "Miniconda3 py313", "io.continuum", "miniconda3-py313"), + (None, "Hello, world!", "io.continuum", "hello-world"), + ], +) +def test_bundle_app_name(rdi, name, bundle_expected, app_name_expected): + bundle_actual, app_name_actual = get_bundle_app_name({"reverse_domain_identifier": rdi}, name) + assert (bundle_actual, app_name_actual) == (bundle_expected, app_name_expected) + + +@pytest.mark.parametrize("rdi", ["", "org"]) +def test_rdi_no_dots(rdi): + with pytest.raises(ValueError, match=f"reverse_domain_identifier '{rdi}' contains no dots"): + get_bundle_app_name({"reverse_domain_identifier": rdi}, "ignored") + + +@pytest.mark.parametrize("rdi", ["org.", "org.hello.", "org.hello.-"]) +def test_rdi_invalid_package(rdi): + with pytest.raises( + ValueError, + match=( + f"Last component of reverse_domain_identifier '{rdi}' " + f"contains no alphanumeric characters" + ), + ): + get_bundle_app_name({"reverse_domain_identifier": rdi}, "ignored") + + +@pytest.mark.parametrize("name", ["", " ", "!", "-", "---"]) +def test_name_no_alphanumeric(name): + with pytest.raises(ValueError, match=f"Name '{name}' contains no alphanumeric characters"): + get_bundle_app_name({}, name) From 36a147999cd4e6c26de39d6d011cd22d8020ac64 Mon Sep 17 00:00:00 2001 From: Robin <34315751+lrandersson@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:53:10 -0500 Subject: [PATCH 50/54] MSI: Integration tests (#1133) * Add tests * Some more fixes * Test commit to see if this resolves test failure * Set version back to X also for the other failing test * Review fixes * pre-commit * Add str conversion * Remove request usage for MSI --- .github/workflows/main.yml | 2 + constructor/briefcase.py | 23 ++- constructor/briefcase/run_installation.bat | 6 +- examples/azure_signtool/construct.yaml | 2 +- examples/custom_nsis_template/construct.yaml | 2 +- examples/customize_controls/construct.yaml | 2 +- .../construct.yaml | 2 +- examples/exe_extra_pages/construct.yaml | 2 +- examples/extra_envs/construct.yaml | 4 +- examples/extra_files/construct.yaml | 2 +- examples/from_env_txt/construct.yaml | 2 +- examples/from_env_yaml/construct.yaml | 4 +- examples/from_existing_env/construct.yaml | 2 +- examples/from_explicit/construct.yaml | 2 +- examples/initialization/construct.yaml | 2 +- examples/miniforge-mamba2/construct.yaml | 2 +- examples/miniforge/construct.yaml | 2 +- examples/mirrored_channels/construct.yaml | 2 +- examples/noconda/constructor_input.yaml | 2 +- examples/outputs/construct.yaml | 2 +- examples/protected_base/construct.yaml | 4 +- examples/register_envs/construct.yaml | 4 +- examples/scripts/construct.yaml | 4 +- examples/scripts/post_install.bat | 2 +- examples/scripts/post_install.sh | 2 +- examples/scripts/pre_install.bat | 2 +- examples/scripts/pre_install.sh | 2 +- examples/shortcuts/construct.yaml | 2 +- examples/signing/construct.yaml | 2 +- examples/virtual_specs_failed/construct.yaml | 2 +- examples/virtual_specs_ok/construct.yaml | 2 +- tests/test_examples.py | 172 ++++++++++++++++-- tests/test_main.py | 2 +- 33 files changed, 217 insertions(+), 54 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 31bbdef95..e9203347a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -134,6 +134,7 @@ jobs: run: conda list - name: conda config run: conda config --show-sources + - name: Run unit tests run: | pytest -vv --cov=constructor --cov-branch tests/ -m "not examples" @@ -152,6 +153,7 @@ jobs: AZURE_SIGNTOOL_KEY_VAULT_URL: ${{ secrets.AZURE_SIGNTOOL_KEY_VAULT_URL }} CONSTRUCTOR_EXAMPLES_KEEP_ARTIFACTS: "${{ runner.temp }}/examples_artifacts" CONSTRUCTOR_SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x86/signtool.exe" + CONSTRUCTOR_VERBOSE: 1 run: | rm -rf coverage.json pytest -vv --cov=constructor --cov-branch tests/test_examples.py diff --git a/constructor/briefcase.py b/constructor/briefcase.py index 70569c794..1d8880485 100644 --- a/constructor/briefcase.py +++ b/constructor/briefcase.py @@ -5,12 +5,17 @@ import logging import re import shutil +import sys import sysconfig import tempfile from pathlib import Path from subprocess import run -import tomli_w +IS_WINDOWS = sys.platform == "win32" +if IS_WINDOWS: + import tomli_w +else: + tomli_w = None # This file is only intended for Windows use from . import preconda from .utils import DEFAULT_REVERSE_DOMAIN_ID, copy_conda_exe, filename_dist @@ -100,6 +105,16 @@ def get_bundle_app_name(info, name): return bundle, app_name +def get_license(info): + """Retrieve the specified license as a dict or return a placeholder if not set.""" + + if "license_file" in info: + return {"file": info["license_file"]} + + placeholder_license = Path(__file__).parent / "nsis" / "placeholder_license.txt" + return {"file": str(placeholder_license)} # convert to str for TOML serialization + + # Create a Briefcase configuration file. Using a full TOML writer rather than a Jinja # template allows us to avoid escaping strings everywhere. def write_pyproject_toml(tmp_dir, info): @@ -110,7 +125,7 @@ def write_pyproject_toml(tmp_dir, info): "project_name": name, "bundle": bundle, "version": version, - "license": ({"file": info["license_file"]} if "license_file" in info else {"text": ""}), + "license": get_license(info), "app": { app_name: { "formal_name": f"{info['name']} {info['version']}", @@ -130,6 +145,9 @@ def write_pyproject_toml(tmp_dir, info): def create(info, verbose=False): + if not IS_WINDOWS: + raise Exception(f"Invalid platform '{sys.platform}'. Only Windows is supported.") + tmp_dir = Path(tempfile.mkdtemp()) write_pyproject_toml(tmp_dir, info) @@ -150,7 +168,6 @@ def create(info, verbose=False): raise FileNotFoundError( f"Dependency 'briefcase' does not seem to be installed.\nTried: {briefcase}" ) - logger.info("Building installer") run( [briefcase, "package"] + (["-v"] if verbose else []), diff --git a/constructor/briefcase/run_installation.bat b/constructor/briefcase/run_installation.bat index 190a6d9f7..267907bec 100644 --- a/constructor/briefcase/run_installation.bat +++ b/constructor/briefcase/run_installation.bat @@ -1,5 +1,5 @@ -set PREFIX=%cd% -_conda constructor --prefix %PREFIX% --extract-conda-pkgs +set "PREFIX=%cd%" +_conda constructor --prefix "%PREFIX%" --extract-conda-pkgs set CONDA_PROTECT_FROZEN_ENVS=0 set CONDA_ROOT_PREFIX=%PREFIX% @@ -7,4 +7,4 @@ set CONDA_SAFETY_CHECKS=disabled set CONDA_EXTRA_SAFETY_CHECKS=no set CONDA_PKGS_DIRS=%PREFIX%\pkgs -_conda install --offline --file %PREFIX%\conda-meta\initial-state.explicit.txt -yp %PREFIX% +_conda install --offline --file "%PREFIX%\conda-meta\initial-state.explicit.txt" -yp "%PREFIX%" diff --git a/examples/azure_signtool/construct.yaml b/examples/azure_signtool/construct.yaml index 86a6fb983..96498a702 100644 --- a/examples/azure_signtool/construct.yaml +++ b/examples/azure_signtool/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: Signed_AzureSignTool -version: X +version: 1.0.0 installer_type: exe channels: - https://repo.anaconda.com/pkgs/main/ diff --git a/examples/custom_nsis_template/construct.yaml b/examples/custom_nsis_template/construct.yaml index 4b8eab0b4..4a59f423a 100644 --- a/examples/custom_nsis_template/construct.yaml +++ b/examples/custom_nsis_template/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: custom -version: X +version: 1.0.0 ignore_duplicate_files: True installer_filename: {{ name }}-installer.exe installer_type: exe diff --git a/examples/customize_controls/construct.yaml b/examples/customize_controls/construct.yaml index 907ba11c9..161ac88e9 100644 --- a/examples/customize_controls/construct.yaml +++ b/examples/customize_controls/construct.yaml @@ -3,7 +3,7 @@ name: NoCondaOptions version: X -installer_type: all +installer_type: {{ "exe" if os.name == "nt" else "all" }} channels: - https://repo.anaconda.com/pkgs/main/ diff --git a/examples/customized_welcome_conclusion/construct.yaml b/examples/customized_welcome_conclusion/construct.yaml index 751e1305c..f02e55265 100644 --- a/examples/customized_welcome_conclusion/construct.yaml +++ b/examples/customized_welcome_conclusion/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: CustomizedWelcomeConclusion -version: X +version: 1.0.0 installer_type: all channels: - https://repo.anaconda.com/pkgs/main/ diff --git a/examples/exe_extra_pages/construct.yaml b/examples/exe_extra_pages/construct.yaml index 5452ce126..957e926c3 100644 --- a/examples/exe_extra_pages/construct.yaml +++ b/examples/exe_extra_pages/construct.yaml @@ -7,7 +7,7 @@ {% set name = "extraPageSingle" %} {% endif %} name: {{ name }} -version: X +version: 1.0.0 installer_type: all channels: - https://repo.anaconda.com/pkgs/main/ diff --git a/examples/extra_envs/construct.yaml b/examples/extra_envs/construct.yaml index e7370e72c..58d1d5c3a 100644 --- a/examples/extra_envs/construct.yaml +++ b/examples/extra_envs/construct.yaml @@ -2,8 +2,8 @@ "$schema": "../../constructor/data/construct.schema.json" name: ExtraEnvs -version: X -installer_type: all +version: 1.0.0 +installer_type: {{ "exe" if os.name == "nt" else "all" }} channels: - https://conda.anaconda.org/conda-forge specs: diff --git a/examples/extra_files/construct.yaml b/examples/extra_files/construct.yaml index 7b8c9a600..fa82dbfad 100644 --- a/examples/extra_files/construct.yaml +++ b/examples/extra_files/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: ExtraFiles -version: X +version: 1.0.0 installer_type: all license_file: TEST_LICENSE.txt check_path_spaces: False diff --git a/examples/from_env_txt/construct.yaml b/examples/from_env_txt/construct.yaml index ee8412dc7..5cb7ae774 100644 --- a/examples/from_env_txt/construct.yaml +++ b/examples/from_env_txt/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: EnvironmentTXT -version: X +version: 1.0.0 installer_type: all environment_file: env.txt initialize_by_default: false diff --git a/examples/from_env_yaml/construct.yaml b/examples/from_env_yaml/construct.yaml index d86bdeafb..6711be0c9 100644 --- a/examples/from_env_yaml/construct.yaml +++ b/examples/from_env_yaml/construct.yaml @@ -2,8 +2,8 @@ "$schema": "../../constructor/data/construct.schema.json" name: EnvironmentYAML -version: X -installer_type: all +version: 1.0.0 +installer_type: {{ "exe" if os.name == "nt" else "all" }} environment_file: env.yaml initialize_by_default: false register_python: False diff --git a/examples/from_existing_env/construct.yaml b/examples/from_existing_env/construct.yaml index 89df7b411..b45540a0b 100644 --- a/examples/from_existing_env/construct.yaml +++ b/examples/from_existing_env/construct.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=../../constructor/data/construct.schema.json "$schema": "../../constructor/data/construct.schema.json" name: Existing -version: X +version: 1.0.0 installer_type: all environment: {{ os.environ.get("CONSTRUCTOR_TEST_EXISTING_ENV", os.environ["CONDA_PREFIX"]) }} channels: diff --git a/examples/from_explicit/construct.yaml b/examples/from_explicit/construct.yaml index 9137fa8f7..6e07790cd 100644 --- a/examples/from_explicit/construct.yaml +++ b/examples/from_explicit/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: Explicit -version: X +version: 1.0.0 installer_type: all environment_file: explicit_linux-64.txt initialize_by_default: false diff --git a/examples/initialization/construct.yaml b/examples/initialization/construct.yaml index 1e980532f..8f38e6631 100644 --- a/examples/initialization/construct.yaml +++ b/examples/initialization/construct.yaml @@ -18,4 +18,4 @@ initialize_by_default: true register_python: false check_path_spaces: true check_path_length: false -installer_type: all +installer_type: {{ "exe" if os.name == "nt" else "all" }} diff --git a/examples/miniforge-mamba2/construct.yaml b/examples/miniforge-mamba2/construct.yaml index 98feefec3..becb523f4 100644 --- a/examples/miniforge-mamba2/construct.yaml +++ b/examples/miniforge-mamba2/construct.yaml @@ -21,7 +21,7 @@ specs: - miniforge_console_shortcut 1.* # [win] # Added for extra testing -installer_type: all +installer_type: {{ "exe" if os.name == "nt" else "all" }} post_install: test_install.sh # [unix] post_install: test_install.bat # [win] initialize_by_default: false diff --git a/examples/miniforge/construct.yaml b/examples/miniforge/construct.yaml index eb894cc91..52b961da9 100644 --- a/examples/miniforge/construct.yaml +++ b/examples/miniforge/construct.yaml @@ -21,7 +21,7 @@ specs: - miniforge_console_shortcut 1.* # [win] # Added for extra testing -installer_type: all +installer_type: {{ "exe" if os.name == "nt" else "all" }} post_install: test_install.sh # [unix] post_install: test_install.bat # [win] initialize_by_default: false diff --git a/examples/mirrored_channels/construct.yaml b/examples/mirrored_channels/construct.yaml index f105c6d0c..6e7ab9d81 100644 --- a/examples/mirrored_channels/construct.yaml +++ b/examples/mirrored_channels/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: Mirrors -version: X +version: 1.0.0 channels: - conda-forge diff --git a/examples/noconda/constructor_input.yaml b/examples/noconda/constructor_input.yaml index 0a17e6cb2..b5641de01 100644 --- a/examples/noconda/constructor_input.yaml +++ b/examples/noconda/constructor_input.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: NoConda -version: X +version: 1.0.0 installer_type: all channels: - https://repo.anaconda.com/pkgs/main/ diff --git a/examples/outputs/construct.yaml b/examples/outputs/construct.yaml index 01aa24a0a..9080dc36d 100644 --- a/examples/outputs/construct.yaml +++ b/examples/outputs/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: Outputs -version: X +version: 1.0.0 installer_type: sh # [unix] installer_type: exe # [win] channels: diff --git a/examples/protected_base/construct.yaml b/examples/protected_base/construct.yaml index c43044761..fcccc41f1 100644 --- a/examples/protected_base/construct.yaml +++ b/examples/protected_base/construct.yaml @@ -2,8 +2,8 @@ "$schema": "../../constructor/data/construct.schema.json" name: ProtectedBaseEnv -version: X -installer_type: all +version: 1.0.0 +installer_type: {{ "exe" if os.name == "nt" else "all" }} channels: - defaults diff --git a/examples/register_envs/construct.yaml b/examples/register_envs/construct.yaml index 86d621561..31d72c9f6 100644 --- a/examples/register_envs/construct.yaml +++ b/examples/register_envs/construct.yaml @@ -2,8 +2,8 @@ "$schema": "../../constructor/data/construct.schema.json" name: RegisterEnvs -version: X -installer_type: all +version: 1.0.0 +installer_type: {{ "exe" if os.name == "nt" else "all" }} channels: - https://repo.anaconda.com/pkgs/main/ specs: diff --git a/examples/scripts/construct.yaml b/examples/scripts/construct.yaml index 33dcfee91..d55057bbd 100644 --- a/examples/scripts/construct.yaml +++ b/examples/scripts/construct.yaml @@ -2,8 +2,8 @@ "$schema": "../../constructor/data/construct.schema.json" name: Scripts -version: X -installer_type: all +version: 1.0.0 +installer_type: {{ "exe" if os.name == "nt" else "all" }} channels: - https://repo.anaconda.com/pkgs/main/ specs: diff --git a/examples/scripts/post_install.bat b/examples/scripts/post_install.bat index 7916add93..9739b6a5f 100644 --- a/examples/scripts/post_install.bat +++ b/examples/scripts/post_install.bat @@ -1,6 +1,6 @@ echo Added by post-install script > "%PREFIX%\post_install_sentinel.txt" if not "%INSTALLER_NAME%" == "Scripts" exit 1 -if not "%INSTALLER_VER%" == "X" exit 1 +if not "%INSTALLER_VER%" == "1.0.0" exit 1 if not "%INSTALLER_PLAT%" == "win-64" exit 1 if not "%INSTALLER_TYPE%" == "EXE" exit 1 if not "%INSTALLER_UNATTENDED%" == "1" exit 1 diff --git a/examples/scripts/post_install.sh b/examples/scripts/post_install.sh index 06bcafb78..5880488c7 100644 --- a/examples/scripts/post_install.sh +++ b/examples/scripts/post_install.sh @@ -15,7 +15,7 @@ echo "CUSTOM_VARIABLE_2=${CUSTOM_VARIABLE_2}" echo "PREFIX=${PREFIX}" test "${INSTALLER_NAME}" = "Scripts" -test "${INSTALLER_VER}" = "X" +test "${INSTALLER_VER}" = "1.0.0" # shellcheck disable=SC2016 # String interpolation disabling is deliberate test "${CUSTOM_VARIABLE_1}" = 'FIR$T-CUSTOM_'\''STRING'\'' WITH SPACES AND @*! "CHARACTERS"' # shellcheck disable=SC2016 # String interpolation disabling is deliberate diff --git a/examples/scripts/pre_install.bat b/examples/scripts/pre_install.bat index ec4fce07c..22a529daa 100644 --- a/examples/scripts/pre_install.bat +++ b/examples/scripts/pre_install.bat @@ -1,5 +1,5 @@ if not "%INSTALLER_NAME%" == "Scripts" exit 1 -if not "%INSTALLER_VER%" == "X" exit 1 +if not "%INSTALLER_VER%" == "1.0.0" exit 1 if not "%INSTALLER_PLAT%" == "win-64" exit 1 if not "%INSTALLER_TYPE%" == "EXE" exit 1 if not "%INSTALLER_UNATTENDED%" == "1" exit 1 diff --git a/examples/scripts/pre_install.sh b/examples/scripts/pre_install.sh index 753db8121..88b111630 100644 --- a/examples/scripts/pre_install.sh +++ b/examples/scripts/pre_install.sh @@ -12,7 +12,7 @@ echo "CUSTOM_VARIABLE_2=${CUSTOM_VARIABLE_2}" echo "PREFIX=${PREFIX}" test "${INSTALLER_NAME}" = "Scripts" -test "${INSTALLER_VER}" = "X" +test "${INSTALLER_VER}" = "1.0.0" # shellcheck disable=SC2016 # String interpolation disabling is deliberate test "${CUSTOM_VARIABLE_1}" = 'FIR$T-CUSTOM_'\''STRING'\'' WITH SPACES AND @*! "CHARACTERS"' # shellcheck disable=SC2016 # String interpolation disabling is deliberate diff --git a/examples/shortcuts/construct.yaml b/examples/shortcuts/construct.yaml index a17be497c..e7c8877f4 100644 --- a/examples/shortcuts/construct.yaml +++ b/examples/shortcuts/construct.yaml @@ -3,7 +3,7 @@ name: MinicondaWithShortcuts version: X -installer_type: all +installer_type: {{ "exe" if os.name == "nt" else "all" }} channels: - conda-test/label/menuinst-tests diff --git a/examples/signing/construct.yaml b/examples/signing/construct.yaml index 07bf9685f..0889a3387 100644 --- a/examples/signing/construct.yaml +++ b/examples/signing/construct.yaml @@ -2,7 +2,7 @@ "$schema": "../../constructor/data/construct.schema.json" name: Signed -version: X +version: 1.0.0 installer_type: all channels: - https://repo.anaconda.com/pkgs/main/ diff --git a/examples/virtual_specs_failed/construct.yaml b/examples/virtual_specs_failed/construct.yaml index f3b554872..12d886b9f 100644 --- a/examples/virtual_specs_failed/construct.yaml +++ b/examples/virtual_specs_failed/construct.yaml @@ -22,4 +22,4 @@ initialize_by_default: false register_python: false check_path_spaces: false check_path_length: false -installer_type: all +installer_type: {{ "exe" if os.name == "nt" else "all" }} diff --git a/examples/virtual_specs_ok/construct.yaml b/examples/virtual_specs_ok/construct.yaml index 41635eefc..15655811b 100644 --- a/examples/virtual_specs_ok/construct.yaml +++ b/examples/virtual_specs_ok/construct.yaml @@ -22,4 +22,4 @@ initialize_by_default: false register_python: false check_path_spaces: false check_path_length: false -installer_type: all +installer_type: {{ "exe" if os.name == "nt" else "all" }} diff --git a/tests/test_examples.py b/tests/test_examples.py index 2561d9c44..a8315eb5c 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,5 +1,6 @@ from __future__ import annotations +import ctypes import getpass import json import os @@ -51,6 +52,7 @@ REPO_DIR = Path(__file__).parent.parent ON_CI = bool(os.environ.get("CI")) and os.environ.get("CI") != "0" CONSTRUCTOR_CONDA_EXE = os.environ.get("CONSTRUCTOR_CONDA_EXE") +CONSTRUCTOR_VERBOSE = os.environ.get("CONSTRUCTOR_VERBOSE") CONDA_EXE, CONDA_EXE_VERSION = identify_conda_exe(CONSTRUCTOR_CONDA_EXE) if CONDA_EXE_VERSION is not None: CONDA_EXE_VERSION = Version(CONDA_EXE_VERSION) @@ -336,6 +338,98 @@ def _sentinel_file_checks(example_path, install_dir): ) +def is_admin() -> bool: + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except Exception: + return False + + +def calculate_msi_install_path(installer: Path) -> Path: + """This is a temporary solution for now since we cannot choose the install location ourselves. + Installers are named --Windows-x86_64.msi. + """ + dir_name = installer.name.replace("-Windows-x86_64.msi", "").replace("-", " ") + if is_admin(): + root_dir = Path(os.environ.get("PROGRAMFILES", r"C:\Program Files")) + else: + local_dir = os.environ.get("LOCALAPPDATA", str(Path.home() / r"AppData\Local")) + root_dir = Path(local_dir) / "Programs" + + assert root_dir.is_dir() # Sanity check to avoid strange unexpected errors + return Path(root_dir) / dir_name + + +def _run_installer_msi( + installer: Path, + install_dir: Path, + installer_input=None, + timeout=420, + check=True, + options: list | None = None, +): + """Runs specified MSI Installer via command line in silent mode. This is work in progress.""" + if not sys.platform.startswith("win"): + raise ValueError("Can only run .msi installers on Windows") + + # Currently we only have 1 test that specifies options, so this is a temporary "fix" + if options: + allusers = "/InstallationType=AllUsers" in options + else: + allusers = False + + cmd = [ + "msiexec.exe", + "/i", + str(installer), + "ALLUSERS=1" + if allusers + else "MSIINSTALLPERUSER=1", # For some reason tests fail on the CI system if "ALLUSERS=1" + "/qn", + ] + + log_path = Path(os.environ.get("TEMP")) / (install_dir.name + ".log") + cmd.extend(["/L*V", str(log_path)]) + try: + process = _execute(cmd, installer_input=installer_input, timeout=timeout, check=check) + except subprocess.CalledProcessError as e: + if log_path.exists(): + # When running on the CI system, it tries to decode a UTF-16 log file as UTF-8, + # therefore we need to specify encoding before printing. + print(f"\n=== MSI LOG {log_path} START ===") + print( + log_path.read_text(encoding="utf-16", errors="replace")[-15000:] + ) # last 15k chars + print(f"\n=== MSI LOG {log_path} END ===") + raise e + if check: + print("A check for MSI Installers not yet implemented") + return process + + +def _run_uninstaller_msi( + installer: Path, + install_dir: Path, + timeout: int = 420, + check: bool = True, +) -> subprocess.CompletedProcess | None: + cmd = [ + "msiexec.exe", + "/x", + str(installer), + "/qn", + ] + process = _execute(cmd, timeout=timeout, check=check) + if check: + # TODO: + # Check log and if there are remaining files, similar to the exe installers + pass + # This is temporary until uninstallation works fine + shutil.rmtree(str(install_dir), ignore_errors=True) + + return process + + def _run_installer( example_path: Path, installer: Path, @@ -381,12 +475,27 @@ def _run_installer( timeout=timeout, check=check_subprocess, ) + elif installer.suffix == ".msi": + process = _run_installer_msi( + installer, + install_dir, + installer_input=installer_input, + timeout=timeout, + check=check_subprocess, + options=options, + ) else: raise ValueError(f"Unknown installer type: {installer.suffix}") - if check_sentinels and not (installer.suffix == ".pkg" and ON_CI): + + if installer.suffix == ".msi": + print("sentinel_file_checks for MSI installers not yet implemented") + elif 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) + if uninstall: + if installer.suffix == ".msi": + _run_uninstaller_msi(installer, install_dir, timeout=timeout, check=check_subprocess) + elif installer.suffix == ".exe": + _run_uninstaller_exe(install_dir, timeout=timeout, check=check_subprocess) return process @@ -406,16 +515,19 @@ def create_installer( output_dir = workspace / "installer" output_dir.mkdir(parents=True, exist_ok=True) - cmd = [ - *COV_CMD, - "constructor", - "-v", + cmd = [*COV_CMD, "constructor"] + # This flag will (if enabled) create a lot of output upon test failures for .exe-installers. + # If debugging generated NSIS templates, it can be worth to enable. + if CONSTRUCTOR_VERBOSE: + cmd.append("-v") + cmd += [ str(input_dir), "--output-dir", str(output_dir), "--config-filename", config_filename, ] + if conda_exe: cmd.extend(["--conda-exe", conda_exe]) if debug: @@ -429,18 +541,21 @@ def create_installer( def _sort_by_extension(path): "Return shell installers first so they are run before the GUI ones" - return {"sh": 1, "pkg": 2, "exe": 3}[path.suffix[1:]], path + return {"sh": 1, "pkg": 2, "exe": 3, "msi": 4}[path.suffix[1:]], path - installers = (p for p in output_dir.iterdir() if p.suffix in (".exe", ".sh", ".pkg")) + installers = (p for p in output_dir.iterdir() if p.suffix in (".exe", ".msi", ".sh", ".pkg")) for installer in sorted(installers, key=_sort_by_extension): if installer.suffix == ".pkg" and ON_CI: install_dir = Path("~").expanduser() / calculate_install_dir( input_dir / config_filename ) + elif installer.suffix == ".msi": + install_dir = calculate_msi_install_path(installer) else: install_dir = ( workspace / f"{install_dir_prefix}-{installer.stem}-{installer.suffix[1:]}" ) + yield installer, install_dir if KEEP_ARTIFACTS_PATH: try: @@ -528,13 +643,23 @@ def test_example_extra_envs(tmp_path, request): assert "@EXPLICIT" in envtxt.read_text() if sys.platform.startswith("win"): - _run_uninstaller_exe(install_dir=install_dir) + if installer.suffix == ".msi": + _run_uninstaller_msi(installer, install_dir) + else: + _run_uninstaller_exe(install_dir=install_dir) def test_example_extra_files(tmp_path, request): input_path = _example_path("extra_files") for installer, install_dir in create_installer(input_path, tmp_path, with_spaces=True): - _run_installer(input_path, installer, install_dir, request=request) + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_sentinels=CONSTRUCTOR_VERBOSE, + check_subprocess=CONSTRUCTOR_VERBOSE, + ) def test_example_mirrored_channels(tmp_path, request): @@ -611,6 +736,10 @@ def test_example_miniforge(tmp_path, request, example): raise AssertionError("Could not find Start Menu folder for miniforge") _run_uninstaller_exe(install_dir) assert not list(start_menu_dir.glob("Miniforge*.lnk")) + elif installer.suffix == ".msi": + # TODO: Start menus + _run_uninstaller_msi(installer, install_dir) + raise NotImplementedError("Test needs to be implemented") def test_example_noconda(tmp_path, request): @@ -774,7 +903,10 @@ def test_example_shortcuts(tmp_path, request): break else: raise AssertionError("No shortcuts found!") - _run_uninstaller_exe(install_dir) + if installer.suffix == ".msi": + _run_uninstaller_msi(installer, install_dir) + else: + _run_uninstaller_exe(install_dir) assert not (package_1 / "A.lnk").is_file() assert not (package_1 / "B.lnk").is_file() elif sys.platform == "darwin": @@ -916,8 +1048,11 @@ def test_example_from_explicit(tmp_path, request): def test_register_envs(tmp_path, request): + """Verify that 'register_envs: False' results in the environment not being registered.""" input_path = _example_path("register_envs") for installer, install_dir in create_installer(input_path, tmp_path): + if installer.suffix == ".msi": + raise NotImplementedError("Test for 'register_envs' not yet implemented for MSI") _run_installer(input_path, installer, install_dir, request=request) environments_txt = Path("~/.conda/environments.txt").expanduser().read_text() assert str(install_dir) not in environments_txt @@ -978,6 +1113,7 @@ def test_cross_osx_building(tmp_path): ) +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Unix only") def test_cross_build_example(tmp_path, platform_conda_exe): platform, conda_exe = platform_conda_exe input_path = _example_path("virtual_specs_ok") @@ -993,6 +1129,7 @@ def test_cross_build_example(tmp_path, platform_conda_exe): def test_virtual_specs_failed(tmp_path, request): + """Verify that virtual packages listed via 'virtual_specs' are satisfied.""" input_path = _example_path("virtual_specs_failed") for installer, install_dir in create_installer(input_path, tmp_path): process = _run_installer( @@ -1008,6 +1145,8 @@ def test_virtual_specs_failed(tmp_path, request): with pytest.raises(AssertionError, match="Failed to check virtual specs"): _check_installer_log(install_dir) continue + elif installer.suffix == ".msi": + raise NotImplementedError("Test for 'virtual_specs' not yet implemented for MSI") elif installer.suffix == ".pkg": if not ON_CI: continue @@ -1070,6 +1209,8 @@ def test_initialization(tmp_path, request, monkeypatch, method): # GHA runs on an admin user account, but AllUsers (admin) installs # do not add to PATH due to CVE-2022-26526, so force single user install options = ["/AddToPath=1", "/InstallationType=JustMe"] + elif installer.suffix == ".msi": + raise NotImplementedError("Test needs to be implemented") else: options = [] _run_installer( @@ -1103,6 +1244,8 @@ def test_initialization(tmp_path, request, monkeypatch, method): finally: _run_uninstaller_exe(install_dir, check=True) + elif installer.suffix == ".msi": + raise NotImplementedError("Test needs to be implemented") else: # GHA's Ubuntu needs interactive, but macOS wants login :shrug: login_flag = "-i" if sys.platform.startswith("linux") else "-l" @@ -1356,7 +1499,8 @@ def test_uninstallation_standalone( check_subprocess=True, uninstall=False, ) - + if installer.suffix == ".msi": + raise NotImplementedError("Test needs to be implemented") # Set up files for removal. # Since conda-standalone is extensively tested upstream, # only set up a minimum set of files. @@ -1475,7 +1619,7 @@ def test_not_in_installed_menu_list_(tmp_path, request, no_registry): """Verify the app is in the Installed Apps Menu (or not), based on the CLI arg '/NoRegistry'. If NoRegistry=0, we expect to find the installer in the Menu, otherwise not. """ - input_path = _example_path("extra_files") # The specific example we use here is not important + input_path = _example_path("register_envs") # The specific example we use here is not important options = ["/InstallationType=JustMe", f"/NoRegistry={no_registry}"] for installer, install_dir in create_installer(input_path, tmp_path): _run_installer( diff --git a/tests/test_main.py b/tests/test_main.py index 896c7d8a6..e17f69f54 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,7 +7,7 @@ def test_dry_run(tmp_path): inputfile = dedent( """ name: test_schema_validation - version: X + version: 1.0.0 installer_type: all channels: - https://repo.anaconda.com/pkgs/main/ From 7e7b8bc5be0f759052c751d7d4de0d48696f7b69 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 25 Nov 2025 16:01:43 -0500 Subject: [PATCH 51/54] Add Install Options Page - WIP --- constructor/briefcase.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/constructor/briefcase.py b/constructor/briefcase.py index 1d8880485..c01ccfb1f 100644 --- a/constructor/briefcase.py +++ b/constructor/briefcase.py @@ -114,6 +114,33 @@ def get_license(info): placeholder_license = Path(__file__).parent / "nsis" / "placeholder_license.txt" return {"file": str(placeholder_license)} # convert to str for TOML serialization +def create_install_options_list(info: dict) -> list[dict]: + """Returns a list of dicts with data formatted for the installation options page.""" + options = [] + register_python = info.get("register_python", True) + if register_python: + options.append( + { + "name": "register_python", + "title": "Register Python as System Default", + "description": "TODO: Register Python description", + "default": info.get("register_python_default", False), + } + ) + initialize_conda = info.get("initialize_conda", "classic") + if initialize_conda: + # TODO: How would we distinguish between True/classic in the UI? Same for NSIS + options.append( + { + "name": "initialize_conda", + "title": "Initialize Conda", + "description": "TODO: Initialize conda description", + "default": info.get("initialize_by_default", False), + } + ) + + return options + # Create a Briefcase configuration file. Using a full TOML writer rather than a Jinja # template allows us to avoid escaping strings everywhere. @@ -134,6 +161,7 @@ def write_pyproject_toml(tmp_dir, info): "use_full_install_path": False, "install_launcher": False, "post_install_script": str(BRIEFCASE_DIR / "run_installation.bat"), + "install_option": create_install_options_list(info), } }, } From a83410cea739e5de1ba93eb4c614ce8848e680d5 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 17 Dec 2025 12:13:54 -0500 Subject: [PATCH 52/54] Review fixes --- constructor/briefcase.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/constructor/briefcase.py b/constructor/briefcase.py index c01ccfb1f..b3dde12ed 100644 --- a/constructor/briefcase.py +++ b/constructor/briefcase.py @@ -119,22 +119,25 @@ def create_install_options_list(info: dict) -> list[dict]: options = [] register_python = info.get("register_python", True) if register_python: + python_version = f"{sys.version_info.major}.{sys.version_info.minor}" options.append( { "name": "register_python", - "title": "Register Python as System Default", - "description": "TODO: Register Python description", + "title": f"Register {info['name']} as my default Python {python_version}.", + "description": "Allows other programs, such as VSCode, PyCharm, etc. to automatically " + f"detect {info['name']} as the primary Python {python_version} on the system.", "default": info.get("register_python_default", False), } ) initialize_conda = info.get("initialize_conda", "classic") if initialize_conda: - # TODO: How would we distinguish between True/classic in the UI? Same for NSIS + # TODO: How would we distinguish between condabin/classic in the UI? options.append( { "name": "initialize_conda", - "title": "Initialize Conda", - "description": "TODO: Initialize conda description", + "title": "Add installation to my PATH environment variable", + "description": "NOT recommended. This can lead to conflicts with other applications. " + "Instead, use the Commmand Prompt and Powershell menus added to the Windows Start Menu.", "default": info.get("initialize_by_default", False), } ) From 575b4543fbc4436e9c3950a1b5cec399d1ecd6d9 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 17 Dec 2025 12:32:48 -0500 Subject: [PATCH 53/54] Fix description --- constructor/briefcase.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/constructor/briefcase.py b/constructor/briefcase.py index b3dde12ed..2f45c4772 100644 --- a/constructor/briefcase.py +++ b/constructor/briefcase.py @@ -132,12 +132,18 @@ def create_install_options_list(info: dict) -> list[dict]: initialize_conda = info.get("initialize_conda", "classic") if initialize_conda: # TODO: How would we distinguish between condabin/classic in the UI? + if initialize_conda == "condabin": + description = "Adds condabin, which only contains the 'conda' executables, to PATH. " + "Does not require special shortcuts but activation needs " + "to be performed manually." + else: + description = "NOT recommended. This can lead to conflicts with other applications. " + "Instead, use the Commmand Prompt and Powershell menus added to the Windows Start Menu." options.append( { "name": "initialize_conda", "title": "Add installation to my PATH environment variable", - "description": "NOT recommended. This can lead to conflicts with other applications. " - "Instead, use the Commmand Prompt and Powershell menus added to the Windows Start Menu.", + "description": description, "default": info.get("initialize_by_default", False), } ) From 6fb710d0e72d4cbe55336b25b947533cd009e609 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 16 Jan 2026 08:18:58 -0500 Subject: [PATCH 54/54] Remove comment and fix format --- constructor/briefcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constructor/briefcase.py b/constructor/briefcase.py index 2f45c4772..2f659c0ef 100644 --- a/constructor/briefcase.py +++ b/constructor/briefcase.py @@ -114,6 +114,7 @@ def get_license(info): placeholder_license = Path(__file__).parent / "nsis" / "placeholder_license.txt" return {"file": str(placeholder_license)} # convert to str for TOML serialization + def create_install_options_list(info: dict) -> list[dict]: """Returns a list of dicts with data formatted for the installation options page.""" options = [] @@ -131,7 +132,6 @@ def create_install_options_list(info: dict) -> list[dict]: ) initialize_conda = info.get("initialize_conda", "classic") if initialize_conda: - # TODO: How would we distinguish between condabin/classic in the UI? if initialize_conda == "condabin": description = "Adds condabin, which only contains the 'conda' executables, to PATH. " "Does not require special shortcuts but activation needs "