diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 18facf0c2..eba3db1d7 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -79,11 +79,8 @@ jobs:
conda-standalone: conda-standalone
- os: windows
python-version: "3.11"
+ # conda-standalone: micromamba
conda-standalone: conda-standalone-nightly
- # Micromamba doesn't support Windows yet (menuinst features missing)
- # - os: windows
- # python-version: 3.11
- # conda-standalone: micromamba
env:
PYTHONUNBUFFERED: "1"
@@ -133,7 +130,6 @@ jobs:
- name: Run examples
env:
CONSTRUCTOR_EXAMPLES_KEEP_ARTIFACTS: "${{ runner.temp }}/examples_artifacts"
- # signtool only exists on Windows, but doesn't cause errors on unix when absent
CONSTRUCTOR_SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe"
run: |
rm -rf coverage.json
@@ -144,10 +140,6 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: integration
- - name: Test with conda-libmamba-solver
- run: |
- conda install -yq conda-libmamba-solver
- CONDA_SOLVER=libmamba CONDA_VERBOSITY=1 pytest -vv tests/test_examples.py -k noconda
- name: Check docs are up-to-date
if: matrix.check-docs
run: |
diff --git a/CONSTRUCT.md b/CONSTRUCT.md
index f737677a3..ccb36a91e 100644
--- a/CONSTRUCT.md
+++ b/CONSTRUCT.md
@@ -128,9 +128,16 @@ _required:_ no
_type:_ list
A list of packages with menu items to be installed. The packages must have
-necessary metadata in `Menu/.json`). Menu items are currently
-only supported on Windows. By default, all menu items will be installed;
-supplying this list allows a subset to be selected instead.
+necessary metadata in `Menu/.json`). By default, all menu items
+found in the installation will be created; supplying this list allows a
+subset to be selected instead. If an empty list is supplied, no shortcuts will
+be created.
+
+If all environments (`extra_envs` included) set `menu_packages` to an empty list,
+no UI options about shortcuts will be offered to the user.
+
+Note: This option is not fully implemented when `micromamba` is used as
+the `--conda-exe` binary. The only accepted value is an empty list (`[]`).
### `ignore_duplicate_files`
@@ -220,12 +227,12 @@ name) to a dictionary of options:
an empty list.
- `user_requested_specs` (list of str): same as the global option, but for this env;
if not provided, global value is _not_ used
+- `menu_packages` (list of str): same as the global option, for this env;
+ if not provided, the global value is _not_ used.
Notes:
- `ignore_duplicate_files` will always be considered `True` if `extra_envs` is in use.
- `conda` needs to be present in the `base` environment (via `specs`)
-- support for `menu_packages` is planned, but not possible right now. For now, all packages
- in an `extra_envs` config will be allowed to create their shortcuts.
- If a global `exclude` option is used, it will have an effect on the environments created
by `extra_envs` too. For example, if the global environment excludes `tk`, none of the
extra environments will have it either. Unlike the global option, an error will not be
diff --git a/constructor/construct.py b/constructor/construct.py
index 55d19f861..1de1dd1ef 100644
--- a/constructor/construct.py
+++ b/constructor/construct.py
@@ -83,11 +83,18 @@
is contained as a result of resolving the specs for `python 2.7`.
'''),
- ('menu_packages', False, list, '''
+ ('menu_packages', False, list, '''
A list of packages with menu items to be installed. The packages must have
-necessary metadata in `Menu/.json`). Menu items are currently
-only supported on Windows. By default, all menu items will be installed;
-supplying this list allows a subset to be selected instead.
+necessary metadata in `Menu/.json`). By default, all menu items
+found in the installation will be created; supplying this list allows a
+subset to be selected instead. If an empty list is supplied, no shortcuts will
+be created.
+
+If all environments (`extra_envs` included) set `menu_packages` to an empty list,
+no UI options about shortcuts will be offered to the user.
+
+Note: This option is not fully implemented when `micromamba` is used as
+the `--conda-exe` binary. The only accepted value is an empty list (`[]`).
'''),
('ignore_duplicate_files', False, bool, '''
@@ -152,12 +159,12 @@
an empty list.
- `user_requested_specs` (list of str): same as the global option, but for this env;
if not provided, global value is _not_ used
+- `menu_packages` (list of str): same as the global option, for this env;
+ if not provided, the global value is _not_ used.
Notes:
- `ignore_duplicate_files` will always be considered `True` if `extra_envs` is in use.
- `conda` needs to be present in the `base` environment (via `specs`)
-- support for `menu_packages` is planned, but not possible right now. For now, all packages
- in an `extra_envs` config will be allowed to create their shortcuts.
- If a global `exclude` option is used, it will have an effect on the environments created
by `extra_envs` too. For example, if the global environment excludes `tk`, none of the
extra environments will have it either. Unlike the global option, an error will not be
@@ -599,9 +606,7 @@
"channels_remap": (list, tuple),
"user_requested_specs": (list, tuple),
"exclude": (list, tuple),
- # TODO: we can't support menu_packages for extra envs yet
- # will implement when the PR for new menuinst lands
- # "menu_packages": (list, tuple),
+ "menu_packages": (list, tuple),
}
logger = logging.getLogger(__name__)
@@ -766,7 +771,7 @@ def verify(info):
sys.exit(
f"Environment names (keys in 'extra_envs') cannot contain any of {disallowed}. "
f"You tried to use: {env_name}"
- )
+ )
for key, value in env_data.items():
if key not in _EXTRA_ENVS_SCHEMA:
sys.exit(f"Key '{key}' not supported in 'extra_envs'.")
diff --git a/constructor/fcp.py b/constructor/fcp.py
index bb888db91..74d36dc3d 100644
--- a/constructor/fcp.py
+++ b/constructor/fcp.py
@@ -56,6 +56,8 @@ def getsize(filename):
def warn_menu_packages_missing(precs, menu_packages):
+ if not menu_packages:
+ return
all_names = {prec.name for prec in precs}
for name in menu_packages:
if name not in all_names:
@@ -236,7 +238,7 @@ def _precs_from_environment(environment, input_dir):
def _solve_precs(name, version, download_dir, platform, channel_urls=(), channels_remap=(),
- specs=(), exclude=(), menu_packages=(), environment=None, environment_file=None,
+ specs=(), exclude=(), menu_packages=None, environment=None, environment_file=None,
verbose=True, conda_exe="conda.exe", extra_env=False, input_dir=""):
# Add python to specs, since all installers need a python interpreter. In the future we'll
# probably want to add conda too.
@@ -376,7 +378,7 @@ def _fetch_precs(precs, download_dir, transmute_file_type=''):
def _main(name, version, download_dir, platform, channel_urls=(), channels_remap=(), specs=(),
- exclude=(), menu_packages=(), ignore_duplicate_files=True, environment=None,
+ exclude=(), menu_packages=None, ignore_duplicate_files=True, environment=None,
environment_file=None, verbose=True, dry_run=False, conda_exe="conda.exe",
transmute_file_type='', extra_envs=None, check_path_spaces=True, input_dir=""):
precs = _solve_precs(
@@ -408,7 +410,7 @@ def _main(name, version, download_dir, platform, channel_urls=(), channels_remap
channels_remap=env_config.get("channels_remap", channels_remap),
specs=env_config.get("specs", ()),
exclude=env_config.get("exclude", exclude),
- menu_packages=env_config.get("menu_packages", ()),
+ menu_packages=env_config.get("menu_packages"),
environment=env_config.get("environment"),
environment_file=env_config.get("environment_file"),
verbose=verbose,
@@ -462,7 +464,7 @@ def main(info, verbose=True, dry_run=False, conda_exe="conda.exe"):
channels_remap = info.get('channels_remap', ())
specs = info.get("specs", ())
exclude = info.get("exclude", ())
- menu_packages = info.get("menu_packages", ())
+ menu_packages = info.get("menu_packages")
ignore_duplicate_files = info.get("ignore_duplicate_files", True)
environment = info.get("environment", None)
environment_file = info.get("environment_file", None)
diff --git a/constructor/header.sh b/constructor/header.sh
index 48db69a8f..d35bab72b 100644
--- a/constructor/header.sh
+++ b/constructor/header.sh
@@ -46,6 +46,9 @@ KEEP_PKGS=1
KEEP_PKGS=0
#endif
SKIP_SCRIPTS=0
+#if enable_shortcuts == "true"
+SKIP_SHORTCUTS=0
+#endif
TEST=0
REINSTALL=0
USAGE="
@@ -70,6 +73,9 @@ Installs ${INSTALLER_NAME} ${INSTALLER_VER}
-p PREFIX install prefix, defaults to $PREFIX
#endif
-s skip running pre/post-link/install scripts
+#if enable_shortcuts == 'true'
+-m disable the creation of menu items / shortcuts
+#endif
-u update an existing installation
#if has_conda
-t run package tests after installation (may install conda-build)
@@ -80,7 +86,11 @@ Installs ${INSTALLER_NAME} ${INSTALLER_VER}
# 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/
+#if enable_shortcuts == "true"
+while getopts "bifhkp:smut" x; do
+#else
while getopts "bifhkp:sut" x; do
+#endif
case "$x" in
h)
printf "%s\\n" "$USAGE"
@@ -104,6 +114,11 @@ while getopts "bifhkp:sut" x; do
s)
SKIP_SCRIPTS=1
;;
+#if enable_shortcuts == "true"
+ m)
+ SKIP_SHORTCUTS=1
+ ;;
+#endif
u)
FORCE=1
;;
@@ -391,7 +406,7 @@ cd "$PREFIX"
unset PYTHON_SYSCONFIGDATA_NAME _CONDA_PYTHON_SYSCONFIGDATA_NAME
# the first binary payload: the standalone conda executable
-CONDA_EXEC="$PREFIX/conda.exe"
+CONDA_EXEC="$PREFIX/_conda"
extract_range "${boundary0}" "${boundary1}" > "$CONDA_EXEC"
chmod +x "$CONDA_EXEC"
@@ -399,6 +414,11 @@ export TMP_BACKUP="${TMP:-}"
export TMP="$PREFIX/install_tmp"
mkdir -p "$TMP"
+# Create $PREFIX/.nonadmin if the installation didn't require superuser permissions
+if [ "$(id -u)" -ne 0 ]; then
+ touch "$PREFIX/.nonadmin"
+fi
+
# the second binary payload: the tarball of packages
printf "Unpacking payload ...\n"
extract_range $boundary1 $boundary2 | \
@@ -445,14 +465,31 @@ test -d ~/.conda || mkdir -p ~/.conda >/dev/null 2>/dev/null || test -d ~/.conda
printf "\nInstalling base environment...\n\n"
+#if enable_shortcuts == "true"
+if [ "$SKIP_SHORTCUTS" = "1" ]; then
+ shortcuts="--no-shortcuts"
+else
+ shortcuts="__SHORTCUTS__"
+fi
+#endif
+#if enable_shortcuts == "false"
+shortcuts="--no-shortcuts"
+#endif
+#if enable_shortcuts == "incompatible"
+shortcuts=""
+#endif
+
+# shellcheck disable=SC2086
+CONDA_ROOT_PREFIX="$PREFIX" \
CONDA_REGISTER_ENVS="__REGISTER_ENVS__" \
CONDA_SAFETY_CHECKS=disabled \
CONDA_EXTRA_SAFETY_CHECKS=no \
CONDA_CHANNELS="__CHANNELS__" \
CONDA_PKGS_DIRS="$PREFIX/pkgs" \
-"$CONDA_EXEC" install --offline --file "$PREFIX/pkgs/env.txt" -yp "$PREFIX" || exit 1
+"$CONDA_EXEC" install --offline --file "$PREFIX/pkgs/env.txt" -yp "$PREFIX" $shortcuts || exit 1
rm -f "$PREFIX/pkgs/env.txt"
+#The templating doesn't support nested if statements
#if has_conda
mkdir -p "$PREFIX/envs"
for env_pkgs in "${PREFIX}"/pkgs/envs/*/; do
@@ -469,14 +506,31 @@ for env_pkgs in "${PREFIX}"/pkgs/envs/*/; do
else
env_channels="__CHANNELS__"
fi
-
- # TODO: custom shortcuts per env?
+#endif
+#if has_conda and enable_shortcuts == "true"
+ if [ "$SKIP_SHORTCUTS" = "1" ]; then
+ env_shortcuts="--no-shortcuts"
+ else
+ # This file is guaranteed to exist, even if empty
+ env_shortcuts=$(cat "${env_pkgs}shortcuts.txt")
+ rm -f "${env_pkgs}shortcuts.txt"
+ fi
+#endif
+#if has_conda and enable_shortcuts == "false"
+ env_shortcuts="--no-shortcuts"
+#endif
+#if has_conda and enable_shortcuts == "incompatible"
+ env_shortcuts=""
+#endif
+#if has_conda
+ # shellcheck disable=SC2086
+ CONDA_ROOT_PREFIX="$PREFIX" \
CONDA_REGISTER_ENVS="__REGISTER_ENVS__" \
CONDA_SAFETY_CHECKS=disabled \
CONDA_EXTRA_SAFETY_CHECKS=no \
CONDA_CHANNELS="$env_channels" \
CONDA_PKGS_DIRS="$PREFIX/pkgs" \
- "$CONDA_EXEC" install --offline --file "${env_pkgs}env.txt" -yp "$PREFIX/envs/$env_name" || exit 1
+ "$CONDA_EXEC" install --offline --file "${env_pkgs}env.txt" -yp "$PREFIX/envs/$env_name" $env_shortcuts || exit 1
rm -f "${env_pkgs}env.txt"
done
#endif
@@ -486,9 +540,6 @@ __INSTALL_COMMANDS__
POSTCONDA="$PREFIX/postconda.tar.bz2"
"$CONDA_EXEC" constructor --prefix "$PREFIX" --extract-tarball < "$POSTCONDA" || exit 1
rm -f "$POSTCONDA"
-
-rm -f "$CONDA_EXEC"
-
rm -rf "$PREFIX/install_tmp"
export TMP="$TMP_BACKUP"
@@ -630,4 +681,5 @@ fi
#endif
exit 0
+# shellcheck disable=SC2317
@@END_HEADER@@
diff --git a/constructor/main.py b/constructor/main.py
index 130198487..6db8071b0 100644
--- a/constructor/main.py
+++ b/constructor/main.py
@@ -14,12 +14,14 @@
from . import __version__
from .build_outputs import process_build_outputs
-from .conda_interface import SUPPORTED_PLATFORMS, cc_platform
+from .conda_interface import SUPPORTED_PLATFORMS
+from .conda_interface import VersionOrder as Version
+from .conda_interface import cc_platform
from .construct import generate_key_info_list, ns_platform
from .construct import parse as construct_parse
from .construct import verify as construct_verify
from .fcp import main as fcp_main
-from .utils import normalize_path, yield_lines
+from .utils import identify_conda_exe, normalize_path, yield_lines
DEFAULT_CACHE_DIR = os.getenv('CONSTRUCTOR_CACHE', '~/.conda/constructor')
@@ -90,7 +92,7 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
if platform != cc_platform and 'pkg' in itypes and not cc_platform.startswith('osx-'):
sys.exit("Error: cannot construct a macOS 'pkg' installer on '%s'" % cc_platform)
if osname == "win" and "micromamba" in os.path.basename(info['_conda_exe']):
- # TODO: Remove when shortcut creation is implemented on micromamba
+ # TODO: Investigate errors on Windows and re-enable
sys.exit("Error: micromamba is not supported on Windows installers.")
logger.debug('conda packages download: %s', info['_download_dir'])
@@ -145,6 +147,23 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
if config_key == "environment_file":
env_config[config_key] = abspath(join(dir_path, value))
+ exe_name, exe_version = identify_conda_exe(info.get("_conda_exe"))
+ if sys.platform != "win32" and (
+ exe_name == "micromamba" or Version(exe_version) < Version("23.11.0")
+ ):
+ logger.warning("conda-standalone 23.11.0 or above is required for shortcuts on Unix.")
+ info['_enable_shortcuts'] = "incompatible"
+ else:
+ # Installers will provide shortcut options and features only if the user
+ # didn't opt-out by setting every `menu_packages` item to an empty list
+ info['_enable_shortcuts'] = bool(
+ info.get("menu_packages", True)
+ or any(
+ env.get("menu_packages", True)
+ for env in info.get("extra_envs", {}).values()
+ )
+ )
+
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/OptionsDialog.nsh b/constructor/nsis/OptionsDialog.nsh
index b1f975eeb..ec67295a0 100644
--- a/constructor/nsis/OptionsDialog.nsh
+++ b/constructor/nsis/OptionsDialog.nsh
@@ -43,7 +43,11 @@ Function mui_AnaCustomOptions_InitDefaults
StrCpy $Ana_RegisterSystemPython_State ${BST_CHECKED}
${EndIf}
${If} $Ana_CreateShortcuts_State == ""
- StrCpy $Ana_CreateShortcuts_State ${BST_CHECKED}
+ ${If} "${ENABLE_SHORTCUTS}" == "yes"
+ StrCpy $Ana_CreateShortcuts_State ${BST_CHECKED}
+ ${Else}
+ StrCpy $Ana_CreateShortcuts_State ${BST_UNCHECKED}
+ ${EndIf}
${EndIf}
${EndIf}
FunctionEnd
@@ -70,12 +74,15 @@ Function mui_AnaCustomOptions_Show
# We will use $5 as the y axis accumulator, starting at 0
# We sum the the number of 'u' units added by 'NSD_Create*' functions
+ IntOp $5 0 + 0
- ${NSD_CreateCheckbox} 0 0u 100% 11u "Create start menu shortcuts (supported packages only)."
- IntOp $5 0 + 11
- Pop $mui_AnaCustomOptions.CreateShortcuts
- ${NSD_SetState} $mui_AnaCustomOptions.CreateShortcuts $Ana_CreateShortcuts_State
- ${NSD_OnClick} $mui_AnaCustomOptions.CreateShortcuts CreateShortcuts_OnClick
+ ${If} "${ENABLE_SHORTCUTS}" == "yes"
+ ${NSD_CreateCheckbox} 0 0u 100% 11u "Create start menu shortcuts (supported packages only)."
+ IntOp $5 $5 + 11
+ Pop $mui_AnaCustomOptions.CreateShortcuts
+ ${NSD_SetState} $mui_AnaCustomOptions.CreateShortcuts $Ana_CreateShortcuts_State
+ ${NSD_OnClick} $mui_AnaCustomOptions.CreateShortcuts CreateShortcuts_OnClick
+ ${EndIf}
${If} "${SHOW_ADD_TO_PATH}" == "yes"
# AddToPath is only an option for JustMe installations; it is disabled for AllUsers
diff --git a/constructor/nsis/_nsis.py b/constructor/nsis/_nsis.py
index 314868f80..01905b5ae 100644
--- a/constructor/nsis/_nsis.py
+++ b/constructor/nsis/_nsis.py
@@ -105,6 +105,10 @@ def get(self, name):
def mk_menus(remove=False, prefix=None, pkg_names=None, root_prefix=None):
+ err(
+ "Deprecation warning: mk_menus is deprecated and will be removed in the future."
+ " Please use menuinst v2 directly or via conda-standalone 23.XXXX+ instead.\n"
+ )
try:
import menuinst
except (ImportError, OSError):
@@ -157,6 +161,10 @@ def get_conda_envs_from_python_api():
def rm_menus(prefix=None, root_prefix=None):
+ err(
+ "Deprecation warning: rm_menus is deprecated and will be removed in the future."
+ " Please use menuinst v2 directly or via conda-standalone 23.XXXX+ instead.\n"
+ )
try:
import menuinst # noqa
from conda.base.context import context
diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl
index a2ae2fb28..664e79bdc 100644
--- a/constructor/nsis/main.nsi.tmpl
+++ b/constructor/nsis/main.nsi.tmpl
@@ -57,7 +57,7 @@ Unicode "true"
!define DEFAULT_PREFIX_ALL_USERS __DEFAULT_PREFIX_ALL_USERS__
!define PRE_INSTALL_DESC __PRE_INSTALL_DESC__
!define POST_INSTALL_DESC __POST_INSTALL_DESC__
-!define MENU_PKGS "@MENU_PKGS@"
+!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})"
@@ -296,14 +296,18 @@ FunctionEnd
${EndIf}
${EndIf}
- ClearErrors
- ${GetOptions} $ARGV "/NoShortcuts=" $ARGV_NoShortcuts
- ${IfNot} ${Errors}
- ${If} $ARGV_NoShortcuts = "1"
- StrCpy $Ana_CreateShortcuts_State ${BST_UNCHECKED}
- ${ElseIf} $ARGV_NoShortcuts = "0"
- StrCpy $Ana_CreateShortcuts_State ${BST_CHECKED}
+ ${If} "${ENABLE_SHORTCUTS}" == "yes"
+ ClearErrors
+ ${GetOptions} $ARGV "/NoShortcuts=" $ARGV_NoShortcuts
+ ${IfNot} ${Errors}
+ ${If} $ARGV_NoShortcuts = "1"
+ StrCpy $Ana_CreateShortcuts_State ${BST_UNCHECKED}
+ ${ElseIf} $ARGV_NoShortcuts = "0"
+ StrCpy $Ana_CreateShortcuts_State ${BST_CHECKED}
+ ${EndIf}
${EndIf}
+ ${Else}
+ StrCpy $Ana_CreateShortcuts_State ${BST_UNCHECKED}
${EndIf}
ClearErrors
@@ -1116,6 +1120,7 @@ Section "Install"
@SCRIPT_ENV_VARIABLES@
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SAFETY_CHECKS", "disabled").r0'
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_EXTRA_SAFETY_CHECKS", "no").r0'
+ System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0'
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_PKGS_DIRS", "$INSTDIR\pkgs")".r0'
# Extra info for pre and post install scripts
# NOTE: If more vars are added, make sure to update the examples/scripts tests too
@@ -1163,14 +1168,6 @@ Section "Install"
AddSize @SIZE@
- ${If} $Ana_CreateShortcuts_State = ${BST_CHECKED}
- DetailPrint "Creating @NAME@ menus..."
- push '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR" --make-menus @MENU_PKGS@'
- push 'Failed to create menus'
- push 'WithLog'
- call AbortRetryNSExecWait
- ${EndIf}
-
#if has_conda is True
DetailPrint "Initializing conda directories..."
push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" mkdirs'
@@ -1272,10 +1269,11 @@ SectionEnd
!macroend
Section "Uninstall"
- # Remove menu items, path entries
+ ${LogSet} on
- DetailPrint "Deleting @NAME@ menus..."
- nsExec::ExecToLog '"$INSTDIR\_conda.exe" constructor --prefix "$INSTDIR" --rm-menus'
+ # Remove menu items, path entries
+ System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0'
+ @UNINSTALL_MENUS@
# ensure that MSVC runtime DLLs are on PATH during uninstallation
ReadEnvStr $0 PATH
diff --git a/constructor/osx/check_shortcuts.sh b/constructor/osx/check_shortcuts.sh
new file mode 100644
index 000000000..b592b2443
--- /dev/null
+++ b/constructor/osx/check_shortcuts.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+set -eux
+
+# $2 is the install location, which is ~ by default
+# but which the user can change.
+PREFIX="$2/__NAME_LOWER__"
+PREFIX=$(cd "$PREFIX"; pwd)
+
+# If the UI selected the "Create shortcuts" option
+# we create a sentinel file that will be checked for existence
+# during run_installation.sh
+# If it doesn't exist, it means that this script never ran
+# due to (A) the user deselected the option, or (B) the installer
+# was created with menu_packages=[], which disables shortcuts altogether,
+# or (C) the installer was created with an incompatible --conda-exe.
+touch "$PREFIX/pkgs/user_wants_shortcuts"
diff --git a/constructor/osx/prepare_installation.sh b/constructor/osx/prepare_installation.sh
index 57d6c4a62..93e34fcea 100644
--- a/constructor/osx/prepare_installation.sh
+++ b/constructor/osx/prepare_installation.sh
@@ -22,7 +22,7 @@ PREFIX="$2/__NAME_LOWER__"
PREFIX=$(cd "$PREFIX"; pwd)
export PREFIX
echo "PREFIX=$PREFIX"
-CONDA_EXEC="$PREFIX/conda.exe"
+CONDA_EXEC="$PREFIX/_conda"
# /COMMON UTILS
chmod +x "$CONDA_EXEC"
@@ -31,6 +31,11 @@ chmod +x "$CONDA_EXEC"
mkdir -p "$PREFIX/conda-meta"
touch "$PREFIX/conda-meta/history"
+# Create $PREFIX/.nonadmin if the installation didn't require superuser permissions
+if [ "$(id -u)" -ne 0 ]; then
+ touch "$PREFIX/.nonadmin"
+fi
+
# Extract the conda packages but avoiding the overwriting of the
# custom metadata we have already put in place
notify "Preparing packages..."
diff --git a/constructor/osx/run_installation.sh b/constructor/osx/run_installation.sh
index b1f562884..acea8d943 100644
--- a/constructor/osx/run_installation.sh
+++ b/constructor/osx/run_installation.sh
@@ -24,17 +24,31 @@ PREFIX="$2/__NAME_LOWER__"
PREFIX=$(cd "$PREFIX"; pwd)
export PREFIX
echo "PREFIX=$PREFIX"
-CONDA_EXEC="$PREFIX/conda.exe"
+CONDA_EXEC="$PREFIX/_conda"
# /COMMON UTILS
+# Check whether the user wants shortcuts or not
+# See check_shortcuts.sh script for details
+ENABLE_SHORTCUTS="__ENABLE_SHORTCUTS__"
+if [[ -f "$PREFIX/pkgs/user_wants_shortcuts" ]]; then # this implies ENABLE_SHORTCUTS==true
+ shortcuts="__SHORTCUTS__"
+elif [[ "$ENABLE_SHORTCUTS" == "incompatible" ]]; then
+ shortcuts=""
+else
+ shortcuts="--no-shortcuts"
+fi
+
# Perform the conda install
notify "Installing packages. This might take a few minutes."
-if ! CONDA_REGISTER_ENVS="__REGISTER_ENVS__" \
+# shellcheck disable=SC2086
+if ! \
+CONDA_REGISTER_ENVS="__REGISTER_ENVS__" \
+CONDA_ROOT_PREFIX="$PREFIX" \
CONDA_SAFETY_CHECKS=disabled \
CONDA_EXTRA_SAFETY_CHECKS=no \
CONDA_CHANNELS=__CHANNELS__ \
CONDA_PKGS_DIRS="$PREFIX/pkgs" \
-"$CONDA_EXEC" install --offline --file "$PREFIX/pkgs/env.txt" -yp "$PREFIX"; then
+"$CONDA_EXEC" install --offline --file "$PREFIX/pkgs/env.txt" -yp "$PREFIX" $shortcuts; then
echo "ERROR: could not complete the conda install"
exit 1
fi
@@ -63,21 +77,30 @@ for env_pkgs in "${PREFIX}"/pkgs/envs/*/; do
else
env_channels="__CHANNELS__"
fi
- # TODO: custom channels per env?
- # TODO: custom shortcuts per env?
+ if [[ -f "$PREFIX/pkgs/user_wants_shortcuts" ]]; then # this implies ENABLE_SHORTCUTS==true
+ # This file is guaranteed to exist, even if empty
+ env_shortcuts=$(cat "${env_pkgs}shortcuts.txt")
+ rm -f "${env_pkgs}shortcuts.txt"
+ elif [[ "$ENABLE_SHORTCUTS" == "incompatible" ]]; then
+ env_shortcuts=""
+ else
+ env_shortcuts="--no-shortcuts"
+ fi
+
+ # shellcheck disable=SC2086
+ CONDA_ROOT_PREFIX="$PREFIX" \
CONDA_REGISTER_ENVS="__REGISTER_ENVS__" \
CONDA_SAFETY_CHECKS=disabled \
CONDA_EXTRA_SAFETY_CHECKS=no \
CONDA_CHANNELS="$env_channels" \
CONDA_PKGS_DIRS="$PREFIX/pkgs" \
- "$CONDA_EXEC" install --offline --file "${env_pkgs}env.txt" -yp "$PREFIX/envs/$env_name" || exit 1
+ "$CONDA_EXEC" install --offline --file "${env_pkgs}env.txt" -yp "$PREFIX/envs/$env_name" $env_shortcuts || exit 1
# Move the prepackaged history file into place
mv "${env_pkgs}/conda-meta/history" "$PREFIX/envs/$env_name/conda-meta/history"
rm -f "${env_pkgs}env.txt"
done
# Cleanup!
-rm -f "$CONDA_EXEC"
find "$PREFIX/pkgs" -type d -empty -exec rmdir {} \; 2>/dev/null || :
__WRITE_CONDARC__
diff --git a/constructor/osx/run_user_script.sh b/constructor/osx/run_user_script.sh
index 9f6291518..61816160f 100644
--- a/constructor/osx/run_user_script.sh
+++ b/constructor/osx/run_user_script.sh
@@ -22,7 +22,7 @@ PREFIX="$2/__NAME_LOWER__"
PREFIX=$(cd "$PREFIX"; pwd)
export PREFIX
echo "PREFIX=$PREFIX"
-CONDA_EXEC="$PREFIX/conda.exe"
+CONDA_EXEC="$PREFIX/_conda"
# /COMMON UTILS
# Expose these to user scripts as well
diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py
index 8c72638c7..32ebf0d7b 100644
--- a/constructor/osxpkg.py
+++ b/constructor/osxpkg.py
@@ -20,6 +20,7 @@
get_final_channels,
preprocess,
rm_rf,
+ shortcuts_flags,
)
OSX_DIR = join(dirname(__file__), "osx")
@@ -216,6 +217,22 @@ def modify_xml(xml_path, info):
path_choice.set('visible', 'false')
path_choice.set('title', 'Apply {}'.format(info['name']))
path_choice.set('enabled', 'false')
+ elif ident.endswith('shortcuts'):
+ # Show this option if menu_packages was set to a non-empty value
+ # or if the option was not set at all. We don't show the option
+ # menu_packages was set to an empty list!
+ path_choice.set('visible', 'true')
+ path_choice.set('title', "Create shortcuts")
+ path_choice.set('enabled', 'true')
+ descr = "Create shortcuts for compatible packages"
+ menu_packages = info.get("menu_packages")
+ if menu_packages is None:
+ menu_packages = []
+ for extra_env in info.get("extra_envs", {}).values():
+ menu_packages += extra_env.get("menu_packages", [])
+ if menu_packages:
+ descr += f" ({', '.join(menu_packages)})"
+ path_choice.set('description', descr)
elif ident.endswith('user_pre_install') and info.get('pre_install_desc'):
path_choice.set('visible', 'true')
path_choice.set('title', "Run the pre-install script")
@@ -306,6 +323,8 @@ def move_script(src, dst, info, ensure_shebang=False, user_script_type=None):
'PROGRESS_NOTIFICATIONS': str(info.get('progress_notifications', False)),
'PRE_OR_POST': user_script_type or '__PRE_OR_POST__',
'CONSTRUCTOR_VERSION': info['CONSTRUCTOR_VERSION'],
+ 'SHORTCUTS': shortcuts_flags(info),
+ 'ENABLE_SHORTCUTS': str(info['_enable_shortcuts']).lower(),
'REGISTER_ENVS': str(info.get("register_envs", True)).lower(),
}
data = preprocess(data, ppd)
@@ -428,7 +447,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.exe,
+ # conda-meta metadata staged into pkgs/conda-meta, _conda (conda-standalone),
# 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)
@@ -461,7 +480,7 @@ def create(info, verbose=False):
for dist in all_dists:
os.link(join(CACHE_DIR, dist), join(pkgs_dir, dist))
- shutil.copyfile(info['_conda_exe'], join(prefix, "conda.exe"))
+ shutil.copyfile(info['_conda_exe'], join(prefix, "_conda"))
# Sign conda-standalone so it can pass notarization
notarization_identity_name = info.get('notarization_identity_name')
@@ -485,7 +504,7 @@ def create(info, verbose=False):
"--options", "runtime",
"--force",
"--entitlements", f.name,
- join(prefix, "conda.exe"),
+ join(prefix, "_conda"),
]
)
os.unlink(f.name)
@@ -506,6 +525,11 @@ def create(info, verbose=False):
)
names.append('user_pre_install')
+ # pre-3. Enable or disable shortcuts creation
+ if info['_enable_shortcuts'] is True:
+ pkgbuild_script('shortcuts', info, 'check_shortcuts.sh')
+ names.append('shortcuts')
+
# 3. Run the installation
# This script-only package will run conda to link and install the packages
pkgbuild_script('run_installation', info, 'run_installation.sh')
diff --git a/constructor/preconda.py b/constructor/preconda.py
index 44350f81f..35fef41d6 100644
--- a/constructor/preconda.py
+++ b/constructor/preconda.py
@@ -26,7 +26,13 @@
)
from .conda_interface import distro as conda_distro
from .conda_interface import get_repodata, write_repodata
-from .utils import ensure_transmuted_ext, filename_dist, get_final_channels, get_final_url
+from .utils import (
+ ensure_transmuted_ext,
+ filename_dist,
+ get_final_channels,
+ get_final_url,
+ shortcuts_flags,
+)
try:
import json
@@ -173,6 +179,8 @@ def write_files(info, dst_dir):
write_env_txt(info, env_dst_dir, env_urls_md5)
# channels
write_channels_txt(info, env_dst_dir, env_config)
+ # shortcuts
+ write_shortcuts_txt(info, env_dst_dir, env_config)
def write_conda_meta(info, dst_dir, final_urls_md5s, user_requested_specs=None):
@@ -261,6 +269,15 @@ def write_channels_txt(info, dst_dir, env_config):
f.write(",".join(get_final_channels(env_config)))
+def write_shortcuts_txt(info, dst_dir, env_config):
+ if "menu_packages" in env_config:
+ contents = shortcuts_flags(env_config)
+ else:
+ contents = shortcuts_flags(info)
+ with open(join(dst_dir, "shortcuts.txt"), "w") as f:
+ f.write(contents)
+
+
def copy_extra_files(
extra_files: List[Union[os.PathLike, Mapping]], workdir: os.PathLike
) -> List[os.PathLike]:
diff --git a/constructor/shar.py b/constructor/shar.py
index 513c6bbf4..299f3ab9f 100644
--- a/constructor/shar.py
+++ b/constructor/shar.py
@@ -24,6 +24,7 @@
hash_files,
preprocess,
read_ascii_only,
+ shortcuts_flags,
)
THIS_DIR = dirname(__file__)
@@ -68,6 +69,7 @@ def get_header(conda_exec, tarball, info):
ppd['initialize_conda'] = info.get('initialize_conda', True)
ppd['initialize_by_default'] = info.get('initialize_by_default', False)
ppd['has_conda'] = info['_has_conda']
+ ppd['enable_shortcuts'] = str(info['_enable_shortcuts']).lower()
ppd['check_path_spaces'] = info.get("check_path_spaces", True)
install_lines = list(add_condarc(info))
# Needs to happen first -- can be templated
@@ -86,6 +88,7 @@ def get_header(conda_exec, tarball, info):
'CHANNELS': ','.join(get_final_channels(info)),
'CONCLUSION_TEXT': info.get("conclusion_text", "installation finished."),
'pycache': '__pycache__',
+ 'SHORTCUTS': shortcuts_flags(info),
'REGISTER_ENVS': str(info.get("register_envs", True)).lower(),
}
if has_license:
@@ -121,6 +124,8 @@ def create(info, verbose=False):
for env_name in info.get("_extra_envs_info", ()):
pre_t.add(join(tmp_dir, "envs", env_name, "env.txt"),
f"pkgs/envs/{env_name}/env.txt")
+ pre_t.add(join(tmp_dir, "envs", env_name, "shortcuts.txt"),
+ f"pkgs/envs/{env_name}/shortcuts.txt")
for key in 'pre_install', 'post_install':
if key in info:
diff --git a/constructor/utils.py b/constructor/utils.py
index b763fab9e..09a96f2eb 100644
--- a/constructor/utils.py
+++ b/constructor/utils.py
@@ -10,9 +10,9 @@
import re
import sys
from os import sep, unlink
-from os.path import basename, isdir, isfile, islink, normpath
+from os.path import basename, isdir, isfile, islink, join, normpath
from shutil import rmtree
-from subprocess import check_call
+from subprocess import check_call, check_output
from ruamel import yaml
@@ -205,6 +205,27 @@ def yield_lines(path):
yield line
+def shortcuts_flags(info, conda_exe=None):
+ menu_packages = info.get("menu_packages")
+ conda_exe = conda_exe or info.get("_conda_exe", "")
+ is_micromamba = "micromamba" in basename(conda_exe).lower()
+ if menu_packages is None:
+ # not set: we create all shortcuts (default behaviour)
+ return ""
+ if menu_packages:
+ if is_micromamba:
+ logger.warning(
+ "Micromamba does not support '--shortcuts-only'. "
+ "Will install all shortcuts."
+ )
+ return ""
+ # set and populated: we only create shortcuts for some
+ # NOTE: This syntax requires conda 23.11 or above
+ return " ".join([f"--shortcuts-only={pkg.strip()}" for pkg in menu_packages])
+ # set but empty: disable all shortcuts
+ return "--no-shortcuts"
+
+
def approx_size_kb(info, which="pkgs"):
valid = ("pkgs", "tarballs", "total")
assert which in valid, f"'which' must be one of {valid}"
@@ -219,3 +240,18 @@ def approx_size_kb(info, which="pkgs"):
# division by 10^3 instead of 2^10 is deliberate here. gives us more room
return int(math.ceil(size_bytes/1000))
+
+
+def identify_conda_exe(conda_exe=None):
+ if conda_exe is None:
+ conda_exe = normalize_path(join(sys.prefix, "standalone_conda", "conda.exe"))
+ output = check_output([conda_exe, "--version"], text=True)
+ output = output.strip()
+ fields = output.split()
+ if "conda" in fields:
+ name = "conda-standalone"
+ version = fields[1]
+ else:
+ name = "micromamba"
+ version = output.strip()
+ return name, version
diff --git a/constructor/winexe.py b/constructor/winexe.py
index 3ea7c47b8..93a6d6a1b 100644
--- a/constructor/winexe.py
+++ b/constructor/winexe.py
@@ -26,6 +26,7 @@
get_final_channels,
make_VIProductVersion,
preprocess,
+ shortcuts_flags,
)
NSIS_DIR = join(abspath(dirname(__file__)), 'nsis')
@@ -121,32 +122,41 @@ def custom_nsi_insert_from_file(filepath: os.PathLike) -> str:
def setup_envs_commands(info, dir_path):
template = r"""
# Set up {name} env
- SetDetailsPrint TextOnly
- DetailPrint "Setting up the {name} environment ..."
SetDetailsPrint both
+ DetailPrint "Setting up the {name} environment ..."
+ SetDetailsPrint listonly
+
# List of packages to install
SetOutPath "{env_txt_dir}"
File "{env_txt_abspath}"
+
# A conda-meta\history file is required for a valid conda prefix
SetOutPath "{conda_meta}"
File "{history_abspath}"
+
# Set channels
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_CHANNELS", "{channels}").r0'
# Set register_envs
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_REGISTER_ENVS", "{register_envs}").r0'
- # Run conda
- SetDetailsPrint TextOnly
- nsExec::ExecToLog '"$INSTDIR\_conda.exe" install --offline -yp "{prefix}" --file "{env_txt}" {shortcuts}'
- Pop $0
- ${{If}} $0 != "0"
- DetailPrint "::error:: Failed to link extracted packages to {prefix}!"
- MessageBox MB_OK|MB_ICONSTOP "Failed to link extracted packages to {prefix}. Please check logs." /SD IDOK
- Abort
+
+ # Run conda install
+ ${{If}} $Ana_CreateShortcuts_State = ${{BST_CHECKED}}
+ DetailPrint "Installing packages for {name}, creating shortcuts if necessary..."
+ push '"$INSTDIR\_conda.exe" install --offline -yp "{prefix}" --file "{env_txt}" {shortcuts}'
+ ${{Else}}
+ DetailPrint "Installing packages for {name}..."
+ push '"$INSTDIR\_conda.exe" install --offline -yp "{prefix}" --file "{env_txt}" --no-shortcuts'
${{EndIf}}
+ push 'Failed to link extracted packages to {prefix}!'
+ push 'WithLog'
+ SetDetailsPrint listonly
+ call AbortRetryNSExecWait
SetDetailsPrint both
+
# Cleanup {name} env.txt
SetOutPath "$INSTDIR"
Delete "{env_txt}"
+
# Restore shipped conda-meta\history for remapped
# channels and retain only the first transaction
SetOutPath "{conda_meta}"
@@ -162,7 +172,7 @@ def setup_envs_commands(info, dir_path):
conda_meta=r"$INSTDIR\conda-meta",
history_abspath=join(dir_path, "conda-meta", "history"),
channels=','.join(get_final_channels(info)),
- shortcuts="--no-shortcuts",
+ shortcuts=shortcuts_flags(info),
register_envs=str(info.get("register_envs", True)).lower(),
).splitlines()
# now we generate one more block per extra env, if present
@@ -182,13 +192,31 @@ def setup_envs_commands(info, dir_path):
conda_meta=join("$INSTDIR", "envs", env_name, "conda-meta"),
history_abspath=join(dir_path, "envs", env_name, "conda-meta", "history"),
channels=",".join(get_final_channels(channel_info)),
- shortcuts="",
+ shortcuts=shortcuts_flags(env_info, conda_exe=info.get("_conda_exe")),
register_envs=str(info.get("register_envs", True)).lower(),
).splitlines()
return [line.strip() for line in lines]
+def uninstall_menus_commands(info):
+ tmpl = r"""
+ SetDetailsPrint both
+ DetailPrint "Deleting {name} menus in {env_name}..."
+ SetDetailsPrint listonly
+ push '"$INSTDIR\_conda.exe" constructor --prefix "{path}" --rm-menus'
+ push 'Failed to delete menus in {env_name}'
+ push 'WithLog'
+ call un.AbortRetryNSExecWait
+ SetDetailsPrint both
+ """
+ lines = tmpl.format(name=info["name"], env_name="base", path="$INSTDIR").splitlines()
+ for env_name in info.get("_extra_envs_info", {}):
+ path = join("$INSTDIR", "envs", env_name)
+ lines += tmpl.format(name=info["name"], env_name=env_name, path=path).splitlines()
+ return [line.strip() for line in lines]
+
+
def signtool_command(info):
"Generates a signtool command to be used in the NSIS template"
pfx_certificate = info.get("signing_certificate")
@@ -230,7 +258,6 @@ def make_nsi(info, dir_path, extra_files=None, temp_extra_files=None):
info['pre_install_desc'] = info.get('pre_install_desc', "")
info['post_install_desc'] = info.get('post_install_desc', "")
- # these appear as ____ in the template, and get escaped
replace = {
'NAME': name,
'VERSION': info['version'],
@@ -248,6 +275,7 @@ def make_nsi(info, dir_path, extra_files=None, temp_extra_files=None):
join('%ALLUSERSPROFILE%', name.lower())),
'PRE_INSTALL_DESC': info['pre_install_desc'],
'POST_INSTALL_DESC': info['post_install_desc'],
+ 'ENABLE_SHORTCUTS': "yes" if info['_enable_shortcuts'] is True else "no",
'SHOW_REGISTER_PYTHON': "yes" if info.get("register_python", True) else "no",
'SHOW_ADD_TO_PATH': "yes" if info.get("initialize_conda", True) else "no",
'OUTFILE': info['_outpath'],
@@ -347,11 +375,11 @@ def make_nsi(info, dir_path, extra_files=None, temp_extra_files=None):
('@SIGNTOOL_COMMAND@', signtool_command(info)),
('@SETUP_ENVS@', '\n '.join(setup_envs_commands(info, dir_path))),
('@WRITE_CONDARC@', '\n '.join(add_condarc(info))),
- ('@MENU_PKGS@', ' '.join(info.get('menu_packages', []))),
('@SIZE@', str(approx_pkgs_size_kb)),
('@UNINSTALL_NAME@', info.get('uninstall_name',
'${NAME} ${VERSION} (Python ${PYVERSION} ${ARCH})'
)),
+ ('@UNINSTALL_MENUS@', '\n '.join(uninstall_menus_commands(info))),
('@EXTRA_FILES@', '\n '.join(extra_files_commands(extra_files, dir_path))),
('@SCRIPT_ENV_VARIABLES@', '\n '.join(setup_script_env_variables(info))),
(
diff --git a/dev/environment.yml b/dev/environment.yml
index 5d5e3fde8..ba74a8fd6 100644
--- a/dev/environment.yml
+++ b/dev/environment.yml
@@ -5,5 +5,5 @@ dependencies:
- python
- conda >=4.6
- ruamel.yaml >=0.11.14,<0.18
- - conda-standalone
+ - conda-standalone # >=23.11.0
- pillow >=3.1 # [osx or win]
diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md
index f737677a3..ccb36a91e 100644
--- a/docs/source/construct-yaml.md
+++ b/docs/source/construct-yaml.md
@@ -128,9 +128,16 @@ _required:_ no
_type:_ list
A list of packages with menu items to be installed. The packages must have
-necessary metadata in `Menu/.json`). Menu items are currently
-only supported on Windows. By default, all menu items will be installed;
-supplying this list allows a subset to be selected instead.
+necessary metadata in `Menu/.json`). By default, all menu items
+found in the installation will be created; supplying this list allows a
+subset to be selected instead. If an empty list is supplied, no shortcuts will
+be created.
+
+If all environments (`extra_envs` included) set `menu_packages` to an empty list,
+no UI options about shortcuts will be offered to the user.
+
+Note: This option is not fully implemented when `micromamba` is used as
+the `--conda-exe` binary. The only accepted value is an empty list (`[]`).
### `ignore_duplicate_files`
@@ -220,12 +227,12 @@ name) to a dictionary of options:
an empty list.
- `user_requested_specs` (list of str): same as the global option, but for this env;
if not provided, global value is _not_ used
+- `menu_packages` (list of str): same as the global option, for this env;
+ if not provided, the global value is _not_ used.
Notes:
- `ignore_duplicate_files` will always be considered `True` if `extra_envs` is in use.
- `conda` needs to be present in the `base` environment (via `specs`)
-- support for `menu_packages` is planned, but not possible right now. For now, all packages
- in an `extra_envs` config will be allowed to create their shortcuts.
- If a global `exclude` option is used, it will have an effect on the environments created
by `extra_envs` too. For example, if the global environment excludes `tk`, none of the
extra environments will have it either. Unlike the global option, an error will not be
diff --git a/docs/source/howto.md b/docs/source/howto.md
index 95fac9315..2481439a0 100644
--- a/docs/source/howto.md
+++ b/docs/source/howto.md
@@ -60,6 +60,11 @@ under `$PREFIX/Menu`, `conda` will process it to create the specified menu items
This happens by default for _all packages_. If you only want this to happen for certain packages,
use the [`menu_packages`](construct-yaml.md#menu_packages) key.
+Starting with `conda` 23.11, `menuinst 2.x` is supported, which means you can create shortcuts in all platforms (Linux, macOS and Windows).
+The JSON document format is slightly different, so make sure to check the [menuinst documentation](https://conda.github.io/menuinst/).
+Your installer will need to be created with `conda-standalone 23.11` or above.
+`micromamba` does not currently support `menuinst 2.x` style shortcuts (only `1.x` on Windows).
+
To learn more about `menuinst`, visit [`conda/menuinst`](https://github.com/conda/menuinst).
## Find out the used constructor version
diff --git a/examples/osxpkg/construct.yaml b/examples/osxpkg/construct.yaml
index d10fb6ed4..b749fa6ec 100644
--- a/examples/osxpkg/construct.yaml
+++ b/examples/osxpkg/construct.yaml
@@ -10,7 +10,6 @@ channels:
- http://repo.anaconda.com/pkgs/main/
attempt_hardlinks: True
-initialize_by_default: false
specs:
- python
@@ -70,3 +69,6 @@ conclusion_text: |
install_path_exists_error_text: >
{CHOSEN_PATH} exists! Please update using our in-app mechanisms or
relaunch the installer and choose a different location.
+
+initialize_by_default: false
+register_python: False
diff --git a/examples/shortcuts/construct.yaml b/examples/shortcuts/construct.yaml
index d7de2d7bb..e22d46d97 100644
--- a/examples/shortcuts/construct.yaml
+++ b/examples/shortcuts/construct.yaml
@@ -3,12 +3,25 @@ version: X
installer_type: all
channels:
+ - conda-test/label/menuinst-tests
- http://repo.anaconda.com/pkgs/main/
specs:
- python
- conda
- - console_shortcut # [win]
+ - console_shortcut # [win]
+ - package_1
+
+menu_packages:
+ - package_1
+
+extra_envs:
+ another_env:
+ specs:
+ - package_1
+ - console_shortcut # [win]
+ menu_packages: # [win]
+ - console_shortcut # [win]
initialize_by_default: false
register_python: False
diff --git a/examples/use_channel_remap/construct.yaml b/examples/use_channel_remap/construct.yaml
index fbd3fe588..2fd86f0ff 100644
--- a/examples/use_channel_remap/construct.yaml
+++ b/examples/use_channel_remap/construct.yaml
@@ -19,3 +19,4 @@ specs:
license_file: eula.txt
initialize_by_default: false
+register_python: false
diff --git a/news/474-menuinst-v2 b/news/474-menuinst-v2
new file mode 100644
index 000000000..585f6c2aa
--- /dev/null
+++ b/news/474-menuinst-v2
@@ -0,0 +1,20 @@
+### Enhancements
+
+* Add support for `menuinst` v2, which extends shortcut (menu items) creation from Windows to Linux and macOS. See [`menuinst` documentation](https://conda.github.io/menuinst/) for more information. Note that this feature requires `conda-standalone 23.11.0` or later. `micromamba` doesn't support v2-style menu items yet. (#474)
+
+### Bug fixes
+
+*
+
+### Deprecations
+
+*
+
+### Docs
+
+*
+
+### Other
+
+* Unix installers now bundle conda-standalone (or micromamba) as `_conda`, instead of `conda.exe`.
+ (#741 via #474)
diff --git a/pyproject.toml b/pyproject.toml
index b9a12345b..4e2d0a129 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,6 +6,9 @@ target-version = ['py37', 'py38', 'py39', 'py310']
profile = "black"
line_length = 100
+[tool.ruff]
+line-length = 100
+
[tool.pytest.ini_options]
markers = [
"examples",
diff --git a/tests/test_examples.py b/tests/test_examples.py
index 55f3b27d2..1c3609403 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -12,6 +12,9 @@
import pytest
from conda.base.context import context
from conda.core.prefix_data import PrefixData
+from conda.models.version import VersionOrder as Version
+
+from constructor.utils import identify_conda_exe
if sys.platform == "darwin":
from constructor.osxpkg import calculate_install_dir
@@ -28,6 +31,7 @@
REPO_DIR = Path(__file__).parent.parent
ON_CI = os.environ.get("CI")
CONSTRUCTOR_CONDA_EXE = os.environ.get("CONSTRUCTOR_CONDA_EXE")
+CONDA_EXE, CONDA_EXE_VERSION = identify_conda_exe(CONSTRUCTOR_CONDA_EXE)
CONSTRUCTOR_DEBUG = bool(os.environ.get("CONSTRUCTOR_DEBUG"))
if artifacts_path := os.environ.get("CONSTRUCTOR_EXAMPLES_KEEP_ARTIFACTS"):
KEEP_ARTIFACTS_PATH = Path(artifacts_path)
@@ -72,6 +76,31 @@ def _execute(
print("Took", timedelta(seconds=time.time() - t0))
+def _check_installer_log(install_dir):
+ # Windows installers won't raise exit codes so we need to check the log file
+ error_lines = []
+ try:
+ log_is_empty = True
+ with open(os.path.join(install_dir, "install.log"), encoding="utf-16-le") as f:
+ print("Installer log:", file=sys.stderr)
+ for line in f:
+ log_is_empty = False
+ print(line, end="", file=sys.stderr)
+ if ":error:" in line.lower():
+ error_lines.append(line)
+ if log_is_empty:
+ error_lines.append("Logfile was unexpectedly empty!")
+ except Exception as exc:
+ error_lines.append(
+ f"Could not read logs! {exc.__class__.__name__}: {exc}\n"
+ "This usually means that the destination folder could not be created.\n"
+ "Possible causes: permissions, non-supported characters, long paths...\n"
+ "Consider setting 'check_path_spaces' and 'check_path_length' to 'False'."
+ )
+ if error_lines:
+ raise AssertionError("\n".join(error_lines))
+
+
def _run_installer_exe(installer, install_dir, installer_input=None, timeout=420):
"""
NSIS manual:
@@ -96,27 +125,7 @@ def _run_installer_exe(installer, install_dir, installer_input=None, timeout=420
)
cmd = ["cmd.exe", "/c", "start", "/wait", installer, "/S", *f"/D={install_dir}".split()]
_execute(cmd, installer_input=installer_input, timeout=timeout)
-
- # Windows installers won't raise exit codes so we need to check the log file
- error_lines = []
- try:
- log_is_empty = True
- with open(os.path.join(install_dir, "install.log"), encoding="utf-16-le") as f:
- for line in f:
- log_is_empty = False
- if ":error:" in line.lower():
- error_lines.append(line)
- if log_is_empty:
- error_lines.append("Logfile was unexpectedly empty!")
- except Exception as exc:
- error_lines.append(
- f"Could not read logs! {exc.__class__.__name__}: {exc}\n"
- "This usually means that the destination folder could not be created.\n"
- "Possible causes: permissions, non-supported characters, long paths...\n"
- "Consider setting 'check_path_spaces' and 'check_path_length' to 'False'."
- )
- if error_lines:
- raise AssertionError("\n".join(error_lines))
+ _check_installer_log(install_dir)
def _run_uninstaller_exe(install_dir, timeout=420):
@@ -128,6 +137,10 @@ def _run_uninstaller_exe(install_dir, timeout=420):
"This is a known issue with our setup, to be fixed."
)
return
+ # Rename install.log
+ install_log = install_dir / "install.log"
+ if install_log.exists():
+ install_log.rename(install_dir / "install.log.bak")
uninstaller = next(install_dir.glob("Uninstall-*.exe"), None)
if not uninstaller:
@@ -145,8 +158,9 @@ def _run_uninstaller_exe(install_dir, timeout=420):
f"/S _?={install_dir}",
]
_execute(cmd, timeout=timeout)
+ _check_installer_log(install_dir)
remaining_files = list(install_dir.iterdir())
- if len(remaining_files) > 2:
+ if len(remaining_files) > 3:
# 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.
@@ -209,6 +223,7 @@ def _run_installer(
installer_input: Optional[str] = None,
check_sentinels=True,
request=None,
+ uninstall=True,
timeout=420,
):
if installer.suffix == ".exe":
@@ -223,7 +238,7 @@ def _run_installer(
raise ValueError(f"Unknown installer type: {installer.suffix}")
if check_sentinels:
_sentinel_file_checks(example_path, install_dir)
- if installer.suffix == ".exe":
+ if uninstall and installer.suffix == ".exe":
_run_uninstaller_exe(install_dir, timeout=timeout)
@@ -237,7 +252,7 @@ def create_installer(
**env_vars,
) -> Tuple[Path, Path]:
if sys.platform.startswith("win") and conda_exe and _is_micromamba(conda_exe):
- pytest.skip("Micromamba is not supported on Windows yet (shortcut creation).")
+ pytest.skip("Micromamba is not supported on Windows yet.")
output_dir = workspace / "installer"
output_dir.mkdir(parents=True, exist_ok=True)
@@ -320,6 +335,10 @@ def test_example_extra_files(tmp_path, request):
_run_installer(input_path, installer, install_dir, request=request)
+@pytest.mark.xfail(
+ CONDA_EXE == "conda-standalone" and Version(CONDA_EXE_VERSION) < Version("23.11.0a0"),
+ reason="Known issue with conda-standalone<=23.10: shortcuts are created but not removed.",
+)
def test_example_miniforge(tmp_path, request):
input_path = _example_path("miniforge")
for installer, install_dir in create_installer(input_path, tmp_path):
@@ -339,9 +358,23 @@ def test_example_miniforge(tmp_path, request):
# PKG installers use their own install path, so we can't check sentinels
# via `install_dir`
check_sentinels=installer.suffix != ".pkg",
+ uninstall=False,
)
if installer.suffix == ".pkg" and ON_CI:
_sentinel_file_checks(input_path, Path(os.environ["HOME"]) / "Miniforge3")
+ if installer.suffix == ".exe":
+ for key in ("ProgramData", "AppData"):
+ start_menu_dir = Path(
+ os.environ[key],
+ "Microsoft/Windows/Start Menu/Programs/Miniforge3",
+ )
+ if start_menu_dir.is_dir():
+ assert list(start_menu_dir.glob("Miniforge*.lnk"))
+ break
+ else:
+ raise AssertionError("Could not find Start Menu folder for miniforge")
+ _run_uninstaller_exe(install_dir)
+ assert not list(start_menu_dir.glob("Miniforge*.lnk"))
def test_example_noconda(tmp_path, request):
@@ -363,10 +396,44 @@ def test_example_scripts(tmp_path, request):
_run_installer(input_path, installer, install_dir, request=request)
+@pytest.mark.skipif(
+ CONDA_EXE == "micromamba" or Version(CONDA_EXE_VERSION) < Version("23.11.0a0"),
+ reason="menuinst v2 requires conda-standalone>=23.11.0; micromamba is not supported yet",
+)
def test_example_shortcuts(tmp_path, request):
input_path = _example_path("shortcuts")
for installer, install_dir in create_installer(input_path, tmp_path):
- _run_installer(input_path, installer, install_dir, request=request)
+ _run_installer(input_path, installer, install_dir, request=request, uninstall=False)
+ # check that the shortcuts are created
+ if sys.platform == "win32":
+ for key in ("ProgramData", "AppData"):
+ start_menu = Path(os.environ[key]) / "Microsoft/Windows/Start Menu/Programs"
+ package_1 = start_menu / "Package 1"
+ anaconda = start_menu / "Anaconda3 (64-bit)"
+ if package_1.is_dir() and anaconda.is_dir():
+ assert (package_1 / "A.lnk").is_file()
+ assert (package_1 / "B.lnk").is_file()
+ # The shortcut created from the 'base' env
+ # should not exist because we filtered it out in the YAML
+ # We do expect one shortcut from 'another_env'
+ assert not (anaconda / "Anaconda Prompt.lnk").is_file()
+ assert (anaconda / "Anaconda Prompt (another_env).lnk").is_file()
+ break
+ else:
+ raise AssertionError("No shortcuts found!")
+ _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":
+ applications = Path("~/Applications").expanduser()
+ print("Shortcuts found:", sorted(applications.glob("**/*.app")))
+ assert (applications / "A.app").exists()
+ assert (applications / "B.app").exists()
+ elif sys.platform == "linux":
+ applications = Path("~/.local/share/applications").expanduser()
+ print("Shortcuts found:", sorted(applications.glob("**/*.desktop")))
+ assert (applications / "package-1_a.desktop").exists()
+ assert (applications / "package-1_b.desktop").exists()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
diff --git a/tests/test_header.py b/tests/test_header.py
index 725fe6077..ab8c5daf9 100644
--- a/tests/test_header.py
+++ b/tests/test_header.py
@@ -54,6 +54,7 @@ def test_linux_template_processing():
initialize_by_default,
has_post_install,
has_pre_install,
+ enable_shortcuts,
check_path_spaces,
arch,
) in itertools.product(
@@ -69,6 +70,7 @@ def test_linux_template_processing():
[False, True],
[False, True],
[False, True],
+ [False, True],
["x86", "x86_64", " ppc64le", "s390x", "aarch64"],
):
params = {
@@ -89,13 +91,15 @@ def test_linux_template_processing():
"direct_execute_post_install": direct_execute_post_install,
"initialize_conda": initialize_conda,
"initialize_by_default": initialize_by_default,
+ "enable_shortcuts": enable_shortcuts,
"check_path_spaces": check_path_spaces,
}
processed = preprocess(template, params)
for template_string in ["#if", "#else", "#endif"]:
if template_string in processed:
- errors.append(f"Found '{template_string}' after "
- f"processing header.sh with '{params}'.")
+ errors.append(
+ f"Found '{template_string}' after " f"processing header.sh with '{params}'."
+ )
assert not errors
@@ -117,13 +121,22 @@ def test_osxpkg_scripts_template_processing(arch, check_path_spaces, script):
@pytest.mark.skipif(available_command("shellcheck") is False, reason="requires shellcheck")
@pytest.mark.parametrize("arch", ["x86_64", "arm64"])
@pytest.mark.parametrize("check_path_spaces", [False, True])
-@pytest.mark.parametrize("script", sorted(Path(OSX_DIR).glob("*.sh")))
+@pytest.mark.parametrize(
+ "script", [pytest.param(path, id=str(path)) for path in sorted(Path(OSX_DIR).glob("*.sh"))]
+)
def test_osxpkg_scripts_shellcheck(arch, check_path_spaces, script):
with script.open() as f:
data = f.read()
- processed = preprocess(data, {"arch": arch, "check_path_spaces": check_path_spaces})
+ processed = preprocess(
+ data,
+ {
+ "arch": arch,
+ "check_path_spaces": check_path_spaces,
+ },
+ )
findings, returncode = run_shellcheck(processed)
+ print(*findings, sep="\n")
assert findings == []
assert returncode == 0
@@ -142,6 +155,7 @@ def test_osxpkg_scripts_shellcheck(arch, check_path_spaces, script):
@pytest.mark.parametrize("has_pre_install", [False])
@pytest.mark.parametrize("arch", ["x86_64", "aarch64"])
@pytest.mark.parametrize("check_path_spaces", [True])
+@pytest.mark.parametrize("enable_shortcuts", ["true"])
def test_template_shellcheck(
osx,
arch,
@@ -156,6 +170,7 @@ def test_template_shellcheck(
direct_execute_pre_install,
direct_execute_post_install,
check_path_spaces,
+ enable_shortcuts,
):
template = read_header_template()
processed = preprocess(
@@ -179,6 +194,7 @@ def test_template_shellcheck(
"initialize_conda": initialize_conda,
"initialize_by_default": initialize_by_default,
"check_path_spaces": check_path_spaces,
+ "enable_shortcuts": enable_shortcuts,
},
)