()
| 317 | } |
| 318 | |
| 319 | fn resolve_python_executable() -> io::Result<String> { |
| 320 | let explicit = env::var("NCODE_PY_REPL_PYTHON_PATH") |
| 321 | .ok() |
| 322 | .filter(|value| !value.trim().is_empty()) |
| 323 | .or_else(|| { |
| 324 | env::var("CLAUDE_CODE_PY_REPL_PYTHON_PATH") |
| 325 | .ok() |
| 326 | .filter(|value| !value.trim().is_empty()) |
| 327 | }); |
| 328 | |
| 329 | let candidates = if let Some(explicit) = explicit { |
| 330 | vec![explicit] |
| 331 | } else { |
| 332 | vec!["python3".to_string(), "python".to_string()] |
| 333 | }; |
| 334 | |
| 335 | let min_version = parse_python_version(PY_REPL_MIN_PYTHON_VERSION.trim()).ok_or_else(|| { |
| 336 | io::Error::new( |
| 337 | io::ErrorKind::InvalidData, |
| 338 | "invalid py_repl minimum Python version", |
| 339 | ) |
| 340 | })?; |
| 341 | |
| 342 | for candidate in candidates { |
| 343 | let output = Command::new(&candidate) |
| 344 | .arg("-c") |
| 345 | .arg("import sys; print(\".\".join(str(part) for part in sys.version_info[:3]))") |
| 346 | .stdout(Stdio::piped()) |
| 347 | .stderr(Stdio::null()) |
| 348 | .output(); |
| 349 | |
| 350 | let Ok(output) = output else { |
| 351 | continue; |
| 352 | }; |
| 353 | if !output.status.success() { |
| 354 | continue; |
| 355 | } |
| 356 | |
| 357 | let stdout = String::from_utf8_lossy(&output.stdout); |
| 358 | let Some(version) = parse_python_version(stdout.trim()) else { |
| 359 | continue; |
| 360 | }; |
| 361 | |
| 362 | if compare_version(&version, &min_version) >= 0 { |
| 363 | return Ok(candidate); |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | Err(io::Error::new( |
| 368 | io::ErrorKind::NotFound, |
| 369 | format!( |
| 370 | "py_repl rust host requires Python {}+", |
| 371 | PY_REPL_MIN_PYTHON_VERSION.trim() |
| 372 | ), |
| 373 | )) |
| 374 | } |
| 375 | |
| 376 | fn parse_python_version(input: &str) -> Option<Vec<u32>> { |
no test coverage detected