use afterburner_cloud::afterburner_afb::Afb;
use afterburner_cloud::afterburner_afb::digest::hex as afb_hex;
use afterburner_cloud::afterburner_afb::manifest::Manifest;
use afterburner_cloud::afterburner_afb::pack::Builder;
use afterburner_cloud::pkg::LocalPackage;
use afterburner_wasi::pyodide_runner::{PyRuntime, resolve_runtime};
use anyhow::{Context, Result};
use std::collections::BTreeMap;
use std::path::Path;
pub const RUNTIME_TARGET: &str = "emscripten-pyodide";
pub const PYODIDE_WASM_MEMBER: &str = "precompiled/emscripten-pyodide/pyodide.wasm";
pub const STDLIB_MEMBER: &str = "precompiled/emscripten-pyodide/python_stdlib.zip";
pub fn compile_python_to_wasm(
local: LocalPackage,
_pkg_dir: &Path,
out_path: &Path,
wasm_only: bool,
) -> Result<()> {
let coord = super::super::registry::coord_str(&local);
let (source_bytes, _) = crate::cli::style::spin("packing source", || local.build())
.context("building source .afb")?;
let afb = Afb::from_bytes(&source_bytes).context("reparsing source .afb (this is a bug)")?;
let rt = crate::cli::style::spin("resolving Python runtime", resolve_runtime)
.map_err(|e| anyhow::anyhow!("resolving Python runtime: {e}"))?;
let pyodide_wasm = std::fs::read(&rt.wasm_path)
.with_context(|| format!("reading pyodide wasm from {}", rt.wasm_path.display()))?;
let stdlib_zip = std::fs::read(&rt.stdlib_path)
.with_context(|| format!("reading Python stdlib from {}", rt.stdlib_path.display()))?;
let pip_section = afb.manifest.pip.clone();
let pip_wheels: BTreeMap<String, Vec<u8>> = if pip_section.is_empty() {
BTreeMap::new()
} else {
crate::cli::style::spin("resolving pip wheels", || resolve_pip_wheels(&pip_section))
.context("resolving [pip] dependencies")?
};
bundle_python_afb(
&afb,
&rt,
pyodide_wasm,
stdlib_zip,
pip_wheels,
out_path,
&coord,
wasm_only,
)
}
fn resolve_pip_wheels(pip_section: &BTreeMap<String, String>) -> Result<BTreeMap<String, Vec<u8>>> {
use afterburner_cloud::pip_client::PipClient;
let client = PipClient::public();
let resolution = client
.resolve_all(pip_section)
.map_err(|e| anyhow::anyhow!("{e}"))?;
let mut out: BTreeMap<String, Vec<u8>> = BTreeMap::new();
for pkg in &resolution.packages {
let wheel_bytes = pack_files_as_wheel(&pkg.files);
let key = format!("vendor/pip/{}-{}-py3-none-any.whl", pkg.name, pkg.version);
out.insert(key, wheel_bytes);
}
Ok(out)
}
fn pack_files_as_wheel(files: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
let mut central: Vec<u8> = Vec::new();
let mut entry_count: u16 = 0;
for (rel, data) in files {
let name = rel.as_bytes();
let name_len = name.len() as u16;
let data_len = data.len() as u32;
let crc = crc32_ieee(data);
let local_offset = out.len() as u32;
out.extend_from_slice(b"PK\x03\x04");
out.extend_from_slice(&20u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&data_len.to_le_bytes());
out.extend_from_slice(&data_len.to_le_bytes());
out.extend_from_slice(&name_len.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(name);
out.extend_from_slice(data);
central.extend_from_slice(b"PK\x01\x02");
central.extend_from_slice(&20u16.to_le_bytes()); central.extend_from_slice(&20u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&crc.to_le_bytes());
central.extend_from_slice(&data_len.to_le_bytes());
central.extend_from_slice(&data_len.to_le_bytes());
central.extend_from_slice(&name_len.to_le_bytes());
central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u32.to_le_bytes()); central.extend_from_slice(&local_offset.to_le_bytes());
central.extend_from_slice(name);
entry_count = entry_count.saturating_add(1);
}
let central_start = out.len() as u32;
let central_len = central.len() as u32;
out.extend_from_slice(¢ral);
out.extend_from_slice(b"PK\x05\x06");
out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&entry_count.to_le_bytes());
out.extend_from_slice(&entry_count.to_le_bytes());
out.extend_from_slice(¢ral_len.to_le_bytes());
out.extend_from_slice(¢ral_start.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out
}
fn crc32_ieee(data: &[u8]) -> u32 {
let mut crc: u32 = !0u32;
for &b in data {
crc ^= b as u32;
for _ in 0..8 {
let carry = crc & 1;
crc >>= 1;
if carry != 0 {
crc ^= 0xEDB8_8320u32;
}
}
}
!crc
}
#[allow(clippy::too_many_arguments)]
fn bundle_python_afb(
afb: &Afb,
rt: &PyRuntime,
pyodide_wasm: Vec<u8>,
stdlib_zip: Vec<u8>,
pip_wheels: BTreeMap<String, Vec<u8>>,
out_path: &Path,
coord: &str,
wasm_only: bool,
) -> Result<()> {
let mut manifest: Manifest = afb.manifest.clone();
manifest.runtime.target = Some(RUNTIME_TARGET.to_owned());
manifest.metadata.insert(
"python_xy".to_owned(),
toml::Value::String(rt.python_xy.clone()),
);
let mut b = Builder::new(manifest, afb.manifold.clone());
if !wasm_only {
for (path, data) in &afb.source {
b = b.source(path.clone(), data.clone());
}
}
b = b.precompiled(PYODIDE_WASM_MEMBER, pyodide_wasm);
b = b.precompiled(STDLIB_MEMBER, stdlib_zip);
for (key, wheel_bytes) in &pip_wheels {
b = b.vendor(key.clone(), wheel_bytes.clone());
}
let (bytes, d) =
crate::cli::style::spin("packing", || b.build()).context("building Python .afb bundle")?;
std::fs::write(out_path, &bytes).with_context(|| format!("writing {}", out_path.display()))?;
println!(
"{} {} {}",
crate::cli::style::ok("compiled"),
crate::cli::style::accent(coord),
crate::cli::style::gold("(emscripten-pyodide bundle)")
);
super::super::registry::print_digest(bytes.len() as u64, &afb_hex(&d));
println!(
" {} {}",
crate::cli::style::muted("->"),
crate::cli::style::value(&out_path.display().to_string())
);
Ok(())
}
pub fn reconstruct_runtime_from_afb(
afb: &Afb,
tmp_root: &Path,
) -> Result<(PyRuntime, Vec<Vec<u8>>)> {
let wasm_bytes = afb.precompiled.get(PYODIDE_WASM_MEMBER).ok_or_else(|| {
anyhow::anyhow!(
"Python compiled .afb is missing {}; re-run `burn compile`",
PYODIDE_WASM_MEMBER
)
})?;
let stdlib_bytes = afb.precompiled.get(STDLIB_MEMBER).ok_or_else(|| {
anyhow::anyhow!(
"Python compiled .afb is missing {}; re-run `burn compile`",
STDLIB_MEMBER
)
})?;
std::fs::create_dir_all(tmp_root)
.with_context(|| format!("creating temp dir {}", tmp_root.display()))?;
let wasm_path = tmp_root.join("pyodide.wasm");
let stdlib_path = tmp_root.join("python_stdlib.zip");
std::fs::write(&wasm_path, wasm_bytes)
.with_context(|| format!("writing {}", wasm_path.display()))?;
std::fs::write(&stdlib_path, stdlib_bytes)
.with_context(|| format!("writing {}", stdlib_path.display()))?;
let python_xy = afb
.manifest
.metadata
.get("python_xy")
.and_then(|v| v.as_str())
.unwrap_or("3.13")
.to_owned();
let rt = PyRuntime {
wasm_path,
stdlib_path,
wheels: Vec::new(), python_xy,
};
let pip_wheel_bytes: Vec<Vec<u8>> = afb
.vendor
.iter()
.filter(|(k, _)| k.starts_with("vendor/pip/") && k.ends_with(".whl"))
.map(|(_, v)| v.clone())
.collect();
Ok((rt, pip_wheel_bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_target_constant() {
assert_eq!(RUNTIME_TARGET, "emscripten-pyodide");
}
#[test]
fn pyodide_wasm_member_path() {
assert!(PYODIDE_WASM_MEMBER.starts_with("precompiled/"));
assert!(PYODIDE_WASM_MEMBER.ends_with(".wasm"));
}
#[test]
fn stdlib_member_path() {
assert!(STDLIB_MEMBER.starts_with("precompiled/"));
assert!(STDLIB_MEMBER.ends_with(".zip"));
}
#[test]
fn pack_files_as_wheel_roundtrips_via_mount_wheel() {
let mut files = BTreeMap::new();
files.insert("mypkg/__init__.py".to_owned(), b"VALUE = 42\n".to_vec());
files.insert(
"mypkg/helper.py".to_owned(),
b"def hi(): return 'hello'\n".to_vec(),
);
let wheel = pack_files_as_wheel(&files);
assert!(wheel.len() > 4, "wheel must be non-empty");
assert_eq!(
&wheel[0..4],
b"PK\x03\x04",
"must start with local header sig"
);
let method = u16::from_le_bytes(wheel[8..10].try_into().unwrap());
assert_eq!(method, 0, "stored compression");
}
#[test]
fn crc32_ieee_known_value() {
assert_eq!(crc32_ieee(b""), 0);
assert_eq!(crc32_ieee(b"a"), 0xe8b7be43);
}
}