use std::io::Cursor;
use sha2::{Digest, Sha256};
#[derive(Debug, Clone)]
pub struct NativeExtension {
pub name: String,
pub bytes: Vec<u8>,
}
impl NativeExtension {
#[must_use]
pub fn new(name: impl Into<String>, bytes: Vec<u8>) -> Self {
Self {
name: name.into(),
bytes,
}
}
}
#[derive(Debug, Clone)]
pub struct WheelInfo {
pub name: String,
pub version: String,
pub python_files: Vec<(String, Vec<u8>)>,
pub native_extensions: Vec<NativeExtension>,
}
impl WheelInfo {
#[must_use]
pub fn has_native_extensions(&self) -> bool {
!self.native_extensions.is_empty()
}
}
pub fn parse_wheel(wheel_bytes: &[u8]) -> Result<WheelInfo, WheelParseError> {
use std::io::Read;
let reader = Cursor::new(wheel_bytes);
let mut archive =
zip::ZipArchive::new(reader).map_err(|e| WheelParseError::InvalidZip(e.to_string()))?;
let mut python_files = Vec::new();
let mut native_extensions = Vec::new();
let mut name = String::new();
let mut version = String::new();
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| WheelParseError::InvalidZip(e.to_string()))?;
let file_name = file.name().to_string();
if file_name.ends_with(".dist-info/METADATA") {
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| WheelParseError::ReadError(e.to_string()))?;
for line in contents.lines() {
if let Some(n) = line.strip_prefix("Name: ") {
name = n.to_string();
} else if let Some(v) = line.strip_prefix("Version: ") {
version = v.to_string();
}
}
}
else if file_name.ends_with(".so") && file_name.contains("wasm32-wasi") {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|e| WheelParseError::ReadError(e.to_string()))?;
let so_name = file_name
.rsplit('/')
.next()
.unwrap_or(&file_name)
.to_string();
native_extensions.push(NativeExtension {
name: so_name,
bytes,
});
}
else if file_name.ends_with(".py") || file_name.ends_with(".pyi") {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|e| WheelParseError::ReadError(e.to_string()))?;
python_files.push((file_name, bytes));
}
}
Ok(WheelInfo {
name,
version,
python_files,
native_extensions,
})
}
#[derive(Debug, Clone)]
pub enum WheelParseError {
InvalidZip(String),
ReadError(String),
}
impl std::fmt::Display for WheelParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidZip(e) => write!(f, "invalid ZIP file: {e}"),
Self::ReadError(e) => write!(f, "failed to read file: {e}"),
}
}
}
impl std::error::Error for WheelParseError {}
pub mod base_libraries {
pub const LIBC: &[u8] =
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/libs/libc.so.zst"));
pub const LIBCXX: &[u8] =
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/libs/libc++.so.zst"));
pub const LIBCXXABI: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/libs/libc++abi.so.zst"
));
pub const LIBPYTHON: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/libs/libpython3.14.so.zst"
));
pub const LIBWASI_EMULATED_MMAN: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/libs/libwasi-emulated-mman.so.zst"
));
pub const LIBWASI_EMULATED_PROCESS_CLOCKS: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/libs/libwasi-emulated-process-clocks.so.zst"
));
pub const LIBWASI_EMULATED_GETPID: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/libs/libwasi-emulated-getpid.so.zst"
));
pub const LIBWASI_EMULATED_SIGNAL: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/libs/libwasi-emulated-signal.so.zst"
));
pub const WASI_ADAPTER: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/libs/wasi_snapshot_preview1.reactor.wasm.zst"
));
pub const LIBERYX_RUNTIME: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/liberyx_runtime.so.zst"));
pub const LIBERYX_BINDINGS: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/liberyx_bindings.so.zst"));
}
#[must_use]
pub fn compute_cache_key(extensions: &[NativeExtension]) -> [u8; 32] {
let mut hasher = Sha256::new();
let mut sorted: Vec<_> = extensions.iter().collect();
sorted.sort_by(|a, b| a.name.cmp(&b.name));
for ext in sorted {
hasher.update(ext.name.as_bytes());
hasher.update((ext.bytes.len() as u64).to_le_bytes());
hasher.update(&ext.bytes);
}
hasher.finalize().into()
}
pub fn link_with_extensions(extensions: &[NativeExtension]) -> Result<Vec<u8>, LinkError> {
use wit_component::Linker;
let libc = decompress_zstd(base_libraries::LIBC)?;
let libcxx = decompress_zstd(base_libraries::LIBCXX)?;
let libcxxabi = decompress_zstd(base_libraries::LIBCXXABI)?;
let libpython = decompress_zstd(base_libraries::LIBPYTHON)?;
let wasi_mman = decompress_zstd(base_libraries::LIBWASI_EMULATED_MMAN)?;
let wasi_clocks = decompress_zstd(base_libraries::LIBWASI_EMULATED_PROCESS_CLOCKS)?;
let wasi_getpid = decompress_zstd(base_libraries::LIBWASI_EMULATED_GETPID)?;
let wasi_signal = decompress_zstd(base_libraries::LIBWASI_EMULATED_SIGNAL)?;
let adapter = decompress_zstd(base_libraries::WASI_ADAPTER)?;
let runtime = decompress_zstd(base_libraries::LIBERYX_RUNTIME)?;
let bindings = decompress_zstd(base_libraries::LIBERYX_BINDINGS)?;
let mut linker = Linker::default().validate(true).use_built_in_libdl(true);
linker = linker
.library("libwasi-emulated-process-clocks.so", &wasi_clocks, false)
.map_err(|e| {
LinkError::Library("libwasi-emulated-process-clocks.so".into(), e.to_string())
})?
.library("libwasi-emulated-signal.so", &wasi_signal, false)
.map_err(|e| LinkError::Library("libwasi-emulated-signal.so".into(), e.to_string()))?
.library("libwasi-emulated-mman.so", &wasi_mman, false)
.map_err(|e| LinkError::Library("libwasi-emulated-mman.so".into(), e.to_string()))?
.library("libwasi-emulated-getpid.so", &wasi_getpid, false)
.map_err(|e| LinkError::Library("libwasi-emulated-getpid.so".into(), e.to_string()))?
.library("libc.so", &libc, false)
.map_err(|e| LinkError::Library("libc.so".into(), e.to_string()))?
.library("libc++abi.so", &libcxxabi, false)
.map_err(|e| LinkError::Library("libc++abi.so".into(), e.to_string()))?
.library("libc++.so", &libcxx, false)
.map_err(|e| LinkError::Library("libc++.so".into(), e.to_string()))?
.library("libpython3.14.so", &libpython, false)
.map_err(|e| LinkError::Library("libpython3.14.so".into(), e.to_string()))?
.library("liberyx_runtime.so", &runtime, false)
.map_err(|e| LinkError::Library("liberyx_runtime.so".into(), e.to_string()))?
.library("liberyx_bindings.so", &bindings, false)
.map_err(|e| LinkError::Library("liberyx_bindings.so".into(), e.to_string()))?;
for ext in extensions {
linker = linker
.library(&ext.name, &ext.bytes, true)
.map_err(|e| LinkError::Extension(ext.name.clone(), e.to_string()))?;
}
linker = linker
.adapter("wasi_snapshot_preview1", &adapter)
.map_err(|e| LinkError::Adapter(e.to_string()))?;
linker
.encode()
.map_err(|e| LinkError::Encode(e.to_string()))
}
fn decompress_zstd(data: &[u8]) -> Result<Vec<u8>, LinkError> {
zstd::decode_all(Cursor::new(data)).map_err(|e| LinkError::Decompress(e.to_string()))
}
#[derive(Debug, Clone)]
pub enum LinkError {
Library(String, String),
Extension(String, String),
Adapter(String),
Encode(String),
Decompress(String),
}
impl std::fmt::Display for LinkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Library(name, e) => write!(f, "failed to add base library {name}: {e}"),
Self::Extension(name, e) => write!(f, "failed to add extension {name}: {e}"),
Self::Adapter(e) => write!(f, "failed to add WASI adapter: {e}"),
Self::Encode(e) => write!(f, "failed to encode component: {e}"),
Self::Decompress(e) => write!(f, "failed to decompress library: {e}"),
}
}
}
impl std::error::Error for LinkError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_key_determinism() {
let ext1 = NativeExtension::new("a.so", vec![1, 2, 3]);
let ext2 = NativeExtension::new("b.so", vec![4, 5, 6]);
let key1 = compute_cache_key(&[ext1.clone(), ext2.clone()]);
let key2 = compute_cache_key(&[ext2, ext1]);
assert_eq!(key1, key2);
}
#[test]
fn test_wheel_parse_error_display() {
let err = WheelParseError::InvalidZip("test error".to_string());
assert!(err.to_string().contains("test error"));
}
}