Skip to content

Split --snapshot installer into installer-exact and installer-updated#122

Merged
jezdez merged 14 commits into
mainfrom
fix-121-preserve-installer-packages
Apr 22, 2026
Merged

Split --snapshot installer into installer-exact and installer-updated#122
jezdez merged 14 commits into
mainfrom
fix-121-preserve-installer-packages

Conversation

@jezdez
Copy link
Copy Markdown
Member

@jezdez jezdez commented Apr 10, 2026

Summary

conda doctor base-protection --fix was stripping installer-provided packages like mamba (Miniforge) and pip from base.

The first attempt used reset(snapshot=initial-state.explicit.txt), but as @marcoesters pointed out that also downgrades any package the user updated, because diff_for_unlink_link_precs pins to the exact URLs from the snapshot.

Now we only read the names from the installer snapshot and keep those packages at whatever versions they currently are.

The old --snapshot installer is split into two:

  • installer-exact: exact restore, may downgrade (factory reset).
  • installer-updated: names from the snapshot, current versions preserved.

conda doctor base-protection --fix uses installer-updated semantics plus permanent_dependencies() (which already composes in context.plugins.self_permanent_packages from #116). Fallback on bare conda self reset is base-protection > installer-updated > current.

names_from_explicit() parses @EXPLICIT URL lines with public MatchSpec. No ProgressiveFetchExtract, no network.

initial-state.explicit.txt comes from conda/constructor#1059, shipped with Miniforge and Miniconda installers built with constructor >= 3.13.0 (Nov 2025).

Closes #121

Breaking change

--snapshot installer is gone. Safe, it was never in a release.

Also in this PR

  • --snapshot choices are backed by a Snapshot enum; execute dispatches via match.
  • PackageInfo.read_manifest classmethod replaces a module-level private helper (unrelated cleanup; happy to split out).

Test plan

  • unit tests for each mode, bare installer rejection, fallback ordering, and the health-check path
  • regression test confirms reset(uninstallable_packages=...) is strictly a keep-list (no installs), covering the rattler/libmamba coexistence case
  • existing tests still pass

jezdez added 4 commits April 11, 2026 00:53
The base-protection fix was computing the keep-list from
permanent_dependencies(), which only includes conda, plugins, and their
deps. This caused installer-provided packages like mamba (in Miniforge)
to be removed during the reset step.

Now the fix checks for the installer snapshot (initial-state.explicit.txt)
and uses it for the reset, preserving whatever the installer shipped.
Falls back to permanent_dependencies() when no snapshot exists.

Closes #121
@jezdez jezdez requested a review from soapy1 April 14, 2026 10:04
Comment thread conda_self/health_checks/base_protection.py
jezdez added 2 commits April 20, 2026 16:47
Addresses Marco's review: resetting base with the installer snapshot
via diff_for_unlink_link_precs would downgrade packages the user has
updated (e.g. mamba in Miniforge).  Splits the ambiguous `installer`
choice into two explicit options:

- `installer-exact`: restores the exact installer state (may downgrade).
- `installer-updates`: keeps the installer-shipped package names at
  their currently installed versions (no downgrade).

`conda doctor base-protection --fix` now uses the names-only strategy
so it no longer downgrades installer-provided packages.  Auto-fallback
order becomes base-protection > installer-updates > current.

Adds `names_from_explicit()` that parses a CEP-23 @explicit file
without fetching, using only public conda APIs (MatchSpec + EXPLICIT_MARKER).
@jezdez jezdez changed the title Use installer snapshot for base-protection reset Split --snapshot installer into installer-exact and installer-updates Apr 20, 2026
@jezdez jezdez requested a review from marcoesters April 20, 2026 15:09
@jezdez jezdez changed the title Split --snapshot installer into installer-exact and installer-updates Split --snapshot installer into installer-exact and installer-updated Apr 20, 2026
…aller-packages

# Conflicts:
#	tests/test_cli_reset.py
Comment thread conda_self/cli/main_reset.py Outdated
Comment thread conda_self/cli/main_reset.py Outdated
jezdez added 3 commits April 22, 2026 08:36
Per review: reserve assert for tests. The invariant "snapshot_choice is
set => reset_file is set" is established earlier, so we reorder the
dispatch to lead with the installer-updated branch whose condition
(`reset_file is not None`) doubles as the type narrowing mypy needs,
and fall through to the other branches otherwise.
Replace the string-literal choices and TypedDict/dict dispatch table in
`conda self reset` with an `enum.Enum` that owns each snapshot's display
name and on-disk file path. The `execute` function now dispatches via a
`match` statement, which makes the exhaustiveness explicit and lets
mypy narrow `reset_file` without an assert.

Also rename `_FALLBACK_ORDER` to `FALLBACK_ORDER` (it's referenced by
tests, so the leading underscore was misleading) and expand the test
suite to cover:
  - `--snapshot current` dispatch
  - `installer-exact` with a missing snapshot file raises FileNotFoundError
  - invalid `--snapshot` values are rejected
  - `--help` lists all four snapshot modes
  - `Snapshot.display_name` and `Snapshot.file_path` unit tests
  - regression: `reset(uninstallable_packages=...)` never installs
    (keep-list semantics, covering the conda-libmamba-solver /
    conda-rattler-solver coexistence case)
Promote `_file_paths_from_extracted_package` from a module-level private
helper to `PackageInfo.read_manifest`, a public classmethod. The helper
is only used by `PackageInfo.from_record`, so scoping it to the class
and giving it an intent-focused name (it reads the on-disk file
manifest, falling back from info/paths.json to info/files) removes an
unnecessary module-level private symbol.

Also switch embedded path literals like `pkg_path / "info/paths.json"`
to the idiomatic chained form `pkg_path / "info" / "paths.json"`.
@jezdez
Copy link
Copy Markdown
Member Author

jezdez commented Apr 22, 2026

@marcoesters — two updates on this PR:

  1. f426ffe refactors --snapshot away from string literals and the TypedDict dispatch table into an enum.Enum whose members own their display name and conda-meta/*.txt file path. execute now dispatches via a match statement, which narrows reset_file without the assert you flagged. On your solver-coexistence concern: tests expanded from 12 to 29, including a parameterized regression test that stubs PrefixSetup and confirms reset(uninstallable_packages=...) only ever unlinks (keep-list semantics) — so the conda-libmamba-solver / conda-rattler-solver case you described cannot lead to both being installed after a reset.

  2. e8a4f2f is unrelated cleanup I folded in: promotes a module-level helper to PackageInfo.read_manifest (public classmethod) and normalizes some pathlib joins. Happy to split it into a separate PR if you'd rather keep this one focused.

@jezdez
Copy link
Copy Markdown
Member Author

jezdez commented Apr 22, 2026

Jesus, Opus 4.7 is NOT good at keeping it straight, those comments are weird!

@jezdez jezdez requested a review from marcoesters April 22, 2026 07:57
@jezdez jezdez merged commit ca7d3ff into main Apr 22, 2026
23 checks passed
@jezdez jezdez deleted the fix-121-preserve-installer-packages branch April 22, 2026 15:48
jezdez added a commit that referenced this pull request Apr 23, 2026
- reset, quickstart: now show `installer-provided (with updates)` snapshot message
- base-protection: uses the new installer-updated strategy in the fix step; tighten comment to drop "to essentials" (installer-provided packages are preserved)
- remove: add a `--force --dry-run` follow-up to showcase the override, and reword the comment since permanent packages are now protected rather than unremovable
jezdez added a commit that referenced this pull request Apr 23, 2026
The WHAT_TO_EXPECT preamble on `conda self reset` claimed the command would
reset base to only conda, its plugins, and their dependencies -- but since
the installer-updated snapshot became the default (#122), the reset also
preserves installer-provided packages. The demos surfaced the contradiction
between this preamble and the trailing "installer-provided (with updates)
snapshot" success line.

- Split WHAT_TO_EXPECT into snapshot/essentials variants and print the
  right one after snapshot resolution
- Update the matching prose in docs/features.md and docs/quickstart.md
- Reword the reset/quickstart tape comments to "installer-provided snapshot"
- Re-record reset, quickstart, and base-protection demos
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Base protection reset should preserve installer-provided packages like mamba

2 participants