// Version information is diagnostic, not a health check. Read package metadata // for the *selected* runtime without launching Python, pip, Certbot or a shell. // A missing/ambiguous version stays None; do not guess an interpreter patch // version from an executable name or report a hard-coded Certbot pin. use std::io::Read; const MAX_VERSION_METADATA_BYTES: u64 = 16 * 1024; const MAX_SITE_DIRECTORIES: usize = 64; const MAX_PACKAGES_PER_SITE: usize = 4096; fn metadata_text(path: &Path) -> Option { let mut bytes = Vec::new(); std::fs::File::open(path) .ok()? .take(MAX_VERSION_METADATA_BYTES) .read_to_end(&mut bytes) .ok()?; String::from_utf8(bytes).ok() } fn safe_version(raw: &str) -> Option { let version = raw.trim(); (version.len() <= 48 && version.bytes().next().is_some_and(|byte| byte.is_ascii_digit()) && version .bytes() .all(|byte| byte.is_ascii_alphanumeric() || b".+-_".contains(&byte))) .then(|| version.to_owned()) } fn python_environment_root(executable: &Path) -> Option { let parent = executable.parent()?; let name = parent.file_name()?.to_str()?; let root = if name.eq_ignore_ascii_case("bin") || name.eq_ignore_ascii_case("scripts") { parent.parent()? } else { parent }; Some(root.to_owned()) } fn interpreter_version(root: &Path) -> Option { let content = metadata_text(&root.join("pyvenv.cfg"))?; // CPython venv writes `version = X.Y.Z`. `version_info` from virtualenv // is a fallback when its standard `version` field is not present. for key in ["version", "version_info"] { for line in content.lines() { if let Some((name, value)) = line.split_once('=') { if name.trim().eq_ignore_ascii_case(key) { if let Some(version) = safe_version(value) { return Some(version); } } } } } None } fn python_minor(version: &str) -> Option { let mut parts = version.split('.'); let major = parts.next()?; let minor = parts.next()?; (major.bytes().all(|b| b.is_ascii_digit()) && minor.bytes().all(|b| b.is_ascii_digit())) .then(|| format!("python{major}.{minor}")) } fn package_sites(root: &Path, python_version: Option<&str>) -> Vec { let mut sites = vec![root.join("Lib/site-packages"), root.join("lib/python3/dist-packages")]; for base in [root.join("lib"), root.join("lib64")] { if let Some(minor) = python_version.and_then(python_minor) { sites.push(base.join(&minor).join("site-packages")); sites.push(base.join(&minor).join("dist-packages")); } else if let Ok(entries) = std::fs::read_dir(&base) { // Python installations with no pyvenv.cfg may contain multiple // interpreter versions. Never choose between conflicting results. for entry in entries.take(MAX_SITE_DIRECTORIES).flatten() { let path = entry.path(); if entry.file_name().to_string_lossy().starts_with("python3.") && path.is_dir() { sites.push(path.join("site-packages")); sites.push(path.join("dist-packages")); } } } } sites } fn package_version(root: &Path, package: &str, python_version: Option<&str>) -> Option { let prefix = format!("{package}-"); let mut result: Option = None; for site in package_sites(root, python_version) { let Ok(entries) = std::fs::read_dir(site) else { continue }; for entry in entries.take(MAX_PACKAGES_PER_SITE).flatten() { let name = entry.file_name(); let Some(dir_name) = name.to_str() else { continue }; let Some(suffix) = dir_name.strip_prefix(prefix.as_str()) .and_then(|name| name.strip_suffix(".dist-info")) else { continue }; let Some(directory_version) = safe_version(suffix) else { continue }; let Some(content) = metadata_text(&entry.path().join("METADATA")) else { continue }; let mut reported_name = None; let mut reported_version = None; for line in content.lines() { if let Some(value) = line.strip_prefix("Name: ") { reported_name = Some(value.trim()); } else if let Some(value) = line.strip_prefix("Version: ") { reported_version = safe_version(value); } if reported_name.is_some() && reported_version.is_some() { break; } } if !reported_name.is_some_and(|name| name.eq_ignore_ascii_case(package)) || reported_version.as_deref() != Some(directory_version.as_str()) { continue; } if result.as_ref().is_some_and(|previous| previous != &directory_version) { return None; } result = Some(directory_version); } } result } #[derive(Default)] struct RuntimeVersions { python: Option, pip: Option, certbot: Option, } fn runtime_versions(python: &Path, has_pip: bool, certbot: Option<&Path>) -> RuntimeVersions { let root = python_environment_root(python); let python_version = root.as_deref().and_then(interpreter_version); let pip_version = if has_pip { root.as_deref().and_then(|root| package_version(root, "pip", python_version.as_deref())) } else { None }; let certbot_version = certbot.and_then(|path| { let certbot_root = python_environment_root(path)?; let certbot_python_version = if root.as_deref() == Some(certbot_root.as_path()) { python_version.as_deref() } else { None }; package_version(&certbot_root, "certbot", certbot_python_version) }); RuntimeVersions { python: python_version, pip: pip_version, certbot: certbot_version } }