Skip to content
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

Add uninstall subcommand #112

Open
wants to merge 64 commits into
base: main
Choose a base branch
from
Open

Conversation

marcoesters
Copy link
Contributor

@marcoesters marcoesters commented Nov 13, 2024

Description

Uninstalling distributions for conda currently only has incomplete solutions with files and broken auto-run/initializer scripts left behind (see conda/constructor#642, conda/constructor#588, and conda/constructor#572). A clean uninstallation is currently not available without manually running a series of commands that are still not 100% safe - e.g., conda init --reverse --all will remove initialization of conda, no matter what installation it is pointed to.

This PR introduces a cross-platform uninstallation subcommand that uses conda-internal features to perform the following tasks:

  • Uninstall all environments within a base environment or environments directory (such as a directory set by envs_dirs). This uninstalls all shortcuts, performs pre-unlink actions, and de-registers the environments from the environments.txt file.
  • Runs conda init reverse if a configuration file points to any of the environments that are removed.
  • Allows a user to remove .condarc files, either for the user, system-wide, or all.
  • Allows the user to run conda clean --all to remove package caches outside the installation directory.
  • Allows the user to remove cache directories, including the .conda file.

This is implemented in conda-standalone so that a single binary can be used on macOS and Linux. It also allows decoupling of the installer "front end" (built by constructor) from the installer "back end" (such as conda-standalone or micromamba), as outlined in the following issue: conda/constructor#549

Known gap: Soft links are currently not well supported: files are unlinked, but no efforts are made to ensure that this doesn't leave unused files behind.

Checklist - did you ...

  • Add a file to the news directory (using the template) for the next release's release notes?
  • Add / update necessary tests?
  • Add / update outdated documentation?

src/entry_point.py Outdated Show resolved Hide resolved
@@ -110,7 +116,7 @@ def __call__(self, parser, namespace, values, option_string=None):
f"Defaults to {DEFAULT_NUM_PROCESSORS}.",
)

g = p.add_mutually_exclusive_group(required=True)
g = p.add_mutually_exclusive_group()
Copy link
Collaborator

@jaimergp jaimergp Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to add checks to make this required manually if uninstall is not in the ARGV. Maybe an else clause in _constructor_main() (line 602, I think?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added that into the same function and created tests to check.

# See: https://github.com/conda/conda/blob/475e6acbdc98122fcbef4733eb8cb8689324c1c8/conda/gateways/disk/create.py#L482-L488 # noqa
ENVS_DIR_MAGIC_FILE = ".conda_envs_dir_test"

uninstall_prefix = Path(uninstall_dir).expanduser().resolve()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to os.path.expandvars too or is that not expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I added it to where we parse args.prefix.

_remove_file_directory(parent)
parent = parent.parent

def _remove_config_file_and_parents(file: Path):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's precedent for users not liking this default behaviour, see conda/conda#13113. Not sure if we should delete empty parents recursively 🤔 Maybe just the config-containing one? Happy to have my mind changed here, though, so let me know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I was intending with rootdir, but it was still a little too aggressive. I changed the selection of which directories to remove. The only thing that might be controversial is the ~/.config directory. I think it's okay to delete if empty. I would at least argue that we should delete it on Windows where configs are stored in different places.

Comment on lines +465 to +466
run_plan(plan)
run_plan_elevated(plan)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, these functions do not raise errors? They just write to plan?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, that's correct. There doesn't seem to be a "failed" state in the Results enum either.

I am printing the plan results at least so that users can check if they don't see the results they expect.

continue
expected_files = [pkgs_dir / "urls", pkgs_dir / "urls.txt"]
if all(file in expected_files for file in pkgs_dir.iterdir()):
_remove_file_and_parents(pkgs_dir)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here about parents (and elsewhere), I don't think we need to recursively remove all parents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can get on board for the package cache, but for the binstar directory and the notices cache, we should remove the parents:

  • The anaconda-client token is either in a $BINSTAR_TOKEN_DIR/data or a binstar/ContinuumIO (Windows) or binstart directory and is created by conda via anaconda-client. I think we can remove the binstar directories at least. To be honest, the best solution would be to make anaconda-client a plug-in handle that directory when running conda remove. Right now, however, conda is the API that's creating that directory, so I decided to handle it here.
  • The notices cache is written into a specific conda cache directory, so we will not remove a user-created directory. Unfortunately, the subdirectories are different for Windows and Unix, but they are all called conda. So, I changed it to _remove_config_file_and_parents here.

README.md Outdated
> This can cause files to be left behind.

> [!WARNING]
> Support for softlinks is still limited.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate? Are errors expected, or undefined behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only unlink softlinks and do not do anything with the underlying files. I made that more explicit.

),
)
uninstall_subcommand.add_argument(
"--remove-caches",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"--remove-caches",
"--remove-additional-caches",

The term is a bit convoluted with pkgs/ and pkgs/cache being taken care of in conda-clean, but this argument targets ~/.conda and anaconda-client stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unfortunately, conda overloads the term "cache". I changed it to --remove-conda-caches because it's more descriptive than --remove-additional-caches.

src/entry_point.py Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla-signed [bot] added once the contributor has signed the CLA
Projects
Status: 🆕 New
Development

Successfully merging this pull request may close these issues.

3 participants