diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 026420a..1362609 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,7 +52,7 @@ jobs: run: cargo check --workspace --all --bins --examples - name: wstd tests - run: cargo test -p wstd -p wstd-axum -p wstd-aws --target wasm32-wasip2 -- --nocapture + run: cargo test -p wstd -p wstd-axum --target wasm32-wasip2 -- --nocapture - name: test-programs tests run: cargo test -p test-programs -- --nocapture diff --git a/Cargo.toml b/Cargo.toml index 79c0f74..3519604 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true [workspace] -members = ["aws", +members = [ "axum", "axum/macro", "macro", @@ -59,7 +59,7 @@ license = "Apache-2.0 WITH LLVM-exception" repository = "https://github.com/bytecodealliance/wstd" keywords = ["WebAssembly", "async", "stdlib", "Components"] categories = ["wasm", "asynchronous"] -rust-version = "1.88" +rust-version = "1.91.1" authors = [ "Yoshua Wuyts ", "Pat Hickey ", @@ -69,11 +69,6 @@ authors = [ [workspace.dependencies] anyhow = "1" async-task = "4.7" -aws-config = { version = "1.8.8", default-features = false } -aws-sdk-s3 = { version = "1.108.0", default-features = false } -aws-smithy-async = { version = "1.2.6", default-features = false } -aws-smithy-types = { version = "1.3.3", default-features = false } -aws-smithy-runtime-api = { version = "1.9.1", default-features = false } axum = { version = "0.8.6", default-features = false } bytes = "1.10.1" cargo_metadata = "0.22" diff --git a/aws/.gitignore b/aws-example/.gitignore similarity index 100% rename from aws/.gitignore rename to aws-example/.gitignore diff --git a/aws-example/Cargo.toml b/aws-example/Cargo.toml new file mode 100644 index 0000000..1e4f034 --- /dev/null +++ b/aws-example/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "wstd-aws-example" +description = "demonstrate use of aws-smithy-wasm wstd support" +publish = false +version = "0.6.5" +edition = "2024" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wstd" +keywords = ["WebAssembly", "async", "stdlib", "Components"] +categories = ["wasm", "asynchronous"] +rust-version = "1.91.1" +authors = [ + "Pat Hickey ", +] +[[bin]] +name = "s3" + +[dependencies] +anyhow = "1" +clap = { version = "4.5", features = ["derive"] } +aws-config = { version = "1.8.15", default-features = false } +aws-sdk-s3 = { version = "1.125.0", default-features = false } +aws-smithy-wasm = "0.1.10" +wstd = "0.6.5" + +[workspace] diff --git a/aws/README.md b/aws-example/README.md similarity index 51% rename from aws/README.md rename to aws-example/README.md index d404bcd..8c391c1 100644 --- a/aws/README.md +++ b/aws-example/README.md @@ -1,8 +1,23 @@ -# wstd-aws: wstd support for the AWS Rust SDK +# wstd-aws-example: using wstd support in the AWS Rust SDK -This crate provides support for using the AWS Rust SDK for the `wasm32-wasip2` -target using the [`wstd`] crate. +The AWS Rust SDK has support for using the [`wstd`] crate on the +`wasm32-wasip2` target to use the wasi-http interface. This example shows how +to use it. + +## TL;DR + +* depend on `aws-*` crates released recently enough to have MSRV of 1.91.1 (on + or after March 4, 2026). Use `default-features = false` so tokio doesn't get + sucked in. +* depend on `aws-smithy-wasm` and setup your `Config` with: + ``` + config + .sleep_impl(aws_smithy_wasm::wasi::WasiSleep) + .http_client(aws_smithy_wasm::wasi::WasiHttpClientBuilder::new().build()) + ``` + +## Explanation In many wasi settings, its necessary or desirable to use the wasi-http interface to make http requests. Wasi-http interfaces provide an http @@ -17,14 +32,16 @@ and if they do, they will not use the wasi-http interfaces. To avoid using http over sockets, make sure to set the `default-features = false` setting when depending on any `aws-*` crates in your project. -To configure `wstd`'s wasi-http client for the AWS Rust SDK, provide -`wstd_aws::sleep_impl()` and `wstd_aws::http_client()` to your +To configure the AWS Rust SDK to use `wstd`'s wasi-http client, use the +[`aws_smithy_crate`](https://docs.rs/aws-smithy-wasm/latest/aws_smithy_wasm/) +at version 0.10.0 or later. Provide `aws_smithy_wasm::wasi::WasiSleep` and +`aws_smithy_wasm::wasi::WasiHttpClientBuilder::new().build()` to your [`aws_config::ConfigLoader`]: ``` let config = aws_config::defaults(BehaviorVersion::latest()) - .sleep_impl(wstd_aws::sleep_impl()) - .http_client(wstd_aws::http_client()) + .sleep_impl(aws_smithy_wasm::wasi::WasiSleep) + .http_client(aws_smithy_wasm::wasi::WasiHttpClientBuilder::new().build()) ...; ``` @@ -44,11 +61,12 @@ a single function. Compile it with: ```sh -cargo build -p wstd-aws --target wasm32-wasip2 --release --examples +cargo build -p wstd-aws-example --target wasm32-wasip2 --release ``` When running this example, you will need AWS credentials provided in environment -variables. +variables, and you should substitute in a region and bucket where your +credentials have permissions to list the bucket and read items. Run it with: ```sh @@ -57,19 +75,20 @@ wasmtime run -Shttp \ --env AWS_SECRET_ACCESS_KEY \ --env AWS_SESSION_TOKEN \ --dir .::. \ - target/wasm32-wasip2/release/examples/s3.wasm + target/wasm32-wasip2/release/s3.wasm \ + --region us-west-2 \ + --bucket wstd-example-bucket ``` or alternatively run it with: ```sh -cargo run --target wasm32-wasip2 -p wstd-aws --example s3 +cargo run --target wasm32-wasip2 -p wstd-aws-example --example s3 -- \ + --region us-west-2 --bucket wstd-example-bucket ``` - which uses the wasmtime cli, as above, via configiration found in this -workspace's `.cargo/config`. - -By default, this script accesses the `wstd-example-bucket` in `us-west-2`. -To change the bucket or region, use the `--bucket` and `--region` cli -flags before the subcommand. +workspace's `.cargo/config.toml`. +By default, the subcommand `list` will be run, listing the contents of the +bucket. To get an item from the bucket, use the subcommand `get [-o +]`. Use `--help` when in doubt. diff --git a/aws/examples/s3.rs b/aws-example/src/bin/s3.rs similarity index 87% rename from aws/examples/s3.rs rename to aws-example/src/bin/s3.rs index 3fcf4ae..eab936a 100644 --- a/aws/examples/s3.rs +++ b/aws-example/src/bin/s3.rs @@ -48,13 +48,12 @@ use aws_sdk_s3::Client; #[derive(Debug, Parser)] #[command(version, about, long_about = None)] struct Opts { - /// The AWS Region. Defaults to us-west-2 if not provided. + /// The AWS Region for the s3 bucket. #[arg(short, long)] - region: Option, - /// The name of the bucket. Defaults to wstd-example-bucket if not - /// provided. + region: String, + /// The name of the s3 bucket. #[arg(short, long)] - bucket: Option, + bucket: String, #[command(subcommand)] command: Option, @@ -73,19 +72,13 @@ enum Command { #[wstd::main] async fn main() -> Result<()> { let opts = Opts::parse(); - let region = opts - .region - .clone() - .unwrap_or_else(|| "us-west-2".to_owned()); - let bucket = opts - .bucket - .clone() - .unwrap_or_else(|| "wstd-example-bucket".to_owned()); + let region = opts.region; + let bucket = opts.bucket; let config = aws_config::defaults(BehaviorVersion::latest()) .region(Region::new(region)) - .sleep_impl(wstd_aws::sleep_impl()) - .http_client(wstd_aws::http_client()) + .sleep_impl(aws_smithy_wasm::wasi::WasiSleep) + .http_client(aws_smithy_wasm::wasi::WasiHttpClientBuilder::new().build()) .load() .await; diff --git a/aws/Cargo.toml b/aws/Cargo.toml deleted file mode 100644 index 8714cc0..0000000 --- a/aws/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "wstd-aws" -description = "AWS rust sdk support for Wasm Components and WASI 0.2, based on wstd" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -keywords.workspace = true -categories.workspace = true -rust-version.workspace = true -authors.workspace = true - -[dependencies] -anyhow.workspace = true -aws-smithy-async = { workspace = true } -aws-smithy-types = { workspace = true, features = ["http-body-1-x"] } -aws-smithy-runtime-api = { workspace = true, features = ["client", "http-1x"] } -http-body-util.workspace = true -sync_wrapper = { workspace = true, features = ["futures"] } -wstd.workspace = true - -[dev-dependencies] -aws-config.workspace = true -aws-sdk-s3.workspace = true -clap.workspace = true -# crc-fast is a transitive dep via aws-smithy-checksums, which -# is a dep of aws-sdk-s3 and others. -# When cargo resolves crc-fast to 1.6.0 via aws-smiothy-checksums 0.63.12, -# wasm-ld will crash building crc-fast. Until this is resolved, pinning -# crc-fast is the simplest way to avoid it -crc-fast = "=1.3.0" diff --git a/aws/src/lib.rs b/aws/src/lib.rs deleted file mode 100644 index 0a03ac0..0000000 --- a/aws/src/lib.rs +++ /dev/null @@ -1,101 +0,0 @@ -use anyhow::anyhow; -use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep}; -use aws_smithy_runtime_api::client::http::{ - HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector, -}; -use aws_smithy_runtime_api::client::orchestrator::HttpRequest; -use aws_smithy_runtime_api::client::result::ConnectorError; -use aws_smithy_runtime_api::client::retries::ErrorKind; -use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; -use aws_smithy_runtime_api::http::Response; -use aws_smithy_types::body::SdkBody; -use http_body_util::{BodyStream, StreamBody}; -use std::time::Duration; -use sync_wrapper::SyncStream; -use wstd::http::{Body as WstdBody, BodyExt, Client}; - -pub fn sleep_impl() -> impl AsyncSleep + 'static { - WstdSleep -} - -#[derive(Debug)] -struct WstdSleep; -impl AsyncSleep for WstdSleep { - fn sleep(&self, duration: Duration) -> Sleep { - Sleep::new(async move { - wstd::task::sleep(wstd::time::Duration::from(duration)).await; - }) - } -} - -pub fn http_client() -> impl HttpClient + 'static { - WstdHttpClient -} - -#[derive(Debug)] -struct WstdHttpClient; - -impl HttpClient for WstdHttpClient { - fn http_connector( - &self, - settings: &HttpConnectorSettings, - // afaict, none of these components are relevant to this - // implementation. - _components: &RuntimeComponents, - ) -> SharedHttpConnector { - let mut client = Client::new(); - if let Some(timeout) = settings.connect_timeout() { - client.set_connect_timeout(timeout); - } - if let Some(timeout) = settings.read_timeout() { - client.set_first_byte_timeout(timeout); - } - SharedHttpConnector::new(WstdHttpConnector(client)) - } -} - -#[derive(Debug)] -struct WstdHttpConnector(Client); - -impl HttpConnector for WstdHttpConnector { - fn call(&self, request: HttpRequest) -> HttpConnectorFuture { - let client = self.0.clone(); - HttpConnectorFuture::new(async move { - let request = request - .try_into_http1x() - // This can only fail if the Extensions fail to convert - .map_err(|e| ConnectorError::other(Box::new(e), None))?; - // smithy's SdkBody Error is a non-'static boxed dyn stderror. - // Anyhow can't represent that, so convert it to the debug impl. - let request = - request.map(|body| WstdBody::from_http_body(body.map_err(|e| anyhow!("{e:?}")))); - // Any error given by send is considered a "ClientError" kind - // which should prevent smithy from retrying like it would for a - // throttling error - let response = client - .send(request) - .await - .map_err(|e| ConnectorError::other(e.into(), Some(ErrorKind::ClientError)))?; - - Response::try_from(response.map(|wstd_body| { - // You'd think that an SdkBody would just be an impl Body with - // the usual error type dance. - let nonsync_body = wstd_body - .into_boxed_body() - .map_err(|e| e.into_boxed_dyn_error()); - // But we have to do this weird dance: because Axum insists - // bodies are not Sync, wstd settled on non-Sync bodies. - // Smithy insists on Sync bodies. The SyncStream type exists - // to assert, because all Stream operations are on &mut self, - // all Streams are Sync. So, turn the Body into a Stream, make - // it sync, then back to a Body. - let nonsync_stream = BodyStream::new(nonsync_body); - let sync_stream = SyncStream::new(nonsync_stream); - let sync_body = StreamBody::new(sync_stream); - SdkBody::from_body_1_x(sync_body) - })) - // This can only fail if the Extensions fail to convert - .map_err(|e| ConnectorError::other(Box::new(e), None)) - }) - } -} diff --git a/test-programs/build.rs b/test-programs/build.rs index 36a4484..4beee02 100644 --- a/test-programs/build.rs +++ b/test-programs/build.rs @@ -7,21 +7,24 @@ use std::process::Command; fn main() { let out_dir = PathBuf::from(var_os("OUT_DIR").expect("OUT_DIR env var exists")); - let meta = MetadataCommand::new().exec().expect("cargo metadata"); + let meta = MetadataCommand::new() + .exec() + .expect("cargo metadata for workspace"); println!( "cargo:rerun-if-changed={}", meta.workspace_root.as_os_str().to_str().unwrap() ); - fn build_examples(pkg: &str, out_dir: &PathBuf) { + fn build_targets(pkg: &str, manifest: &str, kind: &str, out_dir: &PathBuf) { // release build is required for aws sdk to not overflow wasm locals let status = Command::new("cargo") .arg("build") - .arg("--examples") + .arg(kind) .arg("--release") .arg("--target=wasm32-wasip2") .arg(format!("--package={pkg}")) + .arg(format!("--manifest-path={manifest}")) .env("CARGO_TARGET_DIR", out_dir) .env("CARGO_PROFILE_DEV_DEBUG", "2") .env("RUSTFLAGS", rustflags()) @@ -30,25 +33,33 @@ fn main() { .expect("cargo build wstd examples"); assert!(status.success()); } - build_examples("wstd", &out_dir); - build_examples("wstd-axum", &out_dir); - build_examples("wstd-aws", &out_dir); + build_targets("wstd", "../Cargo.toml", "--examples", &out_dir); + build_targets("wstd-axum", "../Cargo.toml", "--examples", &out_dir); + build_targets( + "wstd-aws-example", + "../aws-example/Cargo.toml", + "--bins", + &out_dir, + ); let mut generated_code = "// THIS FILE IS GENERATED CODE\n".to_string(); - fn module_for(name: &str, out_dir: &Path, meta: &Package) -> String { + fn module_for(name: &str, kind: TargetKind, out_dir: &Path, meta: &Package) -> String { let mut generated_code = String::new(); + for target in meta.targets.iter() { + generated_code += &format!("// {target:?} \n"); + } generated_code += &format!("pub mod {name} {{"); - for binary in meta - .targets - .iter() - .filter(|t| t.kind == [TargetKind::Example]) - { - let component_path = out_dir - .join("wasm32-wasip2") - .join("release") - .join("examples") - .join(format!("{}.wasm", binary.name)); + for binary in meta.targets.iter().filter(|t| t.kind == [kind.clone()]) { + let mut component_path = out_dir.join("wasm32-wasip2").join("release"); + match kind { + TargetKind::Bin => {} + TargetKind::Example => { + component_path = component_path.join("examples"); + } + _ => unimplemented!("path interpolation for TargetKind {kind:?}"), + } + component_path = component_path.join(format!("{}.wasm", binary.name)); let const_name = binary.name.to_shouty_snake_case(); generated_code += &format!( @@ -62,6 +73,7 @@ fn main() { generated_code += &module_for( "_wstd", + TargetKind::Example, &out_dir, meta.packages .iter() @@ -71,19 +83,26 @@ fn main() { generated_code += "pub use _wstd::*;\n\n"; generated_code += &module_for( "axum", + TargetKind::Example, &out_dir, meta.packages .iter() .find(|p| *p.name == "wstd-axum") .expect("wstd-axum is in cargo metadata"), ); + let aws_example_meta = MetadataCommand::new() + .manifest_path("../aws-example/Cargo.toml") + .exec() + .expect("cargo metadata for aws-example"); generated_code += &module_for( "aws", + TargetKind::Bin, &out_dir, - meta.packages + aws_example_meta + .packages .iter() - .find(|p| *p.name == "wstd-aws") - .expect("wstd-aws is in cargo metadata"), + .find(|p| *p.name == "wstd-aws-example") + .expect("wstd-aws-example is in cargo metadata"), ); std::fs::write(out_dir.join("gen.rs"), generated_code).unwrap(); diff --git a/test-programs/tests/aws_s3.rs b/test-programs/tests/aws_s3.rs index c411d27..f5f26fb 100644 --- a/test-programs/tests/aws_s3.rs +++ b/test-programs/tests/aws_s3.rs @@ -11,6 +11,14 @@ fn run_s3_example() -> Command { command.args(["--env", "AWS_SESSION_TOKEN"]); command.args(["--dir", ".::."]); command.arg(test_programs::aws::S3); + command.arg(format!( + "--region={}", + std::env::var("AWS_REGION").unwrap_or_else(|_| "us-west-2".to_owned()) + )); + command.arg(format!( + "--bucket={}", + std::env::var("WSTD_EXAMPLE_BUCKET").unwrap_or_else(|_| "wstd-example-bucket".to_owned()) + )); command } @@ -18,18 +26,7 @@ fn run_s3_example() -> Command { #[cfg_attr(feature = "no-aws", ignore)] fn aws_s3() -> Result<()> { // bucket list command - let output = run_s3_example() - .arg(format!( - "--region={}", - std::env::var("AWS_REGION").unwrap_or_else(|_| "us-west-2".to_owned()) - )) - .arg(format!( - "--bucket={}", - std::env::var("WSTD_EXAMPLE_BUCKET") - .unwrap_or_else(|_| "wstd-example-bucket".to_owned()) - )) - .arg("list") - .output()?; + let output = run_s3_example().arg("list").output()?; println!("{:?}", output); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); @@ -37,19 +34,7 @@ fn aws_s3() -> Result<()> { assert!(stdout.contains("shoug.jpg")); // bucket get command - let output = run_s3_example() - .arg(format!( - "--region={}", - std::env::var("AWS_REGION").unwrap_or_else(|_| "us-west-2".to_owned()) - )) - .arg(format!( - "--bucket={}", - std::env::var("WSTD_EXAMPLE_BUCKET") - .unwrap_or_else(|_| "wstd-example-bucket".to_owned()) - )) - .arg("get") - .arg("shoug.jpg") - .output()?; + let output = run_s3_example().arg("get").arg("shoug.jpg").output()?; println!("{:?}", output); assert!(output.status.success());