Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
181519f
Set CONDA_PROTECT_FROZEN_ENVS to 0
Jrice1317 Sep 9, 2025
3ec3015
Implement version check for frozen env
Jrice1317 Sep 9, 2025
1e59d8f
First attempt at testing
Jrice1317 Sep 9, 2025
8150b81
Fix version check
Jrice1317 Sep 11, 2025
7540633
Fix test
Jrice1317 Sep 11, 2025
c43f6bb
Change source in extra files mapping to use empty json
Jrice1317 Sep 15, 2025
bfce427
Only search `extra_files` for frozen file and xfail if version is 25.5.x
Jrice1317 Sep 15, 2025
285d8d8
Handle str and dict instances
Jrice1317 Sep 16, 2025
5ea8513
Add helper function for version ranges
marcoesters Sep 16, 2025
596f10d
Remove stray comma
marcoesters Sep 16, 2025
5d05eac
Merge pull request #1 from Jrice1317/frozen_fork
Jrice1317 Sep 16, 2025
e3b8659
Fix pre-commit errors
Jrice1317 Sep 16, 2025
7509f2a
Merge branch 'protected_envs' into conda-standalone-version-helper
marcoesters Sep 16, 2025
023f815
Invert boolean logic for shortcut check
marcoesters Sep 16, 2025
6ff480e
Merge branch 'conda-standalone-version-helper' of github.com:marcoest…
marcoesters Sep 16, 2025
f4c092e
Merge pull request #2 from marcoesters/conda-standalone-version-helper
Jrice1317 Sep 16, 2025
3255967
Use path for frozen file search
Jrice1317 Sep 16, 2025
0a3acff
Add news file
Jrice1317 Sep 16, 2025
9196951
Merge branch 'main' of https://github.com/conda/constructor into prot…
Jrice1317 Sep 17, 2025
7e758da
Pre-commit fixes
Jrice1317 Sep 17, 2025
adc94a4
Apply suggestions from code review
Jrice1317 Sep 17, 2025
9265135
Apply suggestions from code review
Jrice1317 Sep 18, 2025
c841a22
Fix typo
Jrice1317 Sep 19, 2025
60b87bc
Expand test to test base and extra envs
Jrice1317 Sep 19, 2025
bee6aaf
Fix pre-commit
Jrice1317 Sep 19, 2025
a66bc0c
Merge branch 'main' of https://github.com/conda/constructor into prot…
Jrice1317 Sep 24, 2025
98ba9a4
Apply code suggestions
Jrice1317 Sep 25, 2025
f58bb03
Apply suggestions from code review
Jrice1317 Sep 25, 2025
60cf7ef
Merge branch 'main' into protected_envs
jaimergp Sep 30, 2025
1e1efdf
Merge branch 'main' into protected_envs
marcoesters Sep 30, 2025
d40f731
Merge branch 'main' into protected_envs
marcoesters Sep 30, 2025
5bfd549
Update tests/test_examples.py
marcoesters Oct 1, 2025
dc496d5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 1, 2025
630d022
Swap test_frozen_environment with test_regressions
marcoesters Oct 1, 2025
289cd07
Revert "Update tests/test_examples.py"
marcoesters Oct 1, 2025
84d9326
Remove installation directory after sh installations
marcoesters Oct 1, 2025
1e0b0b0
Add comment to explain finalizer
marcoesters Oct 2, 2025
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
2 changes: 2 additions & 0 deletions constructor/header.sh
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ shortcuts=""

