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.split_whitespace().next()
30 && !dep.is_empty()
31 {
32 unique_deps.insert(dep.to_string());
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 && 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 && let Some(start) = name_line.find(r#""name":""#)
62 {
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 Err("No binary target found".to_string())
71}
72
73pub fn build_release() -> Result<(), String> {
75 let status = Command::new("cargo")
76 .args(["build", "--release", "--quiet"])
77 .status()
78 .map_err(|e| format!("Failed to run cargo build: {}", e))?;
79
80 if !status.success() {
81 return Err("cargo build failed".to_string());
82 }
83
84 Ok(())
85}
86
87pub fn get_binary_size(binary_name: &str) -> Result<u64, String> {
89 let path = format!("target/release/{}", binary_name);
90 std::fs::metadata(&path)
91 .map(|m| m.len())
92 .map_err(|e| format!("Failed to get size for {}: {}", path, e))
93}
94
95pub fn collect_metrics(binary_name: &str, should_build: bool) -> Result<Metrics, String> {
97 if should_build {
98 build_release()?;
99 }
100
101 let dep_count = count_dependencies()?;
102 let binary_size_bytes = get_binary_size(binary_name)?;
103
104 Ok(Metrics {
105 dep_count,
106 binary_size_bytes,
107 })
108}
109
110pub fn format_size(bytes: u64) -> String {
112 if bytes < 1024 {
113 format!("{}B", bytes)
114 } else if bytes < 1024 * 1024 {
115 format!("{:.1}K", bytes as f64 / 1024.0)
116 } else {
117 format!("{:.1}M", bytes as f64 / (1024.0 * 1024.0))
118 }
119}
120
121pub fn generate_badges(metrics: &Metrics, crate_name: &str) -> String {
123 let crates_io_url = format!("https://crates.io/crates/{}", crate_name);
124 let size_formatted = format_size(metrics.binary_size_bytes);
125
126 let deps_badge = format!(
127 "[]({})",
128 metrics.dep_count, metrics.dep_count, crates_io_url
129 );
130
131 let size_badge = format!(
132 "[]({})",
133 size_formatted, size_formatted, crates_io_url
134 );
135
136 format!("{}\n{}", deps_badge, size_badge)
137}
138
139pub fn update_readme(readme_path: &str, badge_content: &str) -> Result<(), String> {
141 use textum::{Boundary, BoundaryMode, Patch, Snippet, Target};
142
143 let start = Boundary::new(
144 Target::Literal("<!-- blazon -->".to_string()),
145 BoundaryMode::Exclude,
146 );
147 let end = Boundary::new(
148 Target::Literal("<!-- /blazon -->".to_string()),
149 BoundaryMode::Exclude,
150 );
151
152 let snippet = Snippet::Between { start, end };
153
154 let patch = Patch {
155 file: Some(readme_path.to_string()),
156 snippet,
157 replacement: format!("\n{}", badge_content),
158 };
159
160 let content = std::fs::read_to_string(readme_path)
162 .map_err(|e| format!("Failed to read {}: {}", readme_path, e))?;
163
164 let updated = patch
166 .apply_to_string(&content)
167 .map_err(|e| format!("Failed to apply patch: {:?}", e))?;
168
169 std::fs::write(readme_path, updated)
171 .map_err(|e| format!("Failed to write {}: {}", readme_path, e))?;
172
173 Ok(())
174}
175
176#[macro_export]
178macro_rules! blazon_debug {
179 ($($arg:tt)*) => {
180 if $crate::debug::is_enabled() {
181 eprintln!("[BLAZON DEBUG] {}", format!($($arg)*));
182 }
183 };
184}