From 7f55f287ba87bb9e435689378b4c29dab76465aa Mon Sep 17 00:00:00 2001 From: Jeremy Fleitz Date: Fri, 31 Jul 2026 15:03:58 -0400 Subject: [PATCH 1/2] add async tag and build tags support Signed-off-by: Jeremy Fleitz --- README.md | 12 ++ .../export_wasi_http_handler/async_tag.go | 9 ++ .../export_wasi_http_handler/handler.go | 5 + src/cmd_build.rs | 62 ++++----- src/cmd_test.rs | 79 +++++------ src/command.rs | 24 +++- src/utils.rs | 131 ++++++++++++++++++ 7 files changed, 243 insertions(+), 79 deletions(-) create mode 100644 examples/wasip3/export_wasi_http_handler/async_tag.go diff --git a/README.md b/README.md index cd6cf6a..6694221 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,15 @@ cargo install --git https://github.com/bytecodealliance/componentize-go ## Usage Please reference the `README.md` and `Makefile` files in each of the directories in [examples](./examples/). + +## Build tags + +When compiling your Go module, `componentize-go` passes `go build` the +`componentizego_async` build tag when the selected WIT world uses async +features (async functions, `stream`s, or `future`s). + +SDKs can use `//go:build componentizego_async` to select between WASI 0.2 and +WASI 0.3 implementations of an API at compile time. + +Any `-tags` you specify via the `GOFLAGS` environment variable are merged with +the tags above (rather than being overridden by them). diff --git a/examples/wasip3/export_wasi_http_handler/async_tag.go b/examples/wasip3/export_wasi_http_handler/async_tag.go new file mode 100644 index 0000000..1424a54 --- /dev/null +++ b/examples/wasip3/export_wasi_http_handler/async_tag.go @@ -0,0 +1,9 @@ +//go:build componentizego_async + +package export_wasi_http_handler + +// componentize-go passes the `componentizego_async` build tag to `go build` +// when the target world uses async features, as this example's +// `wasip3-example` world does. The reference to this constant in handler.go +// will fail to compile if the tag does not reach the Go compiler. +const asyncTagPresent = true diff --git a/examples/wasip3/export_wasi_http_handler/handler.go b/examples/wasip3/export_wasi_http_handler/handler.go index 6a38655..99c8740 100644 --- a/examples/wasip3/export_wasi_http_handler/handler.go +++ b/examples/wasip3/export_wasi_http_handler/handler.go @@ -11,6 +11,11 @@ import ( . "go.bytecodealliance.org/pkg/wit/types" ) +// This fails to compile unless componentize-go passed the +// `componentizego_async` build tag (see async_tag.go), which it must for this +// async world. +var _ = asyncTagPresent + // Handle the specified `Request`, returning a `Response` func Handle(request *Request) Result[*Response, ErrorCode] { method := request.GetMethod().Tag() diff --git a/src/cmd_build.rs b/src/cmd_build.rs index f9ffdb3..341523c 100644 --- a/src/cmd_build.rs +++ b/src/cmd_build.rs @@ -1,4 +1,4 @@ -use crate::utils::{check_go_version, make_path_absolute}; +use crate::utils::{check_go_version, go_tags_arg, make_path_absolute}; use anyhow::{Result, anyhow}; use std::{ path::{Path, PathBuf}, @@ -9,7 +9,16 @@ use std::{ /// /// If the module is not going to be adapted to the component model, /// set the `only_wasip1` arg to true. -pub fn build_module(out: Option<&PathBuf>, go: &Path, only_wasip1: bool) -> Result { +/// +/// `tags` contains any build tags derived from the target world (see +/// [`crate::utils::world_build_tags`]); these are merged with any `-tags` +/// specified via `GOFLAGS` and passed to `go build`. +pub fn build_module( + out: Option<&PathBuf>, + go: &Path, + only_wasip1: bool, + tags: &[String], +) -> Result { check_go_version(go)?; let out_path_buf = match &out { @@ -26,39 +35,24 @@ pub fn build_module(out: Option<&PathBuf>, go: &Path, only_wasip1: bool) -> Resu .to_str() .ok_or_else(|| anyhow!("Output path is not valid unicode"))?; - // The -buildmode flag mutes the module's output, so it is ommitted - let module_args = [ - "build", - "-C", - ".", - "-ldflags=-checklinkname=0", - "-o", - out_path, - ]; - - let component_args = [ - "build", - "-C", - ".", - "-buildmode=c-shared", - "-ldflags=-checklinkname=0", - "-o", - out_path, - ]; + let mut args = vec!["build".to_string(), "-C".to_string(), ".".to_string()]; + // The -buildmode flag mutes the module's output, so it is ommitted when + // building a plain wasip1 module + if !only_wasip1 { + args.push("-buildmode=c-shared".to_string()); + } + args.push("-ldflags=-checklinkname=0".to_string()); + if let Some(tags_arg) = go_tags_arg(tags) { + args.push(tags_arg); + } + args.push("-o".to_string()); + args.push(out_path.to_string()); - let output = if only_wasip1 { - Command::new(go) - .args(module_args) - .env("GOOS", "wasip1") - .env("GOARCH", "wasm") - .output()? - } else { - Command::new(go) - .args(component_args) - .env("GOOS", "wasip1") - .env("GOARCH", "wasm") - .output()? - }; + let output = Command::new(go) + .args(&args) + .env("GOOS", "wasip1") + .env("GOARCH", "wasm") + .output()?; if !output.status.success() { return Err(anyhow!( diff --git a/src/cmd_test.rs b/src/cmd_test.rs index 1cd1954..9f4a4a7 100644 --- a/src/cmd_test.rs +++ b/src/cmd_test.rs @@ -1,4 +1,4 @@ -use crate::utils::{check_go_version, make_path_absolute}; +use crate::utils::{check_go_version, go_tags_arg, make_path_absolute}; use anyhow::{Result, anyhow}; use std::{ path::{Path, PathBuf}, @@ -9,11 +9,16 @@ use std::{ /// /// If the module is not going to be adapted to the component model, /// set the `only_wasip1` arg to true. +/// +/// `tags` contains any build tags derived from the target world (see +/// [`crate::utils::world_build_tags`]); these are merged with any `-tags` +/// specified via `GOFLAGS` and passed to `go test -c`. pub fn build_test_module( path: &Path, output_dir: Option<&PathBuf>, go: &Path, only_wasip1: bool, + tags: &[String], ) -> Result { check_go_version(go)?; @@ -36,51 +41,41 @@ pub fn build_test_module( std::fs::create_dir_all(dir)?; } - // The -buildmode flag mutes the unit test output, so it is ommitted - let module_args = [ - "test", - "-c", - "-ldflags=-checklinkname=0", - "-o", - test_wasm_path - .to_str() - .expect("the combined paths of 'output-dir' and 'pkg' are not valid unicode"), - path.to_str().expect("pkg path is not valid unicode"), - ]; - - // TODO: for when we figure out how wasip2 tests are to be run - #[allow(unused_variables)] - let component_args = [ - "test", - "-c", - "-buildmode=c-shared", - "-ldflags=-checklinkname=0", - "-o", - test_wasm_path - .to_str() - .expect("the combined paths of 'output-dir' and 'pkg' are not valid unicode"), - path.to_str().expect("pkg path is not valid unicode"), - ]; - - let output = if only_wasip1 { - Command::new(go) - .args(module_args) - .env("GOOS", "wasip1") - .env("GOARCH", "wasm") - .output()? - } else { + if !only_wasip1 { + // TODO: for when we figure out how wasip2 tests are to be run; that + // will require adding `-buildmode=c-shared` to the args below. 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)] - Command::new(go) - .args(component_args) - .env("GOOS", "wasip1") - .env("GOARCH", "wasm") - .output()? - }; + // The -buildmode flag mutes the unit test output, so it is ommitted + let mut args = vec![ + "test".to_string(), + "-c".to_string(), + "-ldflags=-checklinkname=0".to_string(), + ]; + if let Some(tags_arg) = go_tags_arg(tags) { + args.push(tags_arg); + } + args.push("-o".to_string()); + args.push( + test_wasm_path + .to_str() + .expect("the combined paths of 'output-dir' and 'pkg' are not valid unicode") + .to_string(), + ); + args.push( + path.to_str() + .expect("pkg path is not valid unicode") + .to_string(), + ); + + let output = Command::new(go) + .args(&args) + .env("GOOS", "wasip1") + .env("GOARCH", "wasm") + .output()?; if !output.status.success() { return Err(anyhow!( diff --git a/src/command.rs b/src/command.rs index 7dee8a8..852a884 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2,7 +2,9 @@ use crate::{ cmd_bindings::generate_bindings, cmd_build::build_module, cmd_test::build_test_module, - utils::{dummy_wit, embed_wit, install_go, module_to_component, parse_wit, pick_go}, + utils::{ + dummy_wit, embed_wit, install_go, module_to_component, parse_wit, pick_go, world_build_tags, + }, }; use anyhow::{Result, anyhow}; use clap::{Parser, Subcommand}; @@ -225,8 +227,16 @@ fn build(wit_opts: WitOpts, build: Build) -> Result<()> { let go = &pick_go(&resolve, world, build.go.as_deref())?; + // Build tags derived from the target world (none for plain wasip1 + // modules, which don't target a world). + let tags = if build.wasip1 { + Vec::new() + } else { + world_build_tags(&resolve, world) + }; + // Build a wasm module using `go build`. - let module = build_module(build.output.as_ref(), go, build.wasip1)?; + let module = build_module(build.output.as_ref(), go, build.wasip1, &tags)?; if !build.wasip1 { // Embed the WIT documents in the wasip1 component. @@ -258,9 +268,17 @@ fn test(wit_opts: WitOpts, test: Test) -> Result<()> { return Err(anyhow!("Path to a package containing Go tests is required")); } + // Build tags derived from the target world (none for plain wasip1 + // modules, which don't target a world). + let tags = if test.wasip1 { + Vec::new() + } else { + world_build_tags(&resolve, world) + }; + for pkg in test.pkg.iter() { // Build a wasm module using `go test -c`. - let module = build_test_module(pkg, test.output.as_ref(), go, test.wasip1)?; + let module = build_test_module(pkg, test.output.as_ref(), go, test.wasip1, &tags)?; if !test.wasip1 { // Embed the WIT documents in the wasm module. diff --git a/src/utils.rs b/src/utils.rs index ec1d5d4..0d64744 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -377,6 +377,67 @@ fn world_needs_async(resolve: &Resolve, world: WorldId) -> bool { }) } +/// Compute the `go build` tags derived from the given world: +/// `componentizego_async`, when the world uses async features. +pub fn world_build_tags(resolve: &Resolve, world: WorldId) -> Vec { + if world_needs_async(resolve, world) { + vec!["componentizego_async".to_string()] + } else { + Vec::new() + } +} + +/// The `-tags` argument to pass on the `go build`/`go test` command line, or +/// `None` if there are no tags to pass. +/// +/// Go reads `GOFLAGS` from the environment, but a `-tags` flag passed on the +/// command line overrides any `-tags` in `GOFLAGS`. Since componentize-go +/// passes its own `-tags` flag, any user-specified tags in `GOFLAGS` are +/// parsed out and merged here so that they survive. +pub fn go_tags_arg(tags: &[String]) -> Option { + let merged = merge_build_tags(std::env::var("GOFLAGS").ok().as_deref(), tags); + if merged.is_empty() { + None + } else { + Some(format!("-tags={}", merged.join(","))) + } +} + +/// Merge any build tags found in a `GOFLAGS` value with `tags`, returning a +/// deduplicated list. +fn merge_build_tags(goflags: Option<&str>, tags: &[String]) -> Vec { + let mut merged = goflags.map(goflags_tags).unwrap_or_default(); + for tag in tags { + if !merged.contains(tag) { + merged.push(tag.clone()); + } + } + merged +} + +/// Extract the values of any `-tags` flags in a `GOFLAGS` value. +/// +/// `GOFLAGS` is a space-separated list of flags, each of which must begin +/// with `-` or `--` and must use the `-flag=value` form if it takes a value. +fn goflags_tags(goflags: &str) -> Vec { + let mut tags = Vec::new(); + for flag in goflags.split_whitespace() { + let Some(flag) = flag.strip_prefix('-') else { + continue; + }; + let flag = flag.strip_prefix('-').unwrap_or(flag); + if let Some(value) = flag.strip_prefix("tags=") { + for tag in value.split(',') { + let tag = tag.trim(); + if !tag.is_empty() && !tags.iter().any(|t| t == tag) { + tags.push(tag.to_string()); + } + } + } + } + tags +} + pub fn install_go( url: Option, timeout: Option, @@ -490,6 +551,76 @@ mod tests { use std::thread; use std::time::Duration; + fn resolve_world(wit: &str) -> (Resolve, WorldId) { + let mut resolve = Resolve::default(); + let pkg = resolve.push_str("test.wit", wit).unwrap(); + let world = resolve.select_world(&[pkg], None).unwrap(); + (resolve, world) + } + + #[test] + fn test_world_build_tags_sync_world() { + let (resolve, world) = resolve_world( + "package wasmcloud:component-go@0.2.0;\n\ + world wasip2 {\n\ + import log: func(message: string);\n\ + }", + ); + assert_eq!(world_build_tags(&resolve, world), Vec::::new()); + } + + #[test] + fn test_world_build_tags_async_world() { + let (resolve, world) = resolve_world( + "package wasmcloud:component-go@0.2.0;\n\ + world wasip3 {\n\ + import download: func(url: string) -> stream;\n\ + }", + ); + assert_eq!(world_build_tags(&resolve, world), ["componentizego_async"]); + } + + #[test] + fn test_goflags_tags() { + assert_eq!(goflags_tags(""), Vec::::new()); + assert_eq!(goflags_tags("-mod=mod -trimpath"), Vec::::new()); + assert_eq!(goflags_tags("-tags=custom"), ["custom"]); + assert_eq!( + goflags_tags("-mod=mod -tags=custom,debug -trimpath"), + ["custom", "debug"] + ); + // Double-dash form and repeated flags are merged and deduplicated + assert_eq!(goflags_tags("--tags=a,b -tags=b,c"), ["a", "b", "c"]); + // Empty entries are dropped + assert_eq!(goflags_tags("-tags=a,,b,"), ["a", "b"]); + // A bare `tags=...` word (no leading dash) is not a flag + assert_eq!(goflags_tags("tags=a"), Vec::::new()); + } + + #[test] + fn test_merge_build_tags() { + let ours = ["componentizego_async".to_string()]; + // No GOFLAGS: just our tags + assert_eq!(merge_build_tags(None, &ours), ["componentizego_async"]); + // GOFLAGS without -tags: just our tags + assert_eq!( + merge_build_tags(Some("-trimpath"), &ours), + ["componentizego_async"] + ); + // User tags come first, ours are appended + assert_eq!( + merge_build_tags(Some("-tags=custom,debug"), &ours), + ["custom", "debug", "componentizego_async"] + ); + // Duplicates between GOFLAGS and our tags are removed + assert_eq!( + merge_build_tags(Some("-tags=componentizego_async,debug"), &ours), + ["componentizego_async", "debug"] + ); + // Nothing at all + assert_eq!(merge_build_tags(None, &[]), Vec::::new()); + } + #[test] fn test_install_go_times_out_on_stalled_server() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); From 3bb44577f131359736072e152d96bf3ad63da51d Mon Sep 17 00:00:00 2001 From: Jeremy Fleitz Date: Fri, 31 Jul 2026 15:11:56 -0400 Subject: [PATCH 2/2] update test package name to bca Signed-off-by: Jeremy Fleitz --- src/utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 0d64744..c4b97d8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -561,7 +561,7 @@ mod tests { #[test] fn test_world_build_tags_sync_world() { let (resolve, world) = resolve_world( - "package wasmcloud:component-go@0.2.0;\n\ + "package bca:component-go@0.2.0;\n\ world wasip2 {\n\ import log: func(message: string);\n\ }", @@ -572,7 +572,7 @@ mod tests { #[test] fn test_world_build_tags_async_world() { let (resolve, world) = resolve_world( - "package wasmcloud:component-go@0.2.0;\n\ + "package bca:component-go@0.2.0;\n\ world wasip3 {\n\ import download: func(url: string) -> stream;\n\ }",