1use anyhow::{Result, anyhow};
39use std::collections::HashSet;
40use std::path::{Path, PathBuf};
41use tempfile::TempDir;
42use wasmtime::{
43 Config, Engine, Store,
44 component::{Component, Func, Instance, Linker, ResourceTable, Val},
45};
46use wasmtime_wasi::{FsPerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
47use wasmtime_wizer::{WasmtimeWizerComponent, Wizer};
48
49use crate::linker::{NativeExtension, link_with_extensions};
50
51#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct CallbackDeclaration {
60 pub name: String,
62 pub description: String,
64 pub parameters_schema_json: String,
66}
67
68#[derive(Debug, Clone)]
70pub struct PreInitOptions {
71 python_stdlib: PathBuf,
72 site_packages: Option<PathBuf>,
73 imports: Vec<String>,
74 extensions: Vec<NativeExtension>,
75 setup_code: Option<String>,
76 callbacks: Vec<CallbackDeclaration>,
77}
78
79impl PreInitOptions {
80 pub fn new(python_stdlib: impl Into<PathBuf>) -> Self {
83 Self {
84 python_stdlib: python_stdlib.into(),
85 site_packages: None,
86 imports: Vec::new(),
87 extensions: Vec::new(),
88 setup_code: None,
89 callbacks: Vec::new(),
90 }
91 }
92
93 #[must_use]
95 pub fn site_packages(mut self, path: impl Into<PathBuf>) -> Self {
96 self.site_packages = Some(path.into());
97 self
98 }
99
100 #[must_use]
102 pub fn imports<I, S>(mut self, imports: I) -> Self
103 where
104 I: IntoIterator<Item = S>,
105 S: Into<String>,
106 {
107 self.imports = imports.into_iter().map(Into::into).collect();
108 self
109 }
110
111 #[must_use]
113 pub fn extensions(mut self, extensions: Vec<NativeExtension>) -> Self {
114 self.extensions = extensions;
115 self
116 }
117
118 #[must_use]
122 pub fn setup_code(mut self, code: impl Into<String>) -> Self {
123 self.setup_code = Some(code.into());
124 self
125 }
126
127 #[must_use]
131 pub fn callbacks(mut self, callbacks: Vec<CallbackDeclaration>) -> Self {
132 self.callbacks = callbacks;
133 self
134 }
135}
136
137struct PreInitCtx {
139 wasi: WasiCtx,
140 table: ResourceTable,
141 #[allow(dead_code)]
143 temp_dir: Option<TempDir>,
144 callbacks: Vec<CallbackDeclaration>,
146}
147
148impl std::fmt::Debug for PreInitCtx {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("PreInitCtx").finish_non_exhaustive()
151 }
152}
153
154impl WasiView for PreInitCtx {
155 fn ctx(&mut self) -> WasiCtxView<'_> {
156 WasiCtxView {
157 ctx: &mut self.wasi,
158 table: &mut self.table,
159 }
160 }
161}
162
163pub async fn pre_initialize(
188 python_stdlib: &Path,
189 site_packages: Option<&Path>,
190 imports: &[&str],
191 extensions: &[NativeExtension],
192 setup_code: Option<&str>,
193) -> Result<Vec<u8>> {
194 let mut options = PreInitOptions::new(python_stdlib)
195 .imports(imports.iter().copied())
196 .extensions(extensions.to_vec());
197 if let Some(path) = site_packages {
198 options = options.site_packages(path);
199 }
200 if let Some(code) = setup_code {
201 options = options.setup_code(code);
202 }
203 pre_initialize_with_options(options).await
204}
205
206pub async fn pre_initialize_with_options(options: PreInitOptions) -> Result<Vec<u8>> {
218 let PreInitOptions {
219 python_stdlib,
220 site_packages,
221 imports,
222 extensions,
223 setup_code,
224 mut callbacks,
225 } = options;
226 let python_stdlib = python_stdlib.as_path();
227 let site_packages = site_packages.as_deref();
228 callbacks.sort_by(|a, b| a.name.cmp(&b.name));
231
232 let original_component = link_with_extensions(&extensions)
234 .map_err(|e| anyhow!("Failed to link component with extensions: {}", e))?;
235
236 let wizer = Wizer::new();
240 let (cx, instrumented_wasm) = wizer
241 .instrument_component(&original_component)
242 .map_err(|e| e.context("Failed to instrument component"))?;
243
244 let mut config = Config::new();
246 config.wasm_component_model(true);
247 config.wasm_component_model_async(true);
248
249 let engine = Engine::new(&config)?;
250 let component = Component::new(&engine, &instrumented_wasm)?;
251
252 let table = ResourceTable::new();
254
255 let mut python_path_parts = vec!["/python-stdlib".to_string()];
257 if site_packages.is_some() {
258 python_path_parts.push("/site-packages".to_string());
259 }
260 let python_path = python_path_parts.join(":");
261
262 let mut wasi_builder = WasiCtxBuilder::new();
263 wasi_builder
264 .env("PYTHONHOME", "/python-stdlib")
265 .env("PYTHONPATH", &python_path)
266 .env("PYTHONUNBUFFERED", "1");
267
268 if python_stdlib.exists() {
270 wasi_builder.preopened_dir(python_stdlib, "python-stdlib", FsPerms::ReadOnly)?;
271 } else {
272 return Err(anyhow!(
273 "Python stdlib not found at {}",
274 python_stdlib.display()
275 ));
276 }
277
278 let temp_dir = if let Some(site_pkg) = site_packages {
280 if site_pkg.exists() {
281 wasi_builder.preopened_dir(site_pkg, "site-packages", FsPerms::ReadOnly)?;
282 }
283 None
284 } else {
285 let temp = TempDir::new()?;
287 wasi_builder.preopened_dir(temp.path(), "site-packages", FsPerms::ReadOnly)?;
288 Some(temp)
289 };
290
291 let wasi = wasi_builder.build();
292
293 let has_callbacks = !callbacks.is_empty();
294 let mut store = Store::new(
295 &engine,
296 PreInitCtx {
297 wasi,
298 table,
299 temp_dir,
300 callbacks,
301 },
302 );
303
304 let mut linker = Linker::new(&engine);
306 wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
307
308 add_sandbox_stubs(&mut linker)?;
311
312 let instance = linker.instantiate_async(&mut store, &component).await?;
315
316 if !imports.is_empty() {
318 call_execute_for_imports(&mut store, &instance, &imports).await?;
319 }
320
321 if let Some(code) = &setup_code {
324 call_execute_code(&mut store, &instance, code, "setup code").await?;
325 }
326
327 if has_callbacks && imports.is_empty() && setup_code.is_none() {
331 call_execute_code(&mut store, &instance, "pass", "callback installation").await?;
332 }
333
334 call_finalize_preinit(&mut store, &instance).await?;
339
340 let snapshot_bytes = wizer
342 .snapshot_component(
343 &cx,
344 &mut WasmtimeWizerComponent {
345 store: &mut store,
346 instance,
347 },
348 )
349 .await
350 .map_err(|e| e.context("Failed to pre-initialize component"))?;
351
352 restore_initialize_exports(&snapshot_bytes)
359}
360
361fn slice_range<'a>(bytes: &'a [u8], range: &std::ops::Range<u64>) -> Result<&'a [u8]> {
374 let start = usize::try_from(range.start)?;
375 let end = usize::try_from(range.end)?;
376 Ok(&bytes[start..end])
377}
378
379fn restore_initialize_exports(component_bytes: &[u8]) -> Result<Vec<u8>> {
380 let mut modules_with_init: HashSet<u32> = HashSet::new();
382 let mut any_module_imports_init = false;
383 let mut module_index = 0u32;
384
385 for payload in wasmparser::Parser::new(0).parse_all(component_bytes) {
386 if let wasmparser::Payload::ModuleSection {
387 unchecked_range: range,
388 ..
389 } = payload?
390 {
391 let module_bytes = slice_range(component_bytes, &range)?;
392 for inner in wasmparser::Parser::new(0).parse_all(module_bytes) {
394 match inner? {
395 wasmparser::Payload::ExportSection(reader) => {
396 for export in reader {
397 if export?.name == "_initialize" {
398 modules_with_init.insert(module_index);
399 }
400 }
401 }
402 wasmparser::Payload::ImportSection(reader) => {
403 for import in reader.into_imports() {
406 if import?.name == "_initialize" {
407 any_module_imports_init = true;
408 }
409 }
410 }
411 _ => {}
412 }
413 }
414 module_index += 1;
415 }
416 }
417
418 if !any_module_imports_init {
419 return Ok(component_bytes.to_vec());
420 }
421
422 let mut component = wasm_encoder::Component::new();
424 module_index = 0;
425 let mut depth = 0u32;
426
427 for payload in wasmparser::Parser::new(0).parse_all(component_bytes) {
428 let payload = payload?;
429
430 match &payload {
432 wasmparser::Payload::Version { .. } => {
433 if depth > 0 {
434 depth += 1;
436 continue;
437 }
438 depth += 1;
439 continue; }
441 wasmparser::Payload::End { .. } => {
442 depth -= 1;
443 continue; }
445 _ => {
446 if depth > 1 {
447 continue;
449 }
450 }
451 }
452
453 match payload {
454 wasmparser::Payload::ModuleSection {
455 unchecked_range: range,
456 ..
457 } => {
458 let module_bytes = slice_range(component_bytes, &range)?;
459
460 if !modules_with_init.contains(&module_index) {
461 let patched = add_noop_initialize(module_bytes)?;
462 component.section(&wasm_encoder::RawSection {
463 id: wasm_encoder::ComponentSectionId::CoreModule as u8,
464 data: &patched,
465 });
466 } else {
467 component.section(&wasm_encoder::RawSection {
468 id: wasm_encoder::ComponentSectionId::CoreModule as u8,
469 data: module_bytes,
470 });
471 }
472 module_index += 1;
473 }
474 other => {
475 if let Some((id, range)) = other.as_section() {
476 component.section(&wasm_encoder::RawSection {
477 id,
478 data: slice_range(component_bytes, &range)?,
479 });
480 }
481 }
482 }
483 }
484
485 Ok(component.finish())
486}
487
488fn add_noop_initialize(module_bytes: &[u8]) -> Result<Vec<u8>> {
494 use wasm_encoder::reencode::{Reencode, RoundtripReencoder};
495
496 let mut num_types = 0u32;
497 let mut num_imported_funcs = 0u32;
498 let mut num_defined_funcs = 0u32;
499 let mut noop_type_idx = None;
500
501 for payload in wasmparser::Parser::new(0).parse_all(module_bytes) {
503 match payload? {
504 wasmparser::Payload::TypeSection(reader) => {
505 for ty in reader.into_iter() {
506 let ty = ty?;
507 for sub in ty.types() {
508 if let wasmparser::CompositeInnerType::Func(func_ty) =
509 &sub.composite_type.inner
510 && func_ty.params().is_empty()
511 && func_ty.results().is_empty()
512 {
513 noop_type_idx = Some(num_types);
514 }
515 num_types += 1;
516 }
517 }
518 }
519 wasmparser::Payload::ImportSection(reader) => {
520 for import in reader.into_imports() {
524 if matches!(import?.ty, wasmparser::TypeRef::Func(_)) {
525 num_imported_funcs += 1;
526 }
527 }
528 }
529 wasmparser::Payload::FunctionSection(reader) => {
530 num_defined_funcs = reader.count();
531 }
532 wasmparser::Payload::CodeSectionStart { .. } => {}
533 _ => {}
534 }
535 }
536
537 let num_funcs = num_imported_funcs + num_defined_funcs;
538 let noop_type = noop_type_idx.unwrap_or(num_types);
539 let noop_func_index = num_funcs;
540 let needs_new_type = noop_type_idx.is_none();
541
542 let mut encoder = wasm_encoder::Module::new();
545 let mut reencode = RoundtripReencoder;
546
547 for payload in wasmparser::Parser::new(0).parse_all(module_bytes) {
548 match payload? {
549 wasmparser::Payload::Version { .. } => {}
550 wasmparser::Payload::TypeSection(reader) => {
551 let mut types = wasm_encoder::TypeSection::new();
552 reencode.parse_type_section(&mut types, reader)?;
553 if needs_new_type {
554 types.ty().function([], []);
555 }
556 encoder.section(&types);
557 }
558 wasmparser::Payload::FunctionSection(reader) => {
559 let mut funcs = wasm_encoder::FunctionSection::new();
560 reencode.parse_function_section(&mut funcs, reader)?;
561 funcs.function(noop_type);
562 encoder.section(&funcs);
563 }
564 wasmparser::Payload::ExportSection(reader) => {
565 let mut exports = wasm_encoder::ExportSection::new();
566 reencode.parse_export_section(&mut exports, reader)?;
567 exports.export(
568 "_initialize",
569 wasm_encoder::ExportKind::Func,
570 noop_func_index,
571 );
572 encoder.section(&exports);
573 }
574 wasmparser::Payload::CodeSectionStart { range, .. } => {
575 let section_data = slice_range(module_bytes, &range)?;
578 let code_reader = wasmparser::CodeSectionReader::new(
579 wasmparser::BinaryReader::new(section_data, 0),
580 )?;
581
582 let mut code = wasm_encoder::CodeSection::new();
583 reencode.parse_code_section(&mut code, code_reader)?;
584
585 let mut noop_func = wasm_encoder::Function::new([]);
587 noop_func.instructions().end();
588 code.function(&noop_func);
589 encoder.section(&code);
590 }
591 wasmparser::Payload::CodeSectionEntry(_) => {
592 }
594 wasmparser::Payload::End { .. } => {}
595 other => {
596 if let Some((id, range)) = other.as_section() {
597 encoder.section(&wasm_encoder::RawSection {
598 id,
599 data: slice_range(module_bytes, &range)?,
600 });
601 }
602 }
603 }
604 }
605
606 Ok(encoder.finish())
607}
608
609fn add_sandbox_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
611 use wasmtime::component::Accessor;
612
613 linker.root().func_wrap_concurrent(
615 "invoke",
616 |_accessor: &Accessor<PreInitCtx>, (_name, _args): (String, String)| {
617 Box::pin(async move {
618 Ok((Result::<String, String>::Err(
619 "callbacks not available during pre-init".into(),
620 ),))
621 })
622 },
623 )?;
624
625 linker.root().func_new(
630 "list-callbacks",
631 |ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
632 _func_ty: wasmtime::component::types::ComponentFunc,
633 _params: &[Val],
634 results: &mut [Val]| {
635 let declared = ctx
636 .data()
637 .callbacks
638 .iter()
639 .map(|cb| {
640 Val::Record(vec![
641 ("name".to_string(), Val::String(cb.name.clone())),
642 (
643 "description".to_string(),
644 Val::String(cb.description.clone()),
645 ),
646 (
647 "parameters-schema-json".to_string(),
648 Val::String(cb.parameters_schema_json.clone()),
649 ),
650 ])
651 })
652 .collect();
653 results[0] = Val::List(declared);
654 Ok(())
655 },
656 )?;
657
658 linker.root().func_new(
660 "report-trace",
661 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
662 _func_ty: wasmtime::component::types::ComponentFunc,
663 _params: &[Val],
664 _results: &mut [Val]| {
665 Ok(())
667 },
668 )?;
669
670 linker.root().func_new(
673 "get-execution-options",
674 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
675 _func_ty: wasmtime::component::types::ComponentFunc,
676 _params: &[Val],
677 results: &mut [Val]| {
678 results[0] = Val::Record(vec![
679 ("python-tracing".to_string(), Val::Bool(false)),
680 ("reuse-empty-callbacks".to_string(), Val::Bool(false)),
681 ]);
682 Ok(())
683 },
684 )?;
685
686 linker.root().func_new(
688 "report-output",
689 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
690 _func_ty: wasmtime::component::types::ComponentFunc,
691 _params: &[Val],
692 _results: &mut [Val]| {
693 Ok(())
695 },
696 )?;
697
698 add_network_stubs(linker)?;
700
701 Ok(())
702}
703
704#[derive(
707 wasmtime::component::ComponentType, wasmtime::component::Lift, wasmtime::component::Lower,
708)]
709#[component(variant)]
710enum PreInitTcpError {
711 #[component(name = "connection-refused")]
712 ConnectionRefused,
713 #[component(name = "connection-reset")]
714 ConnectionReset,
715 #[component(name = "timed-out")]
716 TimedOut,
717 #[component(name = "host-not-found")]
718 HostNotFound,
719 #[component(name = "io-error")]
720 IoError(String),
721 #[component(name = "not-permitted")]
722 NotPermitted(String),
723 #[component(name = "invalid-handle")]
724 InvalidHandle,
725}
726
727#[derive(
730 wasmtime::component::ComponentType, wasmtime::component::Lift, wasmtime::component::Lower,
731)]
732#[component(variant)]
733enum PreInitTlsError {
734 #[component(name = "tcp")]
735 Tcp(PreInitTcpError),
736 #[component(name = "handshake-failed")]
737 HandshakeFailed(String),
738 #[component(name = "certificate-error")]
739 CertificateError(String),
740 #[component(name = "invalid-handle")]
741 InvalidHandle,
742}
743
744fn add_network_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
752 let mut tcp_instance = linker
754 .instance("eryx:net/tcp@0.1.0")
755 .map_err(|e| e.context("Failed to get eryx:net/tcp instance"))?;
756
757 tcp_instance.func_wrap_async(
759 "connect",
760 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
761 (_host, _port, _timeout_ms): (String, u16, u32)| {
762 Box::new(async move {
763 Ok((Result::<u32, PreInitTcpError>::Err(
764 PreInitTcpError::NotPermitted(
765 "networking not available during pre-init".into(),
766 ),
767 ),))
768 })
769 },
770 )?;
771
772 tcp_instance.func_wrap_async(
774 "read",
775 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
776 (_handle, _len, _timeout_ms): (u32, u32, u32)| {
777 Box::new(async move {
778 Ok((Result::<Vec<u8>, PreInitTcpError>::Err(
779 PreInitTcpError::NotPermitted(
780 "networking not available during pre-init".into(),
781 ),
782 ),))
783 })
784 },
785 )?;
786
787 tcp_instance.func_wrap_async(
789 "write",
790 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
791 (_handle, _timeout_ms, _data): (u32, u32, Vec<u8>)| {
792 Box::new(async move {
793 Ok((Result::<u32, PreInitTcpError>::Err(
794 PreInitTcpError::NotPermitted(
795 "networking not available during pre-init".into(),
796 ),
797 ),))
798 })
799 },
800 )?;
801
802 tcp_instance.func_wrap(
804 "close",
805 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
806 Ok(())
808 },
809 )?;
810
811 let mut tls_instance = linker
813 .instance("eryx:net/tls@0.1.0")
814 .map_err(|e| e.context("Failed to get eryx:net/tls instance"))?;
815
816 tls_instance.func_wrap_async(
818 "upgrade",
819 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
820 (_tcp_handle, _hostname, _timeout_ms): (u32, String, u32)| {
821 Box::new(async move {
822 Ok((Result::<u32, PreInitTlsError>::Err(
823 PreInitTlsError::HandshakeFailed(
824 "networking not available during pre-init".into(),
825 ),
826 ),))
827 })
828 },
829 )?;
830
831 tls_instance.func_wrap_async(
833 "read",
834 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
835 (_handle, _len, _timeout_ms): (u32, u32, u32)| {
836 Box::new(async move {
837 Ok((Result::<Vec<u8>, PreInitTlsError>::Err(
838 PreInitTlsError::HandshakeFailed(
839 "networking not available during pre-init".into(),
840 ),
841 ),))
842 })
843 },
844 )?;
845
846 tls_instance.func_wrap_async(
848 "write",
849 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
850 (_handle, _timeout_ms, _data): (u32, u32, Vec<u8>)| {
851 Box::new(async move {
852 Ok((Result::<u32, PreInitTlsError>::Err(
853 PreInitTlsError::HandshakeFailed(
854 "networking not available during pre-init".into(),
855 ),
856 ),))
857 })
858 },
859 )?;
860
861 tls_instance.func_wrap(
863 "close",
864 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
865 Ok(())
867 },
868 )?;
869
870 Ok(())
871}
872
873async fn call_execute_for_imports(
875 store: &mut Store<PreInitCtx>,
876 instance: &Instance,
877 imports: &[String],
878) -> Result<()> {
879 let import_code = imports
880 .iter()
881 .map(|module| format!("import {module}"))
882 .collect::<Vec<_>>()
883 .join("\n");
884
885 call_execute_code(store, instance, &import_code, "imports").await
886}
887
888async fn call_execute_code(
893 store: &mut Store<PreInitCtx>,
894 instance: &Instance,
895 code: &str,
896 label: &str,
897) -> Result<()> {
898 let execute_func = find_execute_func(store, instance)?;
899
900 let args = [Val::String(code.to_string())];
901 let mut results = vec![Val::Bool(false)];
902
903 execute_func
904 .call_async(&mut *store, &args, &mut results)
905 .await
906 .map_err(|e| e.context(format!("Failed to execute {label} during pre-init")))?;
907
908 match &results[0] {
909 Val::Result(Ok(_)) => Ok(()),
910 Val::Result(Err(Some(error_val))) => {
911 let error_msg = match error_val.as_ref() {
912 Val::String(s) => s.clone(),
913 other => format!("unexpected error value: {other:?}"),
914 };
915 Err(anyhow!(
916 "Pre-init {label} execution failed: {error_msg}\nCode:\n{code}"
917 ))
918 }
919 Val::Result(Err(None)) => Err(anyhow!(
920 "Pre-init {label} execution failed with unknown error\nCode:\n{code}"
921 )),
922 other => {
923 tracing::warn!("Unexpected result type from execute during pre-init: {other:?}");
924 Ok(())
925 }
926 }
927}
928
929fn find_execute_func(store: &mut Store<PreInitCtx>, instance: &Instance) -> Result<Func> {
931 if let Some(func) = instance.get_func(&mut *store, "execute") {
932 Ok(func)
933 } else if let Some(func) = instance.get_func(&mut *store, "[async]execute") {
934 Ok(func)
935 } else {
936 let (_item, exports_idx) = instance
937 .get_export(&mut *store, None, "exports")
938 .ok_or_else(|| anyhow!("No 'exports' or 'execute' export found"))?;
939
940 let execute_idx = instance
941 .get_export_index(&mut *store, Some(&exports_idx), "execute")
942 .ok_or_else(|| anyhow!("No 'execute' in exports interface"))?;
943
944 instance
945 .get_func(&mut *store, execute_idx)
946 .ok_or_else(|| anyhow!("Could not get execute func from index"))
947 }
948}
949
950async fn call_finalize_preinit(store: &mut Store<PreInitCtx>, instance: &Instance) -> Result<()> {
952 let finalize_func = instance
954 .get_func(&mut *store, "finalize-preinit")
955 .ok_or_else(|| anyhow!("finalize-preinit export not found"))?;
956
957 let args: [Val; 0] = [];
959 let mut results: [Val; 0] = [];
960
961 finalize_func
962 .call_async(&mut *store, &args, &mut results)
963 .await
964 .map_err(|e| e.context("Failed to call finalize-preinit"))?;
965
966 Ok(())
967}
968
969#[derive(Debug, Clone)]
971#[non_exhaustive]
972pub enum PreInitError {
973 Engine(String),
975 Compile(String),
977 Instantiate(String),
979 PythonInit(String),
981 Import(String),
983 Transform(String),
985}
986
987impl std::fmt::Display for PreInitError {
988 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
989 match self {
990 Self::Engine(e) => write!(f, "failed to create wasmtime engine: {e}"),
991 Self::Compile(e) => write!(f, "failed to compile component: {e}"),
992 Self::Instantiate(e) => write!(f, "failed to instantiate component: {e}"),
993 Self::PythonInit(e) => write!(f, "Python initialization failed: {e}"),
994 Self::Import(e) => write!(f, "import failed during pre-init: {e}"),
995 Self::Transform(e) => write!(f, "component transform failed: {e}"),
996 }
997 }
998}
999
1000impl std::error::Error for PreInitError {}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn test_preinit_error_display() {
1008 let err = PreInitError::PythonInit("test error".to_string());
1009 assert!(err.to_string().contains("test error"));
1010 }
1011
1012 #[test]
1013 fn test_preinit_error_import_display() {
1014 let err = PreInitError::Import("numpy not found".to_string());
1015 assert!(err.to_string().contains("numpy not found"));
1016 }
1017}