{%- set channels = final_channels|join(",") %}
# shellcheck disable=SC2086
CONDA_PROTECT_FROZEN_ENVS="0" \
CONDA_ROOT_PREFIX="$PREFIX" \
CONDA_REGISTER_ENVS="{{ register_envs }}" \
CONDA_SAFETY_CHECKS=disabled \
Expand Down Expand Up @@ -630,6 +631,7 @@ for env_pkgs in "${PREFIX}"/pkgs/envs/*/; do
env_shortcuts=""
{%- endif %}
# shellcheck disable=SC2086
CONDA_PROTECT_FROZEN_ENVS="0" \
CONDA_ROOT_PREFIX="$PREFIX" \
CONDA_REGISTER_ENVS="{{ register_envs }}" \
CONDA_SAFETY_CHECKS=disabled \
Expand Down
36 changes: 32 additions & 4 deletions constructor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
CLI logic and main functions to run constructor on a given input file.
"""

from __future__ import annotations

import argparse
import json
import logging
import os
import sys
from os.path import abspath, expanduser, isdir, join
from pathlib import Path
from textwrap import dedent

from . import __version__
Expand All @@ -25,7 +28,7 @@
from .construct import parse as construct_parse
from .construct import verify as construct_verify
from .fcp import main as fcp_main
from .utils import StandaloneExe, identify_conda_exe, normalize_path, yield_lines
from .utils import StandaloneExe, check_version, identify_conda_exe, normalize_path, yield_lines

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

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

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

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

def has_frozen_file(extra_files: list[str | dict[str, str]]) -> bool:
def is_conda_meta_frozen(path_str: str) -> bool:
path = Path(path_str)
return path.parts == ("conda-meta", "frozen") or (
len(path.parts) == 4
and path.parts[0] == "envs"
and path.parts[-2:] == ("conda-meta", "frozen")
)

for file in extra_files:
if isinstance(file, str) and is_conda_meta_frozen(file):
return True
elif isinstance(file, dict) and any(is_conda_meta_frozen(val) for val in file.values()):
return True
return False

if (
has_frozen_file(info.get("extra_files", []))
and exe_type == StandaloneExe.CONDA
and check_version(exe_version, min_version="25.5.0", max_version="25.7.0")
):
sys.exit(
"Error: handling conda-meta/frozen marker files requires conda-standalone newer than 25.7.x"
)

# normalize paths to be copied; if they are relative, they must be to
# construct.yaml's parent (dir_path)
extras_types = ["extra_files", "temp_extra_files"]
Expand Down Expand Up @@ -215,14 +243,14 @@ def main_build(
"Will assume it is compatible with shortcuts."
)
elif sys.platform != "win32" and (
exe_type != StandaloneExe.CONDA or (exe_version and exe_version < Version("23.11.0"))
exe_type != StandaloneExe.CONDA or check_version(exe_version, max_version="23.11.0")
):
logger.warning("conda-standalone 23.11.0 or above is required for shortcuts on Unix.")
info["_enable_shortcuts"] = "incompatible"

# Add --no-rc option to CONDA_EXE command so that existing
# .condarc files do not pollute the installation process.
if exe_type == StandaloneExe.CONDA and exe_version and exe_version >= Version("24.9.0"):
if exe_type == StandaloneExe.CONDA and check_version(exe_version, min_version="24.9.0"):
info["_ignore_condarcs_arg"] = "--no-rc"
elif exe_type == StandaloneExe.MAMBA:
info["_ignore_condarcs_arg"] = "--no-rc"
Expand Down
7 changes: 4 additions & 3 deletions constructor/nsis/main.nsi.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -1296,11 +1296,12 @@ Section "Install"
{%- endfor %}
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'
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'
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_PROTECT_FROZEN_ENVS", "0").r0'
# Spinners in conda write a new character with each movement of the spinner.
# For long installation times, this may cause a buffer overflow, crashing the installer.
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1")".r0'
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("CONDA_QUIET", "1").r0'
# Extra info for pre and post install scripts
# NOTE: If more vars are added, make sure to update the examples/scripts tests too
# There's a similar block for the pre_uninstall script, further down this file.
Expand Down
2 changes: 2 additions & 0 deletions constructor/osx/run_installation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ cp "$PREFIX/conda-meta/history" "$PREFIX/conda-meta/history.bak"

# shellcheck disable=SC2086
if ! \
CONDA_PROTECT_FROZEN_ENVS="0" \
CONDA_REGISTER_ENVS="{{ register_envs }}" \
CONDA_ROOT_PREFIX="$PREFIX" \
CONDA_SAFETY_CHECKS=disabled \
Expand Down Expand Up @@ -98,6 +99,7 @@ for env_pkgs in "${PREFIX}"/pkgs/envs/*/; do
fi

# shellcheck disable=SC2086
CONDA_PROTECT_FROZEN_ENVS="0" \
CONDA_ROOT_PREFIX="$PREFIX" \
CONDA_REGISTER_ENVS="{{ register_envs }}" \
CONDA_SAFETY_CHECKS=disabled \
Expand Down
21 changes: 21 additions & 0 deletions constructor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from shutil import rmtree
from subprocess import CalledProcessError, check_call, check_output

from conda.models.version import VersionOrder
from ruamel.yaml import YAML

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


def check_version(
exe_version: str | VersionOrder | None = None,
min_version: str | None = None,
max_version: str | None = None,
) -> bool:
"""Check if a version is within a version range.

The minimum version is assumed to be inclusive, the maximum version is not inclusive.
"""
if not exe_version:
return False
if isinstance(exe_version, str):
exe_version = VersionOrder(exe_version)
if min_version and exe_version < VersionOrder(min_version):
return False
if max_version and exe_version >= VersionOrder(max_version):
return False
return True


