diff --git a/conda_self/cli/main_reset.py b/conda_self/cli/main_reset.py index 30fda6a..6629680 100644 --- a/conda_self/cli/main_reset.py +++ b/conda_self/cli/main_reset.py @@ -80,24 +80,20 @@ def file_path(self) -> Path | None: """ ).lstrip() -WHAT_TO_EXPECT = dedent( +WHAT_TO_EXPECT_ESSENTIALS = dedent( """ - This will reset your `base` to ONLY contain `conda`, its plugins, + This will reset your 'base' to ONLY contain 'conda', its plugins, and their dependencies. """ ).lstrip() -SUCCESS = dedent( +WHAT_TO_EXPECT_SNAPSHOT = dedent( """ - SUCCESS! - Reset the `base` environment to only the essential packages and plugins. - """ -).lstrip() -SUCCESS_SNAPSHOT = dedent( - """ - SUCCESS! - Reset the `base` environment to {snapshot_name} snapshot. + This resets your 'base' to the {snapshot_name} snapshot + and removes any packages outside of it. """ ).lstrip() +SUCCESS = "Reset the 'base' environment to only the essential packages and plugins.\n" +SUCCESS_SNAPSHOT = "Reset the 'base' environment to {snapshot_name} snapshot.\n" def configure_parser(parser: argparse.ArgumentParser) -> None: @@ -121,9 +117,6 @@ def execute(args: argparse.Namespace) -> int: from ..query import permanent_dependencies from ..reset import names_from_explicit, reset - if not context.quiet: - print(WHAT_TO_EXPECT) - snapshot: Snapshot | None = args.snapshot reset_file: Path | None = None @@ -139,9 +132,15 @@ def execute(args: argparse.Namespace) -> int: if reset_file is not None and not reset_file.exists(): raise FileNotFoundError( - f"Failed to reset to `{snapshot}`.\nRequired file {reset_file} not found." + f"Failed to reset to '{snapshot}'.\nRequired file {reset_file} not found." ) + if not context.quiet: + if snapshot is not None: + print(WHAT_TO_EXPECT_SNAPSHOT.format(snapshot_name=snapshot.display_name)) + else: + print(WHAT_TO_EXPECT_ESSENTIALS) + prompt = "Proceed with resetting your 'base' environment" if snapshot is not None: prompt += f" to the {snapshot.display_name} snapshot" diff --git a/conda_self/exceptions.py b/conda_self/exceptions.py index 4013a27..7a56c6a 100644 --- a/conda_self/exceptions.py +++ b/conda_self/exceptions.py @@ -8,14 +8,25 @@ from pathlib import Path +def _plural(word: str, count: int) -> str: + return word if count == 1 else f"{word}s" + + class NotAPluginError(CondaError): def __init__(self, specs: list[str]): - super().__init__(f"The following requested specs are not plugins: {specs}.") + names = ", ".join(specs) + if len(specs) == 1: + msg = f"The requested package is not a plugin: {names}" + else: + msg = f"The requested packages are not plugins: {names}" + super().__init__(msg) class PluginRemoveError(CondaError): def __init__(self, specs: list[str]): - super().__init__(f"Packages '{specs}' can not be removed.") + names = ", ".join(specs) + noun = _plural("package", len(specs)) + super().__init__(f"{noun.capitalize()} can not be removed: {names}") class NoDistInfoDirFound(CondaError): diff --git a/conda_self/health_checks/base_protection.py b/conda_self/health_checks/base_protection.py index 30b3fe1..3f2b34e 100644 --- a/conda_self/health_checks/base_protection.py +++ b/conda_self/health_checks/base_protection.py @@ -42,7 +42,7 @@ def check(prefix: str, _verbose: bool) -> None: print(f"{OK_MARK} Base environment is protected (frozen).\n") else: print(f"{X_MARK} Base environment is not protected.\n") - print(" Run `conda doctor --fix` to protect it.\n") + print("Run 'conda doctor --fix' to protect it.\n") def fix(prefix: str, args: Namespace, confirm: ConfirmCallback) -> int: @@ -90,12 +90,12 @@ def fix(prefix: str, args: Namespace, confirm: ConfirmCallback) -> int: if not context.quiet: print(f"This will clone 'base' to '{default_env}', reset base, and freeze it.") - if env.external_packages: - print( - f" Warning: Base environment contains {len(env.external_packages)} " - "non-conda package(s) that will become non-functional after reset.\n" - f" They are preserved in the cloned '{default_env}' environment." - ) + if env.external_packages: + print( + f" Warning: Base environment contains {len(env.external_packages)} " + "non-conda package(s) that will become non-functional after reset.\n" + f" They are preserved in the cloned '{default_env}' environment." + ) confirm("Proceed?") # Prefer the installer snapshot for resetting base so that diff --git a/demos/_settings.tape b/demos/_settings.tape new file mode 100644 index 0000000..b4dac2f --- /dev/null +++ b/demos/_settings.tape @@ -0,0 +1,15 @@ +Set Shell "bash" +Set FontFamily "JetBrains Mono" +Set FontSize 20 +Set Width 1200 +Set Height 600 +Set Margin 20 +Set MarginFill "#19191b" +Set Padding 20 +Set Theme "Dark+" +Set TypingSpeed 40ms +Set WaitTimeout 300s + +Env BAT_OPTS "--color=always --paging=never --plain" +Env BAT_THEME "Visual Studio Dark+" +Env CONDA_PLUGINS_USE_SHARDED_REPODATA "1" diff --git a/demos/_setup.sh b/demos/_setup.sh new file mode 100755 index 0000000..a061f9e --- /dev/null +++ b/demos/_setup.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Sourced by VHS tapes to create a fresh Miniforge with conda-self installed. + +MINIFORGE_DIR="/tmp/miniforge-demo" +INSTALLER="/tmp/miniforge.sh" +INSTALLER_URL="https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname -s)-$(uname -m).sh" +CONDA_SELF_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +unset CONDA_PREFIX CONDA_DEFAULT_ENV CONDA_SHLVL CONDA_PROMPT_MODIFIER +unset PIXI_HOME PIXI_PROJECT_MANIFEST + +if [[ ! -f "$INSTALLER" ]]; then + curl -fsSL -o "$INSTALLER" "$INSTALLER_URL" +fi + +rm -rf "$MINIFORGE_DIR" +bash "$INSTALLER" -b -p "$MINIFORGE_DIR" > /dev/null 2>&1 +"$MINIFORGE_DIR/bin/pip" install -q -e "$CONDA_SELF_SRC" > /dev/null 2>&1 + +eval "$("$MINIFORGE_DIR/bin/conda" shell.bash hook)" +conda activate base + +export PS1="\[\e[1;38;2;67;176;42m\]\$\[\e[0m\] " +export CONDA_CHANNELS=conda-forge diff --git a/demos/base-protection.gif b/demos/base-protection.gif new file mode 100644 index 0000000..b47b28f Binary files /dev/null and b/demos/base-protection.gif differ diff --git a/demos/base-protection.mp4 b/demos/base-protection.mp4 new file mode 100644 index 0000000..d3edfaa Binary files /dev/null and b/demos/base-protection.mp4 differ diff --git a/demos/base-protection.tape b/demos/base-protection.tape new file mode 100644 index 0000000..b7cf86e --- /dev/null +++ b/demos/base-protection.tape @@ -0,0 +1,40 @@ +Source demos/_settings.tape + +Output demos/base-protection.gif +Output demos/base-protection.mp4 + +Hide +Type `source demos/_setup.sh` +Enter +Wait /\$/ +Type "clear" +Enter +Wait /\$/ +Show + +Type "# Check if your base environment is protected" +Enter +Sleep 500ms + +Type@80ms "conda doctor base-protection" +Enter +Wait /\$/ +Sleep 3s + +Type "# Enable protection: clone base, reset, freeze" +Enter +Sleep 500ms + +Type@80ms "conda doctor base-protection --fix --yes" +Enter +Wait /\$/ +Sleep 3s + +Type "# Verify protection status" +Enter +Sleep 500ms + +Type@80ms "conda doctor base-protection" +Enter +Wait /\$/ +Sleep 3s diff --git a/demos/install-plugin.gif b/demos/install-plugin.gif new file mode 100644 index 0000000..6088a27 Binary files /dev/null and b/demos/install-plugin.gif differ diff --git a/demos/install-plugin.mp4 b/demos/install-plugin.mp4 new file mode 100644 index 0000000..b95dce5 Binary files /dev/null and b/demos/install-plugin.mp4 differ diff --git a/demos/install-plugin.tape b/demos/install-plugin.tape new file mode 100644 index 0000000..7f479d2 --- /dev/null +++ b/demos/install-plugin.tape @@ -0,0 +1,35 @@ +Source demos/_settings.tape + +Output demos/install-plugin.gif +Output demos/install-plugin.mp4 + +Hide +Type `source demos/_setup.sh` +Enter +Wait /\$/ +# Pre-update conda so the install step only shows the plugin +Type `conda update --yes --quiet --override-frozen conda 2>&1 | tail -1` +Enter +Wait /\$/ +Type "clear" +Enter +Wait /\$/ +Show + +Type "# Install a conda plugin into the base environment" +Enter +Sleep 500ms + +Type@80ms "conda self install --yes conda-index" +Enter +Wait /\$/ +Sleep 3s + +Type "# Inline channel specs are rejected" +Enter +Sleep 500ms + +Type@80ms "conda self install conda-forge::some-plugin" +Enter +Wait /\$/ +Sleep 3s diff --git a/demos/quickstart.gif b/demos/quickstart.gif new file mode 100644 index 0000000..2fa43a9 Binary files /dev/null and b/demos/quickstart.gif differ diff --git a/demos/quickstart.mp4 b/demos/quickstart.mp4 new file mode 100644 index 0000000..df308e6 Binary files /dev/null and b/demos/quickstart.mp4 differ diff --git a/demos/quickstart.tape b/demos/quickstart.tape new file mode 100644 index 0000000..2f740da --- /dev/null +++ b/demos/quickstart.tape @@ -0,0 +1,44 @@ +Source demos/_settings.tape + +Output demos/quickstart.gif +Output demos/quickstart.mp4 + +Hide +Type `source demos/_setup.sh` +Enter +Wait /\$/ +# Pre-update conda so the install step only shows the plugin +Type `conda update --yes --quiet --override-frozen conda 2>&1 | tail -1` +Enter +Wait /\$/ +Type "clear" +Enter +Wait /\$/ +Show + +Type "# Check base environment protection" +Enter +Sleep 500ms + +Type@80ms "conda doctor base-protection" +Enter +Wait /\$/ +Sleep 2s + +Type "# Install a plugin" +Enter +Sleep 500ms + +Type@80ms "conda self install --yes conda-index" +Enter +Wait /\$/ +Sleep 3s + +Type "# Reset base to the installer-provided snapshot" +Enter +Sleep 500ms + +Type@80ms "conda self reset --yes" +Enter +Wait /\$/ +Sleep 3s diff --git a/demos/remove.gif b/demos/remove.gif new file mode 100644 index 0000000..93bc85c Binary files /dev/null and b/demos/remove.gif differ diff --git a/demos/remove.mp4 b/demos/remove.mp4 new file mode 100644 index 0000000..96ae9af Binary files /dev/null and b/demos/remove.mp4 differ diff --git a/demos/remove.tape b/demos/remove.tape new file mode 100644 index 0000000..d44b30b --- /dev/null +++ b/demos/remove.tape @@ -0,0 +1,44 @@ +Source demos/_settings.tape + +Output demos/remove.gif +Output demos/remove.mp4 + +Hide +Type `source demos/_setup.sh` +Enter +Wait /\$/ +# Install a plugin so we can demonstrate removing it +Type `conda self install --yes --quiet conda-index 2>&1 | tail -1` +Enter +Wait /\$/ +Type "clear" +Enter +Wait /\$/ +Show + +Type "# Remove a plugin from the base environment" +Enter +Sleep 500ms + +Type@80ms "conda self remove --yes conda-index" +Enter +Wait /\$/ +Sleep 3s + +Type "# Essential packages like conda itself are protected" +Enter +Sleep 500ms + +Type@80ms "conda self remove conda" +Enter +Wait /\$/ +Sleep 3s + +Type "# Override protection with --force (use with care)" +Enter +Sleep 500ms + +Type@80ms "conda self remove --force --dry-run conda" +Enter +Wait /\$/ +Sleep 3s diff --git a/demos/reset.gif b/demos/reset.gif new file mode 100644 index 0000000..b86f7ad Binary files /dev/null and b/demos/reset.gif differ diff --git a/demos/reset.mp4 b/demos/reset.mp4 new file mode 100644 index 0000000..745b6ca Binary files /dev/null and b/demos/reset.mp4 differ diff --git a/demos/reset.tape b/demos/reset.tape new file mode 100644 index 0000000..19b2b34 --- /dev/null +++ b/demos/reset.tape @@ -0,0 +1,26 @@ +Source demos/_settings.tape + +Output demos/reset.gif +Output demos/reset.mp4 + +Hide +Type `source demos/_setup.sh` +Enter +Wait /\$/ +# Install a plugin so reset has something to clean up +Type `conda self install --yes --quiet conda-index 2>&1 | tail -1` +Enter +Wait /\$/ +Type "clear" +Enter +Wait /\$/ +Show + +Type "# Reset base to the installer-provided snapshot" +Enter +Sleep 500ms + +Type@80ms "conda self reset --yes" +Enter +Wait /\$/ +Sleep 3s diff --git a/demos/update.gif b/demos/update.gif new file mode 100644 index 0000000..b7f9eea Binary files /dev/null and b/demos/update.gif differ diff --git a/demos/update.mp4 b/demos/update.mp4 new file mode 100644 index 0000000..494378b Binary files /dev/null and b/demos/update.mp4 differ diff --git a/demos/update.tape b/demos/update.tape new file mode 100644 index 0000000..e5ae152 --- /dev/null +++ b/demos/update.tape @@ -0,0 +1,27 @@ +Source demos/_settings.tape + +Output demos/update.gif +Output demos/update.mp4 + +Hide +Type `source demos/_setup.sh` +Enter +Wait /\$/ +# Install an older plugin so the update has something to upgrade +# (use raw conda install to avoid pinning the version in history) +Type `conda install --yes --quiet --override-frozen conda-index=0.6.0 2>&1 | tail -1` +Enter +Wait /\$/ +Type "clear" +Enter +Wait /\$/ +Show + +Type "# Update conda and all plugins" +Enter +Sleep 500ms + +Type@80ms "conda self update --yes --all" +Enter +Wait /\$/ +Sleep 3s diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000..cbd7225 --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1 @@ +/* conda-self custom styles */ diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..c46121d --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,6 @@ +# Changelog + +## Unreleased + +- Added comprehensive documentation with Diataxis structure +- Added VHS terminal demos diff --git a/docs/conf.py b/docs/conf.py index 9d0ca4b..05f6922 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,70 +1,37 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html +"""Sphinx configuration for conda-self documentation.""" -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) project = html_title = "conda-self" -copyright = "2025, conda-self contributors" +copyright = "2025, conda community" author = "conda-self contributors" -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - extensions = [ "myst_parser", - "sphinx.ext.napoleon", - "sphinx.ext.autosummary", - "sphinx.ext.graphviz", - "sphinx.ext.ifconfig", - "sphinx.ext.inheritance_diagram", - "sphinx.ext.viewcode", - "sphinx_sitemap", - "sphinx_design", + "sphinx.ext.intersphinx", "sphinx_copybutton", - "sphinx_reredirects", - "sphinxcontrib.programoutput", + "sphinx_design", ] -myst_heading_anchors = 3 +intersphinx_mapping = { + "conda": ("https://docs.conda.io/projects/conda/en/stable/", None), +} + myst_enable_extensions = [ - "amsmath", "colon_fence", "deflist", - "dollarmath", - "html_admonition", - "html_image", - "linkify", - "replacements", - "smartquotes", - "substitution", + "fieldlist", "tasklist", ] - -templates_path = ["_templates"] -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - html_theme = "conda_sphinx_theme" -html_static_path = ["_static"] - -html_css_files = [ - "css/custom.css", -] - -# Serving the robots.txt since we want to point to the sitemap.xml file -html_extra_path = ["robots.txt"] html_theme_options = { - "navigation_depth": -1, - "use_edit_page_button": True, - "navbar_center": ["navbar_center"], "icon_links": [ { "name": "GitHub", @@ -72,18 +39,6 @@ "icon": "fa-brands fa-square-github", "type": "fontawesome", }, - { - "name": "Zulip", - "url": "https://conda.zulipchat.com", - "icon": "_static/element_logo.svg", - "type": "local", - }, - { - "name": "Discourse", - "url": "https://conda.discourse.group/", - "icon": "fa-brands fa-discourse", - "type": "fontawesome", - }, ], } @@ -94,13 +49,10 @@ "doc_path": "docs", } -html_baseurl = "https://conda-incubator.github.io" - -# We don't have a locale set, so we can safely ignore that for the sitemaps. -sitemap_locales = [None] -# We're hard-coding stable here since that's what we want Google to point to. -sitemap_url_scheme = "{link}" +html_static_path = ["_static"] +html_extra_path = ["../demos"] +html_css_files = ["css/custom.css"] -# -- For sphinx_reredirects ------------------------------------------------ +html_baseurl = "https://conda-incubator.github.io/conda-self/" -redirects = {} +exclude_patterns = ["_build"] diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..f15a207 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,59 @@ +# Configuration + +## conda settings + +conda-self registers one custom setting via conda's `conda_settings` +[plugin hook](inv:conda:std:doc#dev-guide/plugins/index). + +### self_permanent_packages + +A list of package names that should never be removed by +`conda self remove` or stripped during `conda self reset`. + +Configure in [`.condarc`](inv:conda:std:doc#configuration): + +```yaml +self_permanent_packages: + - pip + - setuptools +``` + +These packages are added to the set of "permanent" dependencies +(alongside conda itself and its [plugins](inv:conda:std:doc#dev-guide/plugins/index)) when determining what +can be safely removed. + +## Snapshot files + +Snapshots are stored in `conda-meta/` inside the base prefix and +use conda's `@EXPLICIT` format (a list of exact package URLs). + +| File | Created by | Purpose | +|------|-----------|---------| +| `base-protection-state.explicit.txt` | `conda doctor base-protection --fix` | Pre-protection state of base | +| `installer-state.explicit.txt` | Installer (e.g. Miniforge) | Original installer state | + +These files are used by `conda self reset --snapshot ` to +restore base to a known state without running the solver. + +## Constants + +| Constant | Value | Description | +|----------|-------|-------------| +| `DEFAULT_ENV_NAME` | `"default"` | Name of the environment created when cloning base | +| `SNAPSHOT_FILE_BASE_PROTECTION` | `"base-protection-state.explicit.txt"` | Snapshot filename for base protection | +| `RESET_FILE_INSTALLER` | `"installer-state.explicit.txt"` | Snapshot filename from installer | +| `SELF_PERMANENT_PACKAGES_SETTING` | `"self_permanent_packages"` | Name of the condarc setting | + +## Environment variables + +conda-self does not define its own environment variables. It respects +all standard conda environment variables, including: + +`CONDA_CHANNELS` +: Override configured channels for plugin installation. + +`CONDA_DRY_RUN` +: Enable dry-run mode for all operations. + +`CONDA_JSON` +: Enable JSON output for all operations. diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 0000000..4ad3e0a --- /dev/null +++ b/docs/features.md @@ -0,0 +1,142 @@ +# Features + +An overview of what conda-self provides and how the pieces fit +together. + +## Base environment protection + +The base environment is special -- it contains conda itself and is +always activated. Installing arbitrary packages into base risks +breaking conda. conda-self provides a health check that protects +base: + +```bash +conda doctor base-protection --fix +``` + +This: + +1. **Clones** the current base to a `default` environment, preserving + all your packages for continued use +2. **Saves a snapshot** of base in `@EXPLICIT` format to + `conda-meta/base-protection-state.explicit.txt` +3. **Resets** base to conda, its plugins, their dependencies, and any + installer-provided packages (e.g. `mamba` in Miniforge) +4. **Freezes** base by writing a `PREFIX_FROZEN_FILE`, preventing + regular [conda install](inv:conda:std:doc#commands/install) from modifying it + +After protection, only `conda self` commands can modify base. + +### Checking protection status + +```bash +conda doctor base-protection +``` + +This reports whether base is currently protected (frozen) and whether +a snapshot exists. + +## Plugin management + +conda-self provides three commands for managing plugins in a protected +base environment: + +### Install + +![Install a plugin](../demos/install-plugin.gif) + +```bash +conda self install conda-index +``` + +The install command: + +1. Runs `conda install` as a subprocess with `--override-frozen` +2. After installation, scans `importlib.metadata` entry points for + the `conda` group +3. If the installed package is not a valid [conda plugin](inv:conda:std:doc#dev-guide/plugins/index), uninstalls + it and raises an error + +This prevents non-plugin packages from accumulating in base. + +### Update + +![Update plugins](../demos/update.gif) + +```bash +conda self update # update all +conda self update conda # update specific packages +conda self update --force-reinstall # force reinstall all +``` + +The update command passes package names to `conda install --update-specs` +and lets the solver find the latest compatible versions. No manual +version pinning or repodata queries. + +### Remove + +![Remove a plugin](../demos/remove.gif) + +```bash +conda self remove conda-index +``` + +Essential packages (conda itself, its core dependencies, and packages +listed in `self_permanent_packages`) cannot be removed. + +## Snapshots and reset + +When base protection is enabled, conda-self saves a snapshot of the +pre-protection state. This snapshot can be used to restore base: + +```bash +conda self reset # auto-detect best snapshot +conda self reset --snapshot installer # reset to installer state +conda self reset --snapshot base-protection # reset to protection snapshot +conda self reset --snapshot current # strip to essentials only +``` + +Snapshots are stored as `@EXPLICIT` files in `conda-meta/`: + +- `base-protection-state.explicit.txt` -- saved by `conda doctor --fix` +- `installer-state.explicit.txt` -- saved by the installer (if available) + +## Health check integration + +conda-self registers a `base-protection` health check with conda's +[conda doctor](inv:conda:std:doc#commands/doctor) system: + +```bash +conda doctor --list # see all health checks +conda doctor base-protection # check protection status +conda doctor base-protection --fix # enable protection +``` + +This uses conda's `conda_health_checks` plugin hook, so the health +check appears alongside any other registered checks. + +## Plugin validation + +When installing packages, conda-self validates that they are actual +conda plugins by checking `importlib.metadata.entry_points(group="conda")`. +Package names are normalized (hyphens vs underscores) to handle +differences between conda naming conventions and Python packaging +metadata. + +If a package is not a plugin, it is automatically uninstalled and +a `SpecsAreNotPlugins` error is raised. + +## Permanent packages + +The `self_permanent_packages` setting allows configuring a list of +packages that should never be removed by `conda self remove` or +stripped during `conda self reset`. This is useful for packages that +are essential to your workflow but are not conda plugins. + +Configure it in the [`.condarc` configuration file](inv:conda:std:doc#configuration): + +```yaml +self_permanent_packages: + - pip + - setuptools +``` diff --git a/docs/guides/custom-channels.md b/docs/guides/custom-channels.md new file mode 100644 index 0000000..0a827b0 --- /dev/null +++ b/docs/guides/custom-channels.md @@ -0,0 +1,66 @@ +# Using custom channels + +How to install plugins from custom or private channels. + +## Configure channels first + +conda-self uses your configured channels for all operations. Use +[conda config](inv:conda:std:doc#commands/config) to add a custom channel: + +```bash +conda config --add channels my-channel -n base +``` + +Then install normally: + +```bash +conda self install my-plugin +``` + +## Why inline specs are rejected + +`conda self install conda-forge::my-plugin` is not supported. +Inline channel specs would cause inconsistencies between install +and update operations -- the channel would apply to the install +but not to future updates, leading to unexpected solver behavior. + +Instead, configure channels once and let all operations use the +same configuration. + +## Channel priority + +Channels are searched in the order they appear in your configuration. +The first channel with a matching package wins (in strict mode) or +packages from all channels are considered (in flexible mode). + +You can inspect channels with [conda info](inv:conda:std:doc#commands/info) or by showing config values: + +```bash +conda config --show channels +conda config --show channel_priority +``` + +## Private channels + +For private channels that require authentication (e.g. on +anaconda.org or prefix.dev), configure tokens via: + +```bash +conda token set -c https://my-channel.example.com +``` + +Or use conda's standard authentication mechanisms. conda-self +inherits all authentication settings from your [conda configuration](inv:conda:std:doc#configuration). + +## Multiple channels + +If a plugin is available on multiple channels, conda will use the +one with highest priority: + +```bash +conda config --add channels conda-forge -n base +conda config --add channels my-company-channel -n base + +# my-company-channel has higher priority (added last) +conda self install my-plugin +``` diff --git a/docs/guides/resetting-base.md b/docs/guides/resetting-base.md new file mode 100644 index 0000000..35c6b6e --- /dev/null +++ b/docs/guides/resetting-base.md @@ -0,0 +1,69 @@ +# Resetting the base environment + +How to restore your base environment from a snapshot when things go +wrong. + +## Auto-detect the best snapshot + +```bash +conda self reset +``` + +conda-self tries snapshots in this order: + +1. `base-protection` -- the snapshot saved by [conda doctor base-protection --fix](inv:conda:std:doc#commands/doctor) +2. `installer` -- the snapshot saved by the installer +3. `current` -- strip to essentials without a snapshot + +## Reset to a specific snapshot + +### Base-protection snapshot + +Restore to the state captured when you first protected base: + +```bash +conda self reset --snapshot base-protection +``` + +This uses `conda-meta/base-protection-state.explicit.txt`. + +### Installer snapshot + +Restore to the original state from the installer (e.g. Miniforge): + +```bash +conda self reset --snapshot installer +``` + +This uses `conda-meta/installer-state.explicit.txt`. Not all +installers provide this file. + +### Current essentials + +Strip base to only conda, its [plugins](inv:conda:std:doc#dev-guide/plugins/index), and their dependencies, +without using any snapshot file: + +```bash +conda self reset --snapshot current +``` + +## Dry run + +Preview what a reset would do: + +```bash +conda self reset --dry-run +conda self reset --snapshot installer --dry-run +``` + +## After a reset + +After resetting, your base environment contains only essentials. +[conda info](inv:conda:std:doc#commands/info) lists what is left in base. You may need to reinstall plugins: + +```bash +conda self install conda-index +``` + +Your `default` environment (created during base protection) is +unaffected by resets. diff --git a/docs/index.md b/docs/index.md index 6d736c5..5721c1f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,47 +1,132 @@ # conda-self -`cookiecutter.project_name` provides support in `conda` for... +Manage your conda `base` environment safely. +conda-self provides commands to install, update, and remove +[conda plugins](inv:conda:std:doc#dev-guide/plugins/index) +in a protected base environment. It integrates with +[conda doctor](inv:conda:std:doc#commands/doctor) to set up base +protection -- cloning your current base to a `default` environment, +resetting base to essentials, and freezing it so only `conda self` +commands can modify it. -:::{warning} -This project is still in early stages of development. Don't use it in production (yet). -We do welcome feedback on what the expected behaviour should have been if something doesn't work! -::: +## Quick example -::::{grid} 2 +![Protect base, install a plugin, update, reset](../demos/quickstart.gif) -:::{grid-item-card} 🏡 Getting started +```bash +# Protect your base environment +$ conda doctor base-protection --fix + +# Install a plugin safely +$ conda self install conda-index + +# Update conda and all plugins +$ conda self update + +# Remove a plugin +$ conda self remove conda-index + +# Reset base if things go wrong +$ conda self reset +``` + +## What it does + +`conda self` keeps your base environment minimal and stable: + +1. **Base protection** -- `conda doctor base-protection --fix` clones + base to `default`, resets it to essentials, and freezes it +2. **Plugin management** -- `conda self install`, `update`, and `remove` + bypass the freeze to manage plugins through subprocess calls that + respect all of conda's safety checks +3. **Reset** -- `conda self reset` restores base from a snapshot + (installer-provided, base-protection, or current state) + +## Navigation + +:::::::{grid} 1 1 2 2 +:gutter: 3 + +::::::{grid-item-card} {octicon}`rocket;1em` Getting started :link: quickstart :link-type: doc -New to `conda-self`? Start here to learn the essentials -::: -:::{grid-item-card} 📖 How to -:link: howto +Protect your base environment and manage plugins in under a minute. +:::::: + +::::::{grid-item-card} {octicon}`mortar-board;1em` Tutorials +:link: tutorials/index :link-type: doc -Quick guides that teach how to leverage `conda-self` -::: -:::: +Step-by-step guides for protecting base, managing plugins, and more. +:::::: -::::{grid} 2 +::::::{grid-item-card} {octicon}`star;1em` Features +:link: features +:link-type: doc -:::{grid-item-card} 🖥️ CLI usage -:link: cli +Base protection, plugin validation, snapshots, and health checks. +:::::: + +::::::{grid-item-card} {octicon}`gear;1em` Configuration +:link: configuration :link-type: doc -Consult the command-line interface manual -::: -:::{grid-item-card} 💡 Motivation and vision -:link: why +Settings, snapshot files, and environment variables. +:::::: + +::::::{grid-item-card} {octicon}`terminal;1em` CLI reference +:link: reference/cli :link-type: doc -Read about why `conda-self` exists and when you should use it -::: -:::: +Every `conda self` subcommand and `conda doctor` integration. +:::::: + +::::::{grid-item-card} {octicon}`light-bulb;1em` Motivation +:link: motivation +:link-type: doc + +Why conda-self exists and how it keeps base safe. +:::::: + +::::::: ```{toctree} :hidden: +:caption: Tutorials +quickstart +tutorials/index +``` + +```{toctree} +:hidden: +:caption: How-to guides + +guides/resetting-base +guides/custom-channels +``` + +```{toctree} +:hidden: +:caption: Reference + +reference/cli +configuration +``` + +```{toctree} +:hidden: +:caption: Explanation + +features +motivation +``` + +```{toctree} +:hidden: +:caption: Project +changelog ``` diff --git a/docs/motivation.md b/docs/motivation.md new file mode 100644 index 0000000..ef7f270 --- /dev/null +++ b/docs/motivation.md @@ -0,0 +1,126 @@ +# Motivation + +## What conda-self is + +conda-self is a conda plugin that manages conda installations +themselves -- specifically the `base` environment where conda, its +plugins, and their dependencies live. It provides safe commands to +install, update, and remove plugins in base, and integrates with +[conda doctor](inv:conda:std:doc#commands/doctor) to protect base from accidental modification. + +The name `conda self` reflects this purpose: conda managing itself. + +## The problem + +The conda `base` environment is unique: it contains conda itself and +is always activated. This makes it a tempting target for installing +packages directly, but doing so creates real risks: + +- Installing a package with conflicting dependencies can break conda +- A broken base environment means you cannot use conda to fix it +- Over time, base accumulates packages that are difficult to untangle +- There is no built-in way to "undo" what was installed in base + +Many users have experienced the frustration of a broken base +environment. The usual advice is "don't install anything in base," +but conda itself needs [plugins](inv:conda:std:doc#dev-guide/plugins/index) (like the solver, authentication +handlers, or custom subcommands) installed there to be discovered. + +## How we got here + +conda-self evolved through several iterations, shaped by UX testing +and community feedback: + +1. The project started as a way to protect and manage the base + environment. Early versions had a `conda self protect` command + that froze base. + +2. UX testing showed that "protect" did not communicate what the + command actually does -- cloning base to a working environment + and locking down the original. The command was renamed to + `conda self migrate` (later `conda migrate`). + +3. Rather than introducing a new top-level subcommand, the protection + logic was integrated into conda's existing health check system via + `conda doctor` and `conda doctor --fix`. This makes base + protection discoverable alongside other environment health checks. + +4. The name `conda self` (rather than `conda base`) was chosen + because the plugin manages the conda installation, not just + any environment named "base." + +5. Reset functionality evolved from a simple "strip to essentials" + to supporting multiple snapshot sources: the installer-provided + state (e.g. from Miniforge), the pre-protection state saved by + `conda doctor --fix`, and the current essential packages. + +## Prior art + +### Manual discipline + +The most common approach is to simply avoid installing packages in +base. This works until you need to install a conda plugin, which +must live in base to be discovered. There is no tooling to enforce +this discipline. + +### conda-protect + +An earlier plugin that explored freezing environments to prevent +accidental modification. conda-self builds on this idea by +integrating protection directly into conda's health check system +and providing safe commands for the operations that do need to +modify base. + +### Package managers with self-management + +Tools like `rustup` (Rust), `pipx` (Python CLI tools), and +`brew` (macOS) separate the tool installation from user packages. +conda-self brings a similar separation to conda: the base +environment is for conda and its plugins, while user packages +live in named environments. + +## Design choices + +Subprocess over in-process API +: `conda self install` uses subprocess calls to [conda install](inv:conda:std:doc#commands/install) + rather than the in-process Solver API. This ensures frozen + environment protection (which lives in conda's CLI layer) is + always respected. It also means all of conda's safety checks, + channel resolution, and reporting work exactly as users expect. + See [issue #15](https://github.com/conda-incubator/conda-self/issues/15) + for the discussion that led to this decision. + +Plugin validation after install +: Rather than pre-validating packages (which would require + maintaining a list of known plugins), conda-self installs first + and then checks `importlib.metadata` entry points. If the package + is not a plugin, it is automatically rolled back. This is + future-proof and works with any plugin, including third-party ones. + +Snapshot-based recovery +: Snapshots use conda's `@EXPLICIT` format -- a list of exact + package URLs. This is the most reliable way to reproduce an + environment state, as it bypasses the solver entirely. Multiple + snapshot sources (installer, base-protection, current) give users + flexibility in how far back to restore. + +Channel configuration over inline specs +: `conda self install conda-forge::pkg` is rejected. Instead, + channels are configured via [conda config](inv:conda:std:doc#commands/config), keeping channel + settings consistent across install, update, and dependency + resolution. + +Health checks over custom subcommands +: Base protection is implemented as a `conda doctor` health check + rather than a standalone `conda self protect` or `conda migrate` + command. This makes it discoverable via `conda doctor --list` and + follows the pattern conda already provides for environment + maintenance. + +## What's next + +conda-self is on track to graduate from the conda-incubator +organization to the main conda organization, becoming a default +plugin shipped with conda. See +[issue #89](https://github.com/conda-incubator/conda-self/issues/89) +for details. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..4e14761 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,88 @@ +# Quick start + +This guide walks you through protecting your base environment and +managing plugins with conda-self. + +## Prerequisites + +- conda 26.1.1 or later +- conda-self installed in base (`conda install -n base conda-self`) + +## Protect your base environment + +![Base protection demo](../demos/base-protection.gif) + +Using [conda doctor](inv:conda:std:doc#commands/doctor), check whether base is currently protected: + +```bash +conda doctor base-protection +``` + +If it is not, enable protection: + +```bash +conda doctor base-protection --fix +``` + +This does three things: + +1. Clones your current base environment to a new `default` environment +2. Resets base to conda, its plugins, their dependencies, and any installer-provided packages +3. Freezes base so regular [conda install](inv:conda:std:doc#commands/install) cannot modify it + +:::{tip} +You only need to run this once. After protection, use `conda self` +commands to manage plugins in base. +::: + +## Install a plugin + +![Install plugin demo](../demos/install-plugin.gif) + +```bash +conda self install conda-index +``` + +conda-self installs the package via subprocess, validates that it +registers as a [conda plugin](inv:conda:std:doc#dev-guide/plugins/index) (via entry points), and rolls back if +it does not. + +## Update plugins + +```bash +conda self update +``` + +This updates conda itself and all installed plugins to their latest +compatible versions. + +## Remove a plugin + +![Remove plugin demo](../demos/remove.gif) + +```bash +conda self remove conda-index +``` + +Essential packages (conda itself, its core dependencies) cannot be +removed. + +## Reset base + +![Reset demo](../demos/reset.gif) + +If something goes wrong, reset base to essentials: + +```bash +conda self reset +``` + +## Next steps + +- {doc}`tutorials/protecting-base` -- A deeper walkthrough of base + protection and what happens under the hood +- {doc}`tutorials/managing-plugins` -- Install, update, and remove + plugins with confidence +- {doc}`features` -- How base protection, snapshots, and plugin + validation work +- {doc}`reference/cli` -- Every command and flag diff --git a/docs/reference/cli.md b/docs/reference/cli.md new file mode 100644 index 0000000..4a802d3 --- /dev/null +++ b/docs/reference/cli.md @@ -0,0 +1,191 @@ +# CLI reference + +All commands are available as `conda self `. + +--- + +## self install + +Add conda plugins to the base environment. + +``` +conda self install ... [--force-reinstall] [--dry-run] [--yes] [--json] [--quiet] +``` + +`specs` +: One or more package names to install. Inline channel specs + (`channel::pkg`) are rejected -- use [conda config](inv:conda:std:doc#commands/config) instead. + +`--force-reinstall` +: Reinstall the plugin even if it is already installed. + +`--dry-run` +: Show what would be installed without making changes. + +`--yes` +: Skip confirmation prompts. + +`--json` +: Output in JSON format. + +`--quiet` +: Suppress non-essential output. + +```bash +# Install a plugin +conda self install conda-index + +# Force reinstall +conda self install --force-reinstall conda-index +``` + +After installation, conda-self validates that the package registers +a `conda` entry point. If it does not, the package is automatically +uninstalled and a `SpecsAreNotPlugins` error is raised. + +--- + +## self remove + +Remove conda plugins from the base environment. + +``` +conda self remove ... [--dry-run] [--yes] [--json] [--quiet] +``` + +`specs` +: One or more package names to remove. + +`--dry-run` +: Show what would be removed without making changes. + +`--yes` +: Skip confirmation prompts. + +Essential packages (conda, its core dependencies, and anything in +`self_permanent_packages`) cannot be removed. Attempting to do so +raises a `SpecsCanNotBeRemoved` error. + +```bash +conda self remove conda-index +``` + +--- + +## self update + +Update conda and its plugins in the base environment. + +``` +conda self update [...] [--force-reinstall] [--dry-run] [--yes] [--json] [--quiet] +``` + +`specs` +: Optional package names to update. If omitted, updates all plugins + and conda itself. + +`--force-reinstall` +: Force reinstall of all packages. + +`--dry-run` +: Show what would change without making modifications. + +`--yes` +: Skip confirmation prompts. + +```bash +# Update everything +conda self update + +# Update specific packages +conda self update conda + +# Force reinstall all +conda self update --force-reinstall +``` + +The update command uses `--update-specs` by default and `--all` when +`--force-reinstall` is specified. It lets the solver find the latest +compatible versions rather than pinning to specific version numbers. + +--- + +## self reset + +Reset the base environment to essential packages only. + +``` +conda self reset [--snapshot ] [--dry-run] [--yes] [--json] [--quiet] +``` + +`--snapshot` +: Which snapshot to reset to. Options: + + `current` + : Remove all packages except conda, its plugins, and their + dependencies. + + `installer` + : Reset to the snapshot saved by the installer + (`conda-meta/installer-state.explicit.txt`). + + `base-protection` + : Reset to the snapshot saved by `conda doctor --fix` + (`conda-meta/base-protection-state.explicit.txt`). + + If not specified, conda-self tries `base-protection` first, then + `installer`, and falls back to `current`. + +```bash +# Auto-detect best snapshot +conda self reset + +# Reset to installer state +conda self reset --snapshot installer + +# Reset to base-protection snapshot +conda self reset --snapshot base-protection + +# Strip to current essentials +conda self reset --snapshot current +``` + +--- + +## conda doctor base-protection + +Check and fix the base environment protection status. This is a +health check registered via conda's `conda_health_checks` +[plugin hook](inv:conda:std:doc#dev-guide/plugins/index). See also +[conda doctor](inv:conda:std:doc#commands/doctor) for how health checks work. + +``` +conda doctor base-protection [--fix] [--dry-run] +``` + +`--fix` +: Enable base protection. This: + 1. Clones the current base environment to `default` + 2. Saves a snapshot to `conda-meta/base-protection-state.explicit.txt` + 3. Resets base to essential packages + 4. Freezes base via `PREFIX_FROZEN_FILE` + +Without `--fix`, reports whether base is currently protected. + +```bash +# Check status +conda doctor base-protection + +# Enable protection +conda doctor base-protection --fix + +# See all available health checks +conda doctor --list +``` + +:::{warning} +If your base environment contains non-conda packages (e.g. pip-installed), +`--fix` will warn you before proceeding. These packages are preserved +in the cloned `default` environment but will become non-functional +in the reset base. +::: diff --git a/docs/robots.txt b/docs/robots.txt deleted file mode 100644 index 3452dac..0000000 --- a/docs/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -User-agent: * - -Sitemap: https://conda-incubator.github.io/conda-self/sitemap.xml diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 0000000..bf77c82 --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,32 @@ +# Tutorials + +Step-by-step guides that walk you through common workflows from start +to finish. + +::::{grid} 1 +:gutter: 3 + +:::{grid-item-card} {octicon}`shield-lock;1em` Protecting your base environment +:link: protecting-base +:link-type: doc + +Set up base protection, understand what happens under the hood, +and verify everything works. +::: + +:::{grid-item-card} {octicon}`plug;1em` Managing plugins +:link: managing-plugins +:link-type: doc + +Install, update, and remove conda plugins safely in a protected +base environment. +::: + +:::: + +```{toctree} +:hidden: + +protecting-base +managing-plugins +``` diff --git a/docs/tutorials/managing-plugins.md b/docs/tutorials/managing-plugins.md new file mode 100644 index 0000000..a4d2605 --- /dev/null +++ b/docs/tutorials/managing-plugins.md @@ -0,0 +1,99 @@ +# Managing plugins + +This tutorial covers the complete lifecycle of [conda plugins](inv:conda:std:doc#dev-guide/plugins/index) in a +protected base environment: installing, updating, and removing them. + +## Prerequisites + +- conda-self installed in base (`conda install -n base conda-self`) +- Base environment protected (see {doc}`protecting-base`) + +## Install a plugin + +![Install plugin demo](../../demos/install-plugin.gif) + +```bash +conda self install conda-index +``` + +conda-self runs [conda install](inv:conda:std:doc#commands/install) as a subprocess with +`--override-frozen`, then validates that the installed package is +a real conda plugin by checking its entry points. If validation +fails, the package is automatically uninstalled. + +### Multiple plugins at once + +```bash +conda self install conda-index conda-auth +``` + +## Update plugins + +![Update demo](../../demos/update.gif) + +Update all plugins and conda itself: + +```bash +conda self update +``` + +Update specific packages: + +```bash +conda self update conda +``` + +Force reinstall everything: + +```bash +conda self update --force-reinstall +``` + +The solver finds the latest compatible versions automatically. No +manual version pinning is needed. + +## Remove a plugin + +![Remove demo](../../demos/remove.gif) + +```bash +conda self remove conda-index +``` + +Essential packages (conda, its core dependencies) cannot be removed. +If you try, you will see a `SpecsCanNotBeRemoved` error. + +## Channel configuration + +conda-self uses your configured channels. Use [conda config](inv:conda:std:doc#commands/config) to add or change channels before installing. To install plugins from +a custom channel: + +```bash +# Add the channel first +conda config --add channels my-channel -n base + +# Then install +conda self install my-plugin +``` + +Inline channel specs (`conda-forge::my-plugin`) are not supported +and will produce an error. This keeps channel configuration +consistent across all operations. + +## Verify installed plugins + +After installing, you can verify which plugins are registered with +[conda info](inv:conda:std:doc#commands/info): + +```bash +conda info +``` + +The output includes a "plugins" section listing all discovered +conda plugins and their versions. + +## Next steps + +- {doc}`../guides/resetting-base` -- Restore base from a snapshot +- {doc}`../guides/custom-channels` -- Use custom channels for plugins +- {doc}`../reference/cli` -- Full CLI reference diff --git a/docs/tutorials/protecting-base.md b/docs/tutorials/protecting-base.md new file mode 100644 index 0000000..0d69495 --- /dev/null +++ b/docs/tutorials/protecting-base.md @@ -0,0 +1,111 @@ +# Protecting your base environment + +This tutorial walks through setting up base environment protection +from scratch and understanding what happens at each step. + +## Before you start + +Verify conda-self is available (install with `conda install -n base conda-self`): + +```bash +conda self --version +``` + +## Check current status + +First, see if base is already protected with [conda doctor](inv:conda:std:doc#commands/doctor): + +```bash +conda doctor base-protection +``` + +If base is not protected, you will see a message indicating that the +health check found an issue. + +## Enable protection + +![Base protection demo](../../demos/base-protection.gif) + +Run the fix: + +```bash +conda doctor base-protection --fix +``` + +You will be prompted to confirm. Here is what happens: + +### Step 1: Clone base to default + +Your current base environment is cloned to a new environment called +`default`. All packages, including pip-installed ones, are preserved +in the clone. This is your fallback -- you can activate `default` +and continue working as before. + +```bash +conda activate default +``` + +### Step 2: Save a snapshot + +A snapshot of base is saved in `@EXPLICIT` format to +`conda-meta/base-protection-state.explicit.txt`. This file lists +every package URL in the environment before the reset, enabling +exact restoration later. + +### Step 3: Reset base + +Base is stripped down to conda, its registered [plugins](inv:conda:std:doc#dev-guide/plugins/index), and their +dependencies. Everything else is removed. + +### Step 4: Freeze base + +A freeze file is written to base, preventing regular [conda install](inv:conda:std:doc#commands/install) +from modifying it. Only `conda self` commands (which pass +`--override-frozen`) can make changes. + +## Verify protection + +Run the health check again: + +```bash +conda doctor base-protection +``` + +It should now report that base is protected. + +Try installing a regular package into base: + +```bash +conda install -n base numpy +``` + +This will fail with a frozen environment error. That is the expected +behavior. + +## Use the default environment + +Your previous packages are in the `default` environment: + +```bash +conda activate default +python -c "import numpy; print(numpy.__version__)" +``` + +## Non-conda packages + +If your base environment contains pip-installed packages, you will +see a warning before protection proceeds: + +> Warning: Base environment contains N non-conda package(s) that +> will become non-functional after reset. They are preserved in the +> cloned 'default' environment. + +These packages are copied to `default` but will not work in the +reset base. Reinstall them in `default` or another environment +if needed. + +## Next steps + +- {doc}`managing-plugins` -- Install plugins safely in the protected base +- {doc}`../guides/resetting-base` -- Restore base from a snapshot +- {doc}`../reference/cli` -- Full CLI reference diff --git a/pixi.lock b/pixi.lock index dee1a34..4030cd7 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1139,7 +1139,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.2-hca6bf5a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.2-he237659_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda @@ -1179,14 +1178,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.5.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-favicon-1.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-last-updated-by-git-0.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-reredirects-0.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-sitemap-2.9.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-programoutput-0.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda @@ -1196,7 +1191,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-cpp-0.8.0-h3f2d84a_0.conda @@ -1258,7 +1252,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.2-h5ef1a60_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.2-h8d039ee_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lzo-2.10-h925e9cb_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda @@ -1298,14 +1291,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.5.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-favicon-1.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-last-updated-by-git-0.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-reredirects-0.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-sitemap-2.9.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-programoutput-0.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda @@ -1315,7 +1304,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-cpp-0.8.0-ha1acc90_0.conda @@ -1371,7 +1359,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.2-h692994f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.2-h5d26750_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lzo-2.10-h6a83c73_1002.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda @@ -1409,14 +1396,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.5.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-favicon-1.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-last-updated-by-git-0.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-reredirects-0.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-sitemap-2.9.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-programoutput-0.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda @@ -1426,7 +1409,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda @@ -4859,8 +4841,8 @@ packages: timestamp: 1751548225624 - pypi: ./ name: conda-self - version: 0.1.2.dev78+g2a2ba0451.d20260410 - sha256: 5ca85a78e941ad79db9f6255c9b66f0f3bf2228142ce26f16e2a24bf1871255a + version: 0.1.2.dev81+g7e3cd77a9.d20260410 + sha256: b2ae0256f2399bc39c6f417d755321c3b07200e49f7a8e5748b874d430092936 requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/conda-sphinx-theme-0.4.0-pyhd8ed1ab_1.conda sha256: 201154b7b3cb5a7788e6ea7c98f01309e45a318464ae62f6eede38bfaa6ba5de @@ -7472,18 +7454,6 @@ packages: purls: [] size: 58347 timestamp: 1774072851498 -- conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.1.0-pyhcf101f3_0.conda - sha256: 991a82fbb64aba6d10719a017ce354e28df02ea5df1d9c7b0221da573c168d27 - md5: 1005e1f39083adad2384772e8e384e43 - depends: - - python >=3.10 - - uc-micro-py - license: MIT - license_family: MIT - purls: - - pkg:pypi/linkify-it-py?source=hash-mapping - size: 26062 - timestamp: 1772476821244 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-22.1.3-hd34ed20_0.conda sha256: 5cbb374e00fc3c993eac7cbc54f2b2ab7a2f492a58524242461ef155a0378776 md5: e5bd0715c932e968d69a8aba646786fc @@ -10790,44 +10760,6 @@ packages: - pkg:pypi/sphinx-favicon?source=hash-mapping size: 13665 timestamp: 1771717003462 -- conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-last-updated-by-git-0.3.8-pyhe01879c_0.conda - sha256: f761670b793dcc10a4a2d855de163d9dfd4016636ef093fb3e3d83ac25ed6e97 - md5: 405a232fb900fc631d2f1b5cdf01dea9 - depends: - - python >=3.9 - - sphinx >=1.8 - - python - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/sphinx-last-updated-by-git?source=hash-mapping - size: 17546 - timestamp: 1750694360605 -- conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-reredirects-0.1.6-pyhd8ed1ab_0.conda - sha256: e82c3d75d2ba24f51d29e5771e67cf04cb7ee9a94942aa82648bfe216269b38b - md5: b355f69362a41539d7c2b4cbcad7f1ac - depends: - - python >=3.9 - - sphinx - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/sphinx-reredirects?source=hash-mapping - size: 12161 - timestamp: 1742721211534 -- conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-sitemap-2.9.0-pyhcf101f3_0.conda - sha256: 1be6289124207256df5dfbfe6ff0a652e313ac5c3e50560c9e510afa76eb702b - md5: 3baeff262222dc87e978a68702bc5797 - depends: - - python >=3.10 - - sphinx-last-updated-by-git - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/sphinx-sitemap?source=hash-mapping - size: 13441 - timestamp: 1759753011102 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda sha256: d7433a344a9ad32a680b881c81b0034bc61618d12c39dd6e3309abeffa9577ba md5: 16e3f039c0aa6446513e94ab18a8784b @@ -10875,18 +10807,6 @@ packages: - pkg:pypi/sphinxcontrib-jsmath?source=hash-mapping size: 10462 timestamp: 1733753857224 -- conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-programoutput-0.19-pyhd8ed1ab_0.conda - sha256: a0ffae2b63de1e48d8b3a59219fc57d486f58e7a470cc981ccc819319b546fdb - md5: 4e214c97d3722dc82f3556fb359df92e - depends: - - python >=3.8 - - sphinx >=5 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/sphinxcontrib-programoutput?source=hash-mapping - size: 20670 - timestamp: 1771622545945 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda sha256: c664fefae4acdb5fae973bdde25836faf451f41d04342b64a358f9a7753c92ca md5: 00534ebcc0375929b45c3039b5ba7636 @@ -11087,17 +11007,6 @@ packages: purls: [] size: 119135 timestamp: 1767016325805 -- conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - sha256: 9e5a0e70e876fca50de075b1cc6641cdc009a16c18f4d52ab03edb777729a1bc - md5: 4fdd327852ffefc83173a7c029690d0c - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/uc-micro-py?source=hash-mapping - size: 13378 - timestamp: 1772479008776 - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 md5: 71b24316859acd00bdb8b38f5e2ce328 diff --git a/pyproject.toml b/pyproject.toml index 0b2d413..d1fe770 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,11 @@ conda = ">=26.1.1" [tool.pixi.tasks] conda = { cmd = "python -m conda", env = { CONDA_CHANNELS = "conda-forge" } } +[tool.pixi.tasks.demos] +cmd = """bash -c 'if [ -n "$1" ]; then vhs "demos/$1.tape"; else for tape in demos/*.tape; do [[ "$(basename "$tape")" == _* ]] && continue; vhs "$tape"; done; fi' -- {{ name }}""" +args = [{ arg = "name", default = "" }] +description = "Record demo GIFs" + [tool.pixi.pypi-dependencies] "conda-self" = { path = ".", editable = true } @@ -99,14 +104,10 @@ clean = { cmd = "rm -rf _build", cwd = "docs" } [tool.pixi.feature.docs.dependencies] python = "3.10.*" conda-sphinx-theme = { version = "*", channel = "conda-forge" } -linkify-it-py = "*" myst-parser = "*" sphinx = "*" sphinx-copybutton = "*" sphinx-design = "*" -sphinx-reredirects = "*" -sphinx-sitemap = "*" -sphinxcontrib-programoutput = "*" [tool.pixi.feature.test.tasks] test = 'python -m pytest' diff --git a/tests/conftest.py b/tests/conftest.py index 43b3dac..7265df7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,9 +52,15 @@ def _session_env( with session_tmp_env( "conda", "conda-self", + # Pin libmambapy <2.6 to work around a broken pybind11-abi==11 + # variant on Windows. See conda-forge/mamba-feedstock#384. + "libmambapy <2.6", f"python={python_ver}", f"--channel={channel}", ) as prefix: + # Persist the pin so `conda self update` respects it too. + pinned = prefix / "conda-meta" / "pinned" + pinned.write_text("libmambapy <2.6\n") yield prefix diff --git a/tests/test_cli_reset.py b/tests/test_cli_reset.py index f727d59..2b21c0a 100644 --- a/tests/test_cli_reset.py +++ b/tests/test_cli_reset.py @@ -385,7 +385,11 @@ def test_reset_base_protection( f"python={python_version}", "conda-self", "conda-index", + # Pin libmambapy <2.6 to work around a broken pybind11-abi==11 + # variant on Windows. See conda-forge/mamba-feedstock#384. + "libmambapy <2.6", ) as prefix: + (prefix / "conda-meta" / "pinned").write_text("libmambapy <2.6\n") frozen_file = prefix / PREFIX_FROZEN_FILE protection_state = prefix / "conda-meta" / RESET_FILE_BASE_PROTECTION