Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions example_projects/simple/cache/rv/git/cbbb6a550c
Submodule cbbb6a550c added at d5e89c
63 changes: 61 additions & 2 deletions src/cache/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::consts::BUILD_LOG_FILENAME;
use crate::lockfile::Source;
use crate::package::{BuiltinPackages, Package, get_builtin_versions_from_library};
use crate::system_req::get_system_requirements;
use crate::{RCmd, SystemInfo, Version};
use crate::{RCmd, Repository, SystemInfo, Version};

#[derive(Debug, Clone)]
pub struct PackagePaths {
Expand Down Expand Up @@ -72,7 +72,7 @@ pub struct DiskCache {
pub system_info: SystemInfo,
/// How long the compiled databases are considered fresh for, in seconds
/// Defaults to 3600s (1 hour)
packages_timeout: u64,
pub(crate) packages_timeout: u64,
// TODO: check if it's worth keeping a hashmap of repo_url -> encoded
// TODO: or if the overhead is the same as base64 directly
}
Expand Down Expand Up @@ -308,4 +308,63 @@ impl DiskCache {
sysreq
}
}

pub fn remove_repository<'a>(
&self,
repo: &'a Repository,
) -> Result<Option<PathBuf>, std::io::Error> {
let cache_path = self.root.join(hash_string(repo.url()));
if cache_path.exists() {
log::debug!("Removing {} at {}", repo.url(), cache_path.display());
fs::remove_dir_all(&cache_path)?;
Ok(Some(cache_path))
} else {
Ok(None)
}
}

pub fn remove_dependency<'a>(
&self,
name: &str,
version: &Version,
source: &Source,
) -> Result<(Option<PathBuf>, Option<PathBuf>), std::io::Error> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

maybe return a struct so we don't guess which path is the source and which one is the binary?

let mut res = (None, None);

// Builtins and Local not in cache and would lead to panic in get_package_paths
match source {
Source::Builtin { .. } | Source::Local { .. } => return Ok((None, None)),
_ => ()
}

let cache_path = self.get_package_paths(
&source,
Some(name),
Some(&version.original),
);

if cache_path.binary.exists() {
log::debug!(
"Removing binary {} ({}) at {}",
name,
version,
cache_path.binary.display()
);
fs::remove_dir_all(&cache_path.binary)?;
res.0 = Some(cache_path.binary);
}

if cache_path.source.exists() {
log::debug!(
"Removing source {} ({}) at {}",
name,
version,
cache_path.source.display()
);
fs::remove_dir_all(&cache_path.source)?;
res.1 = Some(cache_path.source);
}

Ok(res)
}
}
284 changes: 284 additions & 0 deletions src/cli/commands/clean_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
use core::fmt;
use std::{collections::HashMap, path::PathBuf};

use crate::{
Resolution, UnresolvedDependency, Version,
cli::{CliContext, context::load_databases},
hash_string,
package::PackageType,
};

use anyhow::Result;
use fs_err as fs;
use serde::Serialize;

/// Remove repositories and/or dependencies from the cache.
/// Dependencies only remove the package version from the resolved source
/// Repositories are aliases corresponding to repos in the config
pub fn purge_cache<'a>(
context: &'a CliContext,
resolution: &'a Resolution<'a>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need a resolution to purge things?

repositories: &'a [String],
dependencies: &'a [String],
) -> std::io::Result<PurgeResults<'a>> {
let mut repo_res = Vec::new();
for r in repositories {
let res = if let Some(repo) = context
.config
.repositories()
.iter()
.find(|repo| &repo.alias == r)
{
let path = context.cache.root.join(hash_string(repo.url()));
if path.exists() {
fs::remove_dir_all(&path)?;
PurgeRepoResult::Removed {
alias: &repo.alias,
url: repo.url(),
path,
}
} else {
PurgeRepoResult::NotInCache {
alias: &repo.alias,
url: repo.url(),
}
}
} else {
PurgeRepoResult::NotInProject(&r)
};
repo_res.push(res);
}

