bphelper_cli/validate/
mod.rs1use anyhow::{Context, Result, bail};
4use std::path::Path;
5
6pub(crate) fn validate_battery_pack_cmd(path: Option<&str>) -> Result<()> {
13 let crate_root = match path {
14 Some(p) => std::path::PathBuf::from(p),
15 None => std::env::current_dir().context("failed to get current directory")?,
16 };
17
18 let cargo_toml = crate_root.join("Cargo.toml");
19 let content = std::fs::read_to_string(&cargo_toml)
20 .with_context(|| format!("failed to read {}", cargo_toml.display()))?;
21
22 let raw: toml::Value = toml::from_str(&content)
24 .with_context(|| format!("failed to parse {}", cargo_toml.display()))?;
25 if raw.get("package").is_none() {
26 if raw.get("workspace").is_some() {
27 bail!(
29 "{} is a workspace manifest, not a battery pack crate.\n\
30 Run this from a battery pack crate directory, or use --path to point to one.",
31 cargo_toml.display()
32 );
33 } else {
34 bail!(
36 "{} has no [package] section — is this a battery pack crate?",
37 cargo_toml.display()
38 );
39 }
40 }
41
42 let spec = bphelper_manifest::parse_battery_pack(&content)
43 .with_context(|| format!("failed to parse {}", cargo_toml.display()))?;
44
45 let mut report = spec.validate_spec();
47 report.merge(bphelper_manifest::validate_on_disk(&spec, &crate_root));
48
49 if report.is_clean() {
51 validate_templates(crate_root.to_str().unwrap_or("."))?;
52 println!("{} is valid", spec.name);
53 return Ok(());
54 }
55
56 let mut errors = 0;
59 let mut warnings = 0;
60 for diag in &report.diagnostics {
61 match diag.severity {
62 bphelper_manifest::Severity::Error => {
63 eprintln!("error[{}]: {}", diag.rule, diag.message);
64 errors += 1;
65 }
66 bphelper_manifest::Severity::Warning => {
67 eprintln!("warning[{}]: {}", diag.rule, diag.message);
68 warnings += 1;
69 }
70 }
71 }
72
73 if errors > 0 {
75 bail!(
76 "validation failed: {} error(s), {} warning(s)",
77 errors,
78 warnings
79 );
80 }
81
82 validate_templates(crate_root.to_str().unwrap_or("."))?;
85 println!("{} is valid ({} warning(s))", spec.name, warnings);
86 Ok(())
87}
88
89pub fn validate_templates(manifest_dir: &str) -> Result<()> {
102 let manifest_dir = Path::new(manifest_dir);
103 let cargo_toml = manifest_dir.join("Cargo.toml");
104 let content = std::fs::read_to_string(&cargo_toml)
105 .with_context(|| format!("failed to read {}", cargo_toml.display()))?;
106
107 let crate_name = manifest_dir
108 .file_name()
109 .and_then(|s| s.to_str())
110 .unwrap_or("unknown");
111 let spec = bphelper_manifest::parse_battery_pack(&content)
112 .map_err(|e| anyhow::anyhow!("failed to parse {}: {e}", cargo_toml.display()))?;
113
114 if spec.templates.is_empty() {
115 println!("no templates to validate");
117 return Ok(());
118 }
119
120 let metadata = cargo_metadata::MetadataCommand::new()
122 .manifest_path(&cargo_toml)
123 .no_deps()
124 .exec()
125 .context("failed to run cargo metadata")?;
126 let shared_target_dir = metadata.target_directory.join("bp-validate");
127
128 for (name, template) in &spec.templates {
129 println!("validating template '{name}'...");
130
131 let tmp = tempfile::tempdir().context("failed to create temp directory")?;
132
133 let project_name = format!("bp-validate-{name}");
134
135 let opts = crate::template_engine::GenerateOpts {
136 render: crate::template_engine::RenderOpts {
137 crate_root: manifest_dir.to_path_buf(),
138 template_path: template.path.clone(),
139 project_name,
140 defines: std::collections::BTreeMap::new(),
141 interactive_override: Some(false),
142 },
143 destination: Some(tmp.path().to_path_buf()),
144 git_init: false,
145 };
146
147 let project_dir = crate::template_engine::generate(opts)
148 .with_context(|| format!("failed to generate template '{name}'"))?;
149
150 write_crates_io_patches(&project_dir, &metadata)?;
151
152 let output = std::process::Command::new("cargo")
154 .args(["check"])
155 .env("CARGO_TARGET_DIR", &*shared_target_dir)
156 .current_dir(&project_dir)
157 .output()
158 .context("failed to run cargo check")?;
159 anyhow::ensure!(
160 output.status.success(),
161 "cargo check failed for template '{name}':\n{}",
162 String::from_utf8_lossy(&output.stderr)
163 );
164
165 let output = std::process::Command::new("cargo")
167 .args(["test"])
168 .env("CARGO_TARGET_DIR", &*shared_target_dir)
169 .current_dir(&project_dir)
170 .output()
171 .context("failed to run cargo test")?;
172 anyhow::ensure!(
173 output.status.success(),
174 "cargo test failed for template '{name}':\n{}",
175 String::from_utf8_lossy(&output.stderr)
176 );
177
178 println!("template '{name}' ok");
179 }
180
181 println!(
182 "all {} template(s) for '{}' validated successfully",
183 spec.templates.len(),
184 crate_name
185 );
186 Ok(())
187}
188
189fn write_crates_io_patches(project_dir: &Path, metadata: &cargo_metadata::Metadata) -> Result<()> {
193 let mut patches = String::from("[patch.crates-io]\n");
194 for pkg in &metadata.workspace_packages() {
195 let path = pkg.manifest_path.parent().unwrap();
196 patches.push_str(&format!("{} = {{ path = \"{}\" }}\n", pkg.name, path));
197 }
198
199 let parent_config = metadata.workspace_root.join(".cargo/config.toml");
203 if let Ok(content) = std::fs::read_to_string(&parent_config)
204 && let Ok(parsed) = content.parse::<toml::Table>()
205 && let Some(toml::Value::Table(patch_section)) = parsed.get("patch")
206 && let Some(toml::Value::Table(crates_io)) = patch_section.get("crates-io")
207 {
208 for (name, value) in crates_io {
209 if metadata
211 .workspace_packages()
212 .iter()
213 .any(|p| p.name == *name)
214 {
215 continue;
216 }
217 patches.push_str(&format!("{name} = {value}\n"));
218 }
219 }
220
221 let cargo_dir = project_dir.join(".cargo");
222 std::fs::create_dir_all(&cargo_dir)
223 .with_context(|| format!("failed to create {}", cargo_dir.display()))?;
224 std::fs::write(cargo_dir.join("config.toml"), patches)
225 .context("failed to write .cargo/config.toml")?;
226 Ok(())
227}
228
229#[cfg(test)]
230mod tests;