1use std::path::Path;
2
3use anyhow::Error;
4use heck::{ToPascalCase, ToSnakeCase};
5use minijinja::Environment;
6use once_cell::sync::Lazy;
7use wai_bindgen_gen_core::Generator;
8use wai_bindgen_gen_wasmer_py::WasmerPy;
9
10use crate::{
11 types::{Interface, Package},
12 Files, Metadata, Module, SourceFile,
13};
14
15static TEMPLATES: Lazy<Environment> = Lazy::new(|| {
16 let mut env = Environment::new();
17 env.add_template(
18 "bindings.__init__.py",
19 include_str!("bindings.__init__.py.j2"),
20 )
21 .unwrap();
22 env.add_template(
23 "top_level.__init__.py",
24 include_str!("top_level.__init__.py.j2"),
25 )
26 .unwrap();
27 env.add_template("MANIFEST.in", include_str!("MANIFEST.in.j2"))
28 .unwrap();
29 env.add_template(
30 "commands.__init__.py",
31 include_str!("commands.__init__.py.j2"),
32 )
33 .unwrap();
34
35 env
36});
37
38pub fn generate_python(package: &Package) -> Result<Files, Error> {
40 let metadata = package.metadata();
41 let package_name = metadata.package_name.python_name();
42
43 let mut files = Files::new();
44
45 let ctx = Context::for_package(package);
46
47 if !ctx.libraries.is_empty() {
48 files.insert_child_directory(
49 Path::new(&package_name).join("bindings"),
50 library_bindings(&ctx)?,
51 );
52 }
53
54 if !ctx.commands.is_empty() {
55 files.insert_child_directory(
56 Path::new(&package_name).join("commands"),
57 command_bindings(&ctx)?,
58 );
59 }
60
61 files.insert(
62 Path::new(&package_name).join("__init__.py"),
63 top_level_dunder_init(package)?,
64 );
65 files.insert(
67 Path::new(&package_name).join("py.typed"),
68 SourceFile::empty(),
69 );
70
71 files.insert(
72 "pyproject.toml",
73 generate_pyproject_toml(metadata, &package_name)?,
74 );
75
76 files.insert("MANIFEST.in", generate_manifest(package, &package_name)?);
77
78 Ok(files)
79}
80
81#[derive(Debug, serde::Serialize)]
82struct Context {
83 commands: Vec<CommandContext>,
84 libraries: Vec<LibraryContext>,
85}
86
87impl Context {
88 fn for_package(pkg: &Package) -> Self {
89 let commands = pkg
90 .commands()
91 .iter()
92 .cloned()
93 .map(CommandContext::from)
94 .collect();
95
96 let libraries = pkg
97 .libraries()
98 .iter()
99 .cloned()
100 .map(LibraryContext::from)
101 .collect();
102
103 Context {
104 commands,
105 libraries,
106 }
107 }
108}
109
110#[derive(Debug, serde::Serialize)]
111struct LibraryContext {
112 ident: String,
113 class_name: String,
114 module_filename: String,
115 wasi: bool,
116 exports: InterfaceContext,
117 imports: Vec<InterfaceContext>,
118 #[serde(skip)]
119 module: Module,
120}
121
122impl From<crate::Library> for LibraryContext {
123 fn from(lib: crate::Library) -> Self {
124 let module_filename = Path::new(lib.module_filename()).with_extension("wasm");
125 let ident = lib.interface_name().to_snake_case();
126 let class_name = lib.class_name();
127
128 LibraryContext {
129 ident,
130 class_name,
131 module_filename: module_filename.display().to_string(),
132 wasi: lib.requires_wasi(),
133 exports: lib.exports.into(),
134 imports: lib
135 .imports
136 .into_iter()
137 .map(InterfaceContext::from)
138 .collect(),
139 module: lib.module,
140 }
141 }
142}
143
144#[derive(Debug, serde::Serialize)]
145struct InterfaceContext {
146 ident: String,
148 interface_name: String,
151 class_name: String,
153 #[serde(skip)]
154 interface: Interface,
155}
156
157impl From<Interface> for InterfaceContext {
158 fn from(interface: Interface) -> Self {
159 InterfaceContext {
160 ident: interface.name().to_snake_case(),
161 interface_name: interface.name().to_string(),
162 class_name: interface.name().to_pascal_case(),
163 interface,
164 }
165 }
166}
167
168#[derive(Debug, serde::Serialize)]
169struct CommandContext {
170 ident: String,
171 module_filename: String,
172 #[serde(skip)]
173 wasm: Vec<u8>,
174}
175
176impl From<crate::Command> for CommandContext {
177 fn from(cmd: crate::Command) -> CommandContext {
178 let ident = cmd.name.replace('-', "_");
179 let module_filename = format!("{ident}.wasm");
180 CommandContext {
181 ident,
182 module_filename,
183 wasm: cmd.wasm,
184 }
185 }
186}
187
188fn command_bindings(ctx: &Context) -> Result<Files, Error> {
189 let mut files = Files::new();
190
191 for cmd in &ctx.commands {
192 files.insert(&cmd.module_filename, SourceFile::from(&cmd.wasm));
193 }
194
195 files.insert(
196 "__init__.py",
197 TEMPLATES
198 .get_template("commands.__init__.py")
199 .unwrap()
200 .render(ctx)?
201 .into(),
202 );
203
204 Ok(files)
205}
206
207fn library_bindings(ctx: &Context) -> Result<Files, Error> {
208 let mut files = Files::new();
209
210 for lib in &ctx.libraries {
211 let mut bindings = generate_bindings(lib);
212 bindings.insert(&lib.module_filename, lib.module.wasm.clone().into());
213 files.insert_child_directory(&lib.ident, bindings);
214 }
215
216 let dunder_init = TEMPLATES
217 .get_template("bindings.__init__.py")
218 .unwrap()
219 .render(ctx)?;
220 files.insert("__init__.py", dunder_init.into());
221
222 Ok(files)
223}
224
225fn generate_manifest(package: &Package, package_name: &str) -> Result<SourceFile, Error> {
226 let ctx = minijinja::context! {
227 package_name,
228 libraries => package.libraries()
229 .iter()
230 .map(|lib| lib.interface_name())
231 .collect::<Vec<_>>(),
232 commands => package.commands()
233 .iter()
234 .map(|cmd| cmd.name.as_str())
235 .collect::<Vec<_>>(),
236 };
237 let rendered = TEMPLATES
238 .get_template("MANIFEST.in")
239 .unwrap()
240 .render(&ctx)?;
241
242 Ok(rendered.into())
243}
244
245fn generate_pyproject_toml(metadata: &Metadata, package_name: &str) -> Result<SourceFile, Error> {
246 let Metadata {
247 version,
248 description,
249 ..
250 } = metadata;
251
252 let project = PyProject {
253 project: Project {
254 name: package_name,
255 version,
256 description: description.as_deref(),
257 readme: None,
258 keywords: Vec::new(),
259 dependencies: vec!["wasmer", "wasmer_compiler_cranelift"],
260 },
261 build_system: BuildSystem {
262 requires: &["setuptools", "setuptools-scm"],
263 build_backend: "setuptools.build_meta",
264 },
265 };
266
267 let serialized = toml::to_string(&project)?;
268
269 Ok(serialized.into())
270}
271
272#[derive(Debug, Clone, PartialEq, serde::Serialize)]
273#[serde(rename_all = "kebab-case")]
274struct PyProject<'a> {
275 project: Project<'a>,
276 build_system: BuildSystem<'a>,
277}
278
279#[derive(Debug, Clone, PartialEq, serde::Serialize)]
280#[serde(rename_all = "kebab-case")]
281struct BuildSystem<'a> {
282 requires: &'a [&'a str],
283 build_backend: &'a str,
284}
285
286#[derive(Debug, Clone, PartialEq, serde::Serialize)]
287struct Project<'a> {
288 name: &'a str,
289 version: &'a str,
290 description: Option<&'a str>,
291 readme: Option<&'a Path>,
292 keywords: Vec<&'a str>,
293 dependencies: Vec<&'a str>,
294}
295
296fn top_level_dunder_init(package: &Package) -> Result<SourceFile, Error> {
297 let Metadata {
298 version,
299 description,
300 package_name,
301 } = package.metadata();
302
303 let ctx = minijinja::context! {
304 version,
305 description,
306 generator => crate::GENERATOR,
307 package_name => package_name.to_string(),
308 ident => package_name.name().to_pascal_case(),
309 commands => !package.commands().is_empty(),
310 libraries => !package.libraries().is_empty(),
311 };
312
313 let rendered = TEMPLATES
314 .get_template("top_level.__init__.py")
315 .unwrap()
316 .render(ctx)?;
317
318 Ok(rendered.into())
319}
320
321fn generate_bindings(lib: &LibraryContext) -> Files {
322 let imports = std::slice::from_ref(&lib.exports.interface.0);
327 let exports: Vec<_> = lib
328 .imports
329 .iter()
330 .map(|ctx| ctx.interface.0.clone())
331 .collect();
332
333 let mut generated = wai_bindgen_gen_core::Files::default();
334
335 WasmerPy::default().generate_all(imports, &exports, &mut generated);
336
337 let mut files = Files::from(generated);
338 files.insert("__init__.py", "from .bindings import *".into());
339
340 files
341}
342
343#[cfg(test)]
344mod tests {
345 use insta::Settings;
346
347 use super::*;
348 use crate::{Command, Library, Module};
349 use std::collections::BTreeSet;
350
351 const WASMER_PACK_EXPORTS: &str = include_str!(concat!(
352 env!("CARGO_MANIFEST_DIR"),
353 "/../wasm/wasmer-pack.exports.wai"
354 ));
355
356 #[test]
357 fn generated_files() {
358 let expected: BTreeSet<&Path> = [
359 "MANIFEST.in",
360 "pyproject.toml",
361 "wasmer_pack/__init__.py",
362 "wasmer_pack/py.typed",
363 "wasmer_pack/commands/__init__.py",
364 "wasmer_pack/commands/first.wasm",
365 "wasmer_pack/commands/second_with_dashes.wasm",
366 "wasmer_pack/bindings/__init__.py",
367 "wasmer_pack/bindings/wasmer_pack/__init__.py",
368 "wasmer_pack/bindings/wasmer_pack/bindings.py",
369 "wasmer_pack/bindings/wasmer_pack/wasmer_pack_wasm.wasm",
370 ]
371 .iter()
372 .map(Path::new)
373 .collect();
374 let metadata = Metadata::new("wasmer/wasmer-pack".parse().unwrap(), "1.2.3");
375 let module = Module {
376 name: "wasmer_pack_wasm.wasm".to_string(),
377 abi: crate::Abi::None,
378 wasm: Vec::new(),
379 };
380 let exports =
381 crate::Interface::from_wit("wasmer-pack.exports.wit", WASMER_PACK_EXPORTS).unwrap();
382 let commands = vec![
383 Command::new("first", []),
384 Command::new("second-with-dashes", []),
385 ];
386 let browser =
387 crate::Interface::from_wit("browser.wit", "greet: func(who: string) -> string")
388 .unwrap();
389 let libraries = vec![Library {
390 module,
391 exports,
392 imports: vec![browser],
393 }];
394 let package = Package::new(metadata, libraries, commands);
395
396 let files = generate_python(&package).unwrap();
397
398 let actual_files: BTreeSet<_> = files.iter().map(|(p, _)| p).collect();
399 assert_eq!(actual_files, expected);
400
401 let mut settings = Settings::clone_current();
402 settings.add_filter(
403 r"Generated by wasmer-pack v\d+\.\d+\.\d+(-\w+(\.\d+)?)?",
404 "Generated by XXX",
405 );
406 settings.bind(|| {
407 insta::assert_display_snapshot!(files["pyproject.toml"].utf8_contents().unwrap());
408 insta::assert_display_snapshot!(files["MANIFEST.in"].utf8_contents().unwrap());
409 insta::assert_display_snapshot!(files["wasmer_pack/__init__.py"]
410 .utf8_contents()
411 .unwrap()
412 .replace(crate::GENERATOR, "XXX"));
413 insta::assert_display_snapshot!(files["wasmer_pack/bindings/__init__.py"]
414 .utf8_contents()
415 .unwrap());
416 insta::assert_display_snapshot!(files["wasmer_pack/commands/__init__.py"]
417 .utf8_contents()
418 .unwrap());
419 });
420 insta::assert_display_snapshot!(files["wasmer_pack/py.typed"].utf8_contents().unwrap());
421
422 let actual_files: BTreeSet<_> = files.iter().map(|(p, _)| p).collect();
423 assert_eq!(actual_files, expected);
424 }
425}