1use std::collections::HashMap;
38use std::fmt;
39use std::path::{Path, PathBuf};
40
41use thiserror::Error;
42use tokio::process::Command;
43use tracing::{debug, info, instrument, trace, warn};
44
45#[derive(Debug, Error)]
47pub enum WasmBuildError {
48 #[error("Could not detect source language in '{path}'")]
50 LanguageNotDetected {
51 path: PathBuf,
53 },
54
55 #[error("Build tool '{tool}' not found: {message}")]
57 ToolNotFound {
58 tool: String,
60 message: String,
62 },
63
64 #[error("Build failed with exit code {exit_code}: {stderr}")]
66 BuildFailed {
67 exit_code: i32,
69 stderr: String,
71 stdout: String,
73 },
74
75 #[error("WASM output not found at expected path: {path}")]
77 OutputNotFound {
78 path: PathBuf,
80 },
81
82 #[error("Configuration error: {message}")]
84 ConfigError {
85 message: String,
87 },
88
89 #[error("IO error: {0}")]
91 Io(#[from] std::io::Error),
92
93 #[error("Failed to read project configuration: {message}")]
95 ProjectConfigError {
96 message: String,
98 },
99}
100
101pub type Result<T, E = WasmBuildError> = std::result::Result<T, E>;
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub enum WasmLanguage {
107 Rust,
109 RustComponent,
111 Go,
113 Python,
115 TypeScript,
117 AssemblyScript,
119 C,
121 Zig,
123}
124
125impl WasmLanguage {
126 #[must_use]
128 pub fn all() -> &'static [WasmLanguage] {
129 &[
130 WasmLanguage::Rust,
131 WasmLanguage::RustComponent,
132 WasmLanguage::Go,
133 WasmLanguage::Python,
134 WasmLanguage::TypeScript,
135 WasmLanguage::AssemblyScript,
136 WasmLanguage::C,
137 WasmLanguage::Zig,
138 ]
139 }
140
141 #[must_use]
143 pub fn name(&self) -> &'static str {
144 match self {
145 WasmLanguage::Rust => "Rust",
146 WasmLanguage::RustComponent => "Rust (cargo-component)",
147 WasmLanguage::Go => "Go (TinyGo)",
148 WasmLanguage::Python => "Python",
149 WasmLanguage::TypeScript => "TypeScript",
150 WasmLanguage::AssemblyScript => "AssemblyScript",
151 WasmLanguage::C => "C",
152 WasmLanguage::Zig => "Zig",
153 }
154 }
155
156 #[must_use]
158 pub fn is_component_native(&self) -> bool {
159 matches!(
160 self,
161 WasmLanguage::RustComponent | WasmLanguage::Python | WasmLanguage::TypeScript
162 )
163 }
164
165 #[must_use]
177 pub fn from_name(name: &str) -> Option<Self> {
178 match name.to_lowercase().as_str() {
179 "rust" => Some(Self::Rust),
180 "rust-component" | "rust_component" | "cargo-component" => Some(Self::RustComponent),
181 "go" | "tinygo" => Some(Self::Go),
182 "python" | "py" => Some(Self::Python),
183 "typescript" | "ts" => Some(Self::TypeScript),
184 "assemblyscript" | "as" => Some(Self::AssemblyScript),
185 "c" => Some(Self::C),
186 "zig" => Some(Self::Zig),
187 _ => None,
188 }
189 }
190}
191
192impl fmt::Display for WasmLanguage {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 write!(f, "{}", self.name())
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
200pub enum WasiTarget {
201 Preview1,
203 #[default]
205 Preview2,
206}
207
208impl WasiTarget {
209 #[must_use]
211 pub fn rust_target(&self) -> &'static str {
212 match self {
213 WasiTarget::Preview1 => "wasm32-wasip1",
214 WasiTarget::Preview2 => "wasm32-wasip2",
215 }
216 }
217
218 #[must_use]
220 pub fn name(&self) -> &'static str {
221 match self {
222 WasiTarget::Preview1 => "WASI Preview 1",
223 WasiTarget::Preview2 => "WASI Preview 2",
224 }
225 }
226}
227
228impl fmt::Display for WasiTarget {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 write!(f, "{}", self.name())
231 }
232}
233
234#[derive(Debug, Clone)]
236pub struct WasmBuildConfig {
237 pub language: Option<WasmLanguage>,
239
240 pub target: WasiTarget,
242
243 pub optimize: bool,
245
246 pub opt_level: String,
248
249 pub wit_path: Option<PathBuf>,
251
252 pub output_path: Option<PathBuf>,
254
255 pub world: Option<String>,
257
258 pub features: Vec<String>,
260
261 pub build_args: HashMap<String, String>,
263
264 pub pre_build: Vec<Vec<String>>,
266
267 pub post_build: Vec<Vec<String>>,
269
270 pub adapter: Option<PathBuf>,
272}
273
274impl Default for WasmBuildConfig {
275 fn default() -> Self {
276 Self {
277 language: None,
278 target: WasiTarget::default(),
279 optimize: false,
280 opt_level: "Oz".to_string(),
281 wit_path: None,
282 output_path: None,
283 world: None,
284 features: Vec::new(),
285 build_args: HashMap::new(),
286 pre_build: Vec::new(),
287 post_build: Vec::new(),
288 adapter: None,
289 }
290 }
291}
292
293impl WasmBuildConfig {
294 #[must_use]
296 pub fn new() -> Self {
297 Self::default()
298 }
299
300 #[must_use]
302 pub fn language(mut self, lang: WasmLanguage) -> Self {
303 self.language = Some(lang);
304 self
305 }
306
307 #[must_use]
309 pub fn target(mut self, target: WasiTarget) -> Self {
310 self.target = target;
311 self
312 }
313
314 #[must_use]
316 pub fn optimize(mut self, optimize: bool) -> Self {
317 self.optimize = optimize;
318 self
319 }
320
321 #[must_use]
323 pub fn wit_path(mut self, path: impl Into<PathBuf>) -> Self {
324 self.wit_path = Some(path.into());
325 self
326 }
327
328 #[must_use]
330 pub fn output_path(mut self, path: impl Into<PathBuf>) -> Self {
331 self.output_path = Some(path.into());
332 self
333 }
334
335 #[must_use]
337 pub fn opt_level(mut self, level: impl Into<String>) -> Self {
338 self.opt_level = level.into();
339 self
340 }
341
342 #[must_use]
344 pub fn world(mut self, world: impl Into<String>) -> Self {
345 self.world = Some(world.into());
346 self
347 }
348
349 #[must_use]
351 pub fn features(mut self, features: Vec<String>) -> Self {
352 self.features = features;
353 self
354 }
355
356 #[must_use]
358 pub fn build_args(mut self, args: HashMap<String, String>) -> Self {
359 self.build_args = args;
360 self
361 }
362
363 #[must_use]
365 pub fn pre_build(mut self, commands: Vec<Vec<String>>) -> Self {
366 self.pre_build = commands;
367 self
368 }
369
370 #[must_use]
372 pub fn post_build(mut self, commands: Vec<Vec<String>>) -> Self {
373 self.post_build = commands;
374 self
375 }
376
377 #[must_use]
379 pub fn adapter(mut self, path: impl Into<PathBuf>) -> Self {
380 self.adapter = Some(path.into());
381 self
382 }
383}
384
385#[derive(Debug, Clone)]
387pub struct WasmBuildResult {
388 pub wasm_path: PathBuf,
390
391 pub language: WasmLanguage,
393
394 pub target: WasiTarget,
396
397 pub size: u64,
399}
400
401#[instrument(level = "debug", skip_all, fields(path = %context.as_ref().display()))]
420pub fn detect_language(context: impl AsRef<Path>) -> Result<WasmLanguage> {
421 let path = context.as_ref();
422 debug!("Detecting WASM source language");
423
424 let cargo_toml = path.join("Cargo.toml");
426 if cargo_toml.exists() {
427 trace!("Found Cargo.toml");
428
429 if is_cargo_component_project(&cargo_toml)? {
431 debug!("Detected Rust (cargo-component) project");
432 return Ok(WasmLanguage::RustComponent);
433 }
434
435 debug!("Detected Rust project");
436 return Ok(WasmLanguage::Rust);
437 }
438
439 if path.join("go.mod").exists() {
441 debug!("Detected Go (TinyGo) project");
442 return Ok(WasmLanguage::Go);
443 }
444
445 if path.join("pyproject.toml").exists()
447 || path.join("requirements.txt").exists()
448 || path.join("setup.py").exists()
449 {
450 debug!("Detected Python project");
451 return Ok(WasmLanguage::Python);
452 }
453
454 let package_json = path.join("package.json");
456 if package_json.exists() {
457 trace!("Found package.json");
458
459 if is_assemblyscript_project(&package_json)? {
461 debug!("Detected AssemblyScript project");
462 return Ok(WasmLanguage::AssemblyScript);
463 }
464
465 debug!("Detected TypeScript project");
466 return Ok(WasmLanguage::TypeScript);
467 }
468
469 if path.join("build.zig").exists() {
471 debug!("Detected Zig project");
472 return Ok(WasmLanguage::Zig);
473 }
474
475 if (path.join("Makefile").exists() || path.join("CMakeLists.txt").exists())
477 && has_c_source_files(path)
478 {
479 debug!("Detected C project");
480 return Ok(WasmLanguage::C);
481 }
482
483 if has_c_source_files(path) {
485 debug!("Detected C project (source files only)");
486 return Ok(WasmLanguage::C);
487 }
488
489 Err(WasmBuildError::LanguageNotDetected {
490 path: path.to_path_buf(),
491 })
492}
493
494fn is_cargo_component_project(cargo_toml: &Path) -> Result<bool> {
496 let content =
497 std::fs::read_to_string(cargo_toml).map_err(|e| WasmBuildError::ProjectConfigError {
498 message: format!("Failed to read Cargo.toml: {e}"),
499 })?;
500
501 if content.contains("[package.metadata.component]") {
505 return Ok(true);
506 }
507
508 if content.contains("wit-bindgen") || content.contains("cargo-component-bindings") {
510 return Ok(true);
511 }
512
513 let component_toml = cargo_toml.parent().map(|p| p.join("cargo-component.toml"));
515 if let Some(ref component_toml) = component_toml {
516 if component_toml.exists() {
517 return Ok(true);
518 }
519 }
520
521 Ok(false)
522}
523
524fn is_assemblyscript_project(package_json: &Path) -> Result<bool> {
526 let content =
527 std::fs::read_to_string(package_json).map_err(|e| WasmBuildError::ProjectConfigError {
528 message: format!("Failed to read package.json: {e}"),
529 })?;
530
531 let json: serde_json::Value =
532 serde_json::from_str(&content).map_err(|e| WasmBuildError::ProjectConfigError {
533 message: format!("Invalid package.json: {e}"),
534 })?;
535
536 let has_assemblyscript = |deps: Option<&serde_json::Value>| -> bool {
538 deps.and_then(|d| d.as_object())
539 .is_some_and(|d| d.contains_key("assemblyscript"))
540 };
541
542 if has_assemblyscript(json.get("dependencies"))
543 || has_assemblyscript(json.get("devDependencies"))
544 {
545 return Ok(true);
546 }
547
548 if let Some(scripts) = json.get("scripts").and_then(|s| s.as_object()) {
550 for script in scripts.values() {
551 if let Some(cmd) = script.as_str() {
552 if cmd.contains("asc ") || cmd.starts_with("asc") {
553 return Ok(true);
554 }
555 }
556 }
557 }
558
559 Ok(false)
560}
561
562fn has_c_source_files(path: &Path) -> bool {
564 if let Ok(entries) = std::fs::read_dir(path) {
565 for entry in entries.flatten() {
566 let file_path = entry.path();
567 if let Some(ext) = file_path.extension() {
568 if ext == "c" || ext == "h" {
569 return true;
570 }
571 }
572 }
573 }
574
575 let src_dir = path.join("src");
577 if src_dir.is_dir() {
578 if let Ok(entries) = std::fs::read_dir(&src_dir) {
579 for entry in entries.flatten() {
580 let file_path = entry.path();
581 if let Some(ext) = file_path.extension() {
582 if ext == "c" || ext == "h" {
583 return true;
584 }
585 }
586 }
587 }
588 }
589
590 false
591}
592
593#[must_use]
595#[allow(clippy::too_many_lines)]
596pub fn get_build_command(language: WasmLanguage, target: WasiTarget, release: bool) -> Vec<String> {
597 get_build_command_with_config(language, target, release, None)
598}
599
600#[must_use]
602#[allow(clippy::too_many_lines)]
603pub fn get_build_command_with_config(
604 language: WasmLanguage,
605 target: WasiTarget,
606 release: bool,
607 config: Option<&WasmBuildConfig>,
608) -> Vec<String> {
609 let world = config.and_then(|c| c.world.as_deref());
610
611 match language {
612 WasmLanguage::Rust => {
613 let mut cmd = vec![
614 "cargo".to_string(),
615 "build".to_string(),
616 "--target".to_string(),
617 target.rust_target().to_string(),
618 ];
619 if release {
620 cmd.push("--release".to_string());
621 }
622 cmd
623 }
624
625 WasmLanguage::RustComponent => {
626 let mut cmd = vec![
627 "cargo".to_string(),
628 "component".to_string(),
629 "build".to_string(),
630 ];
631 if release {
632 cmd.push("--release".to_string());
633 }
634 cmd
638 }
639
640 WasmLanguage::Go => {
641 let wasi_target = match target {
643 WasiTarget::Preview1 => "wasip1",
644 WasiTarget::Preview2 => "wasip2",
645 };
646 let mut cmd = vec![
647 "tinygo".to_string(),
648 "build".to_string(),
649 "-target".to_string(),
650 wasi_target.to_string(),
651 "-o".to_string(),
652 "main.wasm".to_string(),
653 ];
654 if release {
655 cmd.push("-opt".to_string());
656 cmd.push("2".to_string());
657 }
658 cmd.push(".".to_string());
659 cmd
660 }
661
662 WasmLanguage::Python => {
663 let _ = release; let world_name = world.unwrap_or("world");
668 vec![
669 "componentize-py".to_string(),
670 "-d".to_string(),
671 "wit".to_string(),
672 "-w".to_string(),
673 world_name.to_string(),
674 "componentize".to_string(),
675 "app".to_string(),
676 "-o".to_string(),
677 "app.wasm".to_string(),
678 ]
679 }
680
681 WasmLanguage::TypeScript => {
682 let mut cmd = vec![
684 "npx".to_string(),
685 "jco".to_string(),
686 "componentize".to_string(),
687 "src/index.js".to_string(),
688 "--wit".to_string(),
689 "wit".to_string(),
690 ];
691 if let Some(w) = world {
692 cmd.push("--world-name".to_string());
693 cmd.push(w.to_string());
694 }
695 cmd.push("-o".to_string());
696 cmd.push("dist/component.wasm".to_string());
697 cmd
698 }
699
700 WasmLanguage::AssemblyScript => {
701 let mut cmd = vec![
702 "npx".to_string(),
703 "asc".to_string(),
704 "assembly/index.ts".to_string(),
705 "--target".to_string(),
706 "release".to_string(),
707 "-o".to_string(),
708 "build/release.wasm".to_string(),
709 ];
710 if release {
711 cmd.push("--optimize".to_string());
712 }
713 cmd
714 }
715
716 WasmLanguage::C => {
717 let mut cmd = vec![
719 "clang".to_string(),
720 "--target=wasm32-wasi".to_string(),
721 "-o".to_string(),
722 "main.wasm".to_string(),
723 ];
724 if release {
725 cmd.push("-O2".to_string());
726 }
727 cmd.push("src/main.c".to_string());
728 cmd
729 }
730
731 WasmLanguage::Zig => {
732 let mut cmd = vec![
734 "zig".to_string(),
735 "build".to_string(),
736 "-Dtarget=wasm32-wasi".to_string(),
737 ];
738 if release {
739 cmd.push("-Doptimize=ReleaseFast".to_string());
740 }
741 cmd
742 }
743 }
744}
745
746#[instrument(level = "info", skip_all, fields(
757 context = %context.as_ref().display(),
758 language = ?config.language,
759 target = ?config.target
760))]
761pub async fn build_wasm(
762 context: impl AsRef<Path>,
763 config: WasmBuildConfig,
764) -> Result<WasmBuildResult> {
765 let context = context.as_ref();
766 info!("Building WASM component");
767
768 let language = if let Some(lang) = config.language {
770 debug!("Using specified language: {}", lang);
771 lang
772 } else {
773 let detected = detect_language(context)?;
774 info!("Auto-detected language: {}", detected);
775 detected
776 };
777
778 verify_build_tool(language).await?;
780
781 for cmd in &config.pre_build {
783 if cmd.is_empty() {
784 continue;
785 }
786 debug!("Running pre-build command: {:?}", cmd);
787 let output = execute_build_command(context, cmd, &config).await?;
788 if !output.status.success() {
789 let exit_code = output.status.code().unwrap_or(-1);
790 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
791 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
792 return Err(WasmBuildError::BuildFailed {
793 exit_code,
794 stderr,
795 stdout,
796 });
797 }
798 }
799
800 let cmd =
802 get_build_command_with_config(language, config.target, config.optimize, Some(&config));
803 debug!("Build command: {:?}", cmd);
804
805 let output = execute_build_command(context, &cmd, &config).await?;
807
808 if !output.status.success() {
810 let exit_code = output.status.code().unwrap_or(-1);
811 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
812 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
813
814 warn!("Build failed with exit code {}", exit_code);
815 trace!("stdout: {}", stdout);
816 trace!("stderr: {}", stderr);
817
818 return Err(WasmBuildError::BuildFailed {
819 exit_code,
820 stderr,
821 stdout,
822 });
823 }
824
825 let wasm_path = find_wasm_output(context, language, config.target, config.optimize)?;
827
828 for cmd in &config.post_build {
830 if cmd.is_empty() {
831 continue;
832 }
833 debug!("Running post-build command: {:?}", cmd);
834 let output = execute_build_command(context, cmd, &config).await?;
835 if !output.status.success() {
836 let exit_code = output.status.code().unwrap_or(-1);
837 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
838 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
839 return Err(WasmBuildError::BuildFailed {
840 exit_code,
841 stderr,
842 stdout,
843 });
844 }
845 }
846
847 let wasm_path = if config.optimize {
849 optimize_wasm(&wasm_path, &config.opt_level).await?
850 } else {
851 wasm_path
852 };
853
854 let final_path = if let Some(ref output_path) = config.output_path {
856 std::fs::copy(&wasm_path, output_path)?;
857 output_path.clone()
858 } else {
859 wasm_path
860 };
861
862 let metadata = std::fs::metadata(&final_path)?;
864 let size = metadata.len();
865
866 info!("Successfully built {} WASM ({} bytes)", language, size);
867
868 Ok(WasmBuildResult {
869 wasm_path: final_path,
870 language,
871 target: config.target,
872 size,
873 })
874}
875
876async fn verify_build_tool(language: WasmLanguage) -> Result<()> {
878 let (tool, check_cmd) = match language {
879 WasmLanguage::Rust | WasmLanguage::RustComponent => ("cargo", vec!["cargo", "--version"]),
880 WasmLanguage::Go => ("tinygo", vec!["tinygo", "version"]),
881 WasmLanguage::Python => ("componentize-py", vec!["componentize-py", "--version"]),
882 WasmLanguage::TypeScript | WasmLanguage::AssemblyScript => {
883 ("npx", vec!["npx", "--version"])
884 }
885 WasmLanguage::C => ("clang", vec!["clang", "--version"]),
886 WasmLanguage::Zig => ("zig", vec!["zig", "version"]),
887 };
888
889 debug!("Checking for tool: {}", tool);
890
891 let result = Command::new(check_cmd[0])
892 .args(&check_cmd[1..])
893 .output()
894 .await;
895
896 match result {
897 Ok(output) if output.status.success() => {
898 trace!("{} is available", tool);
899 Ok(())
900 }
901 Ok(output) => {
902 let stderr = String::from_utf8_lossy(&output.stderr);
903 Err(WasmBuildError::ToolNotFound {
904 tool: tool.to_string(),
905 message: format!("Command failed: {stderr}"),
906 })
907 }
908 Err(e) => Err(WasmBuildError::ToolNotFound {
909 tool: tool.to_string(),
910 message: format!("Not found in PATH: {e}"),
911 }),
912 }
913}
914
915async fn execute_build_command(
917 context: &Path,
918 cmd: &[String],
919 config: &WasmBuildConfig,
920) -> Result<std::process::Output> {
921 let mut command = Command::new(&cmd[0]);
922 command
923 .args(&cmd[1..])
924 .current_dir(context)
925 .stdout(std::process::Stdio::piped())
926 .stderr(std::process::Stdio::piped());
927
928 if let Some(ref world) = config.world {
930 command.env("CARGO_COMPONENT_WIT_WORLD", world);
931 }
932
933 for (key, value) in &config.build_args {
935 command.env(key, value);
936 }
937
938 debug!("Executing: {} in {}", cmd.join(" "), context.display());
939
940 command.output().await.map_err(WasmBuildError::Io)
941}
942
943fn find_wasm_output(
945 context: &Path,
946 language: WasmLanguage,
947 target: WasiTarget,
948 release: bool,
949) -> Result<PathBuf> {
950 let candidates: Vec<PathBuf> = match language {
952 WasmLanguage::Rust => {
953 let profile = if release { "release" } else { "debug" };
954 let target_name = target.rust_target();
955
956 let package_name =
958 get_rust_package_name(context).unwrap_or_else(|_| "output".to_string());
959
960 vec![
961 context
962 .join("target")
963 .join(target_name)
964 .join(profile)
965 .join(format!("{package_name}.wasm")),
966 context
967 .join("target")
968 .join(target_name)
969 .join(profile)
970 .join(format!("{}.wasm", package_name.replace('-', "_"))),
971 ]
972 }
973
974 WasmLanguage::RustComponent => {
975 let profile = if release { "release" } else { "debug" };
976 let package_name =
977 get_rust_package_name(context).unwrap_or_else(|_| "output".to_string());
978
979 vec![
980 context
982 .join("target")
983 .join("wasm32-wasip1")
984 .join(profile)
985 .join(format!("{package_name}.wasm")),
986 context
987 .join("target")
988 .join("wasm32-wasip2")
989 .join(profile)
990 .join(format!("{package_name}.wasm")),
991 context
992 .join("target")
993 .join("wasm32-wasi")
994 .join(profile)
995 .join(format!("{package_name}.wasm")),
996 ]
997 }
998
999 WasmLanguage::Go | WasmLanguage::C => {
1000 vec![context.join("main.wasm")]
1001 }
1002
1003 WasmLanguage::Python => {
1004 vec![context.join("app.wasm")]
1005 }
1006
1007 WasmLanguage::TypeScript => {
1008 vec![
1009 context.join("dist").join("component.wasm"),
1010 context.join("component.wasm"),
1011 ]
1012 }
1013
1014 WasmLanguage::AssemblyScript => {
1015 vec![
1016 context.join("build").join("release.wasm"),
1017 context.join("build").join("debug.wasm"),
1018 ]
1019 }
1020
1021 WasmLanguage::Zig => {
1022 vec![
1023 context.join("zig-out").join("bin").join("main.wasm"),
1024 context.join("zig-out").join("lib").join("main.wasm"),
1025 ]
1026 }
1027 };
1028
1029 for candidate in &candidates {
1031 if candidate.exists() {
1032 debug!("Found WASM output at: {}", candidate.display());
1033 return Ok(candidate.clone());
1034 }
1035 }
1036
1037 if let Some(wasm_path) = find_any_wasm_file(context) {
1039 debug!("Found WASM file via search: {}", wasm_path.display());
1040 return Ok(wasm_path);
1041 }
1042
1043 Err(WasmBuildError::OutputNotFound {
1044 path: candidates
1045 .first()
1046 .cloned()
1047 .unwrap_or_else(|| context.join("output.wasm")),
1048 })
1049}
1050
1051#[allow(clippy::similar_names)]
1053fn get_rust_package_name(context: &Path) -> Result<String> {
1054 let cargo_toml = context.join("Cargo.toml");
1055 let content =
1056 std::fs::read_to_string(&cargo_toml).map_err(|e| WasmBuildError::ProjectConfigError {
1057 message: format!("Failed to read Cargo.toml: {e}"),
1058 })?;
1059
1060 for line in content.lines() {
1062 let line = line.trim();
1063 if line.starts_with("name") {
1064 if let Some(name) = line
1065 .split('=')
1066 .nth(1)
1067 .map(|s| s.trim().trim_matches('"').trim_matches('\''))
1068 {
1069 return Ok(name.to_string());
1070 }
1071 }
1072 }
1073
1074 Err(WasmBuildError::ProjectConfigError {
1075 message: "Could not find package name in Cargo.toml".to_string(),
1076 })
1077}
1078
1079fn find_any_wasm_file(context: &Path) -> Option<PathBuf> {
1081 let search_dirs = [
1083 context.to_path_buf(),
1084 context.join("target"),
1085 context.join("build"),
1086 context.join("dist"),
1087 context.join("out"),
1088 context.join("zig-out"),
1089 ];
1090
1091 for dir in &search_dirs {
1092 if let Some(path) = search_wasm_recursive(dir, 3) {
1093 return Some(path);
1094 }
1095 }
1096
1097 None
1098}
1099
1100fn search_wasm_recursive(dir: &Path, max_depth: usize) -> Option<PathBuf> {
1102 if max_depth == 0 || !dir.is_dir() {
1103 return None;
1104 }
1105
1106 if let Ok(entries) = std::fs::read_dir(dir) {
1107 for entry in entries.flatten() {
1108 let path = entry.path();
1109
1110 if path.is_file() {
1111 if let Some(ext) = path.extension() {
1112 if ext == "wasm" {
1113 return Some(path);
1114 }
1115 }
1116 } else if path.is_dir() {
1117 if let Some(found) = search_wasm_recursive(&path, max_depth - 1) {
1118 return Some(found);
1119 }
1120 }
1121 }
1122 }
1123
1124 None
1125}
1126
1127pub async fn optimize_wasm(wasm_path: &Path, opt_level: &str) -> Result<PathBuf> {
1142 let flag = match opt_level {
1143 "O" => "-O",
1144 "Os" => "-Os",
1145 "O2" => "-O2",
1146 "O3" => "-O3",
1147 _ => "-Oz",
1149 };
1150
1151 let wasm_opt = which_wasm_opt();
1153
1154 if let Some(wasm_opt_path) = wasm_opt {
1155 let optimized_path = wasm_path.with_extension("opt.wasm");
1156 let output = Command::new(&wasm_opt_path)
1157 .arg(flag)
1158 .arg("-o")
1159 .arg(&optimized_path)
1160 .arg(wasm_path)
1161 .output()
1162 .await
1163 .map_err(|e| WasmBuildError::ConfigError {
1164 message: format!("Failed to run wasm-opt: {e}"),
1165 })?;
1166
1167 if !output.status.success() {
1168 let stderr = String::from_utf8_lossy(&output.stderr);
1169 return Err(WasmBuildError::ConfigError {
1170 message: format!("wasm-opt failed: {stderr}"),
1171 });
1172 }
1173
1174 if let (Ok(original), Ok(optimized)) = (
1176 std::fs::metadata(wasm_path),
1177 std::fs::metadata(&optimized_path),
1178 ) {
1179 let original_size = original.len();
1180 let optimized_size = optimized.len();
1181 #[allow(clippy::cast_precision_loss)]
1182 let reduction = if original_size > 0 {
1183 ((original_size.saturating_sub(optimized_size)) as f64 / original_size as f64)
1184 * 100.0
1185 } else {
1186 0.0
1187 };
1188 info!(
1189 "wasm-opt: {:.1}% size reduction ({} -> {} bytes)",
1190 reduction, original_size, optimized_size,
1191 );
1192 }
1193
1194 Ok(optimized_path)
1195 } else {
1196 warn!(
1197 "wasm-opt not found in PATH; skipping optimization. \
1198 Install binaryen for WASM size optimization."
1199 );
1200 Ok(wasm_path.to_path_buf())
1201 }
1202}
1203
1204fn which_wasm_opt() -> Option<PathBuf> {
1206 if let Ok(output) = std::process::Command::new("which").arg("wasm-opt").output() {
1208 if output.status.success() {
1209 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
1210 if !path.is_empty() {
1211 return Some(PathBuf::from(path));
1212 }
1213 }
1214 }
1215
1216 let common_paths = [
1218 "/usr/local/bin/wasm-opt",
1219 "/usr/bin/wasm-opt",
1220 "/opt/binaryen/bin/wasm-opt",
1221 ];
1222
1223 for path in &common_paths {
1224 let p = PathBuf::from(path);
1225 if p.exists() {
1226 return Some(p);
1227 }
1228 }
1229
1230 None
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236 use std::fs;
1237 use tempfile::TempDir;
1238
1239 fn create_temp_dir() -> TempDir {
1240 TempDir::new().expect("Failed to create temp directory")
1241 }
1242
1243 mod wasm_language_tests {
1248 use super::*;
1249
1250 #[test]
1251 fn test_display_all_variants() {
1252 assert_eq!(WasmLanguage::Rust.to_string(), "Rust");
1253 assert_eq!(
1254 WasmLanguage::RustComponent.to_string(),
1255 "Rust (cargo-component)"
1256 );
1257 assert_eq!(WasmLanguage::Go.to_string(), "Go (TinyGo)");
1258 assert_eq!(WasmLanguage::Python.to_string(), "Python");
1259 assert_eq!(WasmLanguage::TypeScript.to_string(), "TypeScript");
1260 assert_eq!(WasmLanguage::AssemblyScript.to_string(), "AssemblyScript");
1261 assert_eq!(WasmLanguage::C.to_string(), "C");
1262 assert_eq!(WasmLanguage::Zig.to_string(), "Zig");
1263 }
1264
1265 #[test]
1266 fn test_debug_formatting() {
1267 let debug_str = format!("{:?}", WasmLanguage::Rust);
1269 assert_eq!(debug_str, "Rust");
1270
1271 let debug_str = format!("{:?}", WasmLanguage::RustComponent);
1272 assert_eq!(debug_str, "RustComponent");
1273
1274 let debug_str = format!("{:?}", WasmLanguage::Go);
1275 assert_eq!(debug_str, "Go");
1276
1277 let debug_str = format!("{:?}", WasmLanguage::Python);
1278 assert_eq!(debug_str, "Python");
1279
1280 let debug_str = format!("{:?}", WasmLanguage::TypeScript);
1281 assert_eq!(debug_str, "TypeScript");
1282
1283 let debug_str = format!("{:?}", WasmLanguage::AssemblyScript);
1284 assert_eq!(debug_str, "AssemblyScript");
1285
1286 let debug_str = format!("{:?}", WasmLanguage::C);
1287 assert_eq!(debug_str, "C");
1288
1289 let debug_str = format!("{:?}", WasmLanguage::Zig);
1290 assert_eq!(debug_str, "Zig");
1291 }
1292
1293 #[test]
1294 fn test_clone() {
1295 let lang = WasmLanguage::Rust;
1296 let cloned = lang;
1297 assert_eq!(lang, cloned);
1298
1299 let lang = WasmLanguage::Python;
1300 let cloned = lang;
1301 assert_eq!(lang, cloned);
1302 }
1303
1304 #[test]
1305 fn test_copy() {
1306 let lang = WasmLanguage::Go;
1307 let copied = lang; assert_eq!(lang, copied);
1309 assert_eq!(lang, WasmLanguage::Go);
1311 }
1312
1313 #[test]
1314 fn test_partial_eq() {
1315 assert_eq!(WasmLanguage::Rust, WasmLanguage::Rust);
1316 assert_ne!(WasmLanguage::Rust, WasmLanguage::Go);
1317 assert_ne!(WasmLanguage::Rust, WasmLanguage::RustComponent);
1318 assert_eq!(WasmLanguage::TypeScript, WasmLanguage::TypeScript);
1319 assert_ne!(WasmLanguage::TypeScript, WasmLanguage::AssemblyScript);
1320 }
1321
1322 #[test]
1323 fn test_name_method() {
1324 assert_eq!(WasmLanguage::Rust.name(), "Rust");
1325 assert_eq!(WasmLanguage::RustComponent.name(), "Rust (cargo-component)");
1326 assert_eq!(WasmLanguage::Go.name(), "Go (TinyGo)");
1327 assert_eq!(WasmLanguage::Python.name(), "Python");
1328 assert_eq!(WasmLanguage::TypeScript.name(), "TypeScript");
1329 assert_eq!(WasmLanguage::AssemblyScript.name(), "AssemblyScript");
1330 assert_eq!(WasmLanguage::C.name(), "C");
1331 assert_eq!(WasmLanguage::Zig.name(), "Zig");
1332 }
1333
1334 #[test]
1335 fn test_all_returns_all_variants() {
1336 let all = WasmLanguage::all();
1337 assert_eq!(all.len(), 8);
1338 assert!(all.contains(&WasmLanguage::Rust));
1339 assert!(all.contains(&WasmLanguage::RustComponent));
1340 assert!(all.contains(&WasmLanguage::Go));
1341 assert!(all.contains(&WasmLanguage::Python));
1342 assert!(all.contains(&WasmLanguage::TypeScript));
1343 assert!(all.contains(&WasmLanguage::AssemblyScript));
1344 assert!(all.contains(&WasmLanguage::C));
1345 assert!(all.contains(&WasmLanguage::Zig));
1346 }
1347
1348 #[test]
1349 fn test_is_component_native() {
1350 assert!(WasmLanguage::RustComponent.is_component_native());
1352 assert!(WasmLanguage::Python.is_component_native());
1353 assert!(WasmLanguage::TypeScript.is_component_native());
1354
1355 assert!(!WasmLanguage::Rust.is_component_native());
1357 assert!(!WasmLanguage::Go.is_component_native());
1358 assert!(!WasmLanguage::AssemblyScript.is_component_native());
1359 assert!(!WasmLanguage::C.is_component_native());
1360 assert!(!WasmLanguage::Zig.is_component_native());
1361 }
1362
1363 #[test]
1364 fn test_hash() {
1365 use std::collections::HashSet;
1366
1367 let mut set = HashSet::new();
1368 set.insert(WasmLanguage::Rust);
1369 set.insert(WasmLanguage::Go);
1370 set.insert(WasmLanguage::Rust); assert_eq!(set.len(), 2);
1373 assert!(set.contains(&WasmLanguage::Rust));
1374 assert!(set.contains(&WasmLanguage::Go));
1375 }
1376 }
1377
1378 mod wasi_target_tests {
1383 use super::*;
1384
1385 #[test]
1386 fn test_default_returns_preview2() {
1387 let target = WasiTarget::default();
1388 assert_eq!(target, WasiTarget::Preview2);
1389 }
1390
1391 #[test]
1392 fn test_display_preview1() {
1393 assert_eq!(WasiTarget::Preview1.to_string(), "WASI Preview 1");
1394 }
1395
1396 #[test]
1397 fn test_display_preview2() {
1398 assert_eq!(WasiTarget::Preview2.to_string(), "WASI Preview 2");
1399 }
1400
1401 #[test]
1402 fn test_debug_formatting() {
1403 let debug_str = format!("{:?}", WasiTarget::Preview1);
1404 assert_eq!(debug_str, "Preview1");
1405
1406 let debug_str = format!("{:?}", WasiTarget::Preview2);
1407 assert_eq!(debug_str, "Preview2");
1408 }
1409
1410 #[test]
1411 fn test_clone() {
1412 let target = WasiTarget::Preview1;
1413 let cloned = target;
1414 assert_eq!(target, cloned);
1415
1416 let target = WasiTarget::Preview2;
1417 let cloned = target;
1418 assert_eq!(target, cloned);
1419 }
1420
1421 #[test]
1422 fn test_copy() {
1423 let target = WasiTarget::Preview1;
1424 let copied = target; assert_eq!(target, copied);
1426 assert_eq!(target, WasiTarget::Preview1);
1428 }
1429
1430 #[test]
1431 fn test_partial_eq() {
1432 assert_eq!(WasiTarget::Preview1, WasiTarget::Preview1);
1433 assert_eq!(WasiTarget::Preview2, WasiTarget::Preview2);
1434 assert_ne!(WasiTarget::Preview1, WasiTarget::Preview2);
1435 }
1436
1437 #[test]
1438 fn test_rust_target_preview1() {
1439 assert_eq!(WasiTarget::Preview1.rust_target(), "wasm32-wasip1");
1440 }
1441
1442 #[test]
1443 fn test_rust_target_preview2() {
1444 assert_eq!(WasiTarget::Preview2.rust_target(), "wasm32-wasip2");
1445 }
1446
1447 #[test]
1448 fn test_name_method() {
1449 assert_eq!(WasiTarget::Preview1.name(), "WASI Preview 1");
1450 assert_eq!(WasiTarget::Preview2.name(), "WASI Preview 2");
1451 }
1452
1453 #[test]
1454 fn test_hash() {
1455 use std::collections::HashSet;
1456
1457 let mut set = HashSet::new();
1458 set.insert(WasiTarget::Preview1);
1459 set.insert(WasiTarget::Preview2);
1460 set.insert(WasiTarget::Preview1); assert_eq!(set.len(), 2);
1463 assert!(set.contains(&WasiTarget::Preview1));
1464 assert!(set.contains(&WasiTarget::Preview2));
1465 }
1466 }
1467
1468 mod wasm_build_config_tests {
1473 use super::*;
1474
1475 #[test]
1476 fn test_default_trait() {
1477 let config = WasmBuildConfig::default();
1478
1479 assert_eq!(config.language, None);
1480 assert_eq!(config.target, WasiTarget::Preview2); assert!(!config.optimize);
1482 assert_eq!(config.opt_level, "Oz");
1483 assert_eq!(config.wit_path, None);
1484 assert_eq!(config.output_path, None);
1485 assert_eq!(config.world, None);
1486 assert!(config.features.is_empty());
1487 assert!(config.build_args.is_empty());
1488 assert!(config.pre_build.is_empty());
1489 assert!(config.post_build.is_empty());
1490 assert_eq!(config.adapter, None);
1491 }
1492
1493 #[test]
1494 fn test_new_equals_default() {
1495 let new_config = WasmBuildConfig::new();
1496 let default_config = WasmBuildConfig::default();
1497
1498 assert_eq!(new_config.language, default_config.language);
1499 assert_eq!(new_config.target, default_config.target);
1500 assert_eq!(new_config.optimize, default_config.optimize);
1501 assert_eq!(new_config.opt_level, default_config.opt_level);
1502 assert_eq!(new_config.wit_path, default_config.wit_path);
1503 assert_eq!(new_config.output_path, default_config.output_path);
1504 assert_eq!(new_config.world, default_config.world);
1505 assert_eq!(new_config.features, default_config.features);
1506 assert_eq!(new_config.build_args, default_config.build_args);
1507 assert_eq!(new_config.pre_build, default_config.pre_build);
1508 assert_eq!(new_config.post_build, default_config.post_build);
1509 assert_eq!(new_config.adapter, default_config.adapter);
1510 }
1511
1512 #[test]
1513 fn test_with_language() {
1514 let config = WasmBuildConfig::new().language(WasmLanguage::Rust);
1515 assert_eq!(config.language, Some(WasmLanguage::Rust));
1516
1517 let config = WasmBuildConfig::new().language(WasmLanguage::Python);
1518 assert_eq!(config.language, Some(WasmLanguage::Python));
1519 }
1520
1521 #[test]
1522 fn test_with_target() {
1523 let config = WasmBuildConfig::new().target(WasiTarget::Preview1);
1524 assert_eq!(config.target, WasiTarget::Preview1);
1525
1526 let config = WasmBuildConfig::new().target(WasiTarget::Preview2);
1527 assert_eq!(config.target, WasiTarget::Preview2);
1528 }
1529
1530 #[test]
1531 fn test_with_optimize_true() {
1532 let config = WasmBuildConfig::new().optimize(true);
1533 assert!(config.optimize);
1534 }
1535
1536 #[test]
1537 fn test_with_optimize_false() {
1538 let config = WasmBuildConfig::new().optimize(false);
1539 assert!(!config.optimize);
1540 }
1541
1542 #[test]
1543 fn test_with_wit_path_string() {
1544 let config = WasmBuildConfig::new().wit_path("/path/to/wit");
1545 assert_eq!(config.wit_path, Some(PathBuf::from("/path/to/wit")));
1546 }
1547
1548 #[test]
1549 fn test_with_wit_path_pathbuf() {
1550 let path = PathBuf::from("/another/wit/path");
1551 let config = WasmBuildConfig::new().wit_path(path.clone());
1552 assert_eq!(config.wit_path, Some(path));
1553 }
1554
1555 #[test]
1556 fn test_with_output_path_string() {
1557 let config = WasmBuildConfig::new().output_path("/output/file.wasm");
1558 assert_eq!(config.output_path, Some(PathBuf::from("/output/file.wasm")));
1559 }
1560
1561 #[test]
1562 fn test_with_output_path_pathbuf() {
1563 let path = PathBuf::from("/custom/output.wasm");
1564 let config = WasmBuildConfig::new().output_path(path.clone());
1565 assert_eq!(config.output_path, Some(path));
1566 }
1567
1568 #[test]
1569 fn test_builder_pattern_chaining() {
1570 let config = WasmBuildConfig::new()
1571 .language(WasmLanguage::Go)
1572 .target(WasiTarget::Preview1)
1573 .optimize(true)
1574 .wit_path("/wit")
1575 .output_path("/out.wasm");
1576
1577 assert_eq!(config.language, Some(WasmLanguage::Go));
1578 assert_eq!(config.target, WasiTarget::Preview1);
1579 assert!(config.optimize);
1580 assert_eq!(config.wit_path, Some(PathBuf::from("/wit")));
1581 assert_eq!(config.output_path, Some(PathBuf::from("/out.wasm")));
1582 }
1583
1584 #[test]
1585 fn test_debug_formatting() {
1586 let config = WasmBuildConfig::new().language(WasmLanguage::Rust);
1587 let debug_str = format!("{config:?}");
1588
1589 assert!(debug_str.contains("WasmBuildConfig"));
1590 assert!(debug_str.contains("Rust"));
1591 }
1592
1593 #[test]
1594 fn test_clone() {
1595 let config = WasmBuildConfig::new()
1596 .language(WasmLanguage::Python)
1597 .optimize(true);
1598
1599 let cloned = config.clone();
1600
1601 assert_eq!(cloned.language, Some(WasmLanguage::Python));
1602 assert!(cloned.optimize);
1603 }
1604 }
1605
1606 mod wasm_build_result_tests {
1611 use super::*;
1612
1613 #[test]
1614 fn test_struct_creation() {
1615 let result = WasmBuildResult {
1616 wasm_path: PathBuf::from("/path/to/output.wasm"),
1617 language: WasmLanguage::Rust,
1618 target: WasiTarget::Preview2,
1619 size: 1024,
1620 };
1621
1622 assert_eq!(result.wasm_path, PathBuf::from("/path/to/output.wasm"));
1623 assert_eq!(result.language, WasmLanguage::Rust);
1624 assert_eq!(result.target, WasiTarget::Preview2);
1625 assert_eq!(result.size, 1024);
1626 }
1627
1628 #[test]
1629 fn test_struct_creation_all_languages() {
1630 for lang in WasmLanguage::all() {
1631 let result = WasmBuildResult {
1632 wasm_path: PathBuf::from("/test.wasm"),
1633 language: *lang,
1634 target: WasiTarget::Preview1,
1635 size: 512,
1636 };
1637 assert_eq!(result.language, *lang);
1638 }
1639 }
1640
1641 #[test]
1642 fn test_debug_formatting() {
1643 let result = WasmBuildResult {
1644 wasm_path: PathBuf::from("/test.wasm"),
1645 language: WasmLanguage::Go,
1646 target: WasiTarget::Preview1,
1647 size: 2048,
1648 };
1649
1650 let debug_str = format!("{result:?}");
1651 assert!(debug_str.contains("WasmBuildResult"));
1652 assert!(debug_str.contains("test.wasm"));
1653 assert!(debug_str.contains("Go"));
1654 assert!(debug_str.contains("2048"));
1655 }
1656
1657 #[test]
1658 fn test_clone() {
1659 let result = WasmBuildResult {
1660 wasm_path: PathBuf::from("/original.wasm"),
1661 language: WasmLanguage::Zig,
1662 target: WasiTarget::Preview2,
1663 size: 4096,
1664 };
1665
1666 let cloned = result.clone();
1667
1668 assert_eq!(cloned.wasm_path, result.wasm_path);
1669 assert_eq!(cloned.language, result.language);
1670 assert_eq!(cloned.target, result.target);
1671 assert_eq!(cloned.size, result.size);
1672 }
1673
1674 #[test]
1675 fn test_zero_size() {
1676 let result = WasmBuildResult {
1677 wasm_path: PathBuf::from("/empty.wasm"),
1678 language: WasmLanguage::C,
1679 target: WasiTarget::Preview1,
1680 size: 0,
1681 };
1682 assert_eq!(result.size, 0);
1683 }
1684
1685 #[test]
1686 fn test_large_size() {
1687 let result = WasmBuildResult {
1688 wasm_path: PathBuf::from("/large.wasm"),
1689 language: WasmLanguage::AssemblyScript,
1690 target: WasiTarget::Preview2,
1691 size: u64::MAX,
1692 };
1693 assert_eq!(result.size, u64::MAX);
1694 }
1695 }
1696
1697 mod wasm_build_error_tests {
1702 use super::*;
1703
1704 #[test]
1705 fn test_display_language_not_detected() {
1706 let err = WasmBuildError::LanguageNotDetected {
1707 path: PathBuf::from("/test/path"),
1708 };
1709 let display = err.to_string();
1710 assert!(display.contains("Could not detect source language"));
1711 assert!(display.contains("/test/path"));
1712 }
1713
1714 #[test]
1715 fn test_display_tool_not_found() {
1716 let err = WasmBuildError::ToolNotFound {
1717 tool: "cargo".to_string(),
1718 message: "Not in PATH".to_string(),
1719 };
1720 let display = err.to_string();
1721 assert!(display.contains("Build tool 'cargo' not found"));
1722 assert!(display.contains("Not in PATH"));
1723 }
1724
1725 #[test]
1726 fn test_display_build_failed() {
1727 let err = WasmBuildError::BuildFailed {
1728 exit_code: 1,
1729 stderr: "compilation error".to_string(),
1730 stdout: "some output".to_string(),
1731 };
1732 let display = err.to_string();
1733 assert!(display.contains("Build failed with exit code 1"));
1734 assert!(display.contains("compilation error"));
1735 }
1736
1737 #[test]
1738 fn test_display_output_not_found() {
1739 let err = WasmBuildError::OutputNotFound {
1740 path: PathBuf::from("/expected/output.wasm"),
1741 };
1742 let display = err.to_string();
1743 assert!(display.contains("WASM output not found"));
1744 assert!(display.contains("/expected/output.wasm"));
1745 }
1746
1747 #[test]
1748 fn test_display_config_error() {
1749 let err = WasmBuildError::ConfigError {
1750 message: "Invalid configuration".to_string(),
1751 };
1752 let display = err.to_string();
1753 assert!(display.contains("Configuration error"));
1754 assert!(display.contains("Invalid configuration"));
1755 }
1756
1757 #[test]
1758 fn test_display_project_config_error() {
1759 let err = WasmBuildError::ProjectConfigError {
1760 message: "Failed to parse Cargo.toml".to_string(),
1761 };
1762 let display = err.to_string();
1763 assert!(display.contains("Failed to read project configuration"));
1764 assert!(display.contains("Failed to parse Cargo.toml"));
1765 }
1766
1767 #[test]
1768 fn test_display_io_error() {
1769 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
1770 let err = WasmBuildError::Io(io_err);
1771 let display = err.to_string();
1772 assert!(display.contains("IO error"));
1773 assert!(display.contains("file not found"));
1774 }
1775
1776 #[test]
1777 fn test_debug_formatting_all_variants() {
1778 let errors = vec![
1779 WasmBuildError::LanguageNotDetected {
1780 path: PathBuf::from("/test"),
1781 },
1782 WasmBuildError::ToolNotFound {
1783 tool: "test".to_string(),
1784 message: "msg".to_string(),
1785 },
1786 WasmBuildError::BuildFailed {
1787 exit_code: 0,
1788 stderr: String::new(),
1789 stdout: String::new(),
1790 },
1791 WasmBuildError::OutputNotFound {
1792 path: PathBuf::from("/test"),
1793 },
1794 WasmBuildError::ConfigError {
1795 message: "test".to_string(),
1796 },
1797 WasmBuildError::ProjectConfigError {
1798 message: "test".to_string(),
1799 },
1800 ];
1801
1802 for err in errors {
1803 let debug_str = format!("{err:?}");
1804 assert!(!debug_str.is_empty());
1805 }
1806 }
1807
1808 #[test]
1809 fn test_from_io_error() {
1810 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
1811 let wasm_err: WasmBuildError = io_err.into();
1812
1813 match wasm_err {
1814 WasmBuildError::Io(e) => {
1815 assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied);
1816 }
1817 _ => panic!("Expected Io variant"),
1818 }
1819 }
1820
1821 #[test]
1822 fn test_from_io_error_various_kinds() {
1823 let kinds = vec![
1824 std::io::ErrorKind::NotFound,
1825 std::io::ErrorKind::PermissionDenied,
1826 std::io::ErrorKind::AlreadyExists,
1827 std::io::ErrorKind::InvalidData,
1828 ];
1829
1830 for kind in kinds {
1831 let io_err = std::io::Error::new(kind, "test error");
1832 let wasm_err: WasmBuildError = io_err.into();
1833 assert!(matches!(wasm_err, WasmBuildError::Io(_)));
1834 }
1835 }
1836
1837 #[test]
1838 fn test_error_implements_std_error() {
1839 let err = WasmBuildError::ConfigError {
1840 message: "test".to_string(),
1841 };
1842 let _: &dyn std::error::Error = &err;
1844 }
1845 }
1846
1847 mod detect_language_tests {
1852 use super::*;
1853
1854 #[test]
1855 fn test_detect_cargo_toml_rust() {
1856 let dir = create_temp_dir();
1857 fs::write(
1858 dir.path().join("Cargo.toml"),
1859 r#"[package]
1860name = "test"
1861version = "0.1.0"
1862"#,
1863 )
1864 .unwrap();
1865
1866 let lang = detect_language(dir.path()).unwrap();
1867 assert_eq!(lang, WasmLanguage::Rust);
1868 }
1869
1870 #[test]
1871 fn test_detect_cargo_toml_with_cargo_component_metadata() {
1872 let dir = create_temp_dir();
1873 fs::write(
1874 dir.path().join("Cargo.toml"),
1875 r#"[package]
1876name = "test"
1877version = "0.1.0"
1878
1879[package.metadata.component]
1880package = "test:component"
1881"#,
1882 )
1883 .unwrap();
1884
1885 let lang = detect_language(dir.path()).unwrap();
1886 assert_eq!(lang, WasmLanguage::RustComponent);
1887 }
1888
1889 #[test]
1890 fn test_detect_cargo_toml_with_wit_bindgen_dep() {
1891 let dir = create_temp_dir();
1892 fs::write(
1893 dir.path().join("Cargo.toml"),
1894 r#"[package]
1895name = "test"
1896version = "0.1.0"
1897
1898[dependencies]
1899wit-bindgen = "0.20"
1900"#,
1901 )
1902 .unwrap();
1903
1904 let lang = detect_language(dir.path()).unwrap();
1905 assert_eq!(lang, WasmLanguage::RustComponent);
1906 }
1907
1908 #[test]
1909 fn test_detect_cargo_toml_with_cargo_component_bindings() {
1910 let dir = create_temp_dir();
1911 fs::write(
1912 dir.path().join("Cargo.toml"),
1913 r#"[package]
1914name = "test"
1915version = "0.1.0"
1916
1917[dependencies]
1918cargo-component-bindings = "0.1"
1919"#,
1920 )
1921 .unwrap();
1922
1923 let lang = detect_language(dir.path()).unwrap();
1924 assert_eq!(lang, WasmLanguage::RustComponent);
1925 }
1926
1927 #[test]
1928 fn test_detect_cargo_component_toml_file() {
1929 let dir = create_temp_dir();
1930 fs::write(
1931 dir.path().join("Cargo.toml"),
1932 r#"[package]
1933name = "test"
1934version = "0.1.0"
1935"#,
1936 )
1937 .unwrap();
1938 fs::write(
1939 dir.path().join("cargo-component.toml"),
1940 "# cargo-component config",
1941 )
1942 .unwrap();
1943
1944 let lang = detect_language(dir.path()).unwrap();
1945 assert_eq!(lang, WasmLanguage::RustComponent);
1946 }
1947
1948 #[test]
1949 fn test_detect_go_mod() {
1950 let dir = create_temp_dir();
1951 fs::write(
1952 dir.path().join("go.mod"),
1953 "module example.com/test\n\ngo 1.21\n",
1954 )
1955 .unwrap();
1956
1957 let lang = detect_language(dir.path()).unwrap();
1958 assert_eq!(lang, WasmLanguage::Go);
1959 }
1960
1961 #[test]
1962 fn test_detect_pyproject_toml() {
1963 let dir = create_temp_dir();
1964 fs::write(
1965 dir.path().join("pyproject.toml"),
1966 r#"[project]
1967name = "my-package"
1968version = "0.1.0"
1969"#,
1970 )
1971 .unwrap();
1972
1973 let lang = detect_language(dir.path()).unwrap();
1974 assert_eq!(lang, WasmLanguage::Python);
1975 }
1976
1977 #[test]
1978 fn test_detect_requirements_txt() {
1979 let dir = create_temp_dir();
1980 fs::write(
1981 dir.path().join("requirements.txt"),
1982 "flask==2.0.0\nrequests>=2.25.0",
1983 )
1984 .unwrap();
1985
1986 let lang = detect_language(dir.path()).unwrap();
1987 assert_eq!(lang, WasmLanguage::Python);
1988 }
1989
1990 #[test]
1991 fn test_detect_setup_py() {
1992 let dir = create_temp_dir();
1993 fs::write(
1994 dir.path().join("setup.py"),
1995 r#"from setuptools import setup
1996setup(name="mypackage")
1997"#,
1998 )
1999 .unwrap();
2000
2001 let lang = detect_language(dir.path()).unwrap();
2002 assert_eq!(lang, WasmLanguage::Python);
2003 }
2004
2005 #[test]
2006 fn test_detect_package_json_assemblyscript_in_dependencies() {
2007 let dir = create_temp_dir();
2008 fs::write(
2009 dir.path().join("package.json"),
2010 r#"{"name": "test", "dependencies": {"assemblyscript": "^0.27.0"}}"#,
2011 )
2012 .unwrap();
2013
2014 let lang = detect_language(dir.path()).unwrap();
2015 assert_eq!(lang, WasmLanguage::AssemblyScript);
2016 }
2017
2018 #[test]
2019 fn test_detect_package_json_assemblyscript_in_dev_dependencies() {
2020 let dir = create_temp_dir();
2021 fs::write(
2022 dir.path().join("package.json"),
2023 r#"{"name": "test", "devDependencies": {"assemblyscript": "^0.27.0"}}"#,
2024 )
2025 .unwrap();
2026
2027 let lang = detect_language(dir.path()).unwrap();
2028 assert_eq!(lang, WasmLanguage::AssemblyScript);
2029 }
2030
2031 #[test]
2032 fn test_detect_package_json_assemblyscript_in_scripts() {
2033 let dir = create_temp_dir();
2034 fs::write(
2035 dir.path().join("package.json"),
2036 r#"{"name": "test", "scripts": {"build": "asc assembly/index.ts"}}"#,
2037 )
2038 .unwrap();
2039
2040 let lang = detect_language(dir.path()).unwrap();
2041 assert_eq!(lang, WasmLanguage::AssemblyScript);
2042 }
2043
2044 #[test]
2045 fn test_detect_package_json_assemblyscript_asc_command() {
2046 let dir = create_temp_dir();
2047 fs::write(
2048 dir.path().join("package.json"),
2049 r#"{"name": "test", "scripts": {"compile": "asc"}}"#,
2050 )
2051 .unwrap();
2052
2053 let lang = detect_language(dir.path()).unwrap();
2054 assert_eq!(lang, WasmLanguage::AssemblyScript);
2055 }
2056
2057 #[test]
2058 fn test_detect_package_json_typescript() {
2059 let dir = create_temp_dir();
2060 fs::write(
2061 dir.path().join("package.json"),
2062 r#"{"name": "test", "version": "1.0.0", "devDependencies": {"typescript": "^5.0.0"}}"#,
2063 )
2064 .unwrap();
2065
2066 let lang = detect_language(dir.path()).unwrap();
2067 assert_eq!(lang, WasmLanguage::TypeScript);
2068 }
2069
2070 #[test]
2071 fn test_detect_package_json_plain_no_assemblyscript() {
2072 let dir = create_temp_dir();
2073 fs::write(
2074 dir.path().join("package.json"),
2075 r#"{"name": "test", "version": "1.0.0"}"#,
2076 )
2077 .unwrap();
2078
2079 let lang = detect_language(dir.path()).unwrap();
2080 assert_eq!(lang, WasmLanguage::TypeScript);
2081 }
2082
2083 #[test]
2084 fn test_detect_build_zig() {
2085 let dir = create_temp_dir();
2086 fs::write(
2087 dir.path().join("build.zig"),
2088 r#"const std = @import("std");
2089pub fn build(b: *std.build.Builder) void {}
2090"#,
2091 )
2092 .unwrap();
2093
2094 let lang = detect_language(dir.path()).unwrap();
2095 assert_eq!(lang, WasmLanguage::Zig);
2096 }
2097
2098 #[test]
2099 fn test_detect_makefile_with_c_files() {
2100 let dir = create_temp_dir();
2101 fs::write(dir.path().join("Makefile"), "all:\n\t$(CC) main.c -o main").unwrap();
2102 fs::write(dir.path().join("main.c"), "int main() { return 0; }").unwrap();
2103
2104 let lang = detect_language(dir.path()).unwrap();
2105 assert_eq!(lang, WasmLanguage::C);
2106 }
2107
2108 #[test]
2109 fn test_detect_cmakelists_with_c_files() {
2110 let dir = create_temp_dir();
2111 fs::write(
2112 dir.path().join("CMakeLists.txt"),
2113 "cmake_minimum_required(VERSION 3.10)\nproject(test)",
2114 )
2115 .unwrap();
2116 fs::write(dir.path().join("main.c"), "int main() { return 0; }").unwrap();
2117
2118 let lang = detect_language(dir.path()).unwrap();
2119 assert_eq!(lang, WasmLanguage::C);
2120 }
2121
2122 #[test]
2123 fn test_detect_c_header_file_only() {
2124 let dir = create_temp_dir();
2125 fs::write(
2126 dir.path().join("header.h"),
2127 "#ifndef HEADER_H\n#define HEADER_H\n#endif",
2128 )
2129 .unwrap();
2130
2131 let lang = detect_language(dir.path()).unwrap();
2132 assert_eq!(lang, WasmLanguage::C);
2133 }
2134
2135 #[test]
2136 fn test_detect_c_in_src_directory() {
2137 let dir = create_temp_dir();
2138 let src_dir = dir.path().join("src");
2139 fs::create_dir(&src_dir).unwrap();
2140 fs::write(src_dir.join("main.c"), "int main() { return 0; }").unwrap();
2141
2142 let lang = detect_language(dir.path()).unwrap();
2143 assert_eq!(lang, WasmLanguage::C);
2144 }
2145
2146 #[test]
2147 fn test_detect_empty_directory_error() {
2148 let dir = create_temp_dir();
2149 let result = detect_language(dir.path());
2152 assert!(matches!(
2153 result,
2154 Err(WasmBuildError::LanguageNotDetected { .. })
2155 ));
2156 }
2157
2158 #[test]
2159 fn test_detect_unknown_files_error() {
2160 let dir = create_temp_dir();
2161 fs::write(dir.path().join("random.txt"), "some text").unwrap();
2162 fs::write(dir.path().join("data.json"), "{}").unwrap();
2163
2164 let result = detect_language(dir.path());
2165 assert!(matches!(
2166 result,
2167 Err(WasmBuildError::LanguageNotDetected { .. })
2168 ));
2169 }
2170
2171 #[test]
2172 fn test_detect_makefile_without_c_files_error() {
2173 let dir = create_temp_dir();
2174 fs::write(dir.path().join("Makefile"), "all:\n\techo hello").unwrap();
2175
2176 let result = detect_language(dir.path());
2177 assert!(matches!(
2178 result,
2179 Err(WasmBuildError::LanguageNotDetected { .. })
2180 ));
2181 }
2182
2183 #[test]
2184 fn test_detect_priority_rust_over_package_json() {
2185 let dir = create_temp_dir();
2187 fs::write(
2188 dir.path().join("Cargo.toml"),
2189 r#"[package]
2190name = "test"
2191version = "0.1.0"
2192"#,
2193 )
2194 .unwrap();
2195 fs::write(dir.path().join("package.json"), r#"{"name": "test"}"#).unwrap();
2196
2197 let lang = detect_language(dir.path()).unwrap();
2198 assert_eq!(lang, WasmLanguage::Rust);
2199 }
2200
2201 #[test]
2202 fn test_detect_priority_go_over_python() {
2203 let dir = create_temp_dir();
2205 fs::write(dir.path().join("go.mod"), "module test").unwrap();
2206 fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
2207
2208 let lang = detect_language(dir.path()).unwrap();
2209 assert_eq!(lang, WasmLanguage::Go);
2210 }
2211 }
2212
2213 mod get_build_command_tests {
2218 use super::*;
2219
2220 #[test]
2221 fn test_rust_preview1_release() {
2222 let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, true);
2223 assert_eq!(cmd[0], "cargo");
2224 assert_eq!(cmd[1], "build");
2225 assert!(cmd.contains(&"--target".to_string()));
2226 assert!(cmd.contains(&"wasm32-wasip1".to_string()));
2227 assert!(cmd.contains(&"--release".to_string()));
2228 }
2229
2230 #[test]
2231 fn test_rust_preview2_release() {
2232 let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview2, true);
2233 assert_eq!(cmd[0], "cargo");
2234 assert_eq!(cmd[1], "build");
2235 assert!(cmd.contains(&"--target".to_string()));
2236 assert!(cmd.contains(&"wasm32-wasip2".to_string()));
2237 assert!(cmd.contains(&"--release".to_string()));
2238 }
2239
2240 #[test]
2241 fn test_rust_preview1_debug() {
2242 let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, false);
2243 assert_eq!(cmd[0], "cargo");
2244 assert!(cmd.contains(&"wasm32-wasip1".to_string()));
2245 assert!(!cmd.contains(&"--release".to_string()));
2246 }
2247
2248 #[test]
2249 fn test_rust_preview2_debug() {
2250 let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview2, false);
2251 assert_eq!(cmd[0], "cargo");
2252 assert!(cmd.contains(&"wasm32-wasip2".to_string()));
2253 assert!(!cmd.contains(&"--release".to_string()));
2254 }
2255
2256 #[test]
2257 fn test_rust_component_release() {
2258 let cmd = get_build_command(WasmLanguage::RustComponent, WasiTarget::Preview2, true);
2259 assert_eq!(cmd[0], "cargo");
2260 assert_eq!(cmd[1], "component");
2261 assert_eq!(cmd[2], "build");
2262 assert!(cmd.contains(&"--release".to_string()));
2263 }
2264
2265 #[test]
2266 fn test_rust_component_debug() {
2267 let cmd = get_build_command(WasmLanguage::RustComponent, WasiTarget::Preview2, false);
2268 assert_eq!(cmd[0], "cargo");
2269 assert_eq!(cmd[1], "component");
2270 assert_eq!(cmd[2], "build");
2271 assert!(!cmd.contains(&"--release".to_string()));
2272 }
2273
2274 #[test]
2275 fn test_go_preview1() {
2276 let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
2277 assert_eq!(cmd[0], "tinygo");
2278 assert_eq!(cmd[1], "build");
2279 assert!(cmd.contains(&"-target".to_string()));
2280 assert!(cmd.contains(&"wasip1".to_string()));
2281 }
2282
2283 #[test]
2284 fn test_go_preview2() {
2285 let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, false);
2286 assert_eq!(cmd[0], "tinygo");
2287 assert!(cmd.contains(&"-target".to_string()));
2288 assert!(cmd.contains(&"wasip2".to_string()));
2289 }
2290
2291 #[test]
2292 fn test_go_release_optimization() {
2293 let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, true);
2294 assert_eq!(cmd[0], "tinygo");
2295 assert!(cmd.contains(&"-opt".to_string()));
2296 assert!(cmd.contains(&"2".to_string()));
2297 }
2298
2299 #[test]
2300 fn test_go_debug_no_optimization() {
2301 let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, false);
2302 assert_eq!(cmd[0], "tinygo");
2303 assert!(!cmd.contains(&"-opt".to_string()));
2304 }
2305
2306 #[test]
2307 fn test_tinygo_correct_target_flag() {
2308 let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
2310 assert!(cmd.contains(&"-target".to_string()));
2311 assert!(!cmd.contains(&"--target".to_string()));
2312 }
2313
2314 #[test]
2315 fn test_python() {
2316 let cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, true);
2317 assert_eq!(cmd[0], "componentize-py");
2318 assert!(cmd.contains(&"-d".to_string()));
2319 assert!(cmd.contains(&"wit".to_string()));
2320 assert!(cmd.contains(&"-w".to_string()));
2321 assert!(cmd.contains(&"world".to_string()));
2322 assert!(cmd.contains(&"componentize".to_string()));
2323 assert!(cmd.contains(&"app".to_string()));
2324 assert!(cmd.contains(&"-o".to_string()));
2325 assert!(cmd.contains(&"app.wasm".to_string()));
2326 }
2327
2328 #[test]
2329 fn test_python_release_same_as_debug() {
2330 let release_cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, true);
2332 let debug_cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, false);
2333 assert_eq!(release_cmd, debug_cmd);
2334 }
2335
2336 #[test]
2337 fn test_componentize_py_arguments() {
2338 let cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, false);
2339 let cmd_str = cmd.join(" ");
2341 assert!(
2342 cmd_str.contains("componentize-py -d wit -w world componentize app -o app.wasm")
2343 );
2344 }
2345
2346 #[test]
2347 fn test_typescript() {
2348 let cmd = get_build_command(WasmLanguage::TypeScript, WasiTarget::Preview2, true);
2349 assert_eq!(cmd[0], "npx");
2350 assert!(cmd.contains(&"jco".to_string()));
2351 assert!(cmd.contains(&"componentize".to_string()));
2352 assert!(cmd.contains(&"--wit".to_string()));
2353 }
2354
2355 #[test]
2356 fn test_assemblyscript_release() {
2357 let cmd = get_build_command(WasmLanguage::AssemblyScript, WasiTarget::Preview2, true);
2358 assert_eq!(cmd[0], "npx");
2359 assert!(cmd.contains(&"asc".to_string()));
2360 assert!(cmd.contains(&"--optimize".to_string()));
2361 }
2362
2363 #[test]
2364 fn test_assemblyscript_debug() {
2365 let cmd = get_build_command(WasmLanguage::AssemblyScript, WasiTarget::Preview2, false);
2366 assert_eq!(cmd[0], "npx");
2367 assert!(cmd.contains(&"asc".to_string()));
2368 assert!(!cmd.contains(&"--optimize".to_string()));
2369 }
2370
2371 #[test]
2372 fn test_c_release() {
2373 let cmd = get_build_command(WasmLanguage::C, WasiTarget::Preview1, true);
2374 assert_eq!(cmd[0], "clang");
2375 assert!(cmd.contains(&"--target=wasm32-wasi".to_string()));
2376 assert!(cmd.contains(&"-O2".to_string()));
2377 }
2378
2379 #[test]
2380 fn test_c_debug() {
2381 let cmd = get_build_command(WasmLanguage::C, WasiTarget::Preview1, false);
2382 assert_eq!(cmd[0], "clang");
2383 assert!(cmd.contains(&"--target=wasm32-wasi".to_string()));
2384 assert!(!cmd.contains(&"-O2".to_string()));
2385 }
2386
2387 #[test]
2388 fn test_zig_release() {
2389 let cmd = get_build_command(WasmLanguage::Zig, WasiTarget::Preview1, true);
2390 assert_eq!(cmd[0], "zig");
2391 assert_eq!(cmd[1], "build");
2392 assert!(cmd.contains(&"-Dtarget=wasm32-wasi".to_string()));
2393 assert!(cmd.contains(&"-Doptimize=ReleaseFast".to_string()));
2394 }
2395
2396 #[test]
2397 fn test_zig_debug() {
2398 let cmd = get_build_command(WasmLanguage::Zig, WasiTarget::Preview1, false);
2399 assert_eq!(cmd[0], "zig");
2400 assert_eq!(cmd[1], "build");
2401 assert!(cmd.contains(&"-Dtarget=wasm32-wasi".to_string()));
2402 assert!(!cmd.contains(&"-Doptimize=ReleaseFast".to_string()));
2403 }
2404
2405 #[test]
2406 fn test_cargo_uses_double_dash_target() {
2407 let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, false);
2409 assert!(cmd.contains(&"--target".to_string()));
2410 }
2411
2412 #[test]
2413 fn test_go_output_file() {
2414 let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
2415 assert!(cmd.contains(&"-o".to_string()));
2416 assert!(cmd.contains(&"main.wasm".to_string()));
2417 }
2418
2419 #[test]
2420 fn test_all_commands_non_empty() {
2421 for lang in WasmLanguage::all() {
2422 for target in [WasiTarget::Preview1, WasiTarget::Preview2] {
2423 for release in [true, false] {
2424 let cmd = get_build_command(*lang, target, release);
2425 assert!(
2426 !cmd.is_empty(),
2427 "Command for {lang:?}/{target:?}/{release} should not be empty",
2428 );
2429 assert!(
2430 !cmd[0].is_empty(),
2431 "First command element should not be empty"
2432 );
2433 }
2434 }
2435 }
2436 }
2437 }
2438
2439 mod helper_function_tests {
2444 use super::*;
2445
2446 #[test]
2447 fn test_get_rust_package_name_success() {
2448 let dir = create_temp_dir();
2449 fs::write(
2450 dir.path().join("Cargo.toml"),
2451 r#"[package]
2452name = "my-cool-package"
2453version = "0.1.0"
2454"#,
2455 )
2456 .unwrap();
2457
2458 let name = get_rust_package_name(dir.path()).unwrap();
2459 assert_eq!(name, "my-cool-package");
2460 }
2461
2462 #[test]
2463 fn test_get_rust_package_name_with_single_quotes() {
2464 let dir = create_temp_dir();
2465 fs::write(
2466 dir.path().join("Cargo.toml"),
2467 "[package]\nname = 'single-quoted'\nversion = '0.1.0'\n",
2468 )
2469 .unwrap();
2470
2471 let name = get_rust_package_name(dir.path()).unwrap();
2472 assert_eq!(name, "single-quoted");
2473 }
2474
2475 #[test]
2476 fn test_get_rust_package_name_missing_file() {
2477 let dir = create_temp_dir();
2478 let result = get_rust_package_name(dir.path());
2481 assert!(matches!(
2482 result,
2483 Err(WasmBuildError::ProjectConfigError { .. })
2484 ));
2485 }
2486
2487 #[test]
2488 fn test_get_rust_package_name_no_name_field() {
2489 let dir = create_temp_dir();
2490 fs::write(
2491 dir.path().join("Cargo.toml"),
2492 "[package]\nversion = \"0.1.0\"\n",
2493 )
2494 .unwrap();
2495
2496 let result = get_rust_package_name(dir.path());
2497 assert!(matches!(
2498 result,
2499 Err(WasmBuildError::ProjectConfigError { .. })
2500 ));
2501 }
2502
2503 #[test]
2504 fn test_find_any_wasm_file_in_root() {
2505 let dir = create_temp_dir();
2506 fs::write(dir.path().join("test.wasm"), "wasm content").unwrap();
2507
2508 let found = find_any_wasm_file(dir.path());
2509 assert!(found.is_some());
2510 assert!(found.unwrap().ends_with("test.wasm"));
2511 }
2512
2513 #[test]
2514 fn test_find_any_wasm_file_in_target() {
2515 let dir = create_temp_dir();
2516 let target_dir = dir.path().join("target");
2517 fs::create_dir(&target_dir).unwrap();
2518 fs::write(target_dir.join("output.wasm"), "wasm content").unwrap();
2519
2520 let found = find_any_wasm_file(dir.path());
2521 assert!(found.is_some());
2522 }
2523
2524 #[test]
2525 fn test_find_any_wasm_file_in_build() {
2526 let dir = create_temp_dir();
2527 let build_dir = dir.path().join("build");
2528 fs::create_dir(&build_dir).unwrap();
2529 fs::write(build_dir.join("module.wasm"), "wasm content").unwrap();
2530
2531 let found = find_any_wasm_file(dir.path());
2532 assert!(found.is_some());
2533 }
2534
2535 #[test]
2536 fn test_find_any_wasm_file_in_dist() {
2537 let dir = create_temp_dir();
2538 let dist_dir = dir.path().join("dist");
2539 fs::create_dir(&dist_dir).unwrap();
2540 fs::write(dist_dir.join("bundle.wasm"), "wasm content").unwrap();
2541
2542 let found = find_any_wasm_file(dir.path());
2543 assert!(found.is_some());
2544 }
2545
2546 #[test]
2547 fn test_find_any_wasm_file_nested() {
2548 let dir = create_temp_dir();
2549 let nested = dir
2550 .path()
2551 .join("target")
2552 .join("wasm32-wasip2")
2553 .join("release");
2554 fs::create_dir_all(&nested).unwrap();
2555 fs::write(nested.join("deep.wasm"), "wasm content").unwrap();
2556
2557 let found = find_any_wasm_file(dir.path());
2558 assert!(found.is_some());
2559 }
2560
2561 #[test]
2562 fn test_find_any_wasm_file_none() {
2563 let dir = create_temp_dir();
2564 fs::write(dir.path().join("not_wasm.txt"), "text").unwrap();
2565
2566 let found = find_any_wasm_file(dir.path());
2567 assert!(found.is_none());
2568 }
2569
2570 #[test]
2571 fn test_find_any_wasm_file_respects_depth_limit() {
2572 let dir = create_temp_dir();
2573 let deep = dir.path().join("a").join("b").join("c").join("d").join("e");
2575 fs::create_dir_all(&deep).unwrap();
2576 fs::write(deep.join("too_deep.wasm"), "wasm").unwrap();
2577
2578 let _ = find_any_wasm_file(dir.path());
2581 }
2583
2584 #[test]
2585 fn test_has_c_source_files_true_c() {
2586 let dir = create_temp_dir();
2587 fs::write(dir.path().join("main.c"), "int main() {}").unwrap();
2588
2589 assert!(has_c_source_files(dir.path()));
2590 }
2591
2592 #[test]
2593 fn test_has_c_source_files_true_h() {
2594 let dir = create_temp_dir();
2595 fs::write(dir.path().join("header.h"), "#pragma once").unwrap();
2596
2597 assert!(has_c_source_files(dir.path()));
2598 }
2599
2600 #[test]
2601 fn test_has_c_source_files_in_src() {
2602 let dir = create_temp_dir();
2603 let src = dir.path().join("src");
2604 fs::create_dir(&src).unwrap();
2605 fs::write(src.join("lib.c"), "void foo() {}").unwrap();
2606
2607 assert!(has_c_source_files(dir.path()));
2608 }
2609
2610 #[test]
2611 fn test_has_c_source_files_false() {
2612 let dir = create_temp_dir();
2613 fs::write(dir.path().join("main.rs"), "fn main() {}").unwrap();
2614
2615 assert!(!has_c_source_files(dir.path()));
2616 }
2617
2618 #[test]
2619 fn test_is_cargo_component_project_with_metadata() {
2620 let dir = create_temp_dir();
2621 let cargo_toml = dir.path().join("Cargo.toml");
2622 fs::write(
2623 &cargo_toml,
2624 r#"[package]
2625name = "test"
2626[package.metadata.component]
2627package = "test:component"
2628"#,
2629 )
2630 .unwrap();
2631
2632 assert!(is_cargo_component_project(&cargo_toml).unwrap());
2633 }
2634
2635 #[test]
2636 fn test_is_cargo_component_project_with_wit_bindgen() {
2637 let dir = create_temp_dir();
2638 let cargo_toml = dir.path().join("Cargo.toml");
2639 fs::write(
2640 &cargo_toml,
2641 r#"[package]
2642name = "test"
2643[dependencies]
2644wit-bindgen = "0.20"
2645"#,
2646 )
2647 .unwrap();
2648
2649 assert!(is_cargo_component_project(&cargo_toml).unwrap());
2650 }
2651
2652 #[test]
2653 fn test_is_cargo_component_project_plain_rust() {
2654 let dir = create_temp_dir();
2655 let cargo_toml = dir.path().join("Cargo.toml");
2656 fs::write(
2657 &cargo_toml,
2658 r#"[package]
2659name = "test"
2660version = "0.1.0"
2661"#,
2662 )
2663 .unwrap();
2664
2665 assert!(!is_cargo_component_project(&cargo_toml).unwrap());
2666 }
2667
2668 #[test]
2669 fn test_is_assemblyscript_project_dependencies() {
2670 let dir = create_temp_dir();
2671 let package_json = dir.path().join("package.json");
2672 fs::write(
2673 &package_json,
2674 r#"{"dependencies": {"assemblyscript": "^0.27"}}"#,
2675 )
2676 .unwrap();
2677
2678 assert!(is_assemblyscript_project(&package_json).unwrap());
2679 }
2680
2681 #[test]
2682 fn test_is_assemblyscript_project_dev_dependencies() {
2683 let dir = create_temp_dir();
2684 let package_json = dir.path().join("package.json");
2685 fs::write(
2686 &package_json,
2687 r#"{"devDependencies": {"assemblyscript": "^0.27"}}"#,
2688 )
2689 .unwrap();
2690
2691 assert!(is_assemblyscript_project(&package_json).unwrap());
2692 }
2693
2694 #[test]
2695 fn test_is_assemblyscript_project_script_with_asc() {
2696 let dir = create_temp_dir();
2697 let package_json = dir.path().join("package.json");
2698 fs::write(
2699 &package_json,
2700 r#"{"scripts": {"build": "asc assembly/index.ts"}}"#,
2701 )
2702 .unwrap();
2703
2704 assert!(is_assemblyscript_project(&package_json).unwrap());
2705 }
2706
2707 #[test]
2708 fn test_is_assemblyscript_project_false() {
2709 let dir = create_temp_dir();
2710 let package_json = dir.path().join("package.json");
2711 fs::write(&package_json, r#"{"dependencies": {"react": "^18.0.0"}}"#).unwrap();
2712
2713 assert!(!is_assemblyscript_project(&package_json).unwrap());
2714 }
2715
2716 #[test]
2717 fn test_is_assemblyscript_project_invalid_json() {
2718 let dir = create_temp_dir();
2719 let package_json = dir.path().join("package.json");
2720 fs::write(&package_json, "not valid json").unwrap();
2721
2722 let result = is_assemblyscript_project(&package_json);
2723 assert!(matches!(
2724 result,
2725 Err(WasmBuildError::ProjectConfigError { .. })
2726 ));
2727 }
2728 }
2729
2730 mod find_wasm_output_tests {
2735 use super::*;
2736
2737 #[test]
2738 fn test_find_rust_release_output() {
2739 let dir = create_temp_dir();
2740 fs::write(
2741 dir.path().join("Cargo.toml"),
2742 "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
2743 )
2744 .unwrap();
2745
2746 let output_dir = dir
2747 .path()
2748 .join("target")
2749 .join("wasm32-wasip2")
2750 .join("release");
2751 fs::create_dir_all(&output_dir).unwrap();
2752 fs::write(output_dir.join("myapp.wasm"), "wasm").unwrap();
2753
2754 let result =
2755 find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2756 assert!(result.is_ok());
2757 assert!(result.unwrap().ends_with("myapp.wasm"));
2758 }
2759
2760 #[test]
2761 fn test_find_rust_debug_output() {
2762 let dir = create_temp_dir();
2763 fs::write(
2764 dir.path().join("Cargo.toml"),
2765 "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
2766 )
2767 .unwrap();
2768
2769 let output_dir = dir
2770 .path()
2771 .join("target")
2772 .join("wasm32-wasip1")
2773 .join("debug");
2774 fs::create_dir_all(&output_dir).unwrap();
2775 fs::write(output_dir.join("myapp.wasm"), "wasm").unwrap();
2776
2777 let result =
2778 find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview1, false);
2779 assert!(result.is_ok());
2780 }
2781
2782 #[test]
2783 fn test_find_rust_underscore_name() {
2784 let dir = create_temp_dir();
2785 fs::write(
2786 dir.path().join("Cargo.toml"),
2787 "[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
2788 )
2789 .unwrap();
2790
2791 let output_dir = dir
2792 .path()
2793 .join("target")
2794 .join("wasm32-wasip2")
2795 .join("release");
2796 fs::create_dir_all(&output_dir).unwrap();
2797 fs::write(output_dir.join("my_app.wasm"), "wasm").unwrap();
2799
2800 let result =
2801 find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2802 assert!(result.is_ok());
2803 }
2804
2805 #[test]
2806 fn test_find_go_output() {
2807 let dir = create_temp_dir();
2808 fs::write(dir.path().join("main.wasm"), "wasm").unwrap();
2809
2810 let result =
2811 find_wasm_output(dir.path(), WasmLanguage::Go, WasiTarget::Preview1, false);
2812 assert!(result.is_ok());
2813 assert!(result.unwrap().ends_with("main.wasm"));
2814 }
2815
2816 #[test]
2817 fn test_find_python_output() {
2818 let dir = create_temp_dir();
2819 fs::write(dir.path().join("app.wasm"), "wasm").unwrap();
2820
2821 let result =
2822 find_wasm_output(dir.path(), WasmLanguage::Python, WasiTarget::Preview2, true);
2823 assert!(result.is_ok());
2824 assert!(result.unwrap().ends_with("app.wasm"));
2825 }
2826
2827 #[test]
2828 fn test_find_typescript_output() {
2829 let dir = create_temp_dir();
2830 let dist_dir = dir.path().join("dist");
2831 fs::create_dir(&dist_dir).unwrap();
2832 fs::write(dist_dir.join("component.wasm"), "wasm").unwrap();
2833
2834 let result = find_wasm_output(
2835 dir.path(),
2836 WasmLanguage::TypeScript,
2837 WasiTarget::Preview2,
2838 true,
2839 );
2840 assert!(result.is_ok());
2841 }
2842
2843 #[test]
2844 fn test_find_assemblyscript_release_output() {
2845 let dir = create_temp_dir();
2846 let build_dir = dir.path().join("build");
2847 fs::create_dir(&build_dir).unwrap();
2848 fs::write(build_dir.join("release.wasm"), "wasm").unwrap();
2849
2850 let result = find_wasm_output(
2851 dir.path(),
2852 WasmLanguage::AssemblyScript,
2853 WasiTarget::Preview2,
2854 true,
2855 );
2856 assert!(result.is_ok());
2857 }
2858
2859 #[test]
2860 fn test_find_c_output() {
2861 let dir = create_temp_dir();
2862 fs::write(dir.path().join("main.wasm"), "wasm").unwrap();
2863
2864 let result = find_wasm_output(dir.path(), WasmLanguage::C, WasiTarget::Preview1, true);
2865 assert!(result.is_ok());
2866 }
2867
2868 #[test]
2869 fn test_find_zig_output() {
2870 let dir = create_temp_dir();
2871 let zig_out = dir.path().join("zig-out").join("bin");
2872 fs::create_dir_all(&zig_out).unwrap();
2873 fs::write(zig_out.join("main.wasm"), "wasm").unwrap();
2874
2875 let result =
2876 find_wasm_output(dir.path(), WasmLanguage::Zig, WasiTarget::Preview1, true);
2877 assert!(result.is_ok());
2878 }
2879
2880 #[test]
2881 fn test_find_output_not_found() {
2882 let dir = create_temp_dir();
2883 let result =
2886 find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2887 assert!(matches!(result, Err(WasmBuildError::OutputNotFound { .. })));
2888 }
2889
2890 #[test]
2891 fn test_find_output_fallback_search() {
2892 let dir = create_temp_dir();
2893 fs::write(
2894 dir.path().join("Cargo.toml"),
2895 "[package]\nname = \"test\"\n",
2896 )
2897 .unwrap();
2898
2899 let other_dir = dir.path().join("target").join("other");
2901 fs::create_dir_all(&other_dir).unwrap();
2902 fs::write(other_dir.join("unexpected.wasm"), "wasm").unwrap();
2903
2904 let result =
2906 find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2907 assert!(result.is_ok());
2908 }
2909 }
2910}