pub mod doctor;
pub mod export_schema;
pub mod manifest;
mod metadata;
pub mod migrate;
mod version;
mod virtualenv;
pub use export_schema::{EXPORT_SCHEMA_VERSION, ExportSchema};
pub use manifest::ScoopManifest;
pub use metadata::Metadata;
pub use version::VersionService;
pub use virtualenv::VirtualenvService;
pub const SCOOP_ACTIVE_ENV: &str = "SCOOP_ACTIVE";
pub fn parse_pyvenv_version(venv_path: &std::path::Path) -> Option<String> {
let cfg_path = venv_path.join("pyvenv.cfg");
let content = std::fs::read_to_string(&cfg_path).ok()?;
content.lines().find_map(pyvenv_version_from_line)
}
pub(crate) fn pyvenv_version_from_line(line: &str) -> Option<String> {
let (key, value) = line.split_once('=')?;
matches!(key.trim(), "version" | "version_info").then(|| normalize_pyvenv_version(value.trim()))
}
fn normalize_pyvenv_version(raw: &str) -> String {
let numeric: Vec<&str> = raw
.split('.')
.take_while(|seg| !seg.is_empty() && seg.bytes().all(|b| b.is_ascii_digit()))
.take(3)
.collect();
if numeric.is_empty() {
raw.to_string()
} else {
numeric.join(".")
}
}
pub fn list_installed_packages(venv_path: &std::path::Path) -> Vec<(String, String)> {
let pip_path = venv_path.join("bin").join("pip");
if !pip_path.exists() {
return Vec::new();
}
let output = std::process::Command::new(&pip_path)
.args(["list", "--format=json"])
.output();
match output {
Ok(out) if out.status.success() => {
let stdout = String::from_utf8_lossy(&out.stdout);
serde_json::from_str::<Vec<serde_json::Value>>(&stdout)
.unwrap_or_default()
.into_iter()
.filter_map(|p| {
Some((
p.get("name")?.as_str()?.to_string(),
p.get("version")?.as_str()?.to_string(),
))
})
.collect()
}
_ => Vec::new(),
}
}
pub fn get_active_env() -> Option<String> {
std::env::var(SCOOP_ACTIVE_ENV).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn venv_with_cfg(contents: &str) -> tempfile::TempDir {
let dir = tempfile::TempDir::new().unwrap();
let mut f = std::fs::File::create(dir.path().join("pyvenv.cfg")).unwrap();
f.write_all(contents.as_bytes()).unwrap();
dir
}
#[test]
fn parse_pyvenv_version_reads_stdlib_version_key() {
let dir = venv_with_cfg("home = /usr/bin\nversion = 3.12.12\n");
assert_eq!(
parse_pyvenv_version(dir.path()),
Some("3.12.12".to_string())
);
}
#[test]
fn parse_pyvenv_version_reads_uv_version_info_key() {
let dir =
venv_with_cfg("home = /x\nimplementation = CPython\nversion_info = 3.14.3.final.0\n");
assert_eq!(parse_pyvenv_version(dir.path()), Some("3.14.3".to_string()));
}
#[test]
fn parse_pyvenv_version_missing_returns_none() {
let dir = venv_with_cfg("home = /x\nimplementation = CPython\n");
assert_eq!(parse_pyvenv_version(dir.path()), None);
}
#[test]
fn normalize_pyvenv_version_truncates_suffix() {
assert_eq!(normalize_pyvenv_version("3.14.3.final.0"), "3.14.3");
assert_eq!(normalize_pyvenv_version("3.12.12"), "3.12.12");
assert_eq!(normalize_pyvenv_version("3.12"), "3.12");
assert_eq!(normalize_pyvenv_version("3"), "3");
}
#[test]
fn normalize_pyvenv_version_non_numeric_falls_back() {
assert_eq!(normalize_pyvenv_version("garbage"), "garbage");
assert_eq!(normalize_pyvenv_version(""), "");
assert_eq!(normalize_pyvenv_version("3..4"), "3");
assert_eq!(normalize_pyvenv_version("v3.12"), "v3.12");
}
#[test]
fn list_installed_packages_nonexistent_path_returns_empty() {
let pkgs = list_installed_packages(std::path::Path::new("/nonexistent/path/to/venv"));
assert!(pkgs.is_empty());
}
#[test]
fn list_installed_packages_no_pip_returns_empty() {
let temp = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join("bin")).unwrap();
assert!(list_installed_packages(temp.path()).is_empty());
}
}