diff --git a/CONSTRUCT.md b/CONSTRUCT.md
index 9e02ff8e2..67114e00d 100644
--- a/CONSTRUCT.md
+++ b/CONSTRUCT.md
@@ -113,6 +113,18 @@ for example, if `python=3.6` is included, then conda will always seek versions
of packages compatible with Python 3.6. If this is option is not provided, it
will be set equal to the value of `specs`.
+### `virtual_specs`
+
+_required:_ no
+_type:_ list
+
+A list of virtual packages that must be satisfied at install time. Virtual
+packages must start with `__`. For example, `__osx>=11` or `__glibc>=2.24`.
+These specs are dry-run solved offline by the bundled `--conda-exe` binary.
+In SH installers, `__glibc>=x.y` and `__osx>=x.y` specs can be checked with
+Bash only. In PKG installers, `__osx` specs can be checked natively without
+the solver being involved as long as only `>=`, `<` or `,` are used.
+
### `exclude`
_required:_ no
@@ -810,7 +822,7 @@ _required:_ no
_type:_ list
Temporary files that could be referenced in the installation process (i.e. customized
- `welcome_file` and `conclusion_file` (see above)) . Should be a list of
+`welcome_file` and `conclusion_file` (see above)) . Should be a list of
file paths, relative to the directory where `construct.yaml` is. In Windows, these
files will be copied into a temporary folder, the NSIS `$PLUGINSDIR`, during
install process (Windows only).
diff --git a/constructor/construct.py b/constructor/construct.py
index 2fe4c8978..c6b8de183 100644
--- a/constructor/construct.py
+++ b/constructor/construct.py
@@ -81,6 +81,15 @@
for example, if `python=3.6` is included, then conda will always seek versions
of packages compatible with Python 3.6. If this is option is not provided, it
will be set equal to the value of `specs`.
+'''),
+
+ ('virtual_specs', False, list, '''
+A list of virtual packages that must be satisfied at install time. Virtual
+packages must start with `__`. For example, `__osx>=11` or `__glibc>=2.24`.
+These specs are dry-run solved offline by the bundled `--conda-exe` binary.
+In SH installers, `__glibc>=x.y` and `__osx>=x.y` specs can be checked with
+Bash only. In PKG installers, `__osx` specs can be checked natively without
+the solver being involved as long as only `>=`, `<` or `,` are used.
'''),
('exclude', False, list, '''
@@ -594,7 +603,7 @@
('temp_extra_files', False, list, '''
Temporary files that could be referenced in the installation process (i.e. customized
- `welcome_file` and `conclusion_file` (see above)) . Should be a list of
+`welcome_file` and `conclusion_file` (see above)) . Should be a list of
file paths, relative to the directory where `construct.yaml` is. In Windows, these
files will be copied into a temporary folder, the NSIS `$PLUGINSDIR`, during
install process (Windows only).
diff --git a/constructor/header.sh b/constructor/header.sh
index 0fdbee3b5..129d3da95 100644
--- a/constructor/header.sh
+++ b/constructor/header.sh
@@ -21,6 +21,40 @@ if ! echo "$0" | grep '\.sh$' > /dev/null; then
return 1
fi
+#if osx and min_osx_version
+min_osx_version="__MIN_OSX_VERSION__"
+system_osx_version=$(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
+#endif
+#if linux and min_glibc_version
+min_glibc_version="__MIN_GLIBC_VERSION__"
+case "$(ldd --version 2>&1)" in
+ *musl*)
+ # musl ldd will report musl version; call ld.so directly
+ system_glibc_version=$($(find /lib/ /lib64/ -name 'ld-linux-*.so*' 2>/dev/null | head -1) --version | awk 'NR==1{ sub(/\.$/, ""); print $NF}')
+ ;;
+ *)
+ # ldd reports glibc in the last field of the first line
+ system_glibc_version=$(ldd --version | awk 'NR==1{print $NF}')
+ ;;
+esac
+# 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
+#endif
+
# Export variables to make installer metadata available to pre/post install scripts
# NOTE: If more vars are added, make sure to update the examples/scripts tests too
@@ -423,6 +457,17 @@ export TMP_BACKUP="${TMP:-}"
export TMP="$PREFIX/install_tmp"
mkdir -p "$TMP"
+# Check whether the virtual specs can be satisfied
+# We need to specify CONDA_SOLVER=classic for conda-standalone
+# to work around this bug in conda-libmamba-solver:
+# https://github.com/conda/conda-libmamba-solver/issues/480
+# shellcheck disable=SC2050
+if [ "__VIRTUAL_SPECS__" != "" ]; then
+ CONDA_QUIET="$BATCH" \
+ CONDA_SOLVER="classic" \
+ "$CONDA_EXEC" create --dry-run --prefix "$PREFIX" --offline __VIRTUAL_SPECS__
+fi
+
# Create $PREFIX/.nonadmin if the installation didn't require superuser permissions
if [ "$(id -u)" -ne 0 ]; then
touch "$PREFIX/.nonadmin"
diff --git a/constructor/main.py b/constructor/main.py
index c2223a6bf..4625a9a69 100644
--- a/constructor/main.py
+++ b/constructor/main.py
@@ -115,11 +115,18 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
elif info.get("signing_certificate"):
info["windows_signing_tool"] = "signtool"
- for key in 'specs', 'packages':
+ for key in 'specs', 'packages', 'virtual_specs':
if key not in info:
continue
if isinstance(info[key], str):
info[key] = list(yield_lines(join(dir_path, info[key])))
+ if key == "virtual_specs":
+ for value in info[key]:
+ if not value.startswith("__"):
+ raise ValueError(
+ "'virtual_specs' can only include virtual package names like '__name', "
+ f"but you supplied: {value}."
+ )
# normalize paths to be copied; if they are relative, they must be to
# construct.yaml's parent (dir_path)
@@ -137,7 +144,7 @@ def main_build(dir_path, output_dir='.', platform=cc_platform,
new_extras.append({orig: dest})
info[extra_type] = new_extras
- for key in 'channels', 'specs', 'exclude', 'packages', 'menu_packages':
+ for key in 'channels', 'specs', 'exclude', 'packages', 'menu_packages', 'virtual_specs':
if key in info:
# ensure strings in those lists are stripped
info[key] = [line.strip() for line in info[key]]
diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl
index 664e79bdc..96190cf6e 100644
--- a/constructor/nsis/main.nsi.tmpl
+++ b/constructor/nsis/main.nsi.tmpl
@@ -1132,6 +1132,21 @@ Section "Install"
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_PLAT", "${PLATFORM}").r0'
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_TYPE", "EXE").r0'
+ ${If} '@VIRTUAL_SPECS@' != ''
+ # We need to specify CONDA_SOLVER=classic for conda-standalone
+ # to work around this bug in conda-libmamba-solver:
+ # https://github.com/conda/conda-libmamba-solver/issues/480
+ System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SOLVER", "classic").r0'
+ SetDetailsPrint TextOnly
+ DetailPrint "Checking virtual specs..."
+ push '"$INSTDIR\_conda.exe" create --dry-run --prefix "$INSTDIR" --offline @VIRTUAL_SPECS@'
+ push 'Failed to check virtual specs: @VIRTUAL_SPECS@'
+ push 'WithLog'
+ call AbortRetryNSExecWait
+ SetDetailsPrint both
+ System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SOLVER", "").r0'
+ ${EndIf}
+
@PKG_COMMANDS@
SetDetailsPrint TextOnly
diff --git a/constructor/osx/prepare_installation.sh b/constructor/osx/prepare_installation.sh
index 93e34fcea..6d59d60a8 100644
--- a/constructor/osx/prepare_installation.sh
+++ b/constructor/osx/prepare_installation.sh
@@ -31,6 +31,17 @@ chmod +x "$CONDA_EXEC"
mkdir -p "$PREFIX/conda-meta"
touch "$PREFIX/conda-meta/history"
+# Check whether the virtual specs can be satisfied
+# We need to specify CONDA_SOLVER=classic for conda-standalone
+# to work around this bug in conda-libmamba-solver:
+# https://github.com/conda/conda-libmamba-solver/issues/480
+# shellcheck disable=SC2050
+if [ "__VIRTUAL_SPECS__" != "" ]; then
+ CONDA_QUIET="$BATCH" \
+ CONDA_SOLVER="classic" \
+ "$CONDA_EXEC" create --dry-run --prefix "$PREFIX" --offline __VIRTUAL_SPECS__
+fi
+
# Create $PREFIX/.nonadmin if the installation didn't require superuser permissions
if [ "$(id -u)" -ne 0 ]; then
touch "$PREFIX/.nonadmin"
diff --git a/constructor/osxpkg.py b/constructor/osxpkg.py
index ca55934ca..faebf490b 100644
--- a/constructor/osxpkg.py
+++ b/constructor/osxpkg.py
@@ -1,5 +1,6 @@
import logging
import os
+import shlex
import shutil
import sys
import xml.etree.ElementTree as ET
@@ -18,6 +19,7 @@
explained_check_call,
fill_template,
get_final_channels,
+ parse_virtual_specs,
preprocess,
rm_rf,
shortcuts_flags,
@@ -188,6 +190,18 @@ def modify_xml(xml_path, info):
)
root.append(readme)
+ # -- __osx virtual package checks -- #
+ # Reference: https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html # noqa
+ osx_versions = parse_virtual_specs(info).get("__osx")
+ if osx_versions:
+ if "min" not in osx_versions:
+ raise ValueError("Specifying __osx requires a lower bound with `>=`")
+ allowed_os_versions = ET.Element("allowed-os-versions")
+ allowed_os_versions.append(ET.Element("os-version", osx_versions))
+ volume_check = ET.Element("volume-check")
+ volume_check.append(allowed_os_versions)
+ root.append(volume_check)
+
# See below for an explanation of the consequences of this
# customLocation value.
for options in root.findall('options'):
@@ -327,6 +341,7 @@ def move_script(src, dst, info, ensure_shebang=False, user_script_type=None):
'SHORTCUTS': shortcuts_flags(info),
'ENABLE_SHORTCUTS': str(info['_enable_shortcuts']).lower(),
'REGISTER_ENVS': str(info.get("register_envs", True)).lower(),
+ 'VIRTUAL_SPECS': shlex.join(info.get("virtual_specs", ())),
}
data = preprocess(data, ppd)
custom_variables = info.get('script_env_variables', {})
diff --git a/constructor/shar.py b/constructor/shar.py
index 9f2d90425..322077d87 100644
--- a/constructor/shar.py
+++ b/constructor/shar.py
@@ -6,6 +6,7 @@
import logging
import os
+import shlex
import shutil
import stat
import tarfile
@@ -23,6 +24,7 @@
fill_template,
get_final_channels,
hash_files,
+ parse_virtual_specs,
preprocess,
read_ascii_only,
shortcuts_flags,
@@ -92,10 +94,17 @@ def get_header(conda_exec, tarball, info):
'SHORTCUTS': shortcuts_flags(info),
'REGISTER_ENVS': str(info.get("register_envs", True)).lower(),
'TOTAL_INSTALLATION_SIZE_KB': str(approx_size_kb(info, "total")),
+ 'VIRTUAL_SPECS': shlex.join(info.get("virtual_specs", ()))
}
if has_license:
replace['LICENSE'] = read_ascii_only(info['license_file'])
+ virtual_specs = parse_virtual_specs(info)
+ min_osx_version = virtual_specs.get("__osx", {}).get("min") or ""
+ replace['MIN_OSX_VERSION'] = ppd['min_osx_version'] = min_osx_version
+ min_glibc_version = virtual_specs.get("__glibc", {}).get("min") or ""
+ replace['MIN_GLIBC_VERSION'] = ppd['min_glibc_version'] = min_glibc_version
+
data = read_header_template()
data = preprocess(data, ppd)
custom_variables = info.get('script_env_variables', {})
diff --git a/constructor/utils.py b/constructor/utils.py
index 90c4b8cea..7bdf1ec5c 100644
--- a/constructor/utils.py
+++ b/constructor/utils.py
@@ -282,3 +282,30 @@ def check_required_env_vars(env_vars):
raise RuntimeError(
f"Missing required environment variables {', '.join(missing_vars)}."
)
+
+
+def parse_virtual_specs(info) -> dict:
+ from .conda_interface import MatchSpec # prevent circular import
+
+ specs = {"__osx": {}, "__glibc": {}}
+ for spec in info.get("virtual_specs", ()):
+ spec = MatchSpec(spec)
+ if spec.name not in ("__osx", "__glibc"):
+ continue
+ if not spec.version:
+ continue
+ if "|" in spec.version.spec_str:
+ raise ValueError("Can't process `|`-joined versions. Only `,` is allowed.")
+ versions = spec.version.tup if "," in spec.version.spec_str else (spec.version,)
+ for version in versions:
+ operator = version.operator_func.__name__
+ if operator == "ge":
+ specs[spec.name]["min"] = str(version.matcher_vo)
+ elif operator == "lt" and spec.name == "__osx":
+ specs[spec.name]["before"] = str(version.matcher_vo)
+ else:
+ raise ValueError(
+ f"Invalid version operator for {spec}. "
+ "__osx only supports `<` or `>=`; __glibc only supports `>=`."
+ )
+ return specs
diff --git a/constructor/winexe.py b/constructor/winexe.py
index 3afbd8f9a..cacdca569 100644
--- a/constructor/winexe.py
+++ b/constructor/winexe.py
@@ -374,6 +374,8 @@ def make_nsi(
else ''
),
('@TEMP_EXTRA_FILES@', '\n '.join(insert_tempfiles_commands(temp_extra_files))),
+ ('@VIRTUAL_SPECS@', " ".join([f'"{spec}"' for spec in info.get("virtual_specs", ())])),
+
]:
data = data.replace(key, value)
diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md
index 9e02ff8e2..67114e00d 100644
--- a/docs/source/construct-yaml.md
+++ b/docs/source/construct-yaml.md
@@ -113,6 +113,18 @@ for example, if `python=3.6` is included, then conda will always seek versions
of packages compatible with Python 3.6. If this is option is not provided, it
will be set equal to the value of `specs`.
+### `virtual_specs`
+
+_required:_ no
+_type:_ list
+
+A list of virtual packages that must be satisfied at install time. Virtual
+packages must start with `__`. For example, `__osx>=11` or `__glibc>=2.24`.
+These specs are dry-run solved offline by the bundled `--conda-exe` binary.
+In SH installers, `__glibc>=x.y` and `__osx>=x.y` specs can be checked with
+Bash only. In PKG installers, `__osx` specs can be checked natively without
+the solver being involved as long as only `>=`, `<` or `,` are used.
+
### `exclude`
_required:_ no
@@ -810,7 +822,7 @@ _required:_ no
_type:_ list
Temporary files that could be referenced in the installation process (i.e. customized
- `welcome_file` and `conclusion_file` (see above)) . Should be a list of
+`welcome_file` and `conclusion_file` (see above)) . Should be a list of
file paths, relative to the directory where `construct.yaml` is. In Windows, these
files will be copied into a temporary folder, the NSIS `$PLUGINSDIR`, during
install process (Windows only).
diff --git a/examples/virtual_specs/construct.yaml b/examples/virtual_specs/construct.yaml
new file mode 100644
index 000000000..7b04c0b6a
--- /dev/null
+++ b/examples/virtual_specs/construct.yaml
@@ -0,0 +1,22 @@
+name: virtual_specs
+
+version: 0.0.1
+
+keep_pkgs: True
+
+channels:
+ - conda-forge
+
+specs:
+ - ca-certificates
+
+virtual_specs:
+ - __osx>=30,<31 # [osx]
+ - __glibc>=20 # [linux]
+ - __win<0 # [win]
+
+initialize_by_default: false
+register_python: false
+check_path_spaces: false
+check_path_length: false
+installer_type: all
diff --git a/news/809-virtual-specs b/news/809-virtual-specs
new file mode 100644
index 000000000..9bfe6b04e
--- /dev/null
+++ b/news/809-virtual-specs
@@ -0,0 +1,19 @@
+### Enhancements
+
+* A new setting `virtual_specs` allows the installer to run some solver checks before the installation proceeds. Useful for checking whether certain virtual package versions can be satisfied. (#809)
+
+### Bug fixes
+
+*
+
+### Deprecations
+
+*
+
+### Docs
+
+*
+
+### Other
+
+*
diff --git a/tests/test_examples.py b/tests/test_examples.py
index 66edb7895..c41e7bb4b 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -102,7 +102,7 @@ def _check_installer_log(install_dir):
raise AssertionError("\n".join(error_lines))
-def _run_installer_exe(installer, install_dir, installer_input=None, timeout=420):
+def _run_installer_exe(installer, install_dir, installer_input=None, timeout=420, check=True):
"""
NSIS manual:
> /D sets the default installation directory ($INSTDIR), overriding InstallDir
@@ -125,11 +125,13 @@ def _run_installer_exe(installer, install_dir, installer_input=None, timeout=420
"after completion."
)
cmd = ["cmd.exe", "/c", "start", "/wait", installer, "/S", *f"/D={install_dir}".split()]
- _execute(cmd, installer_input=installer_input, timeout=timeout)
- _check_installer_log(install_dir)
+ process = _execute(cmd, installer_input=installer_input, timeout=timeout, check=check)
+ if check:
+ _check_installer_log(install_dir)
+ return process
-def _run_uninstaller_exe(install_dir, timeout=420):
+def _run_uninstaller_exe(install_dir, timeout=420, check=True):
# Now test the uninstallers
if " " in str(install_dir):
# TODO: We can't seem to run the uninstaller when there are spaces in the PATH
@@ -158,25 +160,27 @@ def _run_uninstaller_exe(install_dir, timeout=420):
# us problems with the tempdir cleanup later
f"/S _?={install_dir}",
]
- _execute(cmd, timeout=timeout)
- _check_installer_log(install_dir)
- remaining_files = list(install_dir.iterdir())
- 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.
- # Note this is is not exhaustive, because we are not checking
- # whether the registry was restored, menu items were deleted, etc.
- # TODO :)
- raise AssertionError(f"Uninstaller left too many files: {remaining_files}")
-
-
-def _run_installer_sh(installer, install_dir, installer_input=None, timeout=420):
+ process = _execute(cmd, timeout=timeout, check=check)
+ if check:
+ _check_installer_log(install_dir)
+ remaining_files = list(install_dir.iterdir())
+ 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.
+ # Note this is is not exhaustive, because we are not checking
+ # whether the registry was restored, menu items were deleted, etc.
+ # TODO :)
+ raise AssertionError(f"Uninstaller left too many files: {remaining_files}")
+ return process
+
+
+def _run_installer_sh(installer, install_dir, installer_input=None, timeout=420, check=True):
if installer_input:
cmd = ["/bin/sh", installer]
else:
cmd = ["/bin/sh", installer, "-b", "-p", install_dir]
- return _execute(cmd, installer_input=installer_input, timeout=timeout)
+ return _execute(cmd, installer_input=installer_input, timeout=timeout, check=check)
def _run_installer_pkg(
@@ -185,6 +189,7 @@ def _run_installer_pkg(
example_path=None,
config_filename="construct.yaml",
timeout=420,
+ check=True,
):
if os.environ.get("CI"):
# We want to run it in an arbitrary directory, but the options
@@ -208,7 +213,7 @@ def _run_installer_pkg(
"Export CI=1 to run it, but it will pollute your $HOME."
)
cmd = ["pkgutil", "--expand", installer, install_dir]
- return _execute(cmd, timeout=timeout), install_dir
+ return _execute(cmd, timeout=timeout, check=check), install_dir
def _sentinel_file_checks(example_path, install_dir):
@@ -219,7 +224,7 @@ def _sentinel_file_checks(example_path, install_dir):
if (example_path / script).exists() and not (install_dir / sentinel).exists():
raise AssertionError(
f"Sentinel file for {script_prefix}_install not found! "
- f"{install_dir} contents:\n" + "\n".join(sorted(install_dir.iterdir()))
+ f"{install_dir} contents:\n" + "\n".join(sorted(map(str, install_dir.iterdir())))
)
@@ -230,30 +235,45 @@ def _run_installer(
installer_input: Optional[str] = None,
config_filename="construct.yaml",
check_sentinels=True,
+ check_subprocess=True,
request=None,
uninstall=True,
timeout=420,
-):
+) -> subprocess.CompletedProcess:
if installer.suffix == ".exe":
- _run_installer_exe(installer, install_dir, installer_input=installer_input, timeout=timeout)
+ process = _run_installer_exe(
+ installer,
+ install_dir,
+ installer_input=installer_input,
+ timeout=timeout,
+ check=check_subprocess,
+ )
elif installer.suffix == ".sh":
- _run_installer_sh(installer, install_dir, installer_input=installer_input, timeout=timeout)
+ process = _run_installer_sh(
+ installer,
+ install_dir,
+ installer_input=installer_input,
+ timeout=timeout,
+ check=check_subprocess,
+ )
elif installer.suffix == ".pkg":
if request and ON_CI:
- request.addfinalizer(lambda: shutil.rmtree(str(install_dir)))
- _run_installer_pkg(
+ request.addfinalizer(lambda: shutil.rmtree(str(install_dir), ignore_errors=True))
+ process, _ = _run_installer_pkg(
installer,
install_dir,
example_path=example_path,
config_filename=config_filename,
timeout=timeout,
+ check=check_subprocess,
)
else:
raise ValueError(f"Unknown installer type: {installer.suffix}")
if check_sentinels:
_sentinel_file_checks(example_path, install_dir)
if uninstall and installer.suffix == ".exe":
- _run_uninstaller_exe(install_dir, timeout=timeout)
+ _run_uninstaller_exe(install_dir, timeout=timeout, check=check_subprocess)
+ return process
def create_installer(
@@ -511,12 +531,12 @@ def test_example_signing(tmp_path, request):
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
@pytest.mark.skipif(
- not shutil.which("azuresigntool") and not os.environ.get("AZURE_SIGNTOOL_PATH"),
- reason="AzureSignTool not available"
+ not shutil.which("azuresigntool") and not os.environ.get("AZURE_SIGNTOOL_PATH"),
+ reason="AzureSignTool not available",
)
@pytest.mark.parametrize(
- "auth_method",
- os.environ.get("AZURE_SIGNTOOL_TEST_AUTH_METHODS", "token,secret").split(","),
+ "auth_method",
+ os.environ.get("AZURE_SIGNTOOL_TEST_AUTH_METHODS", "token,secret").split(","),
)
def test_azure_signtool(tmp_path, request, monkeypatch, auth_method):
"""Test signing installers with AzureSignTool.
@@ -662,3 +682,32 @@ def test_cross_osx_building(tmp_path):
extra_constructor_args=["--platform", "osx-arm64"],
config_filename="constructor_input.yaml",
)
+
+
+def test_virtual_specs(tmp_path, request):
+ input_path = _example_path("virtual_specs")
+ for installer, install_dir in create_installer(input_path, tmp_path):
+ process = _run_installer(
+ input_path,
+ installer,
+ install_dir,
+ request=request,
+ check_subprocess=False,
+ uninstall=False,
+ )
+ # This example is configured to fail due to unsatisfiable virtual specs
+ if installer.suffix == ".exe":
+ with pytest.raises(AssertionError, match="Failed to check virtual specs"):
+ _check_installer_log(install_dir)
+ continue
+ elif installer.suffix == ".pkg":
+ # The GUI does provide a better message with the min version and so on
+ # but on the CLI we fail with this one instead
+ msg = "Cannot install on volume"
+ else:
+ # The shell installer has its own Bash code for __glibc and __osx
+ # Other virtual specs like __cuda are checked by conda-standalone/micromamba
+ # and will fail with solver errors like PackagesNotFound etc
+ msg = "Installer requires"
+ assert process.returncode != 0
+ assert msg in process.stdout + process.stderr
diff --git a/tests/test_header.py b/tests/test_header.py
index b72a8d3ef..539d01e0a 100644
--- a/tests/test_header.py
+++ b/tests/test_header.py
@@ -57,6 +57,8 @@ def test_linux_template_processing():
enable_shortcuts,
check_path_spaces,
arch,
+ min_glibc_version,
+ min_osx_version,
) in itertools.product(
[False, True],
[False, True],
@@ -72,6 +74,8 @@ def test_linux_template_processing():
[False, True],
[False, True],
["x86", "x86_64", " ppc64le", "s390x", "aarch64"],
+ [None, "2.17"],
+ [None, "10.13"],
):
params = {
"has_license": has_license,
@@ -93,12 +97,14 @@ def test_linux_template_processing():
"initialize_by_default": initialize_by_default,
"enable_shortcuts": enable_shortcuts,
"check_path_spaces": check_path_spaces,
+ "min_glibc_version": min_glibc_version,
+ "min_osx_version": min_osx_version,
}
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}'."
+ f"Found '{template_string}' after processing header.sh with '{params}'."
)
assert not errors
@@ -156,6 +162,8 @@ def test_osxpkg_scripts_shellcheck(arch, check_path_spaces, script):
@pytest.mark.parametrize("arch", ["x86_64", "aarch64"])
@pytest.mark.parametrize("check_path_spaces", [True])
@pytest.mark.parametrize("enable_shortcuts", ["true"])
+@pytest.mark.parametrize("min_glibc_version", ["2.17"])
+@pytest.mark.parametrize("min_osx_version", ["10.13"])
def test_template_shellcheck(
osx,
arch,
@@ -171,6 +179,8 @@ def test_template_shellcheck(
direct_execute_post_install,
check_path_spaces,
enable_shortcuts,
+ min_glibc_version,
+ min_osx_version,
):
template = read_header_template()
processed = preprocess(
@@ -195,9 +205,12 @@ def test_template_shellcheck(
"initialize_by_default": initialize_by_default,
"check_path_spaces": check_path_spaces,
"enable_shortcuts": enable_shortcuts,
+ "min_glibc_version": min_glibc_version,
+ "min_osx_version": min_osx_version,
},
)
findings, returncode = run_shellcheck(processed)
+ print(*findings, sep="\n")
assert findings == []
assert returncode == 0