diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 39e79609a..d1799f112 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -127,6 +127,7 @@ jobs: conda activate constructor-dev echo "CONSTRUCTOR_CONDA_EXE=$CONDA_PREFIX/standalone_conda/conda.exe" >> $GITHUB_ENV fi + echo "CONDA_PREFIX=$CONDA_PREFIX" >> $GITHUB_ENV - name: conda info run: conda info - name: conda list diff --git a/CONSTRUCT.md b/CONSTRUCT.md index adbcc0bac..611493d04 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -491,7 +491,16 @@ If `header_image` is not provided, use this text when generating the image Add an option to the installer so the user can choose whether to run `conda init` after the installation (Unix), or to add certain subdirectories of the installation -to PATH (Windows). See also `initialize_by_default`. +to PATH (Windows). Requires `conda` to be part of the `base` environment. Valid options: + +- `classic` or `True`: runs `conda init` on Unix, which injects a shell function in the + shell profiles. On Windows, it adds `$INSTDIR`, `$INSTDIR/Scripts`, `$INSTDIR/Library/bin` + to `PATH`. This is the default. +- `condabin`: only adds `$INSTDIR/condabin` to `PATH`. On Unix, `conda>=25.5.0` is required + in `base`. +- `False`: the installer doesn't perform initialization. + +See also `initialize_by_default`. ### `initialize_by_default` @@ -500,7 +509,7 @@ 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`. -Only applies if `initialize_conda` is true. +Only applies if `initialize_conda` is not false. ### `register_python` diff --git a/constructor/_schema.py b/constructor/_schema.py index f456c29c1..6ffc157cf 100644 --- a/constructor/_schema.py +++ b/constructor/_schema.py @@ -50,6 +50,11 @@ class PkgDomains(StrEnum): LOCAL_SYSTEM = "enable_localSystem" +class CondaInitialization(StrEnum): + CLASSIC = "classic" + CONDABIN = "condabin" + + class ChannelRemap(BaseModel): model_config: ConfigDict = _base_config_dict @@ -649,11 +654,20 @@ class ConstructorConfiguration(BaseModel): If `header_image` is not provided, use this text when generating the image (Windows only). Defaults to `name`. """ - initialize_conda: bool = True + initialize_conda: CondaInitialization | bool = True """ Add an option to the installer so the user can choose whether to run `conda init` after the installation (Unix), or to add certain subdirectories of the installation - to PATH (Windows). See also `initialize_by_default`. + to PATH (Windows). Requires `conda` to be part of the `base` environment. Valid options: + + - `classic` or `True`: runs `conda init` on Unix, which injects a shell function in the + shell profiles. On Windows, it adds `$INSTDIR`, `$INSTDIR/Scripts`, `$INSTDIR/Library/bin` + to `PATH`. This is the default. + - `condabin`: only adds `$INSTDIR/condabin` to `PATH`. On Unix, `conda>=25.5.0` is required + in `base`. + - `False`: the installer doesn't perform initialization. + + See also `initialize_by_default`. """ initialize_by_default: bool | None = None """ @@ -662,7 +676,7 @@ class ConstructorConfiguration(BaseModel): is able to change the default during interactive installation. NOTE: For Windows, `AddToPath` is disabled when `InstallationType=AllUsers`. - Only applies if `initialize_conda` is true. + Only applies if `initialize_conda` is not false. """ register_python: bool = True """ diff --git a/constructor/data/construct.schema.json b/constructor/data/construct.schema.json index 3fcf0e078..7dd3b5398 100644 --- a/constructor/data/construct.schema.json +++ b/constructor/data/construct.schema.json @@ -35,6 +35,14 @@ "title": "ChannelRemap", "type": "object" }, + "CondaInitialization": { + "enum": [ + "classic", + "condabin" + ], + "title": "CondaInitialization", + "type": "string" + }, "ExtraEnv": { "additionalProperties": false, "properties": { @@ -749,14 +757,21 @@ } ], "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 true.", + "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.", "title": "Initialize By Default" }, "initialize_conda": { + "anyOf": [ + { + "$ref": "#/$defs/CondaInitialization" + }, + { + "type": "boolean" + } + ], "default": true, - "description": "Add an option to the installer so the user can choose whether to run `conda init` after the installation (Unix), or to add certain subdirectories of the installation to PATH (Windows). See also `initialize_by_default`.", - "title": "Initialize Conda", - "type": "boolean" + "description": "Add an option to the installer so the user can choose whether to run `conda init` after the installation (Unix), or to add certain subdirectories of the installation to PATH (Windows). Requires `conda` to be part of the `base` environment. Valid options:\n- `classic` or `True`: runs `conda init` on Unix, which injects a shell function in the shell profiles. On Windows, it adds `$INSTDIR`, `$INSTDIR/Scripts`, `$INSTDIR/Library/bin` to `PATH`. This is the default.\n- `condabin`: only adds `$INSTDIR/condabin` to `PATH`. On Unix, `conda>=25.5.0` is required in `base`.\n- `False`: the installer doesn't perform initialization.\nSee also `initialize_by_default`.", + "title": "Initialize Conda" }, "install_in_dependency_order": { "anyOf": [ diff --git a/constructor/header.sh b/constructor/header.sh index 61050677d..013ea454c 100644 --- a/constructor/header.sh +++ b/constructor/header.sh @@ -93,6 +93,7 @@ SKIP_SCRIPTS=0 {%- if enable_shortcuts == "true" %} SKIP_SHORTCUTS=0 {%- endif %} +INIT_CONDA=0 TEST=0 REINSTALL=0 USAGE=" @@ -123,18 +124,30 @@ Installs ${INSTALLER_NAME} ${INSTALLER_VER} -u update an existing installation {%- if has_conda %} -t run package tests after installation (may install conda-build) +{%- if initialize_conda %} +-c run 'conda init{{ ' --condabin' if initialize_conda == 'condabin' else ''}}' after installation (only applies to batch mode) +{%- endif %} {%- endif %} " +{#- # We used to have a getopt version here, falling back to getopts if needed # However getopt is not standardized and the version on Mac has different # behaviour. getopts is good enough for what we need :) # More info: https://unix.stackexchange.com/questions/62950/ +#} +{%- set getopts_str = "bifhkp:s" %} {%- if enable_shortcuts == "true" %} -while getopts "bifhkp:smut" x; do -{%- else %} -while getopts "bifhkp:sut" x; do +{%- set getopts_str = getopts_str ~ "m" %} +{%- endif %} +{%- set getopts_str = getopts_str ~ "u" %} +{%- if has_conda %} +{%- set getopts_str = getopts_str ~ "t" %} +{%- if initialize_conda %} +{%- set getopts_str = getopts_str ~ "c" %} +{%- endif %} {%- endif %} +while getopts "{{ getopts_str }}" x; do case "$x" in h) printf "%s\\n" "$USAGE" @@ -170,6 +183,11 @@ while getopts "bifhkp:sut" x; do t) TEST=1 ;; +{%- if initialize_conda %} + c) + INIT_CONDA=1 + ;; +{%- endif %} {%- endif %} ?) printf "ERROR: did not recognize option '%s', please try -h\\n" "$x" @@ -514,7 +532,7 @@ if [ "$(id -u)" -ne 0 ]; then fi # the third binary payload: the tarball of packages -printf "Unpacking payload ...\n" +printf "Unpacking payload...\n" extract_range "${boundary2}" "${boundary3}" | \ CONDA_QUIET="$BATCH" "$CONDA_EXEC" constructor --extract-tarball --prefix "$PREFIX" @@ -547,12 +565,14 @@ MSGS="$PREFIX/.messages.txt" touch "$MSGS" export FORCE +{#- # original issue report: # https://github.com/ContinuumIO/anaconda-issues/issues/11148 # First try to fix it (this apparently didn't work; QA reported the issue again) # https://github.com/conda/conda/pull/9073 # Avoid silent errors when $HOME is not writable # https://github.com/conda/constructor/pull/669 +#} test -d ~/.conda || mkdir -p ~/.conda >/dev/null 2>/dev/null || test -d ~/.conda || mkdir ~/.conda printf "\nInstalling base environment...\n\n" @@ -635,7 +655,6 @@ rm -rf "$PREFIX/install_tmp" export TMP="$TMP_BACKUP" -#The templating doesn't support nested if statements {%- if has_post_install %} if [ "$SKIP_SCRIPTS" = "1" ]; then printf "WARNING: skipping post_install.sh by user request\\n" >&2 @@ -675,9 +694,66 @@ if [ "${PYTHONPATH:-}" != "" ]; then printf " directories of packages that are compatible with the Python interpreter\\n" printf " in %s: %s\\n" "${INSTALLER_NAME}" "$PREFIX" fi +{% if has_conda %} +{%- if initialize_conda == 'condabin' %} +_maybe_run_conda_init_condabin() { + case $SHELL in + # We call the module directly to avoid issues with spaces in shebang + *zsh) "$PREFIX/bin/python" -m conda init --condabin zsh ;; + *) "$PREFIX/bin/python" -m conda init --condabin ;; + esac +} +{%- elif initialize_conda %} +_maybe_run_conda_init() { + case $SHELL in + # We call the module directly to avoid issues with spaces in shebang + *zsh) "$PREFIX/bin/python" -m conda init zsh ;; + *) "$PREFIX/bin/python" -m conda init ;; + esac + if [ -f "$PREFIX/bin/mamba" ]; then + # If the version of mamba is <2.0.0, we preferably use the `mamba` python module + # to perform the initialization. + # + # Otherwise (i.e. as of 2.0.0), we use the `mamba shell init` command + if [ "$("$PREFIX/bin/mamba" --version | head -n 1 | cut -d' ' -f2 | cut -d'.' -f1)" -lt 2 ]; then + case $SHELL in + # We call the module directly to avoid issues with spaces in shebang + *zsh) "$PREFIX/bin/python" -m mamba.mamba init zsh ;; + *) "$PREFIX/bin/python" -m mamba.mamba init ;; + esac + else + case $SHELL in + *zsh) "$PREFIX/bin/mamba" shell init --shell zsh ;; + *) "$PREFIX/bin/mamba" shell init ;; + esac + fi + fi +} +{%- endif %} +{%- endif %} if [ "$BATCH" = "0" ]; then -{%- if has_conda and initialize_conda %} +{%- if has_conda %} +{%- if initialize_conda == 'condabin' %} + DEFAULT={{ 'yes' if initialize_by_default else 'no' }} + + printf "Do you wish to update your shell profile to add '%s/condabin' to PATH?\\n" "$PREFIX" + printf "This will enable you to run 'conda' anywhere, without injecting a shell function.\\n" + printf "You can undo this by running \`conda init --condabin --reverse? [yes|no]\\n" + printf "[%s] >>> " "$DEFAULT" + read -r ans + if [ "$ans" = "" ]; then + ans=$DEFAULT + fi + ans=$(echo "${ans}" | tr '[:lower:]' '[:upper:]') + if [ "$ans" != "YES" ] && [ "$ans" != "Y" ] + then + printf "\\n" + printf "'%s/condabin' will not be added to PATH.\\n" "$PREFIX" + else + _maybe_run_conda_init_condabin + fi +{%- elif initialize_conda %} DEFAULT={{ 'yes' if initialize_by_default else 'no' }} # Interactive mode. @@ -708,34 +784,26 @@ if [ "$BATCH" = "0" ]; then printf "conda init\\n" printf "\\n" else - case $SHELL in - # We call the module directly to avoid issues with spaces in shebang - *zsh) "$PREFIX/bin/python" -m conda init zsh ;; - *) "$PREFIX/bin/python" -m conda init ;; - esac - if [ -f "$PREFIX/bin/mamba" ]; then - # If the version of mamba is <2.0.0, we preferably use the `mamba` python module - # to perform the initialization. - # - # Otherwise (i.e. as of 2.0.0), we use the `mamba shell init` command - if [ "$("$PREFIX/bin/mamba" --version | head -n 1 | cut -d' ' -f2 | cut -d'.' -f1)" -lt 2 ]; then - case $SHELL in - # We call the module directly to avoid issues with spaces in shebang - *zsh) "$PREFIX/bin/python" -m mamba.mamba init zsh ;; - *) "$PREFIX/bin/python" -m mamba.mamba init ;; - esac - else - case $SHELL in - *zsh) "$PREFIX/bin/mamba" shell init --shell zsh ;; - *) "$PREFIX/bin/mamba" shell init ;; - esac - fi - fi + _maybe_run_conda_init fi +{%- endif %} {%- endif %} printf "Thank you for installing %s!\\n" "${INSTALLER_NAME}" -fi # !BATCH +{#- End of Interactive mode #} +{#- Batch mode #} +{%- if has_conda and initialize_conda %} +elif [ "$INIT_CONDA" = "1" ]; then +{%- if initialize_conda == 'condabin' %} + printf "Adding '%s/condabin' to PATH...\\n" "$PREFIX" + _maybe_run_conda_init_condabin +{%- else %} + printf "Initializing '%s' with 'conda init'...\\n" "$PREFIX" + _maybe_run_conda_init +{%- endif %} +{%- endif %} +{#- End of Batch mode #} +fi {%- if has_conda %} diff --git a/constructor/main.py b/constructor/main.py index 39535bda7..c109bd05e 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -262,6 +262,16 @@ def main_build( # '_dists': list[Dist] # '_urls': list[Tuple[url, md5]] + if initialize_conda := info.get("initialize_conda"): + if not info.get("_has_conda"): + sys.exit("Error: 'initialize_conda' requires 'conda' in the base environment.") + if initialize_conda == "condabin" and platform.startswith(("linux-", "osx-")): + conda = next(record for record in info.get("_records", ()) if record.name == "conda") + if Version(conda.version) < Version("25.5.0"): + sys.exit( + "Error: 'initialize_conda == condabin' requires 'conda >=25.5.0' in base env." + ) + os.makedirs(output_dir, exist_ok=True) info_dicts = [] for itype in itypes: diff --git a/constructor/nsis/OptionsDialog.nsh b/constructor/nsis/OptionsDialog.nsh index 55824da22..0b5f15c6d 100644 --- a/constructor/nsis/OptionsDialog.nsh +++ b/constructor/nsis/OptionsDialog.nsh @@ -42,14 +42,14 @@ Function mui_AnaCustomOptions_InitDefaults ${Else} StrCpy $Ana_RegisterSystemPython_State ${BST_CHECKED} ${EndIf} - ${If} $Ana_CreateShortcuts_State == "" + ${EndIf} + ${If} $Ana_CreateShortcuts_State == "" ${If} "${ENABLE_SHORTCUTS}" == "yes" StrCpy $Ana_CreateShortcuts_State ${BST_CHECKED} ${Else} StrCpy $Ana_CreateShortcuts_State ${BST_UNCHECKED} ${EndIf} ${EndIf} - ${EndIf} FunctionEnd ;-------------------------------- @@ -84,18 +84,27 @@ Function mui_AnaCustomOptions_Show ${NSD_OnClick} $mui_AnaCustomOptions.CreateShortcuts CreateShortcuts_OnClick ${EndIf} - ${If} "${SHOW_ADD_TO_PATH}" == "yes" + ${If} "${SHOW_ADD_TO_PATH}" != "no" # AddToPath is only an option for JustMe installations; it is disabled for AllUsers # installations. (Addresses CVE-2022-26526) ${If} $InstMode = ${JUST_ME} - ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Add ${NAME} to my &PATH environment variable" + ${NSD_CreateCheckbox} 0 "$5u" 100% 11u "Add installation to my &PATH \ + environment variable" IntOp $5 $5 + 11 Pop $mui_AnaCustomOptions.AddToPath ${NSD_SetState} $mui_AnaCustomOptions.AddToPath $Ana_AddToPath_State ${NSD_OnClick} $mui_AnaCustomOptions.AddToPath AddToPath_OnClick - ${NSD_CreateLabel} 5% "$5u" 90% 20u \ - "NOT recommended. This can lead to conflicts with other applications. Instead, use \ - the Commmand Prompt and Powershell menus added to the Windows Start Menu." + ${If} "${SHOW_ADD_TO_PATH}" == "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 \ + to be performed manually." + ${Else} + ${NSD_CreateLabel} 5% "$5u" 90% 20u \ + "NOT recommended. This can lead to conflicts with other applications. \ + Instead, use the Commmand Prompt and Powershell menus added \ + to the Windows Start Menu." + ${EndIf} IntOp $5 $5 + 20 Pop $Ana_AddToPath_Label ${EndIf} @@ -113,7 +122,7 @@ Function mui_AnaCustomOptions_Show ${NSD_SetState} $mui_AnaCustomOptions.RegisterSystemPython $Ana_RegisterSystemPython_State ${NSD_OnClick} $mui_AnaCustomOptions.RegisterSystemPython RegisterSystemPython_OnClick ${NSD_CreateLabel} 5% "$5u" 90% 20u \ - "Recommended. Allows other programs, such as VSCode, PyCharm, etc. to automatically \ + "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 @@ -161,9 +170,12 @@ Function AddToPath_OnClick ShowWindow $Ana_AddToPath_Label ${SW_HIDE} ${NSD_GetState} $0 $Ana_AddToPath_State ${If} $Ana_AddToPath_State == ${BST_UNCHECKED} - SetCtlColors $Ana_AddToPath_Label 000000 transparent ${Else} - SetCtlColors $Ana_AddToPath_Label ff0000 transparent + ${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 diff --git a/constructor/nsis/_nsis.py b/constructor/nsis/_nsis.py index 01905b5ae..45160d347 100644 --- a/constructor/nsis/_nsis.py +++ b/constructor/nsis/_nsis.py @@ -271,6 +271,12 @@ def remove_from_path(root_prefix=None): def add_to_path(pyversion, arch): + if allusers: + # To address CVE-2022-26526. + # In AllUsers install mode, do not allow PATH manipulation. + print("PATH manipulation is disabled in All Users mode.", file=sys.stderr) + return + from _system_path import ( add_to_system_path, broadcast_environment_settings_change, @@ -296,6 +302,22 @@ def add_to_path(pyversion, arch): broadcast_environment_settings_change() +def add_condabin_to_path(): + if allusers: + # To address CVE-2022-26526. + # In AllUsers install mode, do not allow PATH manipulation. + print("PATH manipulation is disabled in All Users mode.", file=sys.stderr) + return + + from _system_path import ( + add_to_system_path, + broadcast_environment_settings_change, + ) + + add_to_system_path(os.path.normpath(os.path.join(ROOT_PREFIX, "condabin")), allusers) + broadcast_environment_settings_change() + + def rm_regkeys(): cmdproc_reg_entry = NSISReg(r'Software\Microsoft\Command Processor') cmdproc_autorun_val = cmdproc_reg_entry.get('AutoRun') @@ -373,6 +395,8 @@ def main(): else: arch = '32-bit' if tuple.__itemsize__ == 4 else '64-bit' add_to_path(pyver, arch) + elif cmd == 'addcondabinpath': + add_condabin_to_path() elif cmd == 'rmpath': remove_from_path() elif cmd == 'pre_uninstall': diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index 0538d578c..8583906a5 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -702,8 +702,10 @@ Function .onInit # Initialize the default settings for the anaconda custom options Call mui_AnaCustomOptions_InitDefaults - # Override custom options with explicitly given values from contruct.yaml. - # If initialize_by_default (register_python_default) is None, do nothing. + # 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} @@ -1438,13 +1440,20 @@ Section "Install" call AbortRetryNSExecWait ${EndIf} +{% if initialize_conda %} ${If} $Ana_AddToPath_State = ${BST_CHECKED} - ${Print} "Adding to PATH..." +{%- 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}' - push 'Failed to add {{ NAME }} to the system PATH' +{%- endif %} + push 'Failed to add {{ NAME }} to PATH' push 'WithLog' call AbortRetryNSExecWait ${EndIf} +{%- endif %} # Create registry entries saying this is the system Python # (for this version) diff --git a/constructor/osx/update_path.sh b/constructor/osx/run_conda_init.sh similarity index 93% rename from constructor/osx/update_path.sh rename to constructor/osx/run_conda_init.sh index a1fa3ea25..ef128b727 100644 --- a/constructor/osx/update_path.sh +++ b/constructor/osx/run_conda_init.sh @@ -8,7 +8,7 @@ set -eux PREFIX="$2/{{ pkg_name_lower }}" PREFIX=$(cd "$PREFIX"; pwd) -INIT_FILES=$("$PREFIX/bin/python" -m conda init --all | tee) +INIT_FILES=$("$PREFIX/bin/python" -m conda init --all {{ '--condabin' if initialize_conda == 'condabin' else '' }} | tee) # Just like in run_install.sh, the files generated by the installer # are owned by root when installed outside of $HOME. So, ownership of diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py index fe6c22eff..bc6d7a7ef 100644 --- a/constructor/osxpkg.py +++ b/constructor/osxpkg.py @@ -259,22 +259,34 @@ def modify_xml(xml_path, info): path_choice.set("visible", "true") path_choice.set("title", "Run the post-install script") path_choice.set("description", " ".join(info["post_install_desc"].split())) - elif ident.endswith("pathupdate"): + elif ident.endswith("run_conda_init"): has_conda = info.get("_has_conda", True) + initialize_conda = info.get("initialize_conda", "classic") path_choice.set("visible", "true" if has_conda else "false") path_choice.set( "start_selected", "true" if has_conda and info.get("initialize_by_default", True) else "false", ) - path_choice.set("title", "Add conda initialization to the shell") - path_description = """ - If this box is checked, conda will be automatically activated in your - preferred shell on startup. This will change the command prompt when - activated. If your prefer that conda's base environment not be activated - on startup, run `conda config --set auto_activate_base false`. You can - undo this by running `conda init --reverse ${SHELL}`. - If unchecked, you must this initialization yourself or activate the - environment manually for each shell in which you wish to use it.""" + if initialize_conda == "condabin": + path_choice.set("title", "Add condabin/ to PATH") + path_description = """ + If this box is checked, this will enable you to run 'conda' anywhere, + without injecting a shell function. This will NOT change the command prompt + or activate your environment on shell startup. You can undo this by running + `conda init --condabin --reverse`. If unchecked, you must run this initialization + yourself or activate the environment manually for each shell in which you wish + to use it. + """ + else: + path_choice.set("title", "Add conda initialization to the shell") + path_description = """ + If this box is checked, conda will be automatically activated in your + preferred shell on startup. This will change the command prompt when + activated. If your prefer that conda's base environment not be activated + on startup, run `conda config --set auto_activate_base false`. You can + undo this by running `conda init --reverse ${SHELL}`. + If unchecked, you must run this initialization yourself or activate the + environment manually for each shell in which you wish to use it.""" path_choice.set("description", " ".join(path_description.split())) elif ident.endswith("cacheclean"): path_choice.set("visible", "true") @@ -351,6 +363,7 @@ def move_script(src, dst, info, ensure_shebang=False, user_script_type=None): variables["virtual_specs"] = shlex.join(virtual_specs) 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") data = render_template(data, **variables) @@ -627,9 +640,9 @@ def create(info, verbose=False): names.append("user_post_install") # 5. The script to run conda init - if info.get("initialize_conda", True): - pkgbuild_script("pathupdate", info, "update_path.sh") - names.append("pathupdate") + if info.get("_has_conda") and info.get("initialize_conda", "classic"): + pkgbuild_script("run_conda_init", info, "run_conda_init.sh") + names.append("run_conda_init") # 6. The script to clear the package cache if not info.get("keep_pkgs"): diff --git a/constructor/shar.py b/constructor/shar.py index 3a6c14d90..33a8990d2 100644 --- a/constructor/shar.py +++ b/constructor/shar.py @@ -75,12 +75,12 @@ def get_header(conda_exec, tarball, info): variables["has_%s" % key] = bool(key in info) if key in info: variables["direct_execute_%s" % key] = has_shebang(info[key]) - variables["initialize_conda"] = info.get("initialize_conda", True) + variables["initialize_conda"] = info.get("initialize_conda", "classic") variables["initialize_by_default"] = info.get("initialize_by_default", False) variables["has_conda"] = info["_has_conda"] variables["enable_shortcuts"] = str(info["_enable_shortcuts"]).lower() variables["check_path_spaces"] = info.get("check_path_spaces", True) - # Omit __osx and __glibc because those are tested with shell code direcly + # Omit __osx and __glibc because those are tested with shell code directly virtual_specs = [ spec for spec in info.get("virtual_specs", ()) diff --git a/constructor/winexe.py b/constructor/winexe.py index aef0702f0..053a287c8 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -165,7 +165,7 @@ def make_nsi( "post_install_desc": info["post_install_desc"], "enable_shortcuts": "yes" if info["_enable_shortcuts"] is True else "no", "show_register_python": "yes" if info.get("register_python", True) else "no", - "show_add_to_path": "yes" if info.get("initialize_conda", True) else "no", + "show_add_to_path": info.get("initialize_conda", "classic") or "no", "outfile": info["_outpath"], "vipv": make_VIProductVersion(info["version"]), "constructor_version": info["CONSTRUCTOR_VERSION"], @@ -229,7 +229,7 @@ def make_nsi( # These are mostly booleans we use with if-checks variables.update(ns_platform(info["_platform"])) - variables["initialize_conda"] = info.get("initialize_conda", True) + variables["initialize_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) diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index adbcc0bac..611493d04 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -491,7 +491,16 @@ If `header_image` is not provided, use this text when generating the image Add an option to the installer so the user can choose whether to run `conda init` after the installation (Unix), or to add certain subdirectories of the installation -to PATH (Windows). See also `initialize_by_default`. +to PATH (Windows). Requires `conda` to be part of the `base` environment. Valid options: + +- `classic` or `True`: runs `conda init` on Unix, which injects a shell function in the + shell profiles. On Windows, it adds `$INSTDIR`, `$INSTDIR/Scripts`, `$INSTDIR/Library/bin` + to `PATH`. This is the default. +- `condabin`: only adds `$INSTDIR/condabin` to `PATH`. On Unix, `conda>=25.5.0` is required + in `base`. +- `False`: the installer doesn't perform initialization. + +See also `initialize_by_default`. ### `initialize_by_default` @@ -500,7 +509,7 @@ 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`. -Only applies if `initialize_conda` is true. +Only applies if `initialize_conda` is not false. ### `register_python` diff --git a/docs/source/howto.md b/docs/source/howto.md index 970cc17fd..8087a2803 100644 --- a/docs/source/howto.md +++ b/docs/source/howto.md @@ -201,3 +201,57 @@ finding these files may rely on environment variables, especially `$HOME`. For more detailed implementation notes, see the documentation of the standalone application: * [conda-standalone](https://github.com/conda/conda-standalone) + + +## Control how `conda` runs on your machine + +The traditional installation mechanism for `conda` is for the `constructor`-generated installer to +run the initialization logic once the files have been copied into the target directory `$INSTDIR`: + + +`````{tab-set} +````{tab-item} Windows +```pwsh +%INSTDIR%\_conda.exe init --all +``` +```` +````{tab-item} Linux & macOS +```bash +$INSTDIR/bin/conda init --all +``` +```` +````` + +On most shells, `conda init` will modify your shell configuration (`.bashrc` and similar files) to +inject a `conda` shell function that wraps the actual Python `conda` modules (see [activation deep +dive guide][activation-deepdive] for more details). This is needed so that `conda activate` and +`conda deactivate` can modify the state of the current shell session. + +While very convenient, this shell logic requires significant modifications in the shell profiles +and also adds a runtime overhead everytime a shell session starts. For users that prefer a simpler +`PATH`-based initialization strategy, a alternative method is provided with `conda 25.5.0` and +later: + +``` +conda init --condabin +```` + +This new option only adds `$INSTDIR/condabin` to `PATH`, which is a minimally invasive change to +your shell configuration and has no runtime overhead. This directory is special because it is +guaranteed to only contain the `conda` executables and nothing else. + +As an installer author, you can control which of these options are made available to the end user: + +- `initialize_conda: classic`: the classic, shell-function-based initialization logic. Default. +- `initialize_conda: condabin`: the new, lightweight PATH-only logic. + + +:::{note} +The `--condabin` initialization won't be sufficient to run `conda activate`, and `conda` will error +out saying you need to fully initialize your installation. This might get fixed in the future, but +for now, you can rely on an experimental plugin to use a different activation strategy that doesn't +require shell modifications: [`conda-spawn`](https://github.com/conda-incubator/conda-spawn). Add +it to your `specs` definition and it will be available in your installations as `conda spawn`. +::: + +[activation-deepdive]: https://docs.conda.io/projects/conda/en/stable/dev-guide/deep-dives/activation.html diff --git a/examples/initialization/construct.yaml b/examples/initialization/construct.yaml new file mode 100644 index 000000000..1e980532f --- /dev/null +++ b/examples/initialization/construct.yaml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=../../constructor/data/construct.schema.json +"$schema": "../../constructor/data/construct.schema.json" + +name: initialization + +version: 0.0.1 + +keep_pkgs: True + +channels: + - conda-forge + +specs: + - conda >=25.5.0 + +initialize_conda: {{ os.environ.get("initialization_method", "condabin") }} +initialize_by_default: true +register_python: false +check_path_spaces: true +check_path_length: false +installer_type: all diff --git a/news/965-condabin b/news/965-condabin new file mode 100644 index 000000000..4e655424c --- /dev/null +++ b/news/965-condabin @@ -0,0 +1,20 @@ +### Enhancements + +* Add support for `conda init --condabin` in `initialize_conda` option. (#960 via #965) +* SH installers: Add `-c` flag to control whether to run conda initialization in batch mode. (#1004 via #965) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* Add new howto section explaining the differences between types of conda initialization. (#965) + +### Other + +* diff --git a/tests/test_examples.py b/tests/test_examples.py index 8eb08bf72..cc336eba0 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -484,8 +484,8 @@ def test_example_miniforge(tmp_path, request, example): for installer, install_dir in create_installer(input_path, tmp_path): if installer.suffix == ".sh": # try both batch and interactive installations - install_dirs = install_dir / "batch", install_dir / "interactive" - installer_inputs = None, f"\nyes\n{install_dir / 'interactive'}\nno\n" + install_dirs = (install_dir / "batch", install_dir / "interactive") + installer_inputs = (None, f"\nyes\n{install_dir / 'interactive'}\nno\nno\n") else: install_dirs = (install_dir,) installer_inputs = (None,) @@ -960,6 +960,69 @@ def test_virtual_specs_override(tmp_path, request, monkeypatch): ) +@pytest.mark.skipif(not ON_CI, reason="Run on CI only") +@pytest.mark.parametrize("method", ("classic", "condabin")) +def test_initialization(tmp_path, request, monkeypatch, method): + request.addfinalizer( + lambda: subprocess.run([sys.executable, "-m", "conda", "init", "--reverse"]) + ) + monkeypatch.setenv("initialization_method", method) + input_path = _example_path("initialization") + for installer, install_dir in create_installer(input_path, tmp_path): + if installer.suffix == ".sh": + options = ["-c"] + elif installer.suffix == ".exe": + # 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"] + else: + options = [] + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=False, + options=options, + ) + if installer.suffix == ".exe": + try: + import winreg + + paths = [] + for root, keyname in ( + (winreg.HKEY_CURRENT_USER, r"Environment"), + ( + winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", + ), + ): + with winreg.OpenKey(root, keyname, 0, winreg.KEY_QUERY_VALUE) as key: + value = winreg.QueryValueEx(key, "PATH")[0] + paths += value.strip().split(os.pathsep) + if method == "condabin": + assert str(install_dir / "condabin") in paths + else: + assert str(install_dir) in paths + assert str(install_dir / "Scripts") in paths + assert str(install_dir / "Library" / "bin") in paths + + finally: + _run_uninstaller_exe(install_dir, check=True) + else: + # GHA's Ubuntu needs interactive, but macOS wants login :shrug: + login_flag = "-i" if sys.platform.startswith("linux") else "-l" + out = subprocess.check_output( + [os.environ.get("SHELL", "bash"), login_flag, "-c", "echo $PATH"], + text=True, + ) + if method == "condabin": + assert str(install_dir / "condabin") in out.strip().split(os.pathsep) + else: + assert str(install_dir / "bin") in out.strip().split(os.pathsep) + + @pytest.mark.skipif(not ON_CI, reason="CI only") @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") def test_allusers_exe(tmp_path, request): diff --git a/tests/test_header.py b/tests/test_header.py index dc7791250..94e5c906e 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -68,7 +68,8 @@ def test_osxpkg_scripts_shellcheck(arch, check_path_spaces, script): register_envs=True, virtual_specs="__osx>=10.13", no_rcs_arg="", - script_env_variables="", + script_env_variables={}, + initialize_conda="condabin", ) findings, returncode = run_shellcheck(processed) @@ -85,7 +86,7 @@ def test_osxpkg_scripts_shellcheck(arch, check_path_spaces, script): @pytest.mark.parametrize("keep_pkgs", [True]) @pytest.mark.parametrize("has_conda", [False, True]) @pytest.mark.parametrize("has_license", [True]) -@pytest.mark.parametrize("initialize_conda", [True]) +@pytest.mark.parametrize("initialize_conda", [True, "classic", "condabin", False]) @pytest.mark.parametrize("initialize_by_default", [True]) @pytest.mark.parametrize("has_post_install", [True]) @pytest.mark.parametrize("has_pre_install", [False])