use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::tool::ToolKind;
use crate::types::InstalledTool;
const DEFAULT_INSTALL_ROOT: &str = r"D:\Embedded_Toolchain";
const LEGACY_INSTALLS_DIR_NAME: &str = "tools";
pub fn default_install_root() -> PathBuf {
PathBuf::from(DEFAULT_INSTALL_ROOT)
}
pub fn tool_versions_dir(root: &Path, kind: ToolKind) -> PathBuf {
root.join(kind.id())
}
pub fn tool_version_dir(root: &Path, kind: ToolKind, version: &str) -> PathBuf {
tool_versions_dir(root, kind).join(version)
}
pub fn legacy_tool_versions_dir(root: &Path, kind: ToolKind) -> PathBuf {
root.join(LEGACY_INSTALLS_DIR_NAME).join(kind.id())
}
pub fn cleanup_staging_dirs(root: &Path) -> Result<Vec<PathBuf>> {
let mut removed = Vec::new();
for kind in ToolKind::all() {
let kind_root = tool_versions_dir(root, kind);
if !kind_root.exists() {
continue;
}
for entry in fs::read_dir(&kind_root)
.with_context(|| format!("failed to read {}", kind_root.display()))?
{
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with(".staging-") {
fs::remove_dir_all(&path)
.with_context(|| format!("failed to remove {}", path.display()))?;
removed.push(path);
}
}
}
Ok(removed)
}
pub fn cleanup_old_tool_versions(
root: &Path,
keep_versions: &[(ToolKind, String)],
) -> Result<Vec<PathBuf>> {
let mut removed = Vec::new();
for (kind, keep_version) in keep_versions {
let kind_root = tool_versions_dir(root, *kind);
if !kind_root.exists() {
continue;
}
for entry in fs::read_dir(&kind_root)
.with_context(|| format!("failed to read {}", kind_root.display()))?
{
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let version = entry.file_name().to_string_lossy().to_string();
if version == *keep_version || version.starts_with(".staging-") {
continue;
}
let path = entry.path();
fs::remove_dir_all(&path)
.with_context(|| format!("failed to remove {}", path.display()))?;
removed.push(path);
}
}
removed.sort();
Ok(removed)
}
pub fn discover_installed_tools(root: &Path) -> Result<Vec<InstalledTool>> {
let mut tools = Vec::new();
for kind in ToolKind::all() {
if let Some(tool) = discover_installed_tool(root, kind)? {
tools.push(tool);
}
}
tools.sort_by_key(|tool| tool.kind.id().to_string());
Ok(tools)
}
pub fn installed_tool_from_dir(
kind: ToolKind,
version: impl Into<String>,
install_dir: PathBuf,
) -> Result<InstalledTool> {
let executable_path =
find_file_named(&install_dir, kind.executable_names()).with_context(|| {
format!(
"failed to locate {} in {}",
kind.id(),
install_dir.display()
)
})?;
let executable_dir = executable_path
.parent()
.context("downloaded executable did not have a parent directory")?
.to_path_buf();
Ok(InstalledTool {
kind,
version: version.into(),
executable_path,
executable_dir,
})
}
fn discover_installed_tool(root: &Path, kind: ToolKind) -> Result<Option<InstalledTool>> {
let kind_root = tool_versions_dir(root, kind);
if !kind_root.exists() {
return Ok(None);
}
let mut candidates = Vec::new();
for entry in fs::read_dir(&kind_root)
.with_context(|| format!("failed to read {}", kind_root.display()))?
{
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let version = entry.file_name().to_string_lossy().to_string();
if version.starts_with(".staging-") {
continue;
}
let modified = entry
.metadata()
.and_then(|metadata| metadata.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
let install_dir = entry.path();
if let Ok(tool) = installed_tool_from_dir(kind, version.clone(), install_dir) {
candidates.push((modified, version, tool));
}
}
candidates.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| right.1.cmp(&left.1)));
Ok(candidates.into_iter().next().map(|(_, _, tool)| tool))
}
fn find_file_named(root: &Path, candidates: &[&str]) -> Result<PathBuf> {
let mut stack = vec![root.to_path_buf()];
while let Some(directory) = stack.pop() {
for entry in fs::read_dir(&directory)
.with_context(|| format!("failed to read {}", directory.display()))?
{
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
if file_type.is_dir() {
stack.push(path);
continue;
}
let file_name = entry.file_name().to_string_lossy().to_string();
if candidates
.iter()
.any(|candidate| file_name.eq_ignore_ascii_case(candidate))
{
return Ok(path);
}
}
}
anyhow::bail!(
"none of {:?} were found below {}",
candidates,
root.display()
)
}
#[cfg(test)]
mod tests {
use super::{
cleanup_old_tool_versions, default_install_root, discover_installed_tools, tool_version_dir,
};
use crate::tool::ToolKind;
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn default_install_root_uses_embedded_toolchain_directory() {
assert_eq!(
default_install_root(),
PathBuf::from(r"D:\Embedded_Toolchain")
);
}
#[test]
fn tool_versions_live_directly_under_the_install_root() {
assert_eq!(
tool_version_dir(
&PathBuf::from(r"D:\Embedded_Toolchain"),
ToolKind::Ninja,
"1.12.0"
),
PathBuf::from(r"D:\Embedded_Toolchain\ninja\1.12.0")
);
}
#[test]
fn discover_installed_tools_reads_tools_from_the_install_root() {
let temp = tempdir().unwrap();
let install_dir = temp.path().join("ninja").join("1.12.0");
fs::create_dir_all(&install_dir).unwrap();
fs::write(install_dir.join("ninja.exe"), b"").unwrap();
fs::create_dir_all(temp.path().join("ninja").join(".staging-1.13.0")).unwrap();
let tools = discover_installed_tools(temp.path()).unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].kind, ToolKind::Ninja);
assert_eq!(tools[0].version, "1.12.0");
assert_eq!(tools[0].executable_path, install_dir.join("ninja.exe"));
}
#[test]
fn discover_installed_tools_reads_probe_rs() {
let temp = tempdir().unwrap();
let install_dir = temp.path().join("probe-rs").join("0.31.0");
fs::create_dir_all(&install_dir).unwrap();
fs::write(install_dir.join("probe-rs.exe"), b"").unwrap();
let tools = discover_installed_tools(temp.path()).unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].kind, ToolKind::ProbeRs);
assert_eq!(tools[0].version, "0.31.0");
assert_eq!(tools[0].executable_path, install_dir.join("probe-rs.exe"));
}
#[test]
fn cleanup_old_tool_versions_keeps_selected_version() {
let temp = tempdir().unwrap();
let old_dir = temp.path().join("ninja").join("1.12.0");
let keep_dir = temp.path().join("ninja").join("1.13.2");
fs::create_dir_all(&old_dir).unwrap();
fs::create_dir_all(&keep_dir).unwrap();
fs::create_dir_all(temp.path().join("ninja").join(".staging-1.14.0")).unwrap();
let removed =
cleanup_old_tool_versions(temp.path(), &[(ToolKind::Ninja, "1.13.2".to_string())])
.unwrap();
assert_eq!(removed, vec![old_dir.clone()]);
assert!(!old_dir.exists());
assert!(keep_dir.exists());
assert!(temp.path().join("ninja").join(".staging-1.14.0").exists());
}
}