Skip to content

Commit

Permalink
feat: add pixi clean command (#1325)
Browse files Browse the repository at this point in the history
- `pixi clean` cleans environments
- `pixi clean -e prod` cleans a environment
- `pixi clean cache` cleans cache dir
- `pixi clean cache --conda/--pypi` cleans a specific package cache

fixes: #1321
  • Loading branch information
ruben-arts authored Jun 11, 2024
1 parent debf20e commit b5bb774
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 13 deletions.
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.
// 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());
} 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 @@ -265,6 +266,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

0 comments on commit b5bb774

Please sign in to comment.