use std::path::{Path, PathBuf};
use std::{env, fs};
use xshell::Shell;
const LOG_LEVEL_ENV_VAR: &str = "RBMT_LOG_LEVEL";
#[derive(Clone, Debug)]
pub struct Package {
pub name: String,
pub dir: PathBuf,
pub id: String,
}
pub fn is_quiet_mode() -> bool { env::var(LOG_LEVEL_ENV_VAR).is_ok_and(|v| v == "quiet") }
#[macro_export]
macro_rules! quiet_cmd {
($sh:expr, $($arg:tt)*) => {{
let mut cmd = xshell::cmd!($sh, $($arg)*);
if $crate::environment::is_quiet_mode() {
cmd = cmd.quiet();
}
cmd
}};
}
pub fn quiet_println(msg: &str) {
if !is_quiet_mode() {
eprintln!("{}", msg);
}
}
pub fn configure_log_level(sh: &Shell) {
if is_quiet_mode() {
sh.set_var("CARGO_TERM_VERBOSE", "false");
sh.set_var("CARGO_TERM_QUIET", "true");
} else {
sh.set_var("CARGO_TERM_VERBOSE", "true");
sh.set_var("CARGO_TERM_QUIET", "false");
}
}
pub fn get_packages(
sh: &Shell,
packages: &[String],
) -> Result<Vec<Package>, Box<dyn std::error::Error>> {
let metadata = quiet_cmd!(sh, "cargo metadata --no-deps --format-version 1").read()?;
let json: serde_json::Value = serde_json::from_str(&metadata)?;
let all_packages: Vec<Package> = json["packages"]
.as_array()
.ok_or("Missing 'packages' field in cargo metadata")?
.iter()
.filter_map(|package| {
Some(Package {
name: package["name"].as_str()?.to_string(),
dir: PathBuf::from(
package["manifest_path"].as_str()?.trim_end_matches("/Cargo.toml"),
),
id: package["id"].as_str()?.to_string(),
})
})
.collect();
if packages.is_empty() {
return Ok(all_packages);
}
let mut resolved_names: Vec<String> = Vec::new();
let mut errors: Vec<String> = Vec::new();
for requested in packages {
if all_packages.iter().any(|pkg| &pkg.name == requested) {
resolved_names.push(requested.clone());
continue;
}
let dir_matches: Vec<&Package> = all_packages
.iter()
.filter(|pkg| {
pkg.dir.file_name().and_then(|n| n.to_str()).is_some_and(|n| n == requested)
})
.collect();
match dir_matches.len() {
0 => {
errors.push(format!("Package not found in workspace: '{}'", requested));
}
1 => {
resolved_names.push(dir_matches[0].name.clone());
}
_ => {
errors.push(format!(
"Ambiguous package '{}': use the manifest name to disambiguate.",
requested
));
}
}
}
if !errors.is_empty() {
let mut error_msg = errors.join("\n\n");
error_msg.push_str("\n\nAvailable packages (manifest name / directory):");
for pkg in &all_packages {
error_msg.push_str(&format!("\n - {} ({})", pkg.name, pkg.dir.display()));
}
return Err(error_msg.into());
}
let package_info: Vec<Package> = all_packages
.into_iter()
.filter(|pkg| resolved_names.iter().any(|r| r == &pkg.name))
.collect();
Ok(package_info)
}
pub fn get_workspace_root(sh: &Shell) -> Result<PathBuf, Box<dyn std::error::Error>> {
let metadata = quiet_cmd!(sh, "cargo metadata --no-deps --format-version 1").read()?;
let json: serde_json::Value = serde_json::from_str(&metadata)?;
let root = json["workspace_root"].as_str().ok_or("Missing workspace_root in cargo metadata")?;
Ok(PathBuf::from(root))
}
pub fn get_target_dir(sh: &Shell) -> Result<String, Box<dyn std::error::Error>> {
let metadata = quiet_cmd!(sh, "cargo metadata --no-deps --format-version 1").read()?;
let json: serde_json::Value = serde_json::from_str(&metadata)?;
let target_dir =
json["target_directory"].as_str().ok_or("Missing target_directory in cargo metadata")?;
Ok(target_dir.to_string())
}
pub fn discover_features(
sh: &Shell,
package: &Package,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let metadata = quiet_cmd!(sh, "cargo metadata --format-version 1 --no-deps").read()?;
let json: serde_json::Value = serde_json::from_str(&metadata)?;
let packages =
json["packages"].as_array().ok_or("Missing 'packages' field in cargo metadata")?;
let manifest_path = package.dir.join("Cargo.toml");
let pkg = packages
.iter()
.find(|p| p["manifest_path"].as_str().is_some_and(|path| Path::new(path) == manifest_path))
.ok_or_else(|| format!("Package not found in cargo metadata: {}", package.dir.display()))?;
let mut features: Vec<String> = pkg["features"]
.as_object()
.map(|f| f.keys().filter(|k| *k != "default").cloned().collect())
.unwrap_or_default();
features.sort();
Ok(features)
}
pub fn git_commit_id(sh: &Shell) -> Option<String> {
sh.cmd("git").args(["rev-parse", "HEAD"]).quiet().read().ok().map(|s| s.trim().to_owned())
}
pub struct Manifest {
pub exclude: Vec<String>,
}
impl Manifest {
pub fn read(package_dir: &Path) -> Result<Self, Box<dyn std::error::Error>> {
#[derive(serde::Deserialize)]
struct CargoToml {
package: CargoPackage,
}
#[derive(serde::Deserialize)]
struct CargoPackage {
#[serde(default)]
exclude: Vec<String>,
}
let contents = fs::read_to_string(package_dir.join("Cargo.toml"))?;
let cargo_toml: CargoToml = toml::from_str(&contents)?;
Ok(Self { exclude: cargo_toml.package.exclude })
}
}
#[derive(serde::Deserialize, Default)]
pub(crate) struct PackageManifest<T: Default> {
#[serde(default)]
pub(crate) package: PackageTable<T>,
}
#[derive(serde::Deserialize, Default)]
pub(crate) struct WorkspaceManifest<T: Default> {
#[serde(default)]
pub(crate) workspace: WorkspaceTable<T>,
#[serde(default)]
pub(crate) package: PackageTable<T>,
}
#[derive(serde::Deserialize, Default)]
pub(crate) struct WorkspaceTable<T: Default> {
#[serde(default)]
pub(crate) metadata: MetadataTable<T>,
}
#[derive(serde::Deserialize, Default)]
pub(crate) struct PackageTable<T: Default> {
#[serde(default)]
pub(crate) metadata: MetadataTable<T>,
}
#[derive(serde::Deserialize, Default)]
pub(crate) struct MetadataTable<T: Default> {
#[serde(default)]
pub(crate) rbmt: T,
}