1pub mod debug;
4
5use std::collections::HashSet;
6use std::process::Command;
7
8pub struct Metrics {
10 pub dep_count: usize,
11 pub binary_size_bytes: u64,
12}
13
14pub fn count_dependencies() -> Result<usize, String> {
16 let output = Command::new("cargo")
17 .args(["tree", "--edges", "normal", "--prefix", "none"])
18 .output()
19 .map_err(|e| format!("Failed to run cargo tree: {}", e))?;
20
21 if !output.status.success() {
22 return Err("cargo tree failed".to_string());
23 }
24
25 let stdout = String::from_utf8_lossy(&output.stdout);
26 let mut unique_deps: HashSet<String> = HashSet::new();
27
28 for line in stdout.lines() {
29 if let Some(dep) = line.trim().split_whitespace().next() {
30 if !dep.is_empty() {
31 unique_deps.insert(dep.to_string());
32 }
33 }
34 }
35
36 Ok(unique_deps.len())
37}
38
39pub fn get_binary_name() -> Result<String, String> {
41 let output = Command::new("cargo")
42 .args(["metadata", "--format-version", "1", "--no-deps"])
43 .output()
44 .map_err(|e| format!("Failed to run cargo metadata: {}", e))?;
45
46 if !output.status.success() {
47 return Err("cargo metadata failed".to_string());
48 }
49
50 let stdout = String::from_utf8_lossy(&output.stdout);
51
52 for line in stdout.lines() {
54 if line.contains(r#""kind":["bin"]"#) || line.contains(r#""kind": ["bin"]"#) {
55 if let Some(name_line) = stdout
56 .lines()
57 .skip_while(|l| {
58 !l.contains(r#""kind":["bin"]"#) && !l.contains(r#""kind": ["bin"]"#)
59 })
60 .find(|l| l.contains(r#""name":"#))
61 {
62 if let Some(start) = name_line.find(r#""name":""#) {
63 let name_start = start + r#""name":""#.len();
64 if let Some(end) = name_line[name_start..].find('"') {
65 return Ok(name_line[name_start..name_start + end].to_string());
66 }
67 }
68 }
69 }
70 }
71
72 Err("No binary target found".to_string())
73}
74
75pub fn build_release() -> Result<(), String> {
77 let status = Command::new("cargo")
78 .args(["build", "--release", "--quiet"])
79 .status()
80 .map_err(|e| format!("Failed to run cargo build: {}", e))?;
81
82 if !status.success() {
83 return Err("cargo build failed".to_string());
84 }
85
86 Ok(())
87}
88
89pub fn get_binary_size(binary_name: &str) -> Result<u64, String> {
91 let path = format!("target/release/{}", binary_name);
92 std::fs::metadata(&path)
93 .map(|m| m.len())
94 .map_err(|e| format!("Failed to get size for {}: {}", path, e))
95}
96
97pub fn collect_metrics(binary_name: &str, should_build: bool) -> Result<Metrics, String> {
99 if should_build {
100 build_release()?;
101 }
102
103 let dep_count = count_dependencies()?;
104 let binary_size_bytes = get_binary_size(binary_name)?;
105
106 Ok(Metrics {
107 dep_count,
108 binary_size_bytes,
109 })
110}
111
112pub fn format_size(bytes: u64) -> String {
114 if bytes < 1024 {
115 format!("{}B", bytes)
116 } else if bytes < 1024 * 1024 {
117 format!("{:.1}K", bytes as f64 / 1024.0)
118 } else {
119 format!("{:.1}M", bytes as f64 / (1024.0 * 1024.0))
120 }
121}
122
123pub fn generate_badges(metrics: &Metrics, crate_name: &str) -> String {
125 let crates_io_url = format!("https://crates.io/crates/{}", crate_name);
126 let size_formatted = format_size(metrics.binary_size_bytes);
127
128 let deps_badge = format!(
129 "[]({})",
130 metrics.dep_count, metrics.dep_count, crates_io_url
131 );
132
133 let size_badge = format!(
134 "[]({})",
135 size_formatted, size_formatted, crates_io_url
136 );
137
138 format!("{}\n{}", deps_badge, size_badge)
139}
140
141pub fn update_readme(readme_path: &str, badge_content: &str) -> Result<(), String> {
143 use textum::{Boundary, BoundaryMode, Patch, Snippet, Target};
144
145 let start = Boundary::new(
146 Target::Literal("<!-- auto-generated badges -->".to_string()),
147 BoundaryMode::Exclude,
148 );
149 let end = Boundary::new(
150 Target::Literal("<!-- /auto-generated badges -->".to_string()),
151 BoundaryMode::Exclude,
152 );
153
154 let snippet = Snippet::Between { start, end };
155
156 let patch = Patch {
157 file: Some(readme_path.to_string()),
158 snippet,
159 replacement: format!("\n{}\n", badge_content),
160 };
161
162 let content = std::fs::read_to_string(readme_path)
164 .map_err(|e| format!("Failed to read {}: {}", readme_path, e))?;
165
166 let updated = patch
168 .apply_to_string(&content)
169 .map_err(|e| format!("Failed to apply patch: {:?}", e))?;
170
171 std::fs::write(readme_path, updated)
173 .map_err(|e| format!("Failed to write {}: {}", readme_path, e))?;
174
175 Ok(())
176}
177
178#[macro_export]
180macro_rules! blazon_debug {
181 ($($arg:tt)*) => {
182 if $crate::debug::is_enabled() {
183 eprintln!("[BLAZON DEBUG] {}", format!($($arg)*));
184 }
185 };
186}