python_proto_importer/
doctor.rs1use crate::config::AppConfig;
2use crate::verification::{determine_package_structure, determine_package_structure_legacy};
3use anyhow::{Result, bail};
4use std::path::Path;
5use std::process::Command;
6use which::which;
7
8fn check(cmd: &str) -> Option<String> {
19 which(cmd)
20 .ok()
21 .and_then(|p| p.to_str().map(|s| s.to_string()))
22}
23
24pub fn run() -> Result<()> {
54 println!("== Tool presence ==");
55
56 let py_runner = check("uv")
57 .or_else(|| check("python3"))
58 .or_else(|| check("python"))
59 .unwrap_or_default();
60
61 if let Some(uv_path) = check("uv") {
62 let uv_ver = cmd_version(&uv_path, &["--version"]).unwrap_or_else(|| "unknown".into());
63 println!("{:<14}: {} ({})", "uv", uv_path, uv_ver.trim());
64 } else {
65 println!("{:<14}: not found", "uv");
66 }
67 if let Some(py3) = check("python3") {
68 let py_ver = cmd_version(&py3, &["--version"]).unwrap_or_else(|| "unknown".into());
69 println!("{:<14}: {} ({})", "python3", py3, py_ver.trim());
70 } else if let Some(py) = check("python") {
71 let py_ver = cmd_version(&py, &["--version"]).unwrap_or_else(|| "unknown".into());
72 println!("{:<14}: {} ({})", "python", py, py_ver.trim());
73 } else {
74 println!("{:<14}: not found", "python");
75 }
76
77 let (grpc_tools_found, grpc_tools_ver) = probe_python_pkg(&py_runner, "grpcio-tools");
78 println!(
79 "{:<14}: {}{}",
80 "grpc_tools",
81 if grpc_tools_found {
82 "found"
83 } else {
84 "not found"
85 },
86 grpc_tools_ver
87 .as_deref()
88 .map(|v| format!(" ({})", v))
89 .unwrap_or_default()
90 );
91
92 let (mypy_protobuf_found, mypy_protobuf_ver) = probe_python_pkg(&py_runner, "mypy-protobuf");
93 println!(
94 "{:<14}: {}{}",
95 "mypy-protobuf",
96 if mypy_protobuf_found {
97 "found"
98 } else {
99 "not found"
100 },
101 mypy_protobuf_ver
102 .as_deref()
103 .map(|v| format!(" ({})", v))
104 .unwrap_or_default()
105 );
106 let (mypy_grpc_found, mypy_grpc_ver) = probe_python_pkg(&py_runner, "mypy-grpc");
107 println!(
108 "{:<14}: {}{}",
109 "mypy-grpc",
110 if mypy_grpc_found {
111 "found"
112 } else {
113 "not found"
114 },
115 mypy_grpc_ver
116 .as_deref()
117 .map(|v| format!(" ({})", v))
118 .unwrap_or_default()
119 );
120
121 if let Some(p) = check("protoc") {
122 let v = cmd_version(&p, &["--version"]).unwrap_or_else(|| "unknown".into());
123 println!("{:<14}: {} ({})", "protoc", p, v.trim());
124 } else {
125 println!("{:<14}: not found", "protoc");
126 }
127 if let Some(p) = check("buf") {
128 let v = cmd_version(&p, &["--version"]).unwrap_or_else(|| "unknown".into());
129 println!("{:<14}: {} ({})", "buf", p, v.trim());
130 } else {
131 println!("{:<14}: not found", "buf");
132 }
133
134 if let Some(p) = check("mypy") {
135 let v = cmd_version(&p, &["--version"]).unwrap_or_else(|| "unknown".into());
136 println!("{:<14}: {} ({})", "mypy", p, v.trim());
137 } else {
138 println!("{:<14}: not found", "mypy");
139 }
140 if let Some(p) = check("pyright") {
141 let v = cmd_version(&p, &["--version"]).unwrap_or_else(|| "unknown".into());
142 println!("{:<14}: {} ({})", "pyright", p, v.trim());
143 } else {
144 println!("{:<14}: not found", "pyright");
145 }
146
147 if let Ok(cfg) = AppConfig::load(Some(Path::new("pyproject.toml"))) {
148 println!("\n== Based on pyproject.toml ==");
149 if cfg.generate_mypy && !mypy_protobuf_found {
150 println!(
151 "hint: mypy-protobuf is required (install via 'uv add mypy-protobuf' or 'pip install mypy-protobuf')"
152 );
153 }
154 if cfg.generate_mypy_grpc && !mypy_grpc_found {
155 println!(
156 "hint: mypy-grpc is required (install via 'uv add mypy-grpc' or 'pip install mypy-grpc')"
157 );
158 }
159 if let Some(v) = &cfg.verify {
160 if v.mypy_cmd.is_some() && check("mypy").is_none() {
161 println!(
162 "hint: mypy CLI not found (install via 'uv add mypy' or 'pip install mypy')"
163 );
164 }
165 if v.pyright_cmd.is_some() && check("pyright").is_none() {
166 println!(
167 "hint: pyright CLI not found (install via 'uv add pyright' or 'npm i -g pyright')"
168 );
169 }
170 }
171 }
172
173 if !grpc_tools_found {
174 bail!(
175 "grpc_tools.protoc not found. Install with 'uv add grpcio-tools' or 'pip install grpcio-tools'"
176 );
177 }
178
179 if let Ok(cfg) = AppConfig::load(Some(Path::new("pyproject.toml"))) {
181 println!("\n== Package structure analysis ==");
182
183 let out_abs = cfg.out.canonicalize().unwrap_or_else(|_| cfg.out.clone());
184 println!("Output directory: {}", out_abs.display());
185
186 if !out_abs.exists() {
187 println!(" ❌ Output directory does not exist. Run 'build' first to generate files.");
188 } else {
189 println!(" ✅ Output directory exists");
190
191 let (parent_path, package_name) =
193 determine_package_structure(&out_abs).unwrap_or_else(|e| {
194 println!(" ❌ Failed to determine package structure: {}", e);
195 (std::path::PathBuf::new(), String::new())
196 });
197
198 let (legacy_parent_path, legacy_package_name) =
199 determine_package_structure_legacy(&out_abs).unwrap_or_else(|e| {
200 println!(" ❌ Failed to determine legacy package structure: {}", e);
201 (std::path::PathBuf::new(), String::new())
202 });
203
204 println!(" Current implementation:");
205 println!(" PYTHONPATH: {}", parent_path.display());
206 println!(
207 " Package name: {}",
208 if package_name.is_empty() {
209 "<empty>"
210 } else {
211 &package_name
212 }
213 );
214
215 if parent_path != legacy_parent_path || package_name != legacy_package_name {
216 println!(" Legacy implementation (fallback):");
217 println!(" PYTHONPATH: {}", legacy_parent_path.display());
218 println!(
219 " Package name: {}",
220 if legacy_package_name.is_empty() {
221 "<empty>"
222 } else {
223 &legacy_package_name
224 }
225 );
226 }
227
228 if let Some(parent_dir) = out_abs.parent() {
230 let parent_init = parent_dir.join("__init__.py");
231 if parent_init.exists() {
232 println!(" ✅ Parent directory is a Python package (has __init__.py)");
233 if let Some(parent_name) = parent_dir.file_name().and_then(|n| n.to_str()) {
234 println!(" Package name: {}", parent_name);
235 }
236 if let Some(grandparent) = parent_dir.parent() {
237 println!(" Recommended PYTHONPATH: {}", grandparent.display());
238 }
239 } else {
240 println!(" ℹ️ Parent directory is not a Python package (no __init__.py)");
241 println!(" This is fine for simple structures");
242 }
243 }
244
245 if let Ok(entries) = std::fs::read_dir(&out_abs) {
247 let py_files: Vec<_> = entries
248 .filter_map(Result::ok)
249 .filter(|entry| {
250 entry.path().extension().and_then(|ext| ext.to_str()) == Some("py")
251 })
252 .collect();
253 println!(" Generated files: {} Python files found", py_files.len());
254 }
255 }
256 }
257
258 Ok(())
259}
260
261fn cmd_version(bin: &str, args: &[&str]) -> Option<String> {
262 let out = Command::new(bin).args(args).output().ok()?;
263 if out.status.success() {
264 let s = String::from_utf8_lossy(&out.stdout);
265 let txt = if s.trim().is_empty() {
266 String::from_utf8_lossy(&out.stderr).into_owned()
267 } else {
268 s.into_owned()
269 };
270 Some(txt)
271 } else {
272 None
273 }
274}
275
276fn probe_python_pkg(py_runner: &str, dist_name: &str) -> (bool, Option<String>) {
277 if py_runner.is_empty() {
278 return (false, None);
279 }
280 let code = format!(
281 "import sys\ntry:\n import importlib.metadata as m\n print('1 ' + m.version('{0}'))\nexcept Exception:\n try:\n import pkg_resources as pr\n print('1 ' + pr.get_distribution('{0}').version)\n except Exception:\n print('0')\n",
282 dist_name
283 );
284 let mut cmd = Command::new(py_runner);
285 if py_runner.ends_with("uv") || py_runner == "uv" {
286 cmd.arg("run").arg("python").arg("-c").arg(&code);
287 } else {
288 cmd.arg("-c").arg(&code);
289 }
290 let out_opt = match cmd.output() {
291 Ok(o) if o.status.success() => Some(o),
292 _ => None,
293 };
294 if let Some(out) = out_opt {
295 let s = String::from_utf8_lossy(&out.stdout);
296 let t = s.trim();
297 if let Some(rest) = t.strip_prefix('1') {
298 let ver = rest.trim();
299 let ver = ver.strip_prefix(' ').unwrap_or(ver);
300 return (
301 true,
302 if ver.is_empty() {
303 None
304 } else {
305 Some(ver.to_string())
306 },
307 );
308 }
309 }
310 (false, None)
311}