-
Notifications
You must be signed in to change notification settings - Fork 7
Register base-protection health check for conda doctor --fix
#88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
64d59ed
d9fbc54
5b2ba70
3c100d3
4c55b13
032cf79
a440117
52aca94
453e283
8e3b67a
cdc6a33
0067648
489dbac
8a9c33d
fccb4bb
c6c4e7b
17f9f92
8ae9521
df54a22
494f559
dc5f809
b84b071
8ffb1ca
b160fe6
b2deaf0
f086a3d
1bddac1
9d09d0e
03d340f
ed6f98c
bc58987
b49e351
803593f
2f7875b
c8b74b2
666cc24
7ab3a4b
84eb339
0a4c6b8
0f92612
085d5d7
b8d7ea1
ba21a7f
19a73c7
27a663e
339357e
c51a1a5
65201ff
d1e7518
f6355e0
35cd8b6
2211cb2
d75c018
3267f3f
9038871
d19a10c
c58edc6
ffbd29d
31d6307
c5a4d53
271d898
bb14151
b5f3269
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # Copyright (C) 2012 Anaconda, Inc | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| """Health checks for conda-self.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from . import base_protection | ||
|
|
||
| plugins = [base_protection] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| # Copyright (C) 2012 Anaconda, Inc | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these (seemingly old) copyright notices needed?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is consistent with the copyright in conda. Though it doesn't look like any other file in conda-self includes these. |
||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| """Health check: Base environment protection. | ||
|
|
||
| Checks if the base environment is protected (frozen) and offers to | ||
| protect it by cloning to a default environment and resetting base. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from conda.base.constants import OK_MARK, PREFIX_FROZEN_FILE, X_MARK | ||
| from conda.core.prefix_data import PrefixData | ||
|
|
||
| if TYPE_CHECKING: | ||
| from argparse import Namespace | ||
|
|
||
| from conda.plugins.types import ConfirmCallback | ||
|
|
||
|
|
||
| def is_base_environment(prefix: str) -> bool: | ||
| """Check if the given prefix is the base environment.""" | ||
| return prefix == sys.prefix | ||
|
|
||
|
|
||
| def is_base_protected() -> bool: | ||
| """Check if the base environment is protected (frozen).""" | ||
| frozen_file = PrefixData(sys.prefix).prefix_path / PREFIX_FROZEN_FILE | ||
| return frozen_file.exists() | ||
|
|
||
|
|
||
| def check(prefix: str, _verbose: bool) -> None: | ||
| """Health check: Verify base environment protection status. | ||
|
|
||
| Only runs when checking the base environment. | ||
| """ | ||
| if not is_base_environment(prefix): | ||
| print("Skipping base protection: not running on base environment.\n") | ||
| return | ||
jezdez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if is_base_protected(): | ||
| 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") | ||
|
|
||
|
|
||
| def fix(prefix: str, args: Namespace, confirm: ConfirmCallback) -> int: | ||
| """Fix: Protect the base environment. | ||
|
|
||
| This clones the base environment to a new 'default' environment, | ||
| resets base to essentials, and freezes it. | ||
| """ | ||
| from pathlib import Path | ||
|
|
||
| from conda.base.context import context | ||
|
|
||
| if not is_base_environment(prefix): | ||
| print("Skipping: not running on base environment.") | ||
| return 0 | ||
|
|
||
| if is_base_protected(): | ||
| print("Base environment is already protected.") | ||
| return 0 | ||
|
|
||
| default_env = "default" | ||
| message = "Protected by Base Environment Protection health fix" | ||
|
|
||
| if not context.quiet: | ||
| print(f"This will clone 'base' to '{default_env}', reset base, and freeze it.") | ||
| confirm("Proceed?") | ||
|
|
||
| import io | ||
| import json | ||
| from contextlib import nullcontext, redirect_stdout | ||
| from datetime import datetime | ||
|
|
||
| from conda.cli.condarc import ConfigurationFile | ||
| from conda.exceptions import CondaOSError | ||
| from conda.gateways.disk.delete import rm_rf | ||
| from conda.misc import clone_env | ||
| from conda.models.environment import Environment | ||
|
|
||
| from ..query import permanent_dependencies | ||
| from ..reset import reset | ||
|
|
||
| base_prefix = Path(sys.prefix) | ||
|
|
||
| # Get packages to keep in base | ||
| uninstallable_packages = permanent_dependencies() | ||
|
|
||
| # Check destination environment | ||
| dest_prefix_data = PrefixData.from_name(default_env) | ||
|
|
||
| if dest_prefix_data.is_environment(): | ||
| confirm(f"Environment '{default_env}' already exists. Remove and recreate?") | ||
| rm_rf(dest_prefix_data.prefix_path) | ||
jezdez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| elif dest_prefix_data.exists(): | ||
| confirm(f"Directory exists at '{dest_prefix_data.prefix_path}'. Continue?") | ||
|
|
||
| # Take a snapshot using the environment exporter plugin system, | ||
| # which captures both conda and pip-installed packages. | ||
| env = Environment.from_prefix( | ||
| str(base_prefix), name="base", platform=context.subdir | ||
| ) | ||
| exporter = context.plugin_manager.get_environment_exporter_by_format("yaml") | ||
| snapshot_file = ( | ||
| base_prefix | ||
| / "conda-meta" | ||
| / f"environment.{datetime.now():%Y-%m-%d-%H-%M-%S}.yml" | ||
| ) | ||
| if not context.quiet: | ||
| print(f"Saving snapshot to {snapshot_file}") | ||
| snapshot_file.write_text(exporter.export(env)) | ||
|
|
||
| if not context.quiet: | ||
| print(f"Cloning 'base' to '{default_env}'...") | ||
| print("Resetting 'base' environment...") | ||
|
|
||
| # Suppress conda's transaction spinner output when --quiet | ||
| stdout_ctx = redirect_stdout(io.StringIO()) if context.quiet else nullcontext() | ||
| with stdout_ctx: | ||
| clone_env( | ||
| str(base_prefix), | ||
| str(dest_prefix_data.prefix_path), | ||
| verbose=False, | ||
| quiet=True, | ||
| ) | ||
| reset(uninstallable_packages=uninstallable_packages) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will keep pip-installed packages in the new protected |
||
|
|
||
| # Freeze base | ||
| try: | ||
| frozen_path = base_prefix / PREFIX_FROZEN_FILE | ||
| frozen_path.write_text(json.dumps({"message": message}) if message else "") | ||
| except OSError as e: | ||
| raise CondaOSError(f"Could not protect environment: {e}") from e | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be nice to add an |
||
| # Update default activation environment | ||
| if not context.quiet: | ||
| print(f"Setting default environment to '{default_env}'") | ||
| with ConfigurationFile.from_user_condarc() as config: | ||
| config.set_key("default_activation_env", str(dest_prefix_data.prefix_path)) | ||
|
|
||
| if not context.quiet: | ||
| print(f"\nDone! To use your packages: conda activate {default_env}") | ||
| return 0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,38 @@ | ||
| """ | ||
| Plugin definition for 'conda self' subcommand. | ||
| """ | ||
| """Plugin hook implementations for conda-self.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from conda import plugins | ||
| from conda.plugins.hookspec import hookimpl | ||
| from conda.plugins.types import CondaHealthCheck, CondaSubcommand | ||
|
|
||
| from .cli import configure_parser, execute | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Iterable | ||
|
|
||
|
|
||
| @plugins.hookimpl | ||
| def conda_subcommands() -> Iterable[plugins.CondaSubcommand]: | ||
| yield plugins.CondaSubcommand( | ||
| @hookimpl | ||
| def conda_subcommands() -> Iterable[CondaSubcommand]: | ||
| """Expose the `self` subcommand.""" | ||
| yield CondaSubcommand( | ||
| name="self", | ||
| action=execute, | ||
| configure_parser=configure_parser, | ||
| summary="Manage your conda 'base' environment safely.", | ||
| ) | ||
|
|
||
|
|
||
| @hookimpl | ||
| def conda_health_checks() -> Iterable[CondaHealthCheck]: | ||
| """Register the base environment protection health check.""" | ||
| from .health_checks import base_protection | ||
|
|
||
| yield CondaHealthCheck( | ||
| name="base-protection", | ||
| action=base_protection.check, | ||
| fixer=base_protection.fix, | ||
| summary="Check if base is frozen to prevent accidental modifications", | ||
| fix="Clone base to 'default' environment, reset base, and freeze it", | ||
| ) |
Uh oh!
There was an error while loading. Please reload this page.