use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::error::Error;
const EMBEDDED_STDLIB: &[u8] = include_bytes!("../python-stdlib.tar.zst");
#[derive(Debug, Clone)]
pub struct EmbeddedStdlib {
stdlib_path: PathBuf,
}
impl EmbeddedStdlib {
pub fn get() -> Result<&'static Self, Error> {
static STDLIB: OnceLock<Result<EmbeddedStdlib, String>> = OnceLock::new();
STDLIB
.get_or_init(|| Self::extract().map_err(|e| e.to_string()))
.as_ref()
.map_err(|e| Error::Initialization(e.clone()))
}
fn extract() -> Result<Self, Error> {
let temp_base = std::env::temp_dir().join("eryx-embedded");
std::fs::create_dir_all(&temp_base)
.map_err(|e| Error::Initialization(format!("Failed to create temp directory: {e}")))?;
let stdlib_path = Self::extract_stdlib(&temp_base)?;
Ok(Self { stdlib_path })
}
fn extract_stdlib(temp_dir: &Path) -> Result<PathBuf, Error> {
let stdlib_path = temp_dir.join("python-stdlib");
if stdlib_path.exists() {
if stdlib_path.join("encodings").exists() {
tracing::debug!(path = %stdlib_path.display(), "Using cached stdlib");
return Ok(stdlib_path);
}
let _ = std::fs::remove_dir_all(&stdlib_path);
}
tracing::info!(path = %stdlib_path.display(), "Extracting embedded Python stdlib");
let temp_extract_dir = tempfile::TempDir::with_prefix_in("python-stdlib-", temp_dir)
.map_err(|e| {
Error::Initialization(format!("Failed to create temp extract directory: {e}"))
})?;
let decoder = zstd::Decoder::new(EMBEDDED_STDLIB)
.map_err(|e| Error::Initialization(format!("Failed to create zstd decoder: {e}")))?;
let mut archive = tar::Archive::new(decoder);
archive
.unpack(temp_extract_dir.path())
.map_err(|e| Error::Initialization(format!("Failed to extract stdlib archive: {e}")))?;
let extracted_stdlib = temp_extract_dir.path().join("python-stdlib");
if !extracted_stdlib.join("encodings").exists() {
return Err(Error::Initialization(
"Stdlib extraction failed: encodings/ not found".to_string(),
));
}
match std::fs::rename(&extracted_stdlib, &stdlib_path) {
Ok(()) => {
}
Err(_) if stdlib_path.join("encodings").exists() => {
tracing::debug!("Stdlib extracted by another process");
}
Err(e) => {
return Err(Error::Initialization(format!(
"Failed to rename stdlib directory: {e}"
)));
}
}
Ok(stdlib_path)
}
#[must_use]
pub fn path(&self) -> &Path {
&self.stdlib_path
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_stdlib_is_included() {
assert!(
EMBEDDED_STDLIB.len() > 1_000_000,
"Embedded stdlib should be > 1MB"
);
}
}