use anyhow::{Context, Result, bail};
use std::path::Path;
pub(crate) fn validate_battery_pack_cmd(path: Option<&str>) -> Result<()> {
let crate_root = match path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir().context("failed to get current directory")?,
};
let cargo_toml = crate_root.join("Cargo.toml");
let content = std::fs::read_to_string(&cargo_toml)
.with_context(|| format!("failed to read {}", cargo_toml.display()))?;
let raw: toml::Value = toml::from_str(&content)
.with_context(|| format!("failed to parse {}", cargo_toml.display()))?;
if raw.get("package").is_none() {
if raw.get("workspace").is_some() {
bail!(
"{} is a workspace manifest, not a battery pack crate.\n\
Run this from a battery pack crate directory, or use --path to point to one.",
cargo_toml.display()
);
} else {
bail!(
"{} has no [package] section — is this a battery pack crate?",
cargo_toml.display()
);
}
}
let spec = bphelper_manifest::parse_battery_pack(&content)
.with_context(|| format!("failed to parse {}", cargo_toml.display()))?;
let mut report = spec.validate_spec();
report.merge(bphelper_manifest::validate_on_disk(&spec, &crate_root));
if report.is_clean() {
validate_templates(crate_root.to_str().unwrap_or("."))?;
println!("{} is valid", spec.name);
return Ok(());
}
let mut errors = 0;
let mut warnings = 0;
for diag in &report.diagnostics {
match diag.severity {
bphelper_manifest::Severity::Error => {
eprintln!("error[{}]: {}", diag.rule, diag.message);
errors += 1;
}
bphelper_manifest::Severity::Warning => {
eprintln!("warning[{}]: {}", diag.rule, diag.message);
warnings += 1;
}
}
}
if errors > 0 {
bail!(
"validation failed: {} error(s), {} warning(s)",
errors,
warnings
);
}
validate_templates(crate_root.to_str().unwrap_or("."))?;
println!("{} is valid ({} warning(s))", spec.name, warnings);
Ok(())
}
pub fn validate_templates(manifest_dir: &str) -> Result<()> {
let manifest_dir = Path::new(manifest_dir);
let cargo_toml = manifest_dir.join("Cargo.toml");
let content = std::fs::read_to_string(&cargo_toml)
.with_context(|| format!("failed to read {}", cargo_toml.display()))?;
let crate_name = manifest_dir
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let spec = bphelper_manifest::parse_battery_pack(&content)
.map_err(|e| anyhow::anyhow!("failed to parse {}: {e}", cargo_toml.display()))?;
if spec.templates.is_empty() {
println!("no templates to validate");
return Ok(());
}
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(&cargo_toml)
.no_deps()
.exec()
.context("failed to run cargo metadata")?;
let shared_target_dir = metadata.target_directory.join("bp-validate");
for (name, template) in &spec.templates {
println!("validating template '{name}'...");
let tmp = tempfile::tempdir().context("failed to create temp directory")?;
let project_name = format!("bp-validate-{name}");
let opts = crate::template_engine::GenerateOpts {
render: crate::template_engine::RenderOpts {
crate_root: manifest_dir.to_path_buf(),
template_path: template.path.clone(),
project_name,
defines: std::collections::BTreeMap::new(),
interactive_override: Some(false),
},
destination: Some(tmp.path().to_path_buf()),
git_init: false,
};
let project_dir = crate::template_engine::generate(opts)
.with_context(|| format!("failed to generate template '{name}'"))?;
write_crates_io_patches(&project_dir, &metadata)?;
let output = std::process::Command::new("cargo")
.args(["check"])
.env("CARGO_TARGET_DIR", &*shared_target_dir)
.current_dir(&project_dir)
.output()
.context("failed to run cargo check")?;
anyhow::ensure!(
output.status.success(),
"cargo check failed for template '{name}':\n{}",
String::from_utf8_lossy(&output.stderr)
);
let output = std::process::Command::new("cargo")
.args(["test"])
.env("CARGO_TARGET_DIR", &*shared_target_dir)
.current_dir(&project_dir)
.output()
.context("failed to run cargo test")?;
anyhow::ensure!(
output.status.success(),
"cargo test failed for template '{name}':\n{}",
String::from_utf8_lossy(&output.stderr)
);
println!("template '{name}' ok");
}
println!(
"all {} template(s) for '{}' validated successfully",
spec.templates.len(),
crate_name
);
Ok(())
}
fn write_crates_io_patches(project_dir: &Path, metadata: &cargo_metadata::Metadata) -> Result<()> {
let mut patches = String::from("[patch.crates-io]\n");
for pkg in &metadata.workspace_packages() {
let path = pkg.manifest_path.parent().unwrap();
patches.push_str(&format!("{} = {{ path = \"{}\" }}\n", pkg.name, path));
}
let parent_config = metadata.workspace_root.join(".cargo/config.toml");
if let Ok(content) = std::fs::read_to_string(&parent_config)
&& let Ok(parsed) = content.parse::<toml::Table>()
&& let Some(toml::Value::Table(patch_section)) = parsed.get("patch")
&& let Some(toml::Value::Table(crates_io)) = patch_section.get("crates-io")
{
for (name, value) in crates_io {
if metadata
.workspace_packages()
.iter()
.any(|p| p.name == *name)
{
continue;
}
patches.push_str(&format!("{name} = {value}\n"));
}
}
let cargo_dir = project_dir.join(".cargo");
std::fs::create_dir_all(&cargo_dir)
.with_context(|| format!("failed to create {}", cargo_dir.display()))?;
std::fs::write(cargo_dir.join("config.toml"), patches)
.context("failed to write .cargo/config.toml")?;
Ok(())
}
#[cfg(test)]
mod tests;