use pyo3::prelude::*;
pub(crate) fn configure_python_sys_path(py: Python<'_>) -> PyResult<()> {
let sys = py.import_bound("sys")?;
let workspace_root = match std::env::var("CARGO_MANIFEST_DIR").ok() {
Some(dir) if !dir.is_empty() => discover_venv_root(std::path::Path::new(&dir)),
_ => {
tracing::warn!(
"CARGO_MANIFEST_DIR not set or empty, falling back to current directory for venv discovery"
);
std::env::current_dir().map_err(|e| {
pyo3::exceptions::PyRuntimeError::new_err(format!(
"Failed to determine current directory for venv discovery: {e}"
))
})?
}
};
let venv = workspace_root.join(".venv");
tracing::debug!(
workspace_root = %workspace_root.display(),
venv = %venv.display(),
venv_exists = venv.is_dir(),
"Runtime thread: venv discovery"
);
if !venv.is_dir() {
return Ok(());
}
let os = py.import_bound("os")?;
let environ = os.getattr("environ")?;
environ.set_item("VIRTUAL_ENV", venv.to_string_lossy().to_string())?;
tracing::debug!(path = %venv.display(), "Set VIRTUAL_ENV in Python os.environ");
let version_info = sys.getattr("version_info")?;
let major: u32 = version_info.getattr("major")?.extract()?;
let minor: u32 = version_info.getattr("minor")?.extract()?;
let py_version = format!("{major}.{minor}");
let site_packages = venv
.join("lib")
.join(format!("python{py_version}"))
.join("site-packages");
if site_packages.is_dir() {
let sp_str = site_packages.to_string_lossy().to_string();
let site_mod = py.import_bound("site")?;
site_mod.call_method1("addsitedir", (sp_str.as_str(),))?;
tracing::debug!(path = %sp_str, "Added venv site-packages via site.addsitedir()");
}
Ok(())
}
pub(crate) fn discover_venv_root(start: &std::path::Path) -> std::path::PathBuf {
let mut current = start.to_path_buf();
loop {
if current.join(".venv").is_dir() {
return current;
}
match current.parent() {
Some(p) if p != current => current = p.to_path_buf(),
_ => {
tracing::debug!(
"No .venv found walking up from {}, using start dir",
start.display()
);
return start.to_path_buf();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discover_venv_root_finds_venv_in_current_dir() {
let tmp = tempfile::tempdir().expect("create temp dir");
std::fs::create_dir(tmp.path().join(".venv")).expect("create .venv");
let result = discover_venv_root(tmp.path());
assert_eq!(result, tmp.path());
}
#[test]
fn discover_venv_root_walks_up_to_parent() {
let tmp = tempfile::tempdir().expect("create temp dir");
std::fs::create_dir(tmp.path().join(".venv")).expect("create .venv");
let child = tmp.path().join("crates").join("my-crate");
std::fs::create_dir_all(&child).expect("create child dirs");
let result = discover_venv_root(&child);
assert_eq!(result, tmp.path());
}
#[test]
fn discover_venv_root_falls_back_to_start_when_no_venv() {
let tmp = tempfile::tempdir().expect("create temp dir");
let result = discover_venv_root(tmp.path());
assert_eq!(result, tmp.path());
}
#[test]
fn discover_venv_root_stops_at_nearest_venv() {
let tmp = tempfile::tempdir().expect("create temp dir");
std::fs::create_dir(tmp.path().join(".venv")).expect("create root .venv");
let child = tmp.path().join("sub");
std::fs::create_dir_all(&child).expect("create sub dir");
std::fs::create_dir(child.join(".venv")).expect("create child .venv");
let result = discover_venv_root(&child);
assert_eq!(result, child);
}
}