Skip to content

Commit b14dae3

Browse files
authored
rearrange repo and add spin-python-cli (#8)
* rearrange repo and add `spin-python-cli` `spin-python-cli` is a stand-alone binary for converting a Python app to a Spin-compatible Wasm module. The intention is that we'll publish this as a `Spin` plugin, analogous to the JS/TS SDK's `js2wasm` utility. The implementation is a bit more complicated than `js2wasm` because the Python interpreter loads the core library, the app, and its dependencies from disk, so we have to: - Embed the core library in the binary and extract it to a temporary directory prior to pre-initializing with Wizer - Search for the `pipenv` `site-packages` directory and add it to `PYTHONPATH` - Set various environment variables so the Python interpreter knows where to find things (and not buffer stdio) - Set another environment variable so the pre-init function knows which module the app lives in Signed-off-by: Joel Dice <[email protected]> * make WASI SDK path configurable and clean up Makefile Signed-off-by: Joel Dice <[email protected]> * improve warning message when `site-packages` not found Signed-off-by: Joel Dice <[email protected]> * proceed without error if `pipenv` doesn't recognize the CWD Signed-off-by: Joel Dice <[email protected]> --------- Signed-off-by: Joel Dice <[email protected]>
1 parent fe0a43c commit b14dae3

File tree

16 files changed

+396
-44
lines changed

16 files changed

+396
-44
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/target
22
/Cargo.lock
3-
py/__pycache__
3+
__pycache__
4+
*.wasm

Cargo.toml

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
1-
[package]
2-
name = "python-wasi"
3-
version = "0.1.0"
4-
edition = "2021"
5-
6-
[lib]
7-
crate-type = [ "cdylib" ]
8-
9-
[dependencies]
10-
anyhow = "1"
11-
bytes = { version = "1.2.1", features = ["serde"] }
12-
http = "0.2"
13-
spin-sdk = { git = "https://github.com/fermyon/spin" }
14-
wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "dde4694aaa6acf9370206527a798ac4ba6a8c5b8" }
15-
pyo3 = { version = "0.17.3", features = [ "abi3-py310" ] }
16-
once_cell = "1.16.0"
1+
[workspace]
2+
members = [
3+
"crates/spin-python-engine",
4+
"crates/spin-python-cli"
5+
]

Makefile

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
WASI_SDK_PATH ?= /opt/wasi-sdk
22

3-
.PHONY: build
4-
build: target/config.txt
5-
PYO3_CONFIG_FILE=$$(pwd)/target/config.txt cargo build --release --target=wasm32-wasi
6-
env -i PYTHONUNBUFFERED=1 \
7-
PYTHONHOME=/python \
8-
PYTHONPATH=/python:/py:/site-packages \
9-
$$(which wizer) \
10-
target/wasm32-wasi/release/python_wasi.wasm \
11-
--inherit-env true \
12-
--wasm-bulk-memory true \
13-
--allow-wasi \
14-
--dir py \
15-
--mapdir python::$$(pwd)/cpython/builddir/wasi/install/lib/python3.11 \
16-
--mapdir site-packages::$$(cd py && find $$(pipenv --venv)/lib -name site-packages | head -1) \
17-
-o target/wasm32-wasi/release/python-wasi-wizer.wasm
3+
target/release/spin-python: \
4+
target/wasm32-wasi/release/spin_python_engine.wasm \
5+
crates/spin-python-cli/build.rs \
6+
crates/spin-python-cli/src/main.rs
7+
cd crates/spin-python-cli && \
8+
SPIN_PYTHON_ENGINE_PATH=../../$< \
9+
SPIN_PYTHON_CORE_LIBRARY_PATH=$$(pwd)/../../cpython/builddir/wasi/install/lib/python3.11 \
10+
cargo build --release $(BUILD_TARGET)
1811

19-
target/config.txt:
12+
target/wasm32-wasi/release/spin_python_engine.wasm: \
13+
crates/spin-python-engine/src/lib.rs \
14+
crates/spin-python-engine/build.rs \
15+
target/pyo3-config.txt
16+
cd crates/spin-python-engine && \
17+
PYO3_CONFIG_FILE=$$(pwd)/../../target/config.txt \
18+
cargo build --release --target=wasm32-wasi
19+
20+
target/pyo3-config.txt: crates/spin-python-engine/pyo3-config.txt
2021
mkdir -p target
21-
cp config.txt target
22+
cp $< target
2223
echo "lib_dir=$$(pwd)/cpython/builddir/wasi" >> $@

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This is an experiment to build a Spin Python SDK using CPython, Wizer, and PyO3.
44

55
## Prerequisites
66

