Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for virtual_specs checks before installation #809

Merged
merged 34 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
46c8f19
Add support for virtual_specs checks before installation
jaimergp Jun 21, 2024
65ddaba
ignore shellcheck here
jaimergp Jun 21, 2024
39b61cd
run inside PKG too (for cases other than __osx)
jaimergp Jun 21, 2024
75dfbfa
pre-commit
jaimergp Jun 21, 2024
b6c4233
add news
jaimergp Jun 21, 2024
d9f7133
update docs
jaimergp Jun 21, 2024
a017d7d
reveal exceptions just to make sure
jaimergp Jun 21, 2024
220f762
Make sure we fail for the right reason
jaimergp Jun 21, 2024
2960219
return the process
jaimergp Jun 21, 2024
b36495f
ignore errors, process tuple
jaimergp Jun 21, 2024
26ccbe4
fix test on windows
jaimergp Jun 21, 2024
39f689d
Pass as unescaped variable
jaimergp Jun 21, 2024
3154b02
adjust error message for .pkg
jaimergp Jun 21, 2024
247bd45
Adjust docs
jaimergp Jun 25, 2024
fd55c51
validate virtual_specs
jaimergp Jun 25, 2024
5399871
use <= for osx
jaimergp Jun 25, 2024
ec575b6
add comment about solver=classic
jaimergp Jun 25, 2024
df02fbd
typo
jaimergp Jun 26, 2024
8600666
Parse , (AND) in __osx
jaimergp Jul 12, 2024
adcbab9
Fix PKG os-version control
jaimergp Jul 12, 2024
263bd0e
pre-commit
jaimergp Jul 12, 2024
b17085e
Add Bash-only checks for SH installers
jaimergp Jul 25, 2024
ab60642
fix tests
jaimergp Jul 25, 2024
8b9146c
make it posix
jaimergp Jul 25, 2024
553b41a
pre-commit
jaimergp Jul 25, 2024
86b958d
adjust expected output in test
jaimergp Jul 25, 2024
6576d53
pre-commit
jaimergp Jul 25, 2024
28f74d5
fix dict access?
jaimergp Jul 27, 2024
a59d2c5
always provide a value even if falsey
jaimergp Jul 27, 2024
858586d
Fix TypeError
jaimergp Jul 28, 2024
4f46d6c
better test?
jaimergp Jul 28, 2024
3930766
add workaround for musl
jaimergp Aug 1, 2024
4c6664f
star this too
jaimergp Aug 1, 2024
471bad4
use find
jaimergp Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CONSTRUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<br/>
_type:_ list<br/>

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the condo exe compatible with a much lower bound than one would otherwise expect. Say 2.12 on Linux.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is seems that on Linux you use ldd but that on osx you use a dry run.

any way to also use shell commands on osx? I feel like that would be more robust to increases in minimum requirements in osx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amazing, thanks!

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<br/>
Expand Down Expand Up @@ -810,7 +822,7 @@ _required:_ no<br/>
_type:_ list<br/>

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).
Expand Down
11 changes: 10 additions & 1 deletion constructor/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
marcoesters marked this conversation as resolved.
Show resolved Hide resolved
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, '''
Expand Down Expand Up @@ -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).
Expand Down
37 changes: 37 additions & 0 deletions constructor/header.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,32 @@ 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__"
# ldd reports glibc in the last field of the first line
system_glibc_version=$(ldd --version | awk 'NR==1{print $NF}')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ldd on an alpine docker image with glibc installed will have a different output. ldd here would give musl version and not glibc. /lib64/ld-linux-x86-64.so.2 --version might be better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Isuru! Added a workaround for musl systems. Invoking ld.so directly in CentOS does not work so I'll leave it with those two branches for now.

# 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

Expand Down Expand Up @@ -423,6 +449,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" \
marcoesters marked this conversation as resolved.
Show resolved Hide resolved
"$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"
Expand Down
11 changes: 9 additions & 2 deletions constructor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]]
Expand Down
15 changes: 15 additions & 0 deletions constructor/nsis/main.nsi.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions constructor/osx/prepare_installation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 15 additions & 0 deletions constructor/osxpkg.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import shlex
import shutil
import sys
import xml.etree.ElementTree as ET
Expand All @@ -18,6 +19,7 @@
explained_check_call,
fill_template,
get_final_channels,
parse_virtual_specs,
preprocess,
rm_rf,
shortcuts_flags,
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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', {})
Expand Down
9 changes: 9 additions & 0 deletions constructor/shar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import logging
import os
import shlex
import shutil
import stat
import tarfile
Expand All @@ -23,6 +24,7 @@
fill_template,
get_final_channels,
hash_files,
parse_virtual_specs,
preprocess,
read_ascii_only,
shortcuts_flags,
Expand Down Expand Up @@ -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', {})
Expand Down
27 changes: 27 additions & 0 deletions constructor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm still jet lagged, but is this used anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else:
raise ValueError(
f"Invalid version operator for {spec}. "
"__osx only supports `<` or `>=`; __glibc only supports `>=`."
)
return specs
2 changes: 2 additions & 0 deletions constructor/winexe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
14 changes: 13 additions & 1 deletion docs/source/construct-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<br/>
_type:_ list<br/>

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<br/>
Expand Down Expand Up @@ -810,7 +822,7 @@ _required:_ no<br/>
_type:_ list<br/>

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).
Expand Down
22 changes: 22 additions & 0 deletions examples/virtual_specs/construct.yaml
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions news/809-virtual-specs
Original file line number Diff line number Diff line change
@@ -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

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
Loading
Loading