Skip to content

Commit f3aaad0

Browse files
Jrice1317marcoestersjaimergppre-commit-ci[bot]
authored
Add support for protected conda environments (conda#1058)
* Set CONDA_PROTECT_FROZEN_ENVS to 0 * Implement version check for frozen env * First attempt at testing * Fix version check * Fix test * Change source in extra files mapping to use empty json * Only search `extra_files` for frozen file and xfail if version is 25.5.x * Handle str and dict instances * Add helper function for version ranges * Remove stray comma * Fix pre-commit errors * Invert boolean logic for shortcut check * Use path for frozen file search * Add news file * Pre-commit fixes * Apply suggestions from code review Co-authored-by: jaimergp <jaimergp@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Marco Esters <mesters@anaconda.com> * Fix typo * Expand test to test base and extra envs * Fix pre-commit * Apply code suggestions * Apply suggestions from code review Co-authored-by: Marco Esters <mesters@anaconda.com> * Update tests/test_examples.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Swap test_frozen_environment with test_regressions * Revert "Update tests/test_examples.py" This reverts commit 5bfd549. * Remove installation directory after sh installations * Add comment to explain finalizer --------- Co-authored-by: Marco Esters <mesters@anaconda.com> Co-authored-by: jaimergp <jaimergp@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c95a5c4 commit f3aaad0

File tree

9 files changed

+146
-14
lines changed

9 files changed

+146
-14
lines changed

constructor/header.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ shortcuts=""
591591
592592
{%- set channels = final_channels|join(",") %}
593593
# shellcheck disable=SC2086
594+
CONDA_PROTECT_FROZEN_ENVS="0" \
594595
CONDA_ROOT_PREFIX="$PREFIX" \
595596
CONDA_REGISTER_ENVS="{{ register_envs }}" \
596597
CONDA_SAFETY_CHECKS=disabled \
@@ -630,6 +631,7 @@ for env_pkgs in "${PREFIX}"/pkgs/envs/*/; do
630631
env_shortcuts=""
631632
{%- endif %}
632633
# shellcheck disable=SC2086
634+
CONDA_PROTECT_FROZEN_ENVS="0" \
633635
CONDA_ROOT_PREFIX="$PREFIX" \
634636
CONDA_REGISTER_ENVS="{{ register_envs }}" \
635637
CONDA_SAFETY_CHECKS=disabled \

constructor/main.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
CLI logic and main functions to run constructor on a given input file.
1010
"""
1111

12+
from __future__ import annotations
13+
1214
import argparse
1315
import json
1416
import logging
1517
import os
1618
import sys
1719
from os.path import abspath, expanduser, isdir, join
20+
from pathlib import Path
1821
from textwrap import dedent
1922

2023
from . import __version__
@@ -25,7 +28,7 @@
2528
from .construct import parse as construct_parse
2629
from .construct import verify as construct_verify
2730
from .fcp import main as fcp_main
28-
from .utils import StandaloneExe, identify_conda_exe, normalize_path, yield_lines
31+
from .utils import StandaloneExe, check_version, identify_conda_exe, normalize_path, yield_lines
2932

3033
DEFAULT_CACHE_DIR = os.getenv("CONSTRUCTOR_CACHE", "~/.conda/constructor")
3134

@@ -118,7 +121,7 @@ def main_build(
118121
sys.exit("Error: micromamba is not supported on Windows installers.")
119122

120123
if info.get("uninstall_with_conda_exe") and not (
121-
exe_type == StandaloneExe.CONDA and exe_version and exe_version >= Version("24.11.0")
124+
exe_type == StandaloneExe.CONDA and check_version(exe_version, min_version="24.11.0")
122125
):
123126
sys.exit("Error: uninstalling with conda.exe requires conda-standalone 24.11.0 or newer.")
124127

@@ -162,6 +165,31 @@ def main_build(
162165
if isinstance(info[key], str):
163166
info[key] = list(yield_lines(join(dir_path, info[key])))
164167

168+
def has_frozen_file(extra_files: list[str | dict[str, str]]) -> bool:
169+
def is_conda_meta_frozen(path_str: str) -> bool:
170+
path = Path(path_str)
171+
return path.parts == ("conda-meta", "frozen") or (
172+
len(path.parts) == 4
173+
and path.parts[0] == "envs"
174+
and path.parts[-2:] == ("conda-meta", "frozen")
175+
)
176+
177+
for file in extra_files:
178+
if isinstance(file, str) and is_conda_meta_frozen(file):
179+
return True
180+
elif isinstance(file, dict) and any(is_conda_meta_frozen(val) for val in file.values()):
181+
return True
182+
return False
183+
184+
if (
185+
has_frozen_file(info.get("extra_files", []))
186+
and exe_type == StandaloneExe.CONDA
187+
and check_version(exe_version, min_version="25.5.0", max_version="25.7.0")
188+
):
189+
sys.exit(
190+
"Error: handling conda-meta/frozen marker files requires conda-standalone newer than 25.7.x"
191+
)
192+
165193
# normalize paths to be copied; if they are relative, they must be to
166194
# construct.yaml's parent (dir_path)
167195
extras_types = ["extra_files", "temp_extra_files"]
@@ -215,14 +243,14 @@ def main_build(
215243
"Will assume it is compatible with shortcuts."
216244
)
217245
elif sys.platform != "win32" and (
218-
exe_type != StandaloneExe.CONDA or (exe_version and exe_version < Version("23.11.0"))
246+
exe_type != StandaloneExe.CONDA or check_version(exe_version, max_version="23.11.0")
219247
):
220248
logger.warning("conda-standalone 23.11.0 or above is required for shortcuts on Unix.")
221249
info["_enable_shortcuts"] = "incompatible"
222250

223251
# Add --no-rc option to CONDA_EXE command so that existing
224252
# .condarc files do not pollute the installation process.
225-
if exe_type == StandaloneExe.CONDA and exe_version and exe_version >= Version("24.9.0"):
253+
if exe_type == StandaloneExe.CONDA and check_version(exe_version, min_version="24.9.0"):
226254
info["_ignore_condarcs_arg"] = "--no-rc"
227255
elif exe_type == StandaloneExe.MAMBA:
228256
info["_ignore_condarcs_arg"] = "--no-rc"

constructor/nsis/main.nsi.tmpl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,11 +1296,12 @@ Section "Install"
12961296
{%- endfor %}
12971297
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_SAFETY_CHECKS", "disabled").r0'
12981298
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_EXTRA_SAFETY_CHECKS", "no").r0'
1299-
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR")".r0'
1300-
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_PKGS_DIRS", "$INSTDIR\pkgs")".r0'
1299+
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_ROOT_PREFIX", "$INSTDIR").r0'
1300+
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_PKGS_DIRS", "$INSTDIR\pkgs").r0'
1301+
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_PROTECT_FROZEN_ENVS", "0").r0'
13011302
# Spinners in conda write a new character with each movement of the spinner.
13021303
# For long installation times, this may cause a buffer overflow, crashing the installer.
1303-
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1")".r0'
1304+
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1").r0'
13041305
# Extra info for pre and post install scripts
13051306
# NOTE: If more vars are added, make sure to update the examples/scripts tests too
13061307
# There's a similar block for the pre_uninstall script, further down this file.

constructor/osx/run_installation.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ cp "$PREFIX/conda-meta/history" "$PREFIX/conda-meta/history.bak"
5252

5353
# shellcheck disable=SC2086
5454
if ! \
55+
CONDA_PROTECT_FROZEN_ENVS="0" \
5556
CONDA_REGISTER_ENVS="{{ register_envs }}" \
5657
CONDA_ROOT_PREFIX="$PREFIX" \
5758
CONDA_SAFETY_CHECKS=disabled \
@@ -98,6 +99,7 @@ for env_pkgs in "${PREFIX}"/pkgs/envs/*/; do
9899
fi
99100

100101
# shellcheck disable=SC2086
102+
CONDA_PROTECT_FROZEN_ENVS="0" \
101103
CONDA_ROOT_PREFIX="$PREFIX" \
102104
CONDA_REGISTER_ENVS="{{ register_envs }}" \
103105
CONDA_SAFETY_CHECKS=disabled \

constructor/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from shutil import rmtree
2424
from subprocess import CalledProcessError, check_call, check_output
2525

26+
from conda.models.version import VersionOrder
2627
from ruamel.yaml import YAML
2728

2829
logger = logging.getLogger(__name__)
@@ -343,6 +344,26 @@ def identify_conda_exe(conda_exe: str | Path | None = None) -> tuple[StandaloneE
343344
return None, None
344345

345346

347+
def check_version(
348+
exe_version: str | VersionOrder | None = None,
349+
min_version: str | None = None,
350+
max_version: str | None = None,
351+
) -> bool:
352+
"""Check if a version is within a version range.
353+
354+
The minimum version is assumed to be inclusive, the maximum version is not inclusive.
355+
"""
356+
if not exe_version:
357+
return False
358+
if isinstance(exe_version, str):
359+
exe_version = VersionOrder(exe_version)
360+
if min_version and exe_version < VersionOrder(min_version):
361+
return False
362+
if max_version and exe_version >= VersionOrder(max_version):
363+
return False
364+
return True
365+
366+
346367
def win_str_esc(s, newlines=True):
347368
maps = [("$", "$$"), ('"', '$\\"'), ("\t", "$\\t")]
348369
if newlines:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# yaml-language-server: $schema=../../constructor/data/construct.schema.json
2+
"$schema": "../../constructor/data/construct.schema.json"
3+
4+
name: ProtectedBaseEnv
5+
version: X
6+
installer_type: all
7+
8+
channels:
9+
- defaults
10+
11+
specs:
12+
- python
13+
- conda
14+
15+
extra_envs:
16+
default:
17+
specs:
18+
- python
19+
- pip
20+
- conda
21+
22+
extra_files:
23+
- frozen.json: conda-meta/frozen
24+
- frozen.json: envs/default/conda-meta/frozen
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
### Enhancements
2+
3+
* Add support for installing [protected conda environments](https://conda.org/learn/ceps/cep-0022#specification). (#1058)
4+
5+
### Bug fixes
6+
7+
* <news item>
8+
9+
### Deprecations
10+
11+
* <news item>
12+
13+
### Docs
14+
15+
* <news item>
16+
17+
### Other
18+
19+
* <news item>

tests/test_examples.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from conda.models.version import VersionOrder as Version
2222
from ruamel.yaml import YAML
2323

24-
from constructor.utils import StandaloneExe, identify_conda_exe
24+
from constructor.utils import StandaloneExe, check_version, identify_conda_exe
2525

2626
if TYPE_CHECKING:
2727
from collections.abc import Generator, Iterable
@@ -317,6 +317,9 @@ def _run_installer(
317317
check=check_subprocess,
318318
options=options,
319319
)
320+
if request and ON_CI:
321+
# GitHub runners run out of disk space if installation directories are not cleaned up
322+
request.addfinalizer(lambda: shutil.rmtree(str(install_dir), ignore_errors=True))
320323
elif installer.suffix == ".pkg":
321324
if request and ON_CI:
322325
request.addfinalizer(lambda: shutil.rmtree(str(install_dir), ignore_errors=True))
@@ -512,8 +515,7 @@ def test_example_mirrored_channels(tmp_path, request):
512515
@pytest.mark.xfail(
513516
(
514517
CONDA_EXE == StandaloneExe.CONDA
515-
and CONDA_EXE_VERSION is not None
516-
and CONDA_EXE_VERSION < Version("23.11.0a0")
518+
and not check_version(CONDA_EXE_VERSION, min_version="23.11.0a0")
517519
),
518520
reason="Known issue with conda-standalone<=23.10: shortcuts are created but not removed.",
519521
)
@@ -693,8 +695,7 @@ def test_example_scripts(tmp_path, request):
693695
@pytest.mark.skipif(
694696
(
695697
CONDA_EXE == StandaloneExe.MAMBA
696-
or CONDA_EXE_VERSION is None
697-
or CONDA_EXE_VERSION < Version("23.11.0a0")
698+
and not check_version(CONDA_EXE_VERSION, min_version="23.11.0a0")
698699
),
699700
reason="menuinst v2 requires conda-standalone>=23.11.0; micromamba is not supported yet",
700701
)
@@ -1218,7 +1219,7 @@ def _get_dacl_information(filepath: Path) -> dict:
12181219

12191220

12201221
@pytest.mark.xfail(
1221-
CONDA_EXE == StandaloneExe.CONDA and CONDA_EXE_VERSION < Version("24.9.0"),
1222+
CONDA_EXE == StandaloneExe.CONDA and not check_version(CONDA_EXE_VERSION, min_version="24.9.0"),
12221223
reason="Pre-existing .condarc breaks installation",
12231224
)
12241225
def test_ignore_condarc_files(tmp_path, monkeypatch, request):
@@ -1268,7 +1269,7 @@ def test_ignore_condarc_files(tmp_path, monkeypatch, request):
12681269

12691270

12701271
@pytest.mark.skipif(
1271-
CONDA_EXE == StandaloneExe.CONDA and CONDA_EXE_VERSION < Version("24.11.0"),
1272+
CONDA_EXE == StandaloneExe.CONDA and check_version(CONDA_EXE_VERSION, min_version="24.11.0"),
12721273
reason="Requires conda-standalone 24.11.x or newer",
12731274
)
12741275
@pytest.mark.skipif(not sys.platform == "win32", reason="Windows only")
@@ -1369,6 +1370,39 @@ def test_output_files(tmp_path):
13691370
assert files_exist == []
13701371

13711372

1373+
@pytest.mark.xfail(
1374+
condition=(
1375+
CONDA_EXE == StandaloneExe.CONDA
1376+
and check_version(CONDA_EXE_VERSION, min_version="25.5.0", max_version="25.7.0")
1377+
),
1378+
reason="conda-standalone 25.5.x fails with protected environments",
1379+
strict=True,
1380+
)
1381+
def test_frozen_environment(tmp_path, request):
1382+
input_path = _example_path("protected_base")
1383+
for installer, install_dir in create_installer(input_path, tmp_path):
1384+
_run_installer(
1385+
input_path,
1386+
installer,
1387+
install_dir,
1388+
request=request,
1389+
uninstall=False,
1390+
)
1391+
1392+
expected_frozen_paths = {
1393+
install_dir / "conda-meta" / "frozen",
1394+
install_dir / "envs" / "default" / "conda-meta" / "frozen",
1395+
}
1396+
1397+
actual_frozen_paths = set()
1398+
for env in install_dir.glob("**/conda-meta/history"):
1399+
frozen_file = env.parent / "frozen"
1400+
if frozen_file.exists():
1401+
actual_frozen_paths.add(frozen_file)
1402+
1403+
assert expected_frozen_paths == actual_frozen_paths
1404+
1405+
13721406
def test_regressions(tmp_path, request):
13731407
input_path = _example_path("regressions")
13741408
for installer, install_dir in create_installer(input_path, tmp_path):

0 commit comments

Comments
 (0)