def win_str_esc(s, newlines=True):
maps = [("$", "$$"), ('"', '$\\"'), ("\t", "$\\t")]
if newlines:
Expand Down
24 changes: 24 additions & 0 deletions examples/protected_base/construct.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# yaml-language-server: $schema=../../constructor/data/construct.schema.json
"$schema": "../../constructor/data/construct.schema.json"

name: ProtectedBaseEnv
version: X
installer_type: all

channels:
- defaults

specs:
- python
- conda

extra_envs:
default:
specs:
- python
- pip
- conda

extra_files:
- frozen.json: conda-meta/frozen
- frozen.json: envs/default/conda-meta/frozen
1 change: 1 addition & 0 deletions examples/protected_base/frozen.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
19 changes: 19 additions & 0 deletions news/1058-add-support-for-frozen-envs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Add support for installing [protected conda environments](https://conda.org/learn/ceps/cep-0022#specification). (#1058)

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
48 changes: 41 additions & 7 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from conda.models.version import VersionOrder as Version
from ruamel.yaml import YAML

from constructor.utils import StandaloneExe, identify_conda_exe
from constructor.utils import StandaloneExe, check_version, identify_conda_exe

if TYPE_CHECKING:
from collections.abc import Generator, Iterable
Expand Down Expand Up @@ -317,6 +317,9 @@ def _run_installer(
check=check_subprocess,
options=options,
)
if request and ON_CI:
# GitHub runners run out of disk space if installation directories are not cleaned up
request.addfinalizer(lambda: shutil.rmtree(str(install_dir), ignore_errors=True))
elif installer.suffix == ".pkg":
if request and ON_CI:
request.addfinalizer(lambda: shutil.rmtree(str(install_dir), ignore_errors=True))
Expand Down Expand Up @@ -512,8 +515,7 @@ def test_example_mirrored_channels(tmp_path, request):
@pytest.mark.xfail(
(
CONDA_EXE == StandaloneExe.CONDA
and CONDA_EXE_VERSION is not None
and CONDA_EXE_VERSION < Version("23.11.0a0")
and not check_version(CONDA_EXE_VERSION, min_version="23.11.0a0")
),
reason="Known issue with conda-standalone<=23.10: shortcuts are created but not removed.",
)
Expand Down Expand Up @@ -693,8 +695,7 @@ def test_example_scripts(tmp_path, request):
@pytest.mark.skipif(
(
CONDA_EXE == StandaloneExe.MAMBA
or CONDA_EXE_VERSION is None
or CONDA_EXE_VERSION < Version("23.11.0a0")
and not check_version(CONDA_EXE_VERSION, min_version="23.11.0a0")
),
reason="menuinst v2 requires conda-standalone>=23.11.0; micromamba is not supported yet",
)
Expand Down Expand Up @@ -1218,7 +1219,7 @@ def _get_dacl_information(filepath: Path) -> dict:


@pytest.mark.xfail(
CONDA_EXE == StandaloneExe.CONDA and CONDA_EXE_VERSION < Version("24.9.0"),
CONDA_EXE == StandaloneExe.CONDA and not check_version(CONDA_EXE_VERSION, min_version="24.9.0"),
reason="Pre-existing .condarc breaks installation",
)
def test_ignore_condarc_files(tmp_path, monkeypatch, request):
Expand Down Expand Up @@ -1268,7 +1269,7 @@ def test_ignore_condarc_files(tmp_path, monkeypatch, request):


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


@pytest.mark.xfail(
condition=(
CONDA_EXE == StandaloneExe.CONDA
and check_version(CONDA_EXE_VERSION, min_version="25.5.0", max_version="25.7.0")
),
reason="conda-standalone 25.5.x fails with protected environments",
strict=True,
)
def test_frozen_environment(tmp_path, request):
input_path = _example_path("protected_base")
for installer, install_dir in create_installer(input_path, tmp_path):
_run_installer(
input_path,
installer,
install_dir,
request=request,
uninstall=False,
)

expected_frozen_paths = {
install_dir / "conda-meta" / "frozen",
install_dir / "envs" / "default" / "conda-meta" / "frozen",
}

actual_frozen_paths = set()
for env in install_dir.glob("**/conda-meta/history"):
frozen_file = env.parent / "frozen"
if frozen_file.exists():
actual_frozen_paths.add(frozen_file)

assert expected_frozen_paths == actual_frozen_paths


def test_regressions(tmp_path, request):
input_path = _example_path("regressions")
for installer, install_dir in create_installer(input_path, tmp_path):
Expand Down
Loading