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