Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
69 changes: 45 additions & 24 deletions conda_self/cli/main_reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ class SnapshotData(TypedDict):
Snapshot to reset the `base` environment to.
`current` removes all packages except for `conda`, its plugins,
and their dependencies.
`installer` resets the `base` environment to the snapshot provided
by the installer.
`base-protection` resets the `base` environment to the snapshot saved
`installer-exact` restores the `base` environment to exactly what the
installer shipped (may downgrade packages you have updated).
`installer-updated` keeps the packages the installer shipped at their
currently installed versions (no downgrade).
`base-protection` restores the `base` environment to the snapshot saved
by `conda doctor --fix` before protecting base.

If not set, `conda self` will try to reset to the base-protection snapshot
first, then to the installer-provided, and finally to the current snapshot.
first, then to the installer-provided (preserving updates), and finally
to the current snapshot.
"""
).lstrip()

Expand Down Expand Up @@ -57,7 +60,12 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
add_output_and_prompt_options(parser)
parser.add_argument(
"--snapshot",
choices=("current", "installer", "base-protection"),
choices=(
"current",
"installer-exact",
"installer-updated",
"base-protection",
),
help=SNAPSHOT_HELP,
)
parser.set_defaults(func=execute)
Expand All @@ -69,15 +77,19 @@ def execute(args: argparse.Namespace) -> int:

from ..constants import RESET_FILE_BASE_PROTECTION, RESET_FILE_INSTALLER
from ..query import permanent_dependencies
from ..reset import reset
from ..reset import names_from_explicit, reset

if not context.quiet:
print(WHAT_TO_EXPECT)

reset_data: dict[str, SnapshotData] = {
"installer": {
"installer-exact": {
"file_path": Path(sys.prefix, "conda-meta", RESET_FILE_INSTALLER),
"snapshot_name": "installer-provided",
"snapshot_name": "installer-provided (exact)",
},
"installer-updated": {
"file_path": Path(sys.prefix, "conda-meta", RESET_FILE_INSTALLER),
"snapshot_name": "installer-provided (with updates)",
},
"base-protection": {
"file_path": Path(sys.prefix, "conda-meta", RESET_FILE_BASE_PROTECTION),
Expand All @@ -87,21 +99,23 @@ def execute(args: argparse.Namespace) -> int:

reset_file: Path | None = None
snapshot_name = ""
if not args.snapshot:
for snapshot in ("base-protection", "installer"):
snapshot_data = reset_data[snapshot]
if not snapshot_data["file_path"].exists():
continue
reset_file = snapshot_data["file_path"]
snapshot_name = snapshot_data["snapshot_name"]
break
elif args.snapshot in reset_data:
reset_file = reset_data[args.snapshot]["file_path"]
snapshot_name = reset_data[args.snapshot]["snapshot_name"]
snapshot_choice = args.snapshot

if not snapshot_choice:
for fallback in ("base-protection", "installer-updated"):
snapshot_data = reset_data[fallback]
if snapshot_data["file_path"].exists():
reset_file = snapshot_data["file_path"]
snapshot_name = snapshot_data["snapshot_name"]
snapshot_choice = fallback
break
elif snapshot_choice in reset_data:
reset_file = reset_data[snapshot_choice]["file_path"]
snapshot_name = reset_data[snapshot_choice]["snapshot_name"]

if reset_file and not reset_file.exists():
raise FileNotFoundError(
f"Failed to reset to `{args.snapshot}`.\n"
f"Failed to reset to `{snapshot_choice}`.\n"
f"Required file {reset_file} not found."
)

Expand All @@ -112,10 +126,17 @@ def execute(args: argparse.Namespace) -> int:

if not context.quiet:
print("Resetting 'base' environment...")
uninstallable_packages = (
permanent_dependencies(add_plugins=True) if not reset_file else set()
)
reset(uninstallable_packages=uninstallable_packages, snapshot=reset_file)

if snapshot_choice in ("installer-exact", "base-protection"):
reset(snapshot=reset_file)
elif snapshot_choice == "installer-updated":
assert reset_file is not None
Comment thread
jezdez marked this conversation as resolved.
Outdated
keep = permanent_dependencies(add_plugins=True) | names_from_explicit(
Comment thread
jezdez marked this conversation as resolved.
Outdated
reset_file
)
reset(uninstallable_packages=keep)
else:
reset(uninstallable_packages=permanent_dependencies(add_plugins=True))

if not context.quiet:
if snapshot_name:
Expand Down
16 changes: 11 additions & 5 deletions conda_self/health_checks/base_protection.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ def fix(prefix: str, args: Namespace, confirm: ConfirmCallback) -> int:
from conda.misc import clone_env
from conda.models.environment import Environment

from ..constants import RESET_FILE_BASE_PROTECTION
from ..constants import RESET_FILE_BASE_PROTECTION, RESET_FILE_INSTALLER
from ..query import permanent_dependencies
from ..reset import reset
from ..reset import names_from_explicit, reset

default_env = DEFAULT_ENV_NAME
message = "Protected by Base Environment Protection health fix"
Expand All @@ -98,8 +98,10 @@ def fix(prefix: str, args: Namespace, confirm: ConfirmCallback) -> int:
)
confirm("Proceed?")

# Get packages to keep in base
uninstallable_packages = permanent_dependencies()
# Prefer the installer snapshot for resetting base so that
# installer-provided packages (e.g. mamba in Miniforge) are preserved.
installer_snapshot = base_prefix / "conda-meta" / RESET_FILE_INSTALLER
Comment thread
jezdez marked this conversation as resolved.
use_snapshot = installer_snapshot.exists()

# Check destination environment
dest_prefix_data = PrefixData.from_name(default_env)
Expand Down Expand Up @@ -136,7 +138,11 @@ def fix(prefix: str, args: Namespace, confirm: ConfirmCallback) -> int:
verbose=False,
quiet=True,
)
reset(uninstallable_packages=uninstallable_packages)
if use_snapshot:
keep = permanent_dependencies() | names_from_explicit(installer_snapshot)
reset(uninstallable_packages=keep)
else:
reset(uninstallable_packages=permanent_dependencies())

# Freeze base
try:
Expand Down
15 changes: 15 additions & 0 deletions conda_self/reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,32 @@
from typing import TYPE_CHECKING

from boltons.setutils import IndexedSet
from conda.base.constants import EXPLICIT_MARKER
from conda.base.context import context
from conda.core.link import PrefixSetup, UnlinkLinkTransaction
from conda.core.prefix_data import PrefixData
from conda.core.solve import diff_for_unlink_link_precs
from conda.gateways.disk.read import yield_lines
from conda.misc import get_package_records_from_explicit
from conda.models.match_spec import MatchSpec

if TYPE_CHECKING:
from pathlib import Path


def names_from_explicit(path: Path) -> set[str]:
"""Extract package names from a CEP-23 ``@EXPLICIT`` file without fetching.

Parses each URL line with :class:`~conda.models.match_spec.MatchSpec`,
which reads ``name``/``version``/``build`` from the tarball filename and
strips any ``#md5=…``/``#sha256=…`` checksum fragment as a comment. No
network access, unlike :func:`conda.misc.get_package_records_from_explicit`.
"""
return {
MatchSpec(line).name for line in yield_lines(path) if line != EXPLICIT_MARKER
}


def reset(
prefix: str = sys.prefix,
uninstallable_packages: set[str] = set(),
Expand Down
20 changes: 20 additions & 0 deletions news/121-installer-updated
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### Enhancements

* Split `--snapshot installer` into `--snapshot installer-exact` (restores exact installer state, may downgrade) and `--snapshot installer-updated` (keeps installer-shipped packages at currently installed versions, no downgrade). (#121)
* Auto-fallback now prefers `installer-updated` over `installer-exact` when no `--snapshot` is specified. (#121)

### Bug fixes

* `conda doctor base-protection --fix` no longer downgrades installer-provided packages like `mamba` in Miniforge installs. (#121)

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
Loading