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

feat: add pixi clean command #1325

Merged
merged 10 commits into from
Jun 11, 2024
29 changes: 29 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,35 @@ More information [here](../advanced/explain_info_command.md).
pixi info
pixi info --json --extended
```
## `clean`

Clean the parts of your system which are touched by pixi.
Defaults to cleaning the environments and task cache.
Use the `cache` subcommand to clean the cache

##### Options
- `--manifest-path <MANIFEST_PATH>`: the path to [manifest file](project_configuration.md), by default it searches for one in the parent directories.
- `--environment <ENVIRONMENT> (-e)`: The environment to clean, if none are provided all environments will be removed.

```shell
pixi clean
```

### `clean cache`

Clean the pixi cache on your system.

##### Options
- `--pypi`: Clean the pypi cache.
- `--conda`: Clean the conda cache.
- `--yes`: Skip the confirmation prompt.

```shell
pixi clean cache # clean all pixi caches
pixi clean cache --pypi # clean only the pypi cache
pixi clean cache --conda # clean only the conda cache
pixi clean cache --yes # skip the confirmation prompt
```

## `upload`

Expand Down
170 changes: 170 additions & 0 deletions src/cli/clean.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/// Command to clean the parts of your system which are touched by pixi.
use crate::{config, consts, EnvironmentName, Project};
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;

use crate::progress::{global_multi_progress, long_running_progress_style};
use clap::Parser;
use indicatif::ProgressBar;
use miette::IntoDiagnostic;

#[derive(Parser, Debug)]
#[clap(group(clap::ArgGroup::new("command")))]
pub enum Command {
#[clap(name = "cache")]
Cache(CacheArgs),
}

/// Clean the parts of your system which are touched by pixi.
/// Defaults to cleaning the environments and task cache.
/// Use the `cache` subcommand to clean the cache.
#[derive(Parser, Debug)]
pub struct Args {
#[command(subcommand)]
command: Option<Command>,
/// The path to 'pixi.toml' or 'pyproject.toml'
#[arg(long)]
pub manifest_path: Option<PathBuf>,

/// The environment directory to remove.
#[arg(long, short, conflicts_with = "command")]
pub environment: Option<String>,
}

/// Clean the cache of your system which are touched by pixi.
#[derive(Parser, Debug)]
pub struct CacheArgs {
/// Clean only the pypi related cache.
#[arg(long)]
pub pypi: bool,

/// Clean only the conda related cache.
#[arg(long)]
pub conda: bool,

/// Answer yes to all questions.
#[arg(long)]
pub yes: bool,
// TODO: Would be amazing to have a --unused flag to clean only the unused cache.
ruben-arts marked this conversation as resolved.
Show resolved Hide resolved
// By searching the inode count of the packages and removing based on that.
// #[arg(long)]
// pub unused: bool,
}

pub async fn execute(args: Args) -> miette::Result<()> {
match args.command {
Some(Command::Cache(args)) => clean_cache(args).await?,
None => {
let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; // Extract the passed in environment name.

let explicit_environment = args
.environment
.map(|n| EnvironmentName::from_str(n.as_str()))
.transpose()?
.map(|n| {
project.environment(&n).ok_or_else(|| {
miette::miette!(
"unknown environment '{n}' in {}",
project
.manifest_path()
.to_str()
.expect("expected to have a manifest_path")
)
})
})
.transpose()?;

if let Some(explicit_env) = explicit_environment {
remove_folder_with_progress(explicit_env.dir(), true).await?;
tracing::info!("Skipping removal of task cache and solve group environments for explicit environment '{:?}'", explicit_env.name());
ruben-arts marked this conversation as resolved.
Show resolved Hide resolved
} else {
// Remove all pixi related work from the project.
if !project.environments_dir().starts_with(project.pixi_dir())
&& project.default_environments_dir().exists()
{
remove_folder_with_progress(project.default_environments_dir(), false).await?;
remove_folder_with_progress(
project.default_solve_group_environments_dir(),
false,
)
.await?;
remove_folder_with_progress(project.task_cache_folder(), false).await?;
}
remove_folder_with_progress(project.environments_dir(), true).await?;
remove_folder_with_progress(project.solve_group_environments_dir(), false).await?;
remove_folder_with_progress(project.task_cache_folder(), false).await?;
}

Project::warn_on_discovered_from_env(args.manifest_path.as_deref())
}
}
Ok(())
}

/// Clean the pixi cache folders.
async fn clean_cache(args: CacheArgs) -> miette::Result<()> {
let cache_dir = config::get_cache_dir()?;
let mut dirs = vec![];

if args.pypi {
dirs.push(cache_dir.join(consts::PYPI_CACHE_DIR));
}
if args.conda {
dirs.push(cache_dir.join("pkgs"));
}
if dirs.is_empty() && (args.yes || dialoguer::Confirm::new()
.with_prompt("No cache types specified using the flags.\nDo you really want to remove all cache directories from your machine?")
.interact_opt()
.into_diagnostic()?
.unwrap_or(false))
{
dirs.push(cache_dir);
}

if dirs.is_empty() {
eprintln!("{}", console::style("Nothing to remove.").green());
return Ok(());
}

for dir in dirs {
remove_folder_with_progress(dir, true).await?;
}
Ok(())
}

async fn remove_folder_with_progress(
folder: PathBuf,
warning_non_existent: bool,
) -> miette::Result<()> {
if !folder.exists() {
if warning_non_existent {
eprintln!(
"{}",
console::style(format!("Folder {:?} was already clean.", &folder)).yellow()
);
}
return Ok(());
}
let pb = global_multi_progress().add(ProgressBar::new_spinner());
pb.enable_steady_tick(Duration::from_millis(100));
pb.set_style(long_running_progress_style());
pb.set_message(format!(
"{} {}",
console::style("Removing").green(),
folder.clone().display()
));

// Ignore errors
let result = tokio::fs::remove_dir_all(&folder).await;
if let Err(e) = result {
tracing::info!("Failed to remove folder {:?}: {}", folder, e);
}

pb.finish_with_message(format!(
"{} {}",
console::style("removed").green(),
folder.display()
));
Ok(())
}
4 changes: 3 additions & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use super::util::IndicatifWriter;
use crate::{progress, progress::global_multi_progress};

pub mod add;
pub mod clean;
pub mod completion;
pub mod config;
pub mod global;
Expand Down Expand Up @@ -93,7 +94,6 @@ pub enum Command {
Install(install::Args),
Update(update::Args),

// Execution commands
#[clap(visible_alias = "r")]
Run(run::Args),
#[clap(visible_alias = "s")]
Expand All @@ -119,6 +119,7 @@ pub enum Command {
Upload(upload::Args),
Search(search::Args),
SelfUpdate(self_update::Args),
Clean(clean::Args),
Completion(completion::Args),
}

Expand Down Expand Up @@ -269,6 +270,7 @@ pub async fn execute_command(command: Command) -> miette::Result<()> {
Command::Config(cmd) => config::execute(cmd).await,
Command::Init(cmd) => init::execute(cmd).await,
Command::Add(cmd) => add::execute(cmd).await,
Command::Clean(cmd) => clean::execute(cmd).await,
Command::Run(cmd) => run::execute(cmd).await,
Command::Global(cmd) => global::execute(cmd).await,
Command::Auth(cmd) => rattler::cli::auth::execute(cmd).await.into_diagnostic(),
Expand Down
1 change: 1 addition & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub const SOLVE_GROUP_ENVIRONMENTS_DIR: &str = "solve-group-envs";
pub const PYPI_DEPENDENCIES: &str = "pypi-dependencies";
pub const TASK_CACHE_DIR: &str = "task-cache-v0";
pub const PIXI_UV_INSTALLER: &str = "uv-pixi";
pub const PYPI_CACHE_DIR: &str = "uv-cache";
pub const CONDA_INSTALLER: &str = "conda";

pub const ONE_TIME_MESSAGES_DIR: &str = "one-time-messages";
Expand Down
4 changes: 2 additions & 2 deletions src/lock_file/resolve/uv_resolution_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use uv_types::{HashStrategy, InFlight};

use crate::{
config::{self, get_cache_dir},
Project,
consts, Project,
};

/// Objects that are needed for resolutions which can be shared between different resolutions.
Expand All @@ -25,7 +25,7 @@ pub struct UvResolutionContext {

impl UvResolutionContext {
pub fn from_project(project: &Project) -> miette::Result<Self> {
let uv_cache = get_cache_dir()?.join("uv-cache");
let uv_cache = get_cache_dir()?.join(consts::PYPI_CACHE_DIR);
if !uv_cache.exists() {
std::fs::create_dir_all(&uv_cache)
.into_diagnostic()
Expand Down
33 changes: 23 additions & 10 deletions src/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ impl Project {
&self.root
}

/// Returns the pixi directory
/// Returns the pixi directory of the project [consts::PIXI_DIR]
pub fn pixi_dir(&self) -> PathBuf {
self.root.join(consts::PIXI_DIR)
}
Expand All @@ -323,9 +323,14 @@ impl Project {
}
}

/// Returns the default environment directory without interacting with config.
pub fn default_environments_dir(&self) -> PathBuf {
self.pixi_dir().join(consts::ENVIRONMENTS_DIR)
}

/// Returns the environment directory
pub fn environments_dir(&self) -> PathBuf {
let default_envs_dir = self.pixi_dir().join(consts::ENVIRONMENTS_DIR);
let default_envs_dir = self.default_environments_dir();

// Early out if detached-environments is not set
if self.config().detached_environments().is_false() {
Expand Down Expand Up @@ -367,22 +372,28 @@ impl Project {
default_envs_dir
}

/// Returns the default solve group environments directory, without interacting with config
pub fn default_solve_group_environments_dir(&self) -> PathBuf {
self.default_environments_dir()
.join(consts::SOLVE_GROUP_ENVIRONMENTS_DIR)
}

/// Returns the solve group environments directory
pub fn solve_group_environments_dir(&self) -> PathBuf {
// If the detached-environments path is set, use it instead of the default
// directory.
if let Some(detached_environments_path) = self.detached_environments_path() {
return detached_environments_path.join(consts::SOLVE_GROUP_ENVIRONMENTS_DIR);
}
self.pixi_dir().join(consts::SOLVE_GROUP_ENVIRONMENTS_DIR)
self.default_solve_group_environments_dir()
}

/// Returns the path to the manifest file.
pub fn manifest_path(&self) -> PathBuf {
self.manifest.path.clone()
}

/// Returns the path to the lock file of the project
/// Returns the path to the lock file of the project [consts::PROJECT_LOCK_FILE]
pub fn lock_file_path(&self) -> PathBuf {
self.root.join(consts::PROJECT_LOCK_FILE)
}
Expand Down Expand Up @@ -578,12 +589,14 @@ fn create_symlink(target_dir: &Path, symlink_dir: &Path) {

symlink(target_dir, symlink_dir)
.map_err(|e| {
tracing::error!(
"Failed to create symlink from '{}' to '{}': {}",
target_dir.display(),
symlink_dir.display(),
e
)
if e.kind() != std::io::ErrorKind::AlreadyExists {
tracing::error!(
"Failed to create symlink from '{}' to '{}': {}",
target_dir.display(),
symlink_dir.display(),
e
)
}
})
.ok();
}
Expand Down
Loading