use std::collections::BTreeMap;
use xshell::Shell;
use crate::environment::{get_workspace_root, ProgressGuard, WorkspaceManifest};
enum ToolsLocation {
Workspace,
Package,
}
struct Tools {
map: BTreeMap<String, String>,
location: ToolsLocation,
}
impl Tools {
fn table_name(&self) -> &'static str {
match self.location {
ToolsLocation::Workspace => "[workspace.metadata.rbmt.tools]",
ToolsLocation::Package => "[package.metadata.rbmt.tools]",
}
}
}
#[derive(serde::Deserialize, Default)]
struct RbmtTable {
tools: Option<BTreeMap<String, String>>,
}
fn read_tools(sh: &Shell) -> Result<Option<Tools>, Box<dyn std::error::Error>> {
let root = get_workspace_root(sh)?;
let contents = std::fs::read_to_string(root.join("Cargo.toml"))?;
let cargo_toml = toml::from_str::<WorkspaceManifest<RbmtTable>>(&contents)?;
if let Some(map) = cargo_toml.workspace.metadata.rbmt.tools {
return Ok(Some(Tools { map, location: ToolsLocation::Workspace }));
}
if let Some(map) = cargo_toml.package.metadata.rbmt.tools {
return Ok(Some(Tools { map, location: ToolsLocation::Package }));
}
Ok(None)
}
fn write_tool_version(
sh: &Shell,
name: &str,
version: &str,
location: &ToolsLocation,
) -> Result<(), Box<dyn std::error::Error>> {
let root = get_workspace_root(sh)?;
let path = root.join("Cargo.toml");
let contents = std::fs::read_to_string(&path)?;
let mut doc: toml_edit::DocumentMut = contents.parse()?;
let table = match location {
ToolsLocation::Workspace => &mut doc["workspace"]["metadata"]["rbmt"]["tools"],
ToolsLocation::Package => &mut doc["package"]["metadata"]["rbmt"]["tools"],
};
table[name] = toml_edit::value(version);
std::fs::write(&path, doc.to_string())?;
Ok(())
}
fn installed_version(
sh: &Shell,
crate_name: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let output = rbmt_cmd!(sh, "cargo install --list").read()?;
let prefix = format!("{} v", crate_name);
let version = output
.lines()
.find(|line| line.starts_with(&prefix))
.and_then(|line| line.strip_prefix(&prefix))
.and_then(|rest| rest.split([' ', ':']).next())
.map(str::to_string);
Ok(version)
}
fn install_tool(sh: &Shell, name: &str, version: &str) -> Result<(), Box<dyn std::error::Error>> {
rbmt_eprintln!("Installing {}@{}", name, version);
rbmt_cmd!(sh, "cargo install {name} --version {version} --locked").run()?;
Ok(())
}
fn install_tool_latest(sh: &Shell, name: &str) -> Result<String, Box<dyn std::error::Error>> {
rbmt_eprintln!("Installing {} (latest)", name);
rbmt_cmd!(sh, "cargo install {name}").run()?;
installed_version(sh, name)?
.ok_or_else(|| format!("{} not found in `cargo install --list` after install", name).into())
}
pub fn run(sh: &Shell, update: bool, filter: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let _progress = ProgressGuard::new();
rbmt_eprintln!("Installing tools...");
let Some(mut tools) = read_tools(sh)? else {
rbmt_eprintln!(
"No tools found in [workspace.metadata.rbmt.tools] or [package.metadata.rbmt.tools]."
);
return Ok(());
};
if !filter.is_empty() {
for name in filter {
if !tools.map.contains_key(name) {
return Err(format!("'{}' is not in {}", name, tools.table_name()).into());
}
}
tools.map.retain(|name, _| filter.contains(name));
}
for (name, pinned_version) in &tools.map {
if update {
let latest = install_tool_latest(sh, name)?;
if &latest == pinned_version {
rbmt_eprintln!("{} is already at latest ({})", name, pinned_version);
} else {
rbmt_eprintln!("Updated {} {} -> {}", name, pinned_version, latest);
write_tool_version(sh, name, &latest, &tools.location)?;
}
} else {
install_tool(sh, name, pinned_version)?;
}
}
Ok(())
}