7+
- [WASI SDK](https://github.com/WebAssembly/wasi-sdk) v16 or later, installed in /opt/wasi-sdk
78
- [CPython](https://github.com/python/cpython) build prereqs (e.g. Make, Clang, etc.)
89
- [Rust](https://rustup.rs/) (including `wasm32-wasi` target)
910
- [Wizer](https://github.com/bytecodealliance/wizer) v1.6.0 or later
@@ -30,10 +31,17 @@ make install
3031
cd ../../..
3132
```
3233

33-
Then, build and run this app:
34+
Then, build the `spin-python-cli`:
3435

3536
```
36-
(cd py && pipenv install)
37+
make
38+
```
39+
40+
Finally, build and run the example app:
41+
42+
```
43+
cd examples/hello
44+
pipenv install
3745
spin build
3846
spin up
3947
```

crates/spin-python-cli/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "spin-python-cli"
3+
description = "A Spin plugin to convert Python apps to Spin-compatible WebAssembly modules"
4+
version = "0.1.0"
5+
authors = [ "Fermyon Engineering <[email protected]>" ]
6+
edition = "2021"
7+
8+
[[bin]]
9+
name = "spin-python"
10+
path = "src/main.rs"
11+
12+
[dependencies]
13+
anyhow = "1.0.68"
14+
clap = { version = "4.1.4", features = [ "derive" ] }
15+
tar = "0.4.38"
16+
tempfile = "3.3.0"
17+
wizer = "1.6.0"
18+
zstd = "0.10.0"
19+
20+
[build-dependencies]
21+
anyhow = "1.0.68"
22+
tar = "0.4.38"
23+
zstd = "0.10.0"

crates/spin-python-cli/build.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#![deny(warnings)]
2+
3+
use {
4+
anyhow::{bail, Result},
5+
std::{
6+
env,
7+
fs::{self, File},
8+
io::{self, Write},
9+
path::{Path, PathBuf},
10+
},
11+
tar::Builder,
12+
zstd::Encoder,
13+
};
14+
15+
const ZSTD_COMPRESSION_LEVEL: i32 = 19;
16+
17+
fn main() -> Result<()> {
18+
println!("cargo:rerun-if-changed=build.rs");
19+
20+
if let Ok("cargo-clippy") = env::var("CARGO_CFG_FEATURE").as_deref() {
21+
stubs_for_clippy()
22+
} else {
23+
package_engine_and_core_library()
24+
}
25+
}
26+
27+
fn stubs_for_clippy() -> Result<()> {
28+
println!("cargo:warning=using stubbed engine and core library for static analysis purposes...");
29+
30+
let engine_path = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("engine.wasm.zstd");
31+
32+
if !engine_path.exists() {
33+
Encoder::new(File::create(engine_path)?, ZSTD_COMPRESSION_LEVEL)?.do_finish()?;
34+
}
35+
36+
let core_library_path =
37+
PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("python-lib.tar.zstd");
38+
39+
if !core_library_path.exists() {
40+
Builder::new(Encoder::new(
41+
File::create(core_library_path)?,
42+
ZSTD_COMPRESSION_LEVEL,
43+
)?)
44+
.into_inner()?
45+
.do_finish()?;
46+
}
47+
48+
Ok(())
49+
}
50+
51+
fn package_engine_and_core_library() -> Result<()> {
52+
let override_engine_path = env::var_os("SPIN_PYTHON_ENGINE_PATH");
53+
let engine_path = if let Some(path) = override_engine_path {
54+
PathBuf::from(path)
55+
} else {
56+
let mut path = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
57+
path.pop();
58+
path.pop();
59+
path.join("target/wasm32-wasi/release/spin_python_engine.wasm")
60+
};
61+
62+
println!("cargo:rerun-if-changed={engine_path:?}");
63+
64+
if engine_path.exists() {
65+
let copied_engine_path =
66+
PathBuf::from(env::var("OUT_DIR").unwrap()).join("engine.wasm.zstd");
67+
68+
let mut encoder = Encoder::new(File::create(copied_engine_path)?, ZSTD_COMPRESSION_LEVEL)?;
69+
io::copy(&mut File::open(engine_path)?, &mut encoder)?;
70+
encoder.do_finish()?;
71+
} else {
72+
bail!("no such file: {}", engine_path.display())
73+
}
74+
75+
let override_core_library_path = env::var_os("SPIN_PYTHON_CORE_LIBRARY_PATH");
76+
let core_library_path = if let Some(path) = override_core_library_path {
77+
PathBuf::from(path)
78+
} else {
79+
let mut path = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
80+
path.pop();
81+
path.pop();
82+
path.join("cpython/builddir/wasi/install/lib/python3.11")
83+
};
84+
85+
println!("cargo:rerun-if-changed={core_library_path:?}");
86+
87+
if core_library_path.exists() {
88+
let copied_core_library_path =
89+
PathBuf::from(env::var("OUT_DIR").unwrap()).join("python-lib.tar.zstd");
90+
91+
let mut builder = Builder::new(Encoder::new(
92+
File::create(copied_core_library_path)?,
93+
ZSTD_COMPRESSION_LEVEL,
94+
)?);
95+
96+
add(&mut builder, &core_library_path, &core_library_path)?;
97+
98+
builder.into_inner()?.do_finish()?;
99+
} else {
100+
bail!("no such directory: {}", core_library_path.display())
101+
}
102+
103+
Ok(())
104+
}
105+
106+
fn include(path: &Path) -> bool {
107+
!(matches!(
108+
path.extension().and_then(|e| e.to_str()),
109+
Some("a" | "pyc" | "whl")
110+
) || matches!(
111+
path.file_name().and_then(|e| e.to_str()),
112+
Some("Makefile" | "Changelog" | "NEWS.txt")
113+
))
114+
}
115+
116+
fn add(builder: &mut Builder<impl Write>, root: &Path, path: &Path) -> Result<()> {
117+
if path.is_dir() {
118+
for entry in fs::read_dir(path)? {
119+
add(builder, root, &entry?.path())?;
120+
}
121+
} else if include(path) {
122+
builder.append_file(path.strip_prefix(root)?, &mut File::open(path)?)?;
123+
}
124+
125+
Ok(())
126+
}

0 commit comments

Comments
 (0)