let mut dep_res = Vec::new();
for d in dependencies {
let res = if let Some(dep) = resolution.found.iter().find(|r| &r.name == d) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

if people pass a dep name, don't they want to purge all the versions of that dep?

let (binary_path, source_path) = context.cache.remove_dependency(&dep.name, &dep.version, &dep.source)?;

let mut paths = HashMap::new();
if let Some(bin_path) = binary_path {
paths.insert(PackageType::Binary, bin_path);
}
if let Some(src_path) = source_path {
paths.insert(PackageType::Source, src_path);
}

if paths.is_empty() {
PurgeDepResult::NotInCache {
name: &dep.name,
version: &dep.version,
}
} else {
PurgeDepResult::Removed {
name: &dep.name,
version: &dep.version,
paths,
}
}
} else if let Some(dep) = resolution.failed.iter().find(|r| &r.name == d) {
PurgeDepResult::Unresolved(dep)
} else {
PurgeDepResult::NotInProject(d)
};
dep_res.push(res);
}

Ok(PurgeResults {
repositories: repo_res,
dependencies: dep_res,
})
}

#[derive(Debug, Serialize)]
pub struct PurgeResults<'a> {
repositories: Vec<PurgeRepoResult<'a>>,
dependencies: Vec<PurgeDepResult<'a>>,
}

impl PurgeResults<'_> {
pub fn all_deps_found(&self) -> bool {
self.dependencies
.iter()
.all(|dep| !matches!(dep, PurgeDepResult::Unresolved(_)))
}
}

impl fmt::Display for PurgeResults<'_> {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Decided to leave paths out of the displayed results as it is not what a user would typically want, but recorded them and included as part of the json to leave them accessible if someone needs them.

fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.repositories.is_empty() {
write!(f, "== Repositories ==\n")?;
let mut not_removed = Vec::new();
let mut removed = Vec::new();
for repo in &self.repositories {
match repo {
PurgeRepoResult::NotInProject(alias) => not_removed.push(format!(
"{alias} - Repository alias not found in config file"
)),
PurgeRepoResult::Removed { alias, url, .. } => {
removed.push(format!("{alias} ({url})"));
}
PurgeRepoResult::NotInCache { alias, url } => {
not_removed.push(format!("{alias} ({url}) - Repository not found in cache"))
}
}
}
if !removed.is_empty() {
write!(
f,
"Removed Successfully:\n {}\n\n",
removed.join("\n ")
)?;
}
if !not_removed.is_empty() {
write!(f, "Not Removed:\n {}\n\n", not_removed.join("\n "))?;
}
}

if !self.dependencies.is_empty() {
write!(f, "== Dependencies ==\n")?;
let mut not_removed = Vec::new();
let mut removed = Vec::new();
let mut unresolved = Vec::new();
for dep in &self.dependencies {
match dep {
PurgeDepResult::Unresolved(dep) => {
unresolved.push(dep.to_string());
not_removed.push(format!("{} - Package could not be resolved", dep.name,));
}
PurgeDepResult::Removed {
name,
version,
paths,
} => {
let mut types = paths.keys().map(ToString::to_string).collect::<Vec<_>>();
types.sort();

removed.push(format!("{name} ({version}) - {}", types.join(" and ")))
}
PurgeDepResult::NotInCache { name, version } => not_removed.push(format!(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Deps that are determined to be "NotInCache" are packages that would be in cache, but haven't been synced yet. I'm a bit unsure if I should list it as "removed" or "not removed". The pros/cons I see:

  • Removed - For the sake of the user, the package not being in the cache before the purge is the same result as if it was removed, so we should display it as such
  • Not Removed - Factual and communicates that if you're having issues with this package its not because of the cache

Ultimately, this scenario likely won't appear that often because if you're having issues with a package, its likely in the cache and you're going to target it specifically, but wanted to call it out.

"{name} ({version}) - Dependency not found in cache"
)),
PurgeDepResult::NotInProject(name) => not_removed
.push(format!("{name} - Package not part of project dependencies")),
}
}

if !removed.is_empty() {
write!(
f,
"Removed Successfully:\n {}\n\n",
removed.join("\n ")
)?;
}
if !not_removed.is_empty() {
write!(f, "Not Removed:\n {}\n\n", not_removed.join("\n "))?;
}
if !unresolved.is_empty() {
write!(
f,
"Failed to resolve all dependencies. Packages may not have been purged due to the following resolution issues:\n {}\n\n",
unresolved.join("\n ")
)?;
}
}

