use std::collections::HashMap;
use std::ffi::CString;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList, PyListMethods};
use crate::error::{Error, Result};
pub struct Prepare<'a> {
pub module_paths: &'a [PathBuf],
pub virtual_environment: Option<&'a Path>,
pub environment: &'a HashMap<String, String>,
}
pub fn initialize(virtual_environment: Option<&Path>) -> Result<()> {
static INITIALIZATION: Mutex<()> = Mutex::new(());
static ENVIRONMENT: OnceLock<Option<PathBuf>> = OnceLock::new();
let _initialization = INITIALIZATION.lock().map_err(|_| {
Error::Python("interpreter initialization lock poisoned".to_owned())
})?;
let executable = match virtual_environment {
Some(venv) => {
let config = std::fs::read_to_string(venv.join("pyvenv.cfg"))
.map_err(|error| {
Error::Python(format!(
"cannot read virtual environment pyvenv.cfg: {error}"
))
})?;
if !is_python_315_environment(&config) {
return Err(Error::Python(
"virtual environment must be created from CPython 3.15"
.to_owned(),
));
}
if cfg!(windows) {
venv.join("Scripts").join("python.exe")
} else {
venv.join("bin").join("python")
}
}
None => PathBuf::from(env!("CLAVIRON_PYTHON_EXECUTABLE")),
};
if !executable.is_file() {
return Err(Error::Python(format!(
"selected CPython executable does not exist: {}",
executable.display()
)));
}
let executable = CString::new(path_text(&executable)?)
.map_err(|error| Error::Python(error.to_string()))?;
let environment = virtual_environment
.map(Path::canonicalize)
.transpose()
.map_err(|error| Error::Python(error.to_string()))?;
unsafe {
if pyo3::ffi::Py_IsInitialized() != 0 {
return if ENVIRONMENT.get() == Some(&environment) {
Ok(())
} else {
Err(Error::Python(
"interpreter ownership/environment differs; replace the worker instead of switching interpreters".to_owned(),
))
};
}
let mut config = std::mem::MaybeUninit::<pyo3::ffi::PyConfig>::uninit();
pyo3::ffi::PyConfig_InitPythonConfig(config.as_mut_ptr());
let mut config = config.assume_init();
config.install_signal_handlers = 0;
config.parse_argv = 0;
config.configure_c_stdio = 0;
config.use_environment = 0;
config.user_site_directory = 0;
config.safe_path = 1;
let config_ptr: *mut pyo3::ffi::PyConfig = &mut config;
let status = pyo3::ffi::PyConfig_SetBytesString(
config_ptr,
&raw mut (*config_ptr).executable,
executable.as_ptr(),
);
if pyo3::ffi::PyStatus_Exception(status) != 0 {
pyo3::ffi::PyConfig_Clear(&mut config);
return Err(Error::Python(
"could not set the interpreter's executable".to_owned(),
));
}
let status = pyo3::ffi::Py_InitializeFromConfig(&config);
pyo3::ffi::PyConfig_Clear(&mut config);
if pyo3::ffi::PyStatus_Exception(status) != 0 {
return Err(Error::Python(
"the interpreter failed to start".to_owned(),
));
}
pyo3::ffi::PyEval_SaveThread();
ENVIRONMENT.set(environment).map_err(|_| {
Error::Python(
"interpreter environment was already initialized".to_owned(),
)
})?;
}
Ok(())
}
pub fn prepare(
py: Python<'_>,
setup: &Prepare<'_>,
) -> Result<()> {
let sys = py.import("sys").map_err(|error| python_error(py, error))?;
let version: (u8, u8) = sys
.getattr("version_info")
.and_then(|value| {
Ok((value.get_item(0)?.extract()?, value.get_item(1)?.extract()?))
})
.map_err(|error| python_error(py, error))?;
if version != (3, 15) {
return Err(Error::Python(
"claviron-python requires CPython 3.15".to_owned(),
));
}
if let Some(environment) = setup.virtual_environment {
let prefix: PathBuf = sys
.getattr("prefix")
.and_then(|value| value.extract())
.map_err(|error| python_error(py, error))?;
let requested = environment
.canonicalize()
.map_err(|error| Error::Python(error.to_string()))?;
let current = prefix
.canonicalize()
.map_err(|error| Error::Python(error.to_string()))?;
if requested != current {
return Err(Error::Python(
"the initialized interpreter belongs to a different environment; replace the worker to change environments".to_owned(),
));
}
}
let sys_path =
sys.getattr("path").map_err(|error| python_error(py, error))?;
let sys_path = sys_path
.cast_into::<PyList>()
.map_err(|error| Error::Python(error.to_string()))?;
for path in setup.module_paths.iter().rev() {
let path = path_text(path)?;
if !sys_path.contains(path).map_err(|error| python_error(py, error))? {
sys_path
.insert(0, path)
.map_err(|error| python_error(py, error))?;
}
}
application_environment(py, setup.environment)
.map_err(|error| python_error(py, error))
}
fn application_environment(
py: Python<'_>,
values: &HashMap<String, String>,
) -> PyResult<()> {
let environ = py.import("os")?.getattr("environ")?;
let data = environ.getattr("_data")?;
let encode_key = environ.getattr("encodekey")?;
let encode_value = environ.getattr("encodevalue")?;
let kept = PyDict::new(py);
for key in claviron::runtime::env::RUNTIME_ESSENTIALS {
let encoded = encode_key.call1((*key,))?;
if data.contains(&encoded)? {
kept.set_item(&encoded, data.get_item(&encoded)?)?;
}
}
for (key, value) in values {
kept.set_item(
encode_key.call1((key,))?,
encode_value.call1((value,))?,
)?;
}
data.call_method0("clear")?;
data.call_method1("update", (kept,))?;
Ok(())
}
fn is_python_315_environment(config: &str) -> bool {
let mut declared = false;
for (key, value) in config.lines().filter_map(|line| line.split_once('=')) {
if matches!(key.trim(), "version" | "version_info") {
if !value.trim().split('.').take(2).eq(["3", "15"]) {
return false;
}
declared = true;
}
}
declared
}
#[cfg(test)]
#[path = "../tests/environment.rs"]
mod environment_tests;
pub(crate) fn path_text(path: &Path) -> Result<&str> {
path.to_str().ok_or_else(|| {
Error::Python(format!(
"Python path is not valid UTF-8: {}",
path.display()
))
})
}
pub(crate) fn python_error(
py: Python<'_>,
error: PyErr,
) -> Error {
let rendered = py
.import("traceback")
.and_then(|traceback| traceback.call_method1("format_exception", (error.value(py),)))
.and_then(|lines| lines.call_method0("__iter__"))
.and_then(|iterator| {
let mut rendered = String::new();
loop {
match iterator.call_method0("__next__") {
Ok(line) => rendered.push_str(&line.extract::<String>()?),
Err(stop) if stop.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) => {
break;
}
Err(error) => return Err(error)
}
}
Ok(rendered)
})
.unwrap_or_else(|_| error.to_string());
Error::Python(rendered.trim().to_owned())
}