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 },
142 destination: Some(tmp.path().to_path_buf()),
143 git_init: false,
144 };
145
146 let project_dir = crate::template_engine::generate(opts)
147 .with_context(|| format!("failed to generate template '{name}'"))?;
148
149 write_crates_io_patches(&project_dir, &metadata)?;
150
151 let output = std::process::Command::new("cargo")
153 .args(["check"])
154 .env("CARGO_TARGET_DIR", &*shared_target_dir)
155 .current_dir(&project_dir)
156 .output()
157 .context("failed to run cargo check")?;
158 anyhow::ensure!(
159 output.status.success(),
160 "cargo check failed for template '{name}':\n{}",
161 String::from_utf8_lossy(&output.stderr)
162 );
163
164 let output = std::process::Command::new("cargo")
166 .args(["test"])
167 .env("CARGO_TARGET_DIR", &*shared_target_dir)
168 .current_dir(&project_dir)
169 .output()
170 .context("failed to run cargo test")?;
171 anyhow::ensure!(
172 output.status.success(),
173 "cargo test failed for template '{name}':\n{}",
174 String::from_utf8_lossy(&output.stderr)
175 );
176
177 println!("template '{name}' ok");
178 }
179
180 println!(
181 "all {} template(s) for '{}' validated successfully",
182 spec.templates.len(),
183 crate_name
184 );
185 Ok(())
186}
187
188fn write_crates_io_patches(project_dir: &Path, metadata: &cargo_metadata::Metadata) -> Result<()> {
192 let mut patches = String::from("[patch.crates-io]\n");
193 for pkg in &metadata.workspace_packages() {
194 let path = pkg.manifest_path.parent().unwrap();
195 patches.push_str(&format!("{} = {{ path = \"{}\" }}\n", pkg.name, path));
196 }
197
198 let parent_config = metadata.workspace_root.join(".cargo/config.toml");
202 if let Ok(content) = std::fs::read_to_string(&parent_config)
203 && let Ok(parsed) = content.parse::<toml::Table>()
204 && let Some(toml::Value::Table(patch_section)) = parsed.get("patch")
205 && let Some(toml::Value::Table(crates_io)) = patch_section.get("crates-io")
206 {
207 for (name, value) in crates_io {
208 if metadata
210 .workspace_packages()
211 .iter()
212 .any(|p| p.name == *name)
213 {
214 continue;
215 }
216 patches.push_str(&format!("{name} = {value}\n"));
217 }
218 }
219
220 let cargo_dir = project_dir.join(".cargo");
221 std::fs::create_dir_all(&cargo_dir)
222 .with_context(|| format!("failed to create {}", cargo_dir.display()))?;
223 std::fs::write(cargo_dir.join("config.toml"), patches)
224 .context("failed to write .cargo/config.toml")?;
225 Ok(())
226}
227
228#[cfg(test)]
229mod tests;