Ok(())
}
}

#[derive(Debug, Serialize)]
enum PurgeRepoResult<'a> {
Removed {
alias: &'a str,
url: &'a str,
path: PathBuf,
},
/// Alias is not in config file
NotInProject(&'a str),
/// Repository not in cache (nothing to remove)
NotInCache { alias: &'a str, url: &'a str },
}

#[derive(Debug, Clone, Serialize, PartialEq)]
enum PurgeDepResult<'a> {
/// Package is part of the unresolved dependencies
Unresolved(&'a UnresolvedDependency<'a>),
Removed {
name: &'a str,
version: &'a Version,
paths: HashMap<PackageType, PathBuf>,
},
/// Dependency not part of the resolved dependency burden
NotInProject(&'a str),
/// Dependency resolved, but not in cache (nothing to remove)
NotInCache { name: &'a str, version: &'a Version },
}

/// refresh the repository database by invalidating the packages.bin and re-loading it
/// returns a list of repositories that were refreshed and a list of repos that could not be found in the config
pub fn refresh_cache<'a>(
context: &'a CliContext,
repositories: &'a [String],
) -> Result<(Vec<RefreshedRepo<'a>>, Vec<&'a str>)> {
let mut cache = context.cache.clone();
// need to set cache timeout to refresh the databases for the found repositories
cache.packages_timeout = 0;

// if no repositories supplied, we'll refresh all repos listed in the config
let res = if repositories.is_empty() {
let res = context
.config
.repositories()
.iter()
.map(|repo| RefreshedRepo {
alias: &repo.alias,
url: repo.url(),
path: cache.get_package_db_entry(repo.url()).0,
})
.collect::<Vec<_>>();

load_databases(context.config.repositories(), &cache)?;
(res, Vec::new())
} else {
let mut repos = Vec::new();
let mut refreshed = Vec::new();
// unresolved meaning that we can't find a corresponding entry in the config
let mut unresolved = Vec::new();

for r in repositories {
if let Some(repo) = context
.config
.repositories()
.iter()
.find(|repo| &repo.alias == r)
{
repos.push(repo.clone());
let (path, _) = cache.get_package_db_entry(repo.url());
refreshed.push(RefreshedRepo {
alias: &repo.alias,
url: repo.url(),
path,
});
} else {
unresolved.push(r.as_str());
}
}

load_databases(&repos, &cache)?;
(refreshed, unresolved)
};

Ok(res)
}

#[derive(Debug, Clone, Serialize)]
pub struct RefreshedRepo<'a> {
alias: &'a str,
url: &'a str,
path: PathBuf,
}

impl fmt::Display for RefreshedRepo<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.alias, self.url)
}
}
2 changes: 2 additions & 0 deletions src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod clean_cache;
mod init;
mod migrate;
mod tree;

pub use clean_cache::{purge_cache, refresh_cache};
pub use init::{find_r_repositories, init, init_structure};
pub use migrate::migrate_renv;
pub use tree::tree;
3 changes: 2 additions & 1 deletion src/cli/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ pub struct CliContext {
}

impl CliContext {
pub fn new(config_file: &PathBuf, r_command_lookup: RCommandLookup) -> Result<Self> {
pub fn new(config_file: impl AsRef<Path>, r_command_lookup: RCommandLookup) -> Result<Self> {
let config_file = config_file.as_ref();
let config = Config::from_file(config_file)?;

// This can only be set to false if the user passed a r_version to rv plan
Expand Down
Loading
Loading