diff --git a/examples/wasip1/Makefile b/examples/wasip1/Makefile new file mode 100644 index 0000000..9570a4a --- /dev/null +++ b/examples/wasip1/Makefile @@ -0,0 +1,6 @@ +build: + @componentize-go build --wasip1 + +.PHONY: run +run: build + @wasmtime run main.wasm diff --git a/examples/wasip1/README.md b/examples/wasip1/README.md new file mode 100644 index 0000000..2cc0970 --- /dev/null +++ b/examples/wasip1/README.md @@ -0,0 +1,14 @@ +# `wasip1` Example + +## Usage + +### Prerequisites + +- [**componentize-go**](https://github.com/bytecodealliance/componentize-go) - Latest version +- [**go**](https://go.dev/dl/) - v1.25+ +- [**wasmtime**](https://github.com/bytecodealliance/wasmtime) - v40.0.2 + +### Run + +```sh +make run diff --git a/examples/wasip1/main.go b/examples/wasip1/main.go new file mode 100644 index 0000000..a3dd973 --- /dev/null +++ b/examples/wasip1/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +} diff --git a/examples/wasip2/README.md b/examples/wasip2/README.md index fa1bf11..d88d800 100644 --- a/examples/wasip2/README.md +++ b/examples/wasip2/README.md @@ -9,6 +9,7 @@ - [**wasmtime**](https://github.com/bytecodealliance/wasmtime) - v40.0.2 ### Run + ```sh # Start the application make run @@ -18,6 +19,7 @@ curl localhost:8080 ``` ### Run unit tests + ```sh make run-tests ``` diff --git a/examples/wasip3/README.md b/examples/wasip3/README.md index 24edbae..44e053c 100644 --- a/examples/wasip3/README.md +++ b/examples/wasip3/README.md @@ -1,4 +1,5 @@ # `wasip3` Example + This demonstrates how to implement a `wasi:http@0.3.0-rc-2025-09-16` handler using Go, based on a new `wit-bindgen-go` bindings generator which supports idiomatic, goroutine-based concurrency on top of the [Component Model @@ -11,7 +12,9 @@ Go](https://github.com/dicej/go/releases/tag/go1.25.5-wasi-on-idle). Once everything is merged, we'll be able to switch to the upstream releases. ## Building and Running + ### Prerequisites + - [**componentize-go**](https://github.com/bytecodealliance/componentize-go) - Latest version - [**go**](https://github.com/dicej/go/releases/tag/go1.25.5-wasi-on-idle) - The [Makefile](./Makefile) installs the patched version of Go. - [**wasmtime**](https://github.com/bytecodealliance/wasmtime) - v40.0.2 @@ -56,4 +59,5 @@ curl -i \ ``` ## Note + This was originally built by [@dicej](https://github.com/dicej) and has been adapted from the [original example](https://github.com/dicej/go-wasi-http-example/tree/main) to use componentize-go. diff --git a/src/cmd_build.rs b/src/cmd_build.rs index c7dd212..156cdcf 100644 --- a/src/cmd_build.rs +++ b/src/cmd_build.rs @@ -44,13 +44,12 @@ pub fn build_module( None => ".", }; - // TODO: for when/if we decide to allow users to build wasm modules without componentizing them - #[allow(unused_variables)] + // The -buildmode flag mutes the module's output, so it is ommitted let module_args = [ "build", "-C", module_path, - "-buildmode=c-shared", + "-ldflags=-checklinkname=0", "-o", out_path, ]; @@ -66,9 +65,6 @@ pub fn build_module( ]; let output = if only_wasip1 { - unimplemented!("Building wasip1 Go apps isn't supported quite yet."); - // TODO: for when/if we decide to allow users to build wasm modules without componentizing them - #[allow(unreachable_code)] Command::new(&go) .args(module_args) .env("GOOS", "wasip1") diff --git a/src/cmd_test.rs b/src/cmd_test.rs index b5a5b99..7b47f50 100644 --- a/src/cmd_test.rs +++ b/src/cmd_test.rs @@ -74,7 +74,9 @@ pub fn build_test_module( .env("GOARCH", "wasm") .output()? } else { - unimplemented!("Please use the --wasip1 flag when building unit tests"); + unimplemented!( + "Building Go test components is not yet supported. Please use the --wasip1 flag when building unit tests." + ); // TODO: for when we figure out how wasip2 tests are to be run #[allow(unreachable_code)] diff --git a/tests/src/lib.rs b/tests/src/lib.rs index ab74a89..b7ed602 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -92,76 +92,88 @@ mod tests { go_bin } - struct App<'a> { + struct App { /// The path to the example application path: String, /// The WIT world to target - world: String, + world: Option, /// The output path of the wasm file wasm_path: String, /// The path to the directory containing the WIT files - wit_path: String, + wit_path: Option, /// The child process ID of a running wasm app process: Option, /// Any tests that need to be compiled and run as such - tests: Option<&'a [Test<'a>]>, + tests: Option>, } - struct Test<'a> { + #[derive(Clone)] + struct Test { should_fail: bool, - pkg_path: &'a str, + pkg_path: String, } - impl<'a> App<'a> { + impl App { /// Create a new app runner. - fn new(path: &'a str, world: &'a str, tests: Option<&'a [Test<'a>]>) -> Self { + fn new( + path: &str, + world: Option<&str>, + tests: Option>, + is_component: bool, + ) -> Self { let path = componentize_go::utils::make_path_absolute(&PathBuf::from(path)) .expect("failed to make app path absolute"); + let world = if is_component { + Some(world.expect("WIT world must be specified").to_string()) + } else { + None + }; + + let wit_path = if is_component { + Some( + path.join("wit") + .to_str() + .expect("wit_path is not valid unicode") + .to_string(), + ) + } else { + None + }; + App { path: path .clone() .to_str() .expect("app path is not valid unicode") .to_string(), - world: world.to_string(), + world, wasm_path: path .join("main.wasm") .to_str() .expect("wasm_path is not valid unicode") .to_string(), - wit_path: path - .join("wit") - .to_str() - .expect("wit_path is not valid unicode") - .to_string(), + wit_path, process: None, tests, } } - // Build unit tests with componentize-go - fn build_tests(&self, go: Option<&PathBuf>) -> Result<()> { - let test_pkgs = if let Some(pkgs) = self.tests { - pkgs - } else { - return Err(anyhow!( - "Please include the test_pkg_paths when creating App::new()" - )); - }; + fn build_test_modules(&self, go: Option<&PathBuf>) -> Result<()> { + let test_pkgs = self.tests.as_ref().expect("missing test_pkg_paths"); self.generate_bindings(go)?; let mut test_cmd = Command::new(COMPONENTIZE_GO_PATH.as_path()); test_cmd - .args(["-w", &self.world]) - .args(["-d", &self.wit_path]) + .args(["-w", self.world.as_ref().expect("missing WIT world")]) + .args(["-d", self.wit_path.as_ref().expect("missing WIT path")]) .arg("test") .arg("--wasip1"); // Add all the paths to the packages that have unit tests to compile for test in test_pkgs.iter() { - test_cmd.args(["--pkg", test.pkg_path]); + test_cmd.args(["--pkg", &test.pkg_path]); } // `go test -c` needs to be in the same path as the go.mod file. @@ -183,13 +195,30 @@ mod tests { Ok(()) } - fn run_tests(&self) -> Result<()> { + fn run_module(&self) -> Result<()> { + let output = Command::new("wasmtime") + .arg("run") + .arg(&self.wasm_path) + .output()?; + + if !output.status.success() { + return Err(anyhow!( + "Failed to run wasm module for application at '{}':\n{} ", + &self.wasm_path, + String::from_utf8_lossy(&output.stdout) + )); + } + + Ok(()) + } + + fn run_test_modules(&self) -> Result<()> { let example_dir = PathBuf::from(&self.path); - if let Some(tests) = self.tests { + if let Some(tests) = &self.tests { let mut test_errors: Vec = vec![]; for test in tests.iter() { let wasm_file = example_dir.join(componentize_go::cmd_test::get_test_filename( - &PathBuf::from(test.pkg_path), + &PathBuf::from(&test.pkg_path), )); match Command::new("wasmtime") .args(["run", wasm_file.to_str().unwrap()]) @@ -233,15 +262,45 @@ mod tests { Ok(()) } - /// Build the app with componentize-go. - fn build(&self, go: Option<&PathBuf>) -> Result<()> { + fn build_module(&self, go: Option<&PathBuf>) -> Result<()> { + // Build component + let mut build_cmd = Command::new(COMPONENTIZE_GO_PATH.as_path()); + build_cmd + .arg("build") + .arg("--wasip1") + .args(["-o", &self.wasm_path]); + + if let Some(go_path) = go.as_ref() { + build_cmd.args(["--go", go_path.to_str().unwrap()]); + } + + // Run `go build` in the same directory as the go.mod file. + build_cmd.current_dir(&self.path); + + let build_output = build_cmd.output().expect(&format!( + "failed to execute componentize-go for \"{}\"", + self.path + )); + + if !build_output.status.success() { + return Err(anyhow!( + "failed to build application \"{}\": {}", + self.path, + String::from_utf8_lossy(&build_output.stderr) + )); + } + + Ok(()) + } + + fn build_component(&self, go: Option<&PathBuf>) -> Result<()> { self.generate_bindings(go)?; // Build component let mut build_cmd = Command::new(COMPONENTIZE_GO_PATH.as_path()); build_cmd - .args(["-w", &self.world]) - .args(["-d", &self.wit_path]) + .args(["-w", self.world.as_ref().expect("missing WIT world")]) + .args(["-d", self.wit_path.as_ref().expect("missing WIT path")]) .arg("build") .args(["-o", &self.wasm_path]); @@ -270,8 +329,8 @@ mod tests { fn generate_bindings(&self, go: Option<&PathBuf>) -> Result<()> { let bindings_output = Command::new(COMPONENTIZE_GO_PATH.as_path()) - .args(["-w", &self.world]) - .args(["-d", &self.wit_path]) + .args(["-w", self.world.as_ref().expect("missing WIT world")]) + .args(["-d", self.wit_path.as_ref().expect("missing WIT path")]) .arg("bindings") .args(["-o", &self.path]) .current_dir(&self.path) @@ -307,7 +366,7 @@ mod tests { } /// Run the app and check the output. - async fn run(&mut self, route: &str, expected_response: &str) -> Result<()> { + async fn run_component(&mut self, route: &str, expected_response: &str) -> Result<()> { let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to a free port"); let addr = listener.local_addr().expect("Failed to get local address"); let port = addr.port(); @@ -345,7 +404,7 @@ mod tests { } } - impl<'a> Drop for App<'a> { + impl Drop for App { fn drop(&mut self) { if let Some(child) = &mut self.process { _ = child.kill() @@ -353,40 +412,52 @@ mod tests { } } + #[test] + fn example_wasip1() { + let app = App::new("../examples/wasip1", None, None, false); + app.build_module(None).expect("failed to build app module"); + app.run_module().expect("failed to run app module"); + } + #[tokio::test] async fn example_wasip2() { - let unit_tests = [ + let unit_tests = vec![ Test { should_fail: false, - pkg_path: "./unit_tests_should_pass", + pkg_path: String::from("./unit_tests_should_pass"), }, Test { should_fail: true, - pkg_path: "./unit_tests_should_fail", + pkg_path: String::from("./unit_tests_should_fail"), }, ]; - let mut app = App::new("../examples/wasip2", "wasip2-example", Some(&unit_tests)); + let mut app = App::new( + "../examples/wasip2", + Some("wasip2-example"), + Some(unit_tests), + true, + ); - app.build(None).expect("failed to build app"); + app.build_component(None).expect("failed to build app"); - app.run("/", "Hello, world!") + app.run_component("/", "Hello, world!") .await .expect("app failed to run"); - app.build_tests(None) + app.build_test_modules(None) .expect("failed to build app unit tests"); - app.run_tests() + app.run_test_modules() .expect("tests succeeded/failed when they should not have"); } #[tokio::test] async fn example_wasip3() { - let mut app = App::new("../examples/wasip3", "wasip3-example", None); - app.build(Some(&patched_go_path().await)) + let mut app = App::new("../examples/wasip3", Some("wasip3-example"), None, true); + app.build_component(Some(&patched_go_path().await)) .expect("failed to build app"); - app.run("/hello", "Hello, world!") + app.run_component("/hello", "Hello, world!") .await .expect("app failed to run"); }