From e2b3011b26bf5395daf3450d0397d974d0b4c62d Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Tue, 11 Apr 2023 16:51:19 +0200 Subject: [PATCH] feat: add support for transitive dependencies Signed-off-by: Roman Volosatovs --- CHANGELOG.md | 8 + README.md | 26 +- crates/wit-deps/src/lib.rs | 240 ++++++++++++----- crates/wit-deps/src/lock.rs | 60 ++++- crates/wit-deps/src/manifest.rs | 208 +++++++++----- examples/github/wit/deps.lock | 44 ++- examples/github/wit/deps.toml | 16 +- .../deps/clocks/instance-monotonic-clock.wit | 15 ++ .../wit/deps/clocks/instance-wall-clock.wit | 15 ++ .../wit/deps/clocks/monotonic-clock.wit | 40 +++ examples/github/wit/deps/clocks/timezone.wit | 61 +++++ .../github/wit/deps/clocks/wall-clock.wit | 48 ++++ examples/github/wit/deps/clocks/world.wit | 7 + examples/github/wit/deps/keyvalue/atomic.wit | 20 ++ examples/github/wit/deps/keyvalue/batch.wit | 27 ++ examples/github/wit/deps/keyvalue/error.wit | 10 + .../github/wit/deps/keyvalue/handle-watch.wit | 12 + .../github/wit/deps/keyvalue/readwrite.wit | 28 ++ examples/github/wit/deps/keyvalue/types.wit | 57 ++++ examples/github/wit/deps/keyvalue/world.wit | 12 + examples/github/wit/deps/logging/world.wit | 3 - .../github/wit/deps/messaging/consumer.wit | 10 + .../github/wit/deps/messaging/handler.wit | 7 + .../github/wit/deps/messaging/messaging.wit | 5 + .../github/wit/deps/messaging/producer.wit | 7 + examples/github/wit/deps/messaging/types.wit | 41 +++ examples/github/wit/deps/poll/world.wit | 3 - examples/github/wit/deps/random/world.wit | 3 - .../wit/deps/sockets/instance-network.wit | 9 + .../wit/deps/sockets/ip-name-lookup.wit | 69 +++++ examples/github/wit/deps/sockets/network.wit | 186 +++++++++++++ .../wit/deps/sockets/tcp-create-socket.wit | 27 ++ examples/github/wit/deps/sockets/tcp.wit | 255 ++++++++++++++++++ .../wit/deps/sockets/udp-create-socket.wit | 27 ++ examples/github/wit/deps/sockets/udp.wit | 211 +++++++++++++++ examples/github/wit/deps/sockets/world.wit | 10 + examples/github/wit/deps/sql/readwrite.wit | 12 + examples/github/wit/deps/sql/sql.wit | 3 + examples/github/wit/deps/sql/types.wit | 43 +++ examples/github/wit/world.wit | 9 +- src/bin/wit-deps/main.rs | 29 +- tests/build/build.rs | 5 +- tests/build/subcrate/build.rs | 5 +- tests/build/subcrate/wit/deps.lock | 22 +- tests/build/subcrate/wit/deps.toml | 5 - tests/build/wit/build.wit | 1 - tests/build/wit/deps.lock | 16 +- tests/build/wit/deps.toml | 3 - 48 files changed, 1754 insertions(+), 226 deletions(-) create mode 100644 examples/github/wit/deps/clocks/instance-monotonic-clock.wit create mode 100644 examples/github/wit/deps/clocks/instance-wall-clock.wit create mode 100644 examples/github/wit/deps/clocks/monotonic-clock.wit create mode 100644 examples/github/wit/deps/clocks/timezone.wit create mode 100644 examples/github/wit/deps/clocks/wall-clock.wit create mode 100644 examples/github/wit/deps/clocks/world.wit create mode 100644 examples/github/wit/deps/keyvalue/atomic.wit create mode 100644 examples/github/wit/deps/keyvalue/batch.wit create mode 100644 examples/github/wit/deps/keyvalue/error.wit create mode 100644 examples/github/wit/deps/keyvalue/handle-watch.wit create mode 100644 examples/github/wit/deps/keyvalue/readwrite.wit create mode 100644 examples/github/wit/deps/keyvalue/types.wit create mode 100644 examples/github/wit/deps/keyvalue/world.wit delete mode 100644 examples/github/wit/deps/logging/world.wit create mode 100644 examples/github/wit/deps/messaging/consumer.wit create mode 100644 examples/github/wit/deps/messaging/handler.wit create mode 100644 examples/github/wit/deps/messaging/messaging.wit create mode 100644 examples/github/wit/deps/messaging/producer.wit create mode 100644 examples/github/wit/deps/messaging/types.wit delete mode 100644 examples/github/wit/deps/poll/world.wit delete mode 100644 examples/github/wit/deps/random/world.wit create mode 100644 examples/github/wit/deps/sockets/instance-network.wit create mode 100644 examples/github/wit/deps/sockets/ip-name-lookup.wit create mode 100644 examples/github/wit/deps/sockets/network.wit create mode 100644 examples/github/wit/deps/sockets/tcp-create-socket.wit create mode 100644 examples/github/wit/deps/sockets/tcp.wit create mode 100644 examples/github/wit/deps/sockets/udp-create-socket.wit create mode 100644 examples/github/wit/deps/sockets/udp.wit create mode 100644 examples/github/wit/deps/sockets/world.wit create mode 100644 examples/github/wit/deps/sql/readwrite.wit create mode 100644 examples/github/wit/deps/sql/sql.wit create mode 100644 examples/github/wit/deps/sql/types.wit diff --git a/CHANGELOG.md b/CHANGELOG.md index 0569826..7d4e0e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Transitive dependencies will now be pulled in from `wit/deps` of dependencies in the manifest + ### Fixed - Relative manifest path support in `wit-deps` binary +### Removed + +- `package` argument to binary and library update and lock functions + ## [0.2.2] - 2023-04-11 ### Added diff --git a/README.md b/README.md index 86ff022..04b8f98 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,22 @@ Example: ```toml # wit/deps.toml -http = "https://github.com/WebAssembly/wasi-http/archive/6c6855a7329fb040a48ecdbad1765be8e694416c.tar.gz" -io = "https://github.com/rvolosatovs/wasi-io/archive/v0.1.0.tar.gz" -logging = "https://github.com/WebAssembly/wasi-logging/archive/d106e59b25297d0496e6a5d221ad090e19c3aaa3.tar.gz" -poll = "https://github.com/WebAssembly/wasi-poll/archive/3ff76670b0d43bc7c8a224c2e65880a963416835.tar.gz" -random = "https://github.com/WebAssembly/wasi-random/archive/28970c50c3797c0087fa75a15e88bfa39b91e0a0.tar.gz" +# Use `wit-deps update` to pull in latest changes from "dynamic" branch references +clocks = "https://github.com/WebAssembly/wasi-clocks/archive/main.tar.gz" +http = "https://github.com/WebAssembly/wasi-http/archive/main.tar.gz" +messaging = "https://github.com/WebAssembly/wasi-messaging/archive/main.tar.gz" +sockets = "https://github.com/WebAssembly/wasi-sockets/archive/main.tar.gz" +sql = "https://github.com/WebAssembly/wasi-sql/archive/main.tar.gz" + +# Pin to a tag +io = "https://github.com/rvolosatovs/wasi-io/archive/v0.1.0.tar.gz" # this fork renames `streams` interface for compatiblity with wasi-snapshot-preview1 + +# Pin a dependency to a particular revision and source digests. Each digest is optional +[keyvalue] +url = "https://github.com/WebAssembly/wasi-keyvalue/archive/6f3bd6bca07cb7b25703a13f633e05258d56a2dc.tar.gz" +sha256 = "1755b8f1e9f2e70d0bde06198bf50d12603b454b52bf1f59064c1877baa33dff" +sha512 = "7bc43665a9de73ec7bef075e32f67ed0ebab04a1e47879f9328e8e52edfb35359512c899ab8a52240fecd0c53ff9c036abefe549e5fb99225518a2e0446d66e0" + ``` A source specfication can also be a structure with the following fields: @@ -22,11 +33,16 @@ A source specfication can also be a structure with the following fields: - `url` - same format as the URL string - `sha256` - (optional) hex-encoded sha256 digest of the contents of the URL - `sha512` (optional) hex-encoded sha512 digest of the contents of the URL +- `path` path to the directory containing the WIT definitions + +Either `url` or `path` must be specified (both support string format) Example: ```toml # wit/deps.toml +mywit = "./path/to/my/wit" + [logging] url = "https://github.com/WebAssembly/wasi-logging/archive/d106e59b25297d0496e6a5d221ad090e19c3aaa3.tar.gz" sha256 = "4bb4aeab99e7323b30d107aab78e88b2265c1598cc438bc5fbc0d16bb63e798f" diff --git a/crates/wit-deps/src/lib.rs b/crates/wit-deps/src/lib.rs index c844348..3e40fc5 100644 --- a/crates/wit-deps/src/lib.rs +++ b/crates/wit-deps/src/lib.rs @@ -17,15 +17,15 @@ pub use manifest::{Entry as ManifestEntry, Manifest}; pub use futures; pub use tokio; -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::ffi::{OsStr, OsString}; -use std::path::Path; +use std::path::{Path, PathBuf}; -use anyhow::{bail, Context}; +use anyhow::Context; use futures::{try_join, AsyncRead, AsyncWrite, FutureExt, Stream, TryStreamExt}; use tokio::fs; use tokio_stream::wrappers::ReadDirStream; -use tracing::{debug, instrument}; +use tracing::{debug, instrument, trace}; /// WIT dependency identifier pub type Identifier = String; @@ -40,98 +40,221 @@ fn is_wit(path: impl AsRef) -> bool { .unwrap_or_default() } +#[instrument(level = "trace", skip(path))] +async fn remove_dir_all(path: impl AsRef) -> std::io::Result<()> { + let path = path.as_ref(); + match fs::remove_dir_all(path).await { + Ok(()) => { + trace!("removed `{}`", path.display()); + Ok(()) + } + Err(e) => Err(std::io::Error::new( + e.kind(), + format!("failed to remove `{}`: {e}", path.display()), + )), + } +} + +#[instrument(level = "trace", skip(path))] +async fn recreate_dir(path: impl AsRef) -> std::io::Result<()> { + let path = path.as_ref(); + match remove_dir_all(path).await { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + }; + fs::create_dir_all(path) + .await + .map(|()| trace!("recreated `{}`", path.display())) + .map_err(|e| { + std::io::Error::new( + e.kind(), + format!("failed to create `{}`: {e}", path.display()), + ) + }) +} + /// Returns a stream of WIT file names within a directory at `path` #[instrument(level = "trace", skip(path))] async fn read_wits( path: impl AsRef, ) -> std::io::Result>> { - let st = fs::read_dir(path).await.map(ReadDirStream::new)?; + let path = path.as_ref(); + let st = fs::read_dir(path) + .await + .map(ReadDirStream::new) + .map_err(|e| { + std::io::Error::new( + e.kind(), + format!("failed to read directory at `{}`: {e}", path.display()), + ) + })?; Ok(st.try_filter_map(|e| async move { let name = e.file_name(); if !is_wit(&name) { + trace!("{} is not a WIT definition, skip", name.to_string_lossy()); return Ok(None); } if e.file_type().await?.is_dir() { + trace!("{} is a directory, skip", name.to_string_lossy()); return Ok(None); } Ok(Some(name)) })) } -/// Copies all WIT files from directory at `src` to `dst` +/// Copies all WIT definitions from directory at `src` to `dst` creating `dst` directory, if it does not exist. #[instrument(level = "trace", skip(src, dst))] -async fn copy_wits(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { +async fn install_wits(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { let src = src.as_ref(); let dst = dst.as_ref(); + recreate_dir(dst).await?; read_wits(src) - .await - .map_err(|e| std::io::Error::new(e.kind(), format!("failed to read `{}`", src.display())))? + .await? .try_for_each_concurrent(None, |name| async { let src = src.join(&name); let dst = dst.join(name); - if let Some(parent) = dst.parent() { - fs::create_dir_all(parent).await.map_err(|e| { + fs::copy(&src, &dst) + .await + .map(|_| trace!("copied `{}` to `{}`", src.display(), dst.display())) + .map_err(|e| { std::io::Error::new( e.kind(), format!( - "failed to create destination parent directory `{}`: {e}", - parent.display() + "failed to copy `{}` to `{}`: {e}", + src.display(), + dst.display() ), ) - })?; - } - fs::copy(&src, &dst).await.map(|_| ()).map_err(|e| { - std::io::Error::new( - e.kind(), - format!( - "failed to copy `{}` to `{}`: {e}", - src.display(), - dst.display() - ), - ) - }) + }) }) .await } -/// Unpacks all WIT interfaces found within `wit` subtree of a tar archive read from `tar` to `dst` +/// Copies all WIT files from directory at `src` to `dst` and returns a vector identifiers of all copied +/// transitive dependencies. +#[instrument(level = "trace", skip(src, dst, skip_deps))] +async fn copy_wits( + src: impl AsRef, + dst: impl AsRef, + skip_deps: &HashSet, +) -> std::io::Result> { + let src = src.as_ref(); + let deps = src.join("deps"); + let dst = dst.as_ref(); + try_join!(install_wits(src, dst), async { + match (dst.parent(), fs::read_dir(&deps).await) { + (Some(base), Ok(dir)) => { + ReadDirStream::new(dir) + .try_filter_map(|e| async move { + let name = e.file_name(); + let Some(id) = name.to_str().map(Identifier::from) else { + return Ok(None) + }; + if skip_deps.contains(&id) { + return Ok(None); + } + let ft = e.file_type().await?; + if !(ft.is_dir() + || ft.is_symlink() && fs::metadata(e.path()).await?.is_dir()) + { + return Ok(None); + } + Ok(Some(id)) + }) + .and_then(|id| async { + let dst = base.join(&id); + install_wits(deps.join(&id), &dst).await?; + Ok((id, dst)) + }) + .try_collect() + .await + } + (None, _) => Ok(HashMap::default()), + (_, Err(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(HashMap::default()), + (_, Err(e)) => Err(std::io::Error::new( + e.kind(), + format!("failed to read directory at `{}`: {e}", deps.display()), + )), + } + }) + .map(|((), ids)| ids) +} + +/// Unpacks all WIT interfaces found within `wit` subtree of a tar archive read from `tar` to +/// `dst` and returns a vector of all unpacked transitive dependency identifiers. /// /// # Errors /// /// Returns and error if the operation fails -#[instrument(level = "trace", skip(tar, dst))] -pub async fn untar(tar: impl AsyncRead + Unpin, dst: impl AsRef) -> anyhow::Result<()> { - let dst = dst.as_ref(); +#[instrument(level = "trace", skip(tar, dst, skip_deps))] +pub async fn untar( + tar: impl AsyncRead + Unpin, + dst: impl AsRef, + skip_deps: &HashSet, +) -> std::io::Result> { + use std::io::{Error, Result}; - match fs::remove_dir_all(dst).await { - Ok(()) => {} - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => bail!("failed to remove `{}`: {e}", dst.display()), - }; - fs::create_dir_all(dst) - .await - .with_context(|| format!("failed to create `{}`", dst.display()))?; + async fn unpack(e: &mut async_tar::Entry, dst: &Path) -> Result<()> { + e.unpack(dst).await.map_err(|e| { + Error::new( + e.kind(), + format!("failed to unpack `{}`: {e}", dst.display()), + ) + })?; + trace!("unpacked `{}`", dst.display()); + Ok(()) + } + let dst = dst.as_ref(); + recreate_dir(dst).await?; async_tar::Archive::new(tar) .entries() - .context("failed to unpack archive metadata")? - .try_for_each(|mut e| async move { - let path = e.path()?; + .map_err(|e| Error::new(e.kind(), format!("failed to unpack archive metadata: {e}")))? + .try_filter_map(|mut e| async move { + let path = e + .path() + .map_err(|e| Error::new(e.kind(), format!("failed to query entry path: {e}")))?; let mut path = path.into_iter().map(OsStr::to_str); - match (path.next(), path.next(), path.next(), path.next()) { - (Some(Some("wit")), Some(Some(name)), None, None) - | (Some(_), Some(Some("wit")), Some(Some(name)), None) + match ( + path.next(), + path.next(), + path.next(), + path.next(), + path.next(), + ) { + (Some(Some("wit")), Some(Some(name)), None, None, None) + | (Some(_), Some(Some("wit")), Some(Some(name)), None, None) if is_wit(name) => { - e.unpack(dst.join(name)).await?; - Ok(()) + let dst = dst.join(name); + unpack(&mut e, &dst).await?; + Ok(None) + } + (Some(Some("wit")), Some(Some("deps")), Some(Some(id)), Some(Some(name)), None) + | ( + Some(_), + Some(Some("wit")), + Some(Some("deps")), + Some(Some(id)), + Some(Some(name)), + ) if !skip_deps.contains(id) && is_wit(name) => { + let id = Identifier::from(id); + if let Some(base) = dst.parent() { + let dst = base.join(&id); + recreate_dir(&dst).await?; + let wit = dst.join(name); + unpack(&mut e, &wit).await?; + Ok(Some((id, dst))) + } else { + Ok(None) + } } - _ => Ok(()), + _ => Ok(None), } }) + .try_collect() .await - .context("failed to unpack archive")?; - Ok(()) } /// Packages path into a `wit` subtree in deterministic `tar` archive and writes it to `dst`. @@ -168,13 +291,12 @@ fn cache() -> Option { /// # Errors /// /// Returns an error if anything in the pipeline fails -#[instrument(level = "trace", skip(at, manifest, lock, deps, packages))] +#[instrument(level = "trace", skip(at, manifest, lock, deps))] pub async fn lock( at: Option>, manifest: impl AsRef, lock: Option>, deps: impl AsRef, - packages: impl IntoIterator, ) -> anyhow::Result> { let manifest: Manifest = toml::from_str(manifest.as_ref()).context("failed to decode manifest")?; @@ -188,7 +310,7 @@ pub async fn lock( let deps = deps.as_ref(); let lock = manifest - .lock(at, deps, old_lock.as_ref(), cache().as_ref(), packages) + .lock(at, deps, old_lock.as_ref(), cache().as_ref()) .await .with_context(|| format!("failed to lock deps to `{}`", deps.display()))?; match old_lock { @@ -206,19 +328,18 @@ pub async fn lock( /// # Errors /// /// Returns an error if anything in the pipeline fails -#[instrument(level = "trace", skip(at, manifest, deps, packages))] +#[instrument(level = "trace", skip(at, manifest, deps))] pub async fn update( at: Option>, manifest: impl AsRef, deps: impl AsRef, - packages: impl IntoIterator, ) -> anyhow::Result { let manifest: Manifest = toml::from_str(manifest.as_ref()).context("failed to decode manifest")?; let deps = deps.as_ref(); let lock = manifest - .lock(at, deps, None, cache().map(WriteCache).as_ref(), packages) + .lock(at, deps, None, cache().map(WriteCache).as_ref()) .await .with_context(|| format!("failed to lock deps to `{}`", deps.display()))?; toml::to_string(&lock).context("failed to encode lock") @@ -262,12 +383,11 @@ async fn write_lock(path: impl AsRef, buf: impl AsRef<[u8]>) -> std::io::R /// # Errors /// /// Returns an error if anything in the pipeline fails -#[instrument(level = "trace", skip(manifest_path, lock_path, deps, packages))] +#[instrument(level = "trace", skip(manifest_path, lock_path, deps))] pub async fn lock_path( manifest_path: impl AsRef, lock_path: impl AsRef, deps: impl AsRef, - packages: impl IntoIterator, ) -> anyhow::Result { let manifest_path = manifest_path.as_ref(); let lock_path = lock_path.as_ref(); @@ -282,7 +402,7 @@ pub async fn lock_path( )), }), )?; - if let Some(lock) = self::lock(manifest_path.parent(), manifest, lock, deps, packages) + if let Some(lock) = self::lock(manifest_path.parent(), manifest, lock, deps) .await .context("failed to lock dependencies")? { @@ -298,16 +418,15 @@ pub async fn lock_path( /// # Errors /// /// Returns an error if anything in the pipeline fails -#[instrument(level = "trace", skip(manifest_path, lock_path, deps, packages))] +#[instrument(level = "trace", skip(manifest_path, lock_path, deps))] pub async fn update_path( manifest_path: impl AsRef, lock_path: impl AsRef, deps: impl AsRef, - packages: impl IntoIterator, ) -> anyhow::Result<()> { let manifest_path = manifest_path.as_ref(); let manifest = read_manifest_string(manifest_path).await?; - let lock = self::update(manifest_path.parent(), manifest, deps, packages) + let lock = self::update(manifest_path.parent(), manifest, deps) .await .context("failed to lock dependencies")?; write_lock(lock_path, lock).await?; @@ -345,7 +464,6 @@ macro_rules! lock { include_str!(concat!($dir, "/deps.toml")), lock, concat!($dir, "/deps"), - None, ) .await { diff --git a/crates/wit-deps/src/lock.rs b/crates/wit-deps/src/lock.rs index 5cf3a37..cbdc17d 100644 --- a/crates/wit-deps/src/lock.rs +++ b/crates/wit-deps/src/lock.rs @@ -1,8 +1,8 @@ use crate::{tar, Digest, DigestWriter, Identifier}; -use core::ops::Deref; +use core::ops::{Deref, DerefMut}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; use anyhow::Context; @@ -22,21 +22,28 @@ pub enum EntrySource { } /// WIT dependency [Lock] entry -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Entry { - /// Resource source + /// Resource source, [None] if the dependency is transitive #[serde(flatten)] - pub source: EntrySource, + pub source: Option, /// Resource digest #[serde(flatten)] pub digest: Digest, + /// Transitive dependency identifiers + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] + pub deps: BTreeSet, } impl Entry { /// Create a new entry given a dependency source and path containing it #[must_use] - pub fn new(source: EntrySource, digest: Digest) -> Self { - Self { source, digest } + pub fn new(source: Option, digest: Digest, deps: BTreeSet) -> Self { + Self { + source, + digest, + deps, + } } /// Create a new entry given a dependency url and path containing the unpacked contents of it @@ -44,11 +51,15 @@ impl Entry { /// # Errors /// /// Returns an error if [`Self::digest`] of `path` fails - pub async fn from_url(url: Url, path: impl AsRef) -> anyhow::Result { + pub async fn from_url( + url: Url, + path: impl AsRef, + deps: BTreeSet, + ) -> anyhow::Result { let digest = Self::digest(path) .await .context("failed to compute digest")?; - Ok(Self::new(EntrySource::Url(url), digest)) + Ok(Self::new(Some(EntrySource::Url(url)), digest, deps)) } /// Create a new entry given a dependency path @@ -56,11 +67,27 @@ impl Entry { /// # Errors /// /// Returns an error if [`Self::digest`] of `path` fails - pub async fn from_path(src: PathBuf, dst: impl AsRef) -> anyhow::Result { + pub async fn from_path( + src: PathBuf, + dst: impl AsRef, + deps: BTreeSet, + ) -> anyhow::Result { + let digest = Self::digest(dst) + .await + .context("failed to compute digest")?; + Ok(Self::new(Some(EntrySource::Path(src)), digest, deps)) + } + + /// Create a new entry given a transitive dependency path + /// + /// # Errors + /// + /// Returns an error if [`Self::digest`] of `path` fails + pub async fn from_transitive_path(dst: impl AsRef) -> anyhow::Result { let digest = Self::digest(dst) .await .context("failed to compute digest")?; - Ok(Self::new(EntrySource::Path(src), digest)) + Ok(Self::new(None, digest, BTreeSet::default())) } /// Compute the digest of an entry from path @@ -85,6 +112,12 @@ impl Deref for Lock { } } +impl DerefMut for Lock { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl FromIterator<(Identifier, Entry)> for Lock { fn from_iter>(iter: T) -> Self { Self(BTreeMap::from_iter(iter)) @@ -121,15 +154,16 @@ mod tests { lock == Lock::from([( "foo".parse().expect("failed to `foo` parse identifier"), Entry { - source: EntrySource::Url( + source: Some(EntrySource::Url( FOO_URL.parse().expect("failed to parse `foo` URL") - ), + )), digest: Digest { sha256: FromHex::from_hex(FOO_SHA256) .expect("failed to decode `foo` sha256"), sha512: FromHex::from_hex(FOO_SHA512) .expect("failed to decode `foo` sha512"), }, + deps: BTreeSet::default(), } )]) ); diff --git a/crates/wit-deps/src/manifest.rs b/crates/wit-deps/src/manifest.rs index 8aa3072..b3f6f38 100644 --- a/crates/wit-deps/src/manifest.rs +++ b/crates/wit-deps/src/manifest.rs @@ -1,17 +1,19 @@ use crate::{ - copy_wits, untar, Cache, Digest, DigestReader, Identifier, Lock, LockEntry, LockEntrySource, + copy_wits, remove_dir_all, untar, Cache, Digest, DigestReader, Identifier, Lock, LockEntry, + LockEntrySource, }; use core::convert::identity; +use core::convert::Infallible; use core::fmt; use core::ops::Deref; use core::str::FromStr; use std::collections::{HashMap, HashSet}; -use std::convert::Infallible; use std::path::{Path, PathBuf}; use std::sync::Arc; +use anyhow::ensure; use anyhow::{bail, Context as _}; use async_compression::futures::bufread::GzipDecoder; use futures::io::BufReader; @@ -19,8 +21,7 @@ use futures::lock::Mutex; use futures::{stream, AsyncWriteExt, StreamExt, TryStreamExt}; use hex::FromHex; use serde::{de, Deserialize}; -use tokio::fs; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, error, info, instrument, trace, warn}; use url::Url; /// WIT dependency [Manifest] entry @@ -163,23 +164,51 @@ fn source_matches( && sha512.map_or(true, |sha512| sha512 == digest.sha512) } +#[instrument(level = "trace", skip(deps))] +async fn lock_deps( + deps: impl IntoIterator, +) -> anyhow::Result> { + stream::iter(deps.into_iter().map(|(id, path)| async { + let entry = LockEntry::from_transitive_path(path).await?; + Ok((id, entry)) + })) + .then(identity) + .try_collect() + .await +} + impl Entry { - #[instrument(level = "trace", skip(at, out, lock, cache))] + #[instrument(level = "trace", skip(at, out, lock, cache, skip_deps))] async fn lock( self, at: Option>, out: impl AsRef, lock: Option<&LockEntry>, cache: Option<&impl Cache>, - ) -> anyhow::Result { + skip_deps: &HashSet, + ) -> anyhow::Result<(LockEntry, HashMap)> { let out = out.as_ref(); match self { Self::Path(path) => { let dst = at.map(|at| at.as_ref().join(&path)); - copy_wits(&dst.as_ref().unwrap_or(&path), out).await?; - let digest = LockEntry::digest(dst.as_ref().unwrap_or(&path)).await?; - Ok(LockEntry::new(LockEntrySource::Path(path), digest)) + let deps = copy_wits(&dst.as_ref().unwrap_or(&path), out, skip_deps).await?; + trace!(?deps, "copied WIT definitions to `{}`", out.display()); + let deps = lock_deps(deps).await?; + trace!( + ?deps, + "locked transitive dependencies of `{}`", + out.display() + ); + let digest = LockEntry::digest(&dst.as_ref().unwrap_or(&path)).await?; + Ok(( + LockEntry::new( + Some(LockEntrySource::Path(path)), + digest, + deps.keys().cloned().collect(), + ), + deps, + )) } Self::Url { url, @@ -188,27 +217,50 @@ impl Entry { } => { if let Some(LockEntry { source, - digest: lock_digest, + digest: ldigest, + deps: ldeps, }) = lock { - match (LockEntry::digest(out).await, source) { - (Ok(digest), LockEntrySource::Url(lock_url)) - if digest == *lock_digest && url == *lock_url => + let deps = if ldeps.is_empty() { + Ok(HashMap::default()) + } else { + let base = out.parent().with_context(|| { + format!("`{}` does not have a parent", out.display()) + })?; + lock_deps(ldeps.iter().cloned().map(|id| { + let path = base.join(&id); + (id, path) + })) + .await + }; + match (LockEntry::digest(out).await, source, deps) { + (Ok(digest), Some(LockEntrySource::Url(lurl)), Ok(deps)) + if digest == *ldigest && url == *lurl => { debug!("`{}` is already up-to-date, skip fetch", out.display()); - return Ok(LockEntry::new(LockEntrySource::Url(url), digest)); + // NOTE: Manually deleting transitive dependencies of this + // dependency from `dst` is considered user error + // TODO: Check that transitive dependencies are in sync + return Ok(( + LockEntry::new( + Some(LockEntrySource::Url(url)), + digest, + deps.keys().cloned().collect(), + ), + deps, + )); } - (Ok(digest), _) => { + (Ok(digest), _, _) => { debug!( "`{}` is out-of-date (sha256: {})", out.display(), hex::encode(digest.sha256) ); } - (Err(e), _) if e.kind() == std::io::ErrorKind::NotFound => { + (Err(e), _, _) if e.kind() == std::io::ErrorKind::NotFound => { debug!("locked dependency for `{url}` missing"); } - (Err(e), _) => { + (Err(e), _, _) => { error!("failed to compute dependency digest for `{url}`: {e}"); } } @@ -219,16 +271,30 @@ impl Entry { Ok(None) => debug!("`{url}` not present in cache"), Ok(Some(tar_gz)) => { let mut hashed = DigestReader::from(tar_gz); - match untar(GzipDecoder::new(BufReader::new(&mut hashed)), out).await { - Ok(()) if source_matches(hashed, sha256, sha512) => { + match untar( + GzipDecoder::new(BufReader::new(&mut hashed)), + out, + skip_deps, + ) + .await + { + Ok(deps) if source_matches(hashed, sha256, sha512) => { debug!("unpacked `{url}` from cache"); - return LockEntry::from_url(url, out).await; + let deps = lock_deps(deps).await?; + let entry = LockEntry::from_url( + url, + out, + deps.keys().cloned().collect(), + ) + .await?; + return Ok((entry, deps)); } - Ok(()) => { + Ok(deps) => { warn!("cache hash mismatch for `{url}`"); - fs::remove_dir_all(out).await.with_context(|| { - format!("failed to remove `{}`", out.display()) - })?; + remove_dir_all(out).await?; + for (_, dep) in deps { + remove_dir_all(&dep).await?; + } } Err(e) => { error!("failed to unpack `{url}` contents from cache: {e}"); @@ -245,7 +311,7 @@ impl Entry { None }; let cache = Arc::new(Mutex::new(cache)); - let digest = match url.scheme() { + let (digest, deps) = match url.scheme() { "http" | "https" => { info!("fetch `{url}` into `{}`", out.display()); let res = reqwest::get(url.clone()) @@ -284,10 +350,14 @@ impl Entry { }) .into_async_read(); let mut hashed = DigestReader::from(Box::pin(tar_gz)); - untar(GzipDecoder::new(BufReader::new(&mut hashed)), out) - .await - .with_context(|| format!("failed to unpack contents of `{url}`"))?; - Digest::from(hashed) + let deps = untar( + GzipDecoder::new(BufReader::new(&mut hashed)), + out, + skip_deps, + ) + .await + .with_context(|| format!("failed to unpack contents of `{url}`"))?; + (Digest::from(hashed), deps) } "file" => bail!( r#"`file` scheme is not supported for `url` field, use `path` instead. Try: @@ -308,9 +378,7 @@ path = "/path/to/my/dep" }; if let Some(sha256) = sha256 { if digest.sha256 != sha256 { - fs::remove_dir_all(out) - .await - .with_context(|| format!("failed to remove `{}`", out.display()))?; + remove_dir_all(out).await?; bail!( r#"sha256 hash mismatch for `{url}` got: {} @@ -322,9 +390,7 @@ expected: {}"#, } if let Some(sha512) = sha512 { if digest.sha512 != sha512 { - fs::remove_dir_all(out) - .await - .with_context(|| format!("failed to remove `{}`", out.display()))?; + remove_dir_all(out).await?; bail!( r#"sha512 hash mismatch for `{url}` got: {} @@ -334,7 +400,11 @@ expected: {}"#, ); } } - LockEntry::from_url(url, out).await + trace!(?deps, "fetched contents of `{url}` to `{}`", out.display()); + let deps = lock_deps(deps).await?; + trace!(?deps, "locked transitive dependencies of `{url}`"); + let entry = LockEntry::from_url(url, out, deps.keys().cloned().collect()).await?; + Ok((entry, deps)) } } } @@ -346,43 +416,59 @@ pub struct Manifest(HashMap); impl Manifest { /// Lock the manifest populating `deps` - #[instrument(level = "trace", skip(at, deps, lock, cache, packages))] + #[instrument(level = "trace", skip(at, deps, lock, cache))] pub async fn lock( self, at: Option>, deps: impl AsRef, lock: Option<&Lock>, cache: Option<&impl Cache>, - packages: impl IntoIterator, ) -> anyhow::Result { let at = at.as_ref(); let deps = deps.as_ref(); - let packages: HashSet<_> = packages.into_iter().collect(); - if let Some(id) = packages.iter().find(|id| !self.contains_key(**id)) { - bail!("selected package `{id}` not found in manifest") - } + // Dependency ids, which are pinned in the manifest + let pinned = self.0.keys().cloned().collect(); stream::iter(self.0.into_iter().map(|(id, entry)| async { - if packages.is_empty() || packages.contains(&id) { - let out = deps.join(&id); - let lock = lock.and_then(|lock| lock.get(&id)); - let entry = entry - .lock(at, out, lock, cache) - .await - .with_context(|| format!("failed to lock `{id}`"))?; - Ok((id, entry)) - } else if let Some(Some(entry)) = lock.map(|lock| lock.get(&id)) { - Ok((id, entry.clone())) - } else { - debug!("locking unselected manifest package `{id}` missing from lock"); - let entry = entry - .lock(at, deps.join(&id), None, cache) - .await - .with_context(|| format!("failed to lock `{id}`"))?; - Ok((id, entry)) - } + let out = deps.join(&id); + let lock = lock.and_then(|lock| lock.get(&id)); + let (entry, deps) = entry + .lock(at, out, lock, cache, &pinned) + .await + .with_context(|| format!("failed to lock `{id}`"))?; + Ok(((id, entry), deps)) })) .then(identity) - .try_collect() + .try_fold(Lock::default(), |mut lock, ((id, entry), deps)| async { + use std::collections::btree_map::Entry::{Occupied, Vacant}; + + match lock.entry(id) { + Occupied(e) => { + error!("duplicate lock entry for direct dependency `{}`", e.key()); + } + Vacant(e) => { + trace!("record lock entry for direct dependency `{}`", e.key()); + e.insert(entry); + } + } + for (id, entry) in deps { + match lock.entry(id) { + Occupied(e) => { + let other = e.get(); + debug_assert!(other.source.is_none()); + ensure!(other.digest == entry.digest, "transitive dependency conflict for `{}`, add `{}` to dependency manifest to resolve it", e.key(), e.key()); + trace!( + "transitive dependency on `{}` already locked, skip", + e.key() + ); + } + Vacant(e) => { + trace!("record lock entry for transitive dependency `{}`", e.key()); + e.insert(entry); + } + } + } + Ok(lock) + }) .await } } diff --git a/examples/github/wit/deps.lock b/examples/github/wit/deps.lock index 34712b8..9b0dc30 100644 --- a/examples/github/wit/deps.lock +++ b/examples/github/wit/deps.lock @@ -1,24 +1,50 @@ +[clocks] +url = "https://github.com/WebAssembly/wasi-clocks/archive/main.tar.gz" +sha256 = "c9245497108b87930b8e66f0207f7e9397f601fe77dffd1ab73fffa04cb60af2" +sha512 = "1ce6457c8e3b95998feef337bea9807aa1a39e1a32715ca4368fc22bc6ea76d2a768864bf9d98c9558fae87be876b231efc3cf1d84fdb839de65b1753d13f28d" +deps = ["poll"] + [http] url = "https://github.com/WebAssembly/wasi-http/archive/main.tar.gz" sha256 = "192e0a0418776415401297ff11151972d5f78500785ad427ff0ac3bbdd298281" sha512 = "387915dad450e61e73b1eae82b1702fbae6e8bdc7a92165a195edd7a3eb1ac25f4ea1fc230d69e860fec8e2c05bc5be8937077966c928c9f179ee35a964d276f" +deps = ["logging", "poll", "random"] [io] url = "https://github.com/rvolosatovs/wasi-io/archive/v0.1.0.tar.gz" sha256 = "50e907cde3a0b49b2358c564a10db26e3585a5466029baaecb00424dccea77a1" sha512 = "096953066bc1e690b90e01d0a8ed92650052184173df8dac1e24d2a277a6f864a078eca0d555fa2de0b38be1ba8ed25db7949a396f60c9a35def7ea956a66ff1" +deps = ["poll"] + +[keyvalue] +url = "https://github.com/WebAssembly/wasi-keyvalue/archive/6f3bd6bca07cb7b25703a13f633e05258d56a2dc.tar.gz" +sha256 = "f49426526e2ecc2333b2b0ed71927a45991111887124f71f66fc0d9f7002c887" +sha512 = "cb437e7263690b26d6ff6196034a956e71a29b529fa2356ab541cc83f5d3abd1feb25861234a29b5da346881c93e13d21e3af3b4a558eb33627dd15ba90adfdc" [logging] -url = "https://github.com/WebAssembly/wasi-logging/archive/d106e59b25297d0496e6a5d221ad090e19c3aaa3.tar.gz" -sha256 = "f378a2b6a36af096528ec5e7031da474990cecaec1949c663befc5066d719f6f" -sha512 = "ba09307b12f5428c3058d878001c9fb15df21933c6203328f785d5009a4fc0cf304950271590d4146305a2d8b8988595f396d955961168cb6bfa7ea9af1cc362" +sha256 = "15d45b50fec7b2a7e0972e841fe2af6a29fe1f4e9e7333f7b4823090ab7c708a" +sha512 = "3a110e9289040a4d7dca59fe3dcb83a6e2e35c0f8a96003982675d3d5cf40e89970c4810a9e10ed530d0be192cf260880547d8c0b2b9d605aba599607e3494d4" + +[messaging] +url = "https://github.com/WebAssembly/wasi-messaging/archive/main.tar.gz" +sha256 = "d1e34ec5b36e63bf91e2424702512d40e3305abcc6beb53a05f43dc38822a23c" +sha512 = "3eb09a1f48fbd1274216acff0128319ee7ae88c299b8b12e4e10d70accd5ef5c392232a891b908dfce1165797c84fd45439e571470944e6255c23d6ac2d493c6" [poll] -url = "https://github.com/WebAssembly/wasi-poll/archive/main.tar.gz" -sha256 = "065422b0ea6ccb2a9facc6b87b902eef110c53d76fc31f341a6bc8d0b0285b6a" -sha512 = "19a55cd3072a19ae6a1774723a4962e7a155a5ce89a27175e8c76020efb4d00bc92ebb78427d92bcb8effd4f0d03ebf0a0daa747ecd981b8d31d5abc2ad86423" +sha256 = "9f2e6b5ea1a017575f96a3c415c737fe31513b043d9b47aefeb21f6c27ab8425" +sha512 = "f65965395742a0290fd9e4e55408c2b5ce3a4eae0ee22be1ff012e3db75910110da21c5f0a61083e043b4c510967a0a73ff4491ae9719e9158543f512dbeea5f" [random] -url = "https://github.com/WebAssembly/wasi-random/archive/main.tar.gz" -sha256 = "79b6417bb1ab3c82c241c56021e717f258e2a0b3ab94db93907ca11d7f92ed66" -sha512 = "1e657ac56420e65bde75eabea920a605a0ff4adc6f2ba8b7cf1c37a603710449bc1a29568dda2b61c04ae9889d2b1a82b9935d4b9d573eb48a83e0ecf9cad591" +sha256 = "19d57f2262530016ec2b32991db3d8b6705b3a0bc5a7b6ae8fff3bcef0cf1088" +sha512 = "f9eba7dfd858d1edcaa868ce636f69545323bf010c3b0d4a2a1b23584d8027240c592d13e91882617e70a4dbf290754a8697724b5633147d11fa3db7869a2afe" + +[sockets] +url = "https://github.com/WebAssembly/wasi-sockets/archive/main.tar.gz" +sha256 = "2a6158a0d7d6ac4d912f272c108d19b741a44566639191defaf8838e232417db" +sha512 = "d2ae6f7b9e1a33c1f3ca840f5a8f1c0f757c139569cee62027def2bbe43350795461e2fd6bae09ada1c6c94237eb03da65330169762121b036dad8cd430ba462" +deps = ["poll"] + +[sql] +url = "https://github.com/WebAssembly/wasi-sql/archive/main.tar.gz" +sha256 = "d4a0e9c12e668699775d44ef883dc52540f4d7915a653b3389d8e3859487b1f0" +sha512 = "284926b4fbc31bb9d1218f630af54482e0c0108ae6f76ec2dbd6e7f3b6c5ead577384503b79b7da6f8a37e44c9c601231cb2527377b0ce5cb3bafee89dc9375e" diff --git a/examples/github/wit/deps.toml b/examples/github/wit/deps.toml index 66fe176..1459e9d 100644 --- a/examples/github/wit/deps.toml +++ b/examples/github/wit/deps.toml @@ -1,13 +1,15 @@ # Use `wit-deps update` to pull in latest changes from "dynamic" branch references +clocks = "https://github.com/WebAssembly/wasi-clocks/archive/main.tar.gz" http = "https://github.com/WebAssembly/wasi-http/archive/main.tar.gz" -poll = "https://github.com/WebAssembly/wasi-poll/archive/main.tar.gz" -random = "https://github.com/WebAssembly/wasi-random/archive/main.tar.gz" +messaging = "https://github.com/WebAssembly/wasi-messaging/archive/main.tar.gz" +sockets = "https://github.com/WebAssembly/wasi-sockets/archive/main.tar.gz" +sql = "https://github.com/WebAssembly/wasi-sql/archive/main.tar.gz" # Pin to a tag io = "https://github.com/rvolosatovs/wasi-io/archive/v0.1.0.tar.gz" # this fork renames `streams` interface for compatiblity with wasi-snapshot-preview1 -# Pin a dependency to a particular revision and source digests -[logging] -url = "https://github.com/WebAssembly/wasi-logging/archive/d106e59b25297d0496e6a5d221ad090e19c3aaa3.tar.gz" -sha256 = "4bb4aeab99e7323b30d107aab78e88b2265c1598cc438bc5fbc0d16bb63e798f" -sha512 = "13b52b59afd98dd4938e3a651fad631d41a2e84ce781df5d8957eded77a8e1ac4277e771a10225cd4a3a9eae369ed7e8fee6e26f9991a2caa7c97c4a758b1ae6" +# Pin a dependency to a particular revision and source digests. Each digest is optional +[keyvalue] +url = "https://github.com/WebAssembly/wasi-keyvalue/archive/6f3bd6bca07cb7b25703a13f633e05258d56a2dc.tar.gz" +sha256 = "1755b8f1e9f2e70d0bde06198bf50d12603b454b52bf1f59064c1877baa33dff" +sha512 = "7bc43665a9de73ec7bef075e32f67ed0ebab04a1e47879f9328e8e52edfb35359512c899ab8a52240fecd0c53ff9c036abefe549e5fb99225518a2e0446d66e0" diff --git a/examples/github/wit/deps/clocks/instance-monotonic-clock.wit b/examples/github/wit/deps/clocks/instance-monotonic-clock.wit new file mode 100644 index 0000000..4cffa4a --- /dev/null +++ b/examples/github/wit/deps/clocks/instance-monotonic-clock.wit @@ -0,0 +1,15 @@ +/// This interfaces proves a clock handles for monotonic clock, suitable for +/// general-purpose application needs. +default interface instance-monotonic-clock { + use pkg.monotonic-clock.{monotonic-clock} + + /// Return a handle to a monotonic clock, suitable for general-purpose + /// application needs. + /// + /// This allocates a new handle, so applications with frequent need of a + /// clock handle should call this function once and reuse the handle + /// instead of calling this function each time. + /// + /// This [represents a value import](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Value_Imports). + instance-monotonic-clock: func() -> monotonic-clock +} diff --git a/examples/github/wit/deps/clocks/instance-wall-clock.wit b/examples/github/wit/deps/clocks/instance-wall-clock.wit new file mode 100644 index 0000000..3a55b55 --- /dev/null +++ b/examples/github/wit/deps/clocks/instance-wall-clock.wit @@ -0,0 +1,15 @@ +/// This interfaces proves a clock handles for wall clock, suitable for +/// general-purpose application needs. +default interface instance-wall-clock { + use pkg.wall-clock.{wall-clock} + + /// Return a handle to a wall clock, suitable for general-purpose + /// application needs. + /// + /// This allocates a new handle, so applications with frequent need of a + /// clock handle should call this function once and reuse the handle + /// instead of calling this function each time. + /// + /// This [represents a value import](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Value_Imports). + instance-wall-clock: func() -> wall-clock +} diff --git a/examples/github/wit/deps/clocks/monotonic-clock.wit b/examples/github/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 0000000..51c2203 --- /dev/null +++ b/examples/github/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,40 @@ +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +default interface monotonic-clock { + use poll.poll.{pollable} + + /// A timestamp in nanoseconds. + type instant = u64 + + /// A monotonic clock is a clock which has an unspecified initial value, and + /// successive reads of the clock will produce non-decreasing values. + /// + /// It is intended for measuring elapsed time. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type monotonic-clock = u32 + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + now: func(this: monotonic-clock) -> instant + + /// Query the resolution of the clock. + resolution: func(this: monotonic-clock) -> instant + + /// Create a `pollable` which will resolve once the specified time has been + /// reached. + subscribe: func( + this: monotonic-clock, + when: instant, + absolute: bool + ) -> pollable + + /// Dispose of the specified `monotonic-clock`, after which it may no longer + /// be used. + drop-monotonic-clock: func(this: monotonic-clock) +} diff --git a/examples/github/wit/deps/clocks/timezone.wit b/examples/github/wit/deps/clocks/timezone.wit new file mode 100644 index 0000000..63f99cc --- /dev/null +++ b/examples/github/wit/deps/clocks/timezone.wit @@ -0,0 +1,61 @@ +default interface timezone { + use pkg.wall-clock.{datetime} + + /// A timezone. + /// + /// In timezones that recognize daylight saving time, also known as daylight + /// time and summer time, the information returned from the functions varies + /// over time to reflect these adjustments. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type timezone = u32 + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + display: func(this: timezone, when: datetime) -> timezone-display + + /// The same as `display`, but only return the UTC offset. + utc-offset: func(this: timezone, when: datetime) -> s32 + + /// Dispose of the specified input-stream, after which it may no longer + /// be used. + drop-timezone: func(this: timezone) + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/examples/github/wit/deps/clocks/wall-clock.wit b/examples/github/wit/deps/clocks/wall-clock.wit new file mode 100644 index 0000000..c60861a --- /dev/null +++ b/examples/github/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,48 @@ +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +default interface wall-clock { + /// A time and date in seconds plus nanoseconds. + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// A wall clock is a clock which measures the date and time according to + /// some external reference. + /// + /// External references may be reset, so this clock is not necessarily + /// monotonic, making it unsuitable for measuring elapsed time. + /// + /// It is intended for reporting the current date and time for humans. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type wall-clock = u32 + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + now: func(this: wall-clock) -> datetime + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + resolution: func(this: wall-clock) -> datetime + + /// Dispose of the specified `wall-clock`, after which it may no longer + /// be used. + drop-wall-clock: func(this: wall-clock) +} diff --git a/examples/github/wit/deps/clocks/world.wit b/examples/github/wit/deps/clocks/world.wit new file mode 100644 index 0000000..e0eb083 --- /dev/null +++ b/examples/github/wit/deps/clocks/world.wit @@ -0,0 +1,7 @@ +default world example-world { + import monotonic-clock: pkg.monotonic-clock + import wall-clock: pkg.wall-clock + import timezone: pkg.timezone + import instance-monotonic-clock: pkg.instance-monotonic-clock + import instance-wall-clock: pkg.instance-wall-clock +} diff --git a/examples/github/wit/deps/keyvalue/atomic.wit b/examples/github/wit/deps/keyvalue/atomic.wit new file mode 100644 index 0000000..4b45c84 --- /dev/null +++ b/examples/github/wit/deps/keyvalue/atomic.wit @@ -0,0 +1,20 @@ +/// A keyvalue interface that provides atomic operations. +default interface keyvalue-atomic { + /// A keyvalue interface that provides atomic operations. + use pkg.types.{bucket, error, key} + + /// Atomically increment the value associated with the key in the bucket by the + /// given delta. It returns the new value. + /// + /// If the key does not exist in the bucket, it creates a new key-value pair + /// with the value set to the given delta. + /// + /// If any other error occurs, it returns an error. + increment: func(bucket: bucket, key: key, delta: u64) -> result + + /// Atomically compare and swap the value associated with the key in the bucket. + /// It returns a boolean indicating if the swap was successful. + /// + /// If the key does not exist in the bucket, it returns an error. + compare-and-swap: func(bucket: bucket, key: key, old: u64, new: u64) -> result +} \ No newline at end of file diff --git a/examples/github/wit/deps/keyvalue/batch.wit b/examples/github/wit/deps/keyvalue/batch.wit new file mode 100644 index 0000000..fa2658f --- /dev/null +++ b/examples/github/wit/deps/keyvalue/batch.wit @@ -0,0 +1,27 @@ +/// A keyvalue interface that provides batch operations. +default interface keyvalue-batch { + /// A keyvalue interface that provides batch get operations. + use pkg.types.{bucket, error, key, keys, incoming-value, outgoing-value} + + /// Get the values associated with the keys in the bucket. It returns a list of + /// incoming-values that can be consumed to get the values. + /// + /// If any of the keys do not exist in the bucket, it returns an error. + get-many: func(bucket: bucket, keys: keys) -> result, error> + + /// Get all the keys in the bucket. It returns a list of keys. + get-keys: func(bucket: bucket) -> keys + + /// Set the values associated with the keys in the bucket. If the key already + /// exists in the bucket, it overwrites the value. + /// + /// If any of the keys do not exist in the bucket, it creates a new key-value pair. + /// If any other error occurs, it returns an error. + set-many: func(bucket: bucket, keys: keys, values: list>) -> result<_, error> + + /// Delete the key-value pairs associated with the keys in the bucket. + /// + /// If any of the keys do not exist in the bucket, it skips the key. + /// If any other error occurs, it returns an error. + delete-many: func(bucket: bucket, keys: keys) -> result<_, error> +} diff --git a/examples/github/wit/deps/keyvalue/error.wit b/examples/github/wit/deps/keyvalue/error.wit new file mode 100644 index 0000000..a390170 --- /dev/null +++ b/examples/github/wit/deps/keyvalue/error.wit @@ -0,0 +1,10 @@ +default interface wasi-cloud-error { + /// An error resource type for keyvalue operations. + /// Currently, this provides only one function to return a string representation + /// of the error. In the future, this will be extended to provide more information + /// about the error. + // Soon: switch to `resource error { ... }` + type error = u32 + drop-error: func(error: error) + trace: func(error: error) -> string +} \ No newline at end of file diff --git a/examples/github/wit/deps/keyvalue/handle-watch.wit b/examples/github/wit/deps/keyvalue/handle-watch.wit new file mode 100644 index 0000000..488193c --- /dev/null +++ b/examples/github/wit/deps/keyvalue/handle-watch.wit @@ -0,0 +1,12 @@ +/// A keyvalue interface that provides handle-watch operations. +default interface handle-watch { + /// A keyvalue interface that provides handle-watch operations. + use pkg.types.{bucket, key, incoming-value} + + /// Handle the set event for the given bucket and key. + /// It returns a incoming-value that can be consumed to get the value. + on-set: func(bucket: bucket, key: key, incoming-value: incoming-value) + + /// Handle the delete event for the given bucket and key. + on-delete: func(bucket: bucket, key: key) +} diff --git a/examples/github/wit/deps/keyvalue/readwrite.wit b/examples/github/wit/deps/keyvalue/readwrite.wit new file mode 100644 index 0000000..f022868 --- /dev/null +++ b/examples/github/wit/deps/keyvalue/readwrite.wit @@ -0,0 +1,28 @@ +/// A keyvalue interface that provides simple read and write operations. +default interface keyvalue-readwrite { + /// A keyvalue interface that provides simple read and write operations. + use pkg.types.{bucket, error, incoming-value, key, outgoing-value} + + /// Get the value associated with the key in the bucket. It returns a incoming-value + /// that can be consumed to get the value. + /// + /// If the key does not exist in the bucket, it returns an error. + get: func(bucket: bucket, key: key) -> result + + /// Set the value associated with the key in the bucket. If the key already + /// exists in the bucket, it overwrites the value. + /// + /// If the key does not exist in the bucket, it creates a new key-value pair. + /// If any other error occurs, it returns an error. + set: func(bucket: bucket, key: key, outgoing-value: outgoing-value) -> result<_, error> + + /// Delete the key-value pair associated with the key in the bucket. + /// + /// If the key does not exist in the bucket, it returns an error. + delete: func(bucket: bucket, key: key) -> result<_, error> + + /// Check if the key exists in the bucket. + /// + /// If the key does not exist in the bucket, it returns an error. + exists: func(bucket: bucket, key: key) -> result +} \ No newline at end of file diff --git a/examples/github/wit/deps/keyvalue/types.wit b/examples/github/wit/deps/keyvalue/types.wit new file mode 100644 index 0000000..b4aee34 --- /dev/null +++ b/examples/github/wit/deps/keyvalue/types.wit @@ -0,0 +1,57 @@ +// A generic keyvalue interface for WASI. +default interface types { + /// A bucket is a collection of key-value pairs. Each key-value pair is stored + /// as a entry in the bucket, and the bucket itself acts as a collection of all + /// these entries. + /// + /// It is worth noting that the exact terminology for bucket in key-value stores + /// can very depending on the specific implementation. For example, + /// 1. Amazon DynamoDB calls a collection of key-value pairs a table + /// 2. Redis has hashes, sets, and sorted sets as different types of collections + /// 3. Cassandra calls a collection of key-value pairs a column family + /// 4. MongoDB calls a collection of key-value pairs a collection + /// 5. Riak calls a collection of key-value pairs a bucket + /// 6. Memcached calls a collection of key-value pairs a slab + /// 7. Azure Cosmos DB calls a collection of key-value pairs a container + /// + /// In this interface, we use the term `bucket` to refer to a collection of key-value + // Soon: switch to `resource bucket { ... }` + type bucket = u32 + drop-bucket: func(bucket: bucket) + open-bucket: func(name: string) -> result + + /// A key is a unique identifier for a value in a bucket. The key is used to + /// retrieve the value from the bucket. + type key = string + + /// A list of keys + type keys = list + + use io.streams.{input-stream, output-stream} + use pkg.error.{ error } + /// A value is the data stored in a key-value pair. The value can be of any type + /// that can be represented in a byte array. It provides a way to write the value + /// to the output-stream defined in the `wasi-io` interface. + // Soon: switch to `resource value { ... }` + type outgoing-value = u32 + drop-outgoing-value: func(outgoing-value: outgoing-value) + new-outgoing-value: func() -> outgoing-value + outgoing-value-write-body: func(outgoing-value: outgoing-value) -> result + + /// A incoming-value is a wrapper around a value. It provides a way to read the value + /// from the input-stream defined in the `wasi-io` interface. + /// + /// The incoming-value provides two ways to consume the value: + /// 1. `incoming-value-consume-sync` consumes the value synchronously and returns the + /// value as a list of bytes. + /// 2. `incoming-value-consume-async` consumes the value asynchronously and returns the + /// value as an input-stream. + // Soon: switch to `resource incoming-value { ... }` + type incoming-value = u32 + type incoming-value-async-body = input-stream + type incoming-value-sync-body = list + drop-incoming-value: func(incoming-value: incoming-value) + incoming-value-consume-sync: func(incoming-value: incoming-value) -> result + incoming-value-consume-async: func(incoming-value: incoming-value) -> result + size: func(incoming-value: incoming-value) -> u64 +} \ No newline at end of file diff --git a/examples/github/wit/deps/keyvalue/world.wit b/examples/github/wit/deps/keyvalue/world.wit new file mode 100644 index 0000000..bb75c14 --- /dev/null +++ b/examples/github/wit/deps/keyvalue/world.wit @@ -0,0 +1,12 @@ +default world keyvalue { + import readwrite: pkg.readwrite + import atomic: pkg.atomic + import batch: pkg.batch +} + +world keyvalue-handle-watch { + import readwrite: pkg.readwrite + import atomic: pkg.atomic + import batch: pkg.batch + export handle-watch: pkg.handle-watch +} diff --git a/examples/github/wit/deps/logging/world.wit b/examples/github/wit/deps/logging/world.wit deleted file mode 100644 index b5af151..0000000 --- a/examples/github/wit/deps/logging/world.wit +++ /dev/null @@ -1,3 +0,0 @@ -default world example-world { - import logging: pkg.logging -} diff --git a/examples/github/wit/deps/messaging/consumer.wit b/examples/github/wit/deps/messaging/consumer.wit new file mode 100644 index 0000000..1a6a392 --- /dev/null +++ b/examples/github/wit/deps/messaging/consumer.wit @@ -0,0 +1,10 @@ +/// An interface for a generic consumer. +default interface consumer { + use pkg.types.{broker, channel, subscription-token, error} + + /// Subscribes to a channel in a broker. + subscribe: func(b: broker, c: channel) -> result + + /// Unsubscribes from a channel via subscription token. + unsubscribe: func(b: broker, st: subscription-token) -> result<_, error> +} \ No newline at end of file diff --git a/examples/github/wit/deps/messaging/handler.wit b/examples/github/wit/deps/messaging/handler.wit new file mode 100644 index 0000000..59e2986 --- /dev/null +++ b/examples/github/wit/deps/messaging/handler.wit @@ -0,0 +1,7 @@ +/// An interface for a consumer relying on push-based message delivery. +default interface handler { + use pkg.types.{event, error} + + /// Creates an on-receive handler push-based message delivery. + on-receive: func(e: event) -> result<_, error> +} \ No newline at end of file diff --git a/examples/github/wit/deps/messaging/messaging.wit b/examples/github/wit/deps/messaging/messaging.wit new file mode 100644 index 0000000..ab0d9cc --- /dev/null +++ b/examples/github/wit/deps/messaging/messaging.wit @@ -0,0 +1,5 @@ +default world messaging { + import producer: pkg.producer + import consumer: pkg.consumer + export handler: pkg.handler +} \ No newline at end of file diff --git a/examples/github/wit/deps/messaging/producer.wit b/examples/github/wit/deps/messaging/producer.wit new file mode 100644 index 0000000..d50f234 --- /dev/null +++ b/examples/github/wit/deps/messaging/producer.wit @@ -0,0 +1,7 @@ +/// An interface for a producer. +default interface producer { + use pkg.types.{broker, channel, event, error} + + /// Publishes an event to a channel in a broker. + publish: func(b: broker, c: channel, e: event) -> result<_, error> +} \ No newline at end of file diff --git a/examples/github/wit/deps/messaging/types.wit b/examples/github/wit/deps/messaging/types.wit new file mode 100644 index 0000000..ebc55ae --- /dev/null +++ b/examples/github/wit/deps/messaging/types.wit @@ -0,0 +1,41 @@ +/// An interface grouping all necessary types for messaging. +default interface messaging-types { + /// A broker type that allows the exchange of messages. + type broker = u32 + drop-broker: func(b: broker) + open-broker: func(name: string) -> result + + /// An event type that follows the CloudEvents specification (https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md). We + /// assume the type of the data is a byte sequence. It is up to the data schema to determine what type of the data payload the event + /// contains. + record event { + specversion: string, + ty: string, + source: string, + id: string, + data: option>, + datacontenttype: option, + dataschema: option, + subject: option, + time: option, + extensions: option>> + } + + /// Channels specify where a published message should land. There are two types of channels: + /// - queue: competitive consumers, and + /// - topic: non-competitive consumers. + variant channel { + queue(string), + topic(string) + } + + /// A subscription token that allows receives from a specific subscription + type subscription-token = string + /// An error resource type. + /// Currently, this provides only one function to return a string representation + /// of the error. In the future, this will be extended to provide more information. + // TODO: switch to `resource error { ... }` + type error = u32 + drop-error: func(e: error) + trace-error: func(e: error) -> string +} \ No newline at end of file diff --git a/examples/github/wit/deps/poll/world.wit b/examples/github/wit/deps/poll/world.wit deleted file mode 100644 index 3a002e0..0000000 --- a/examples/github/wit/deps/poll/world.wit +++ /dev/null @@ -1,3 +0,0 @@ -default world example-world { - import poll: pkg.poll -} diff --git a/examples/github/wit/deps/random/world.wit b/examples/github/wit/deps/random/world.wit deleted file mode 100644 index 8d54b3b..0000000 --- a/examples/github/wit/deps/random/world.wit +++ /dev/null @@ -1,3 +0,0 @@ -default world example-world { - import random: pkg.random -} diff --git a/examples/github/wit/deps/sockets/instance-network.wit b/examples/github/wit/deps/sockets/instance-network.wit new file mode 100644 index 0000000..b1f5c98 --- /dev/null +++ b/examples/github/wit/deps/sockets/instance-network.wit @@ -0,0 +1,9 @@ + +/// This interface provides a value-export of the default network handle.. +default interface instance-network { + use pkg.network.{network} + + /// Get a handle to the default network. + instance-network: func() -> network + +} diff --git a/examples/github/wit/deps/sockets/ip-name-lookup.wit b/examples/github/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 0000000..c4cc726 --- /dev/null +++ b/examples/github/wit/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,69 @@ + +default interface ip-name-lookup { + use poll.poll.{pollable} + use pkg.network.{network, error-code, ip-address, ip-address-family} + + + /// Resolve an internet host name to a list of IP addresses. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// # Parameters + /// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted + /// to ASCII using IDNA encoding. + /// - `address-family`: If provided, limit the results to addresses of this specific address family. + /// - `include-unavailable`: When set to true, this function will also return addresses of which the runtime + /// thinks (or knows) can't be connected to at the moment. For example, this will return IPv6 addresses on + /// systems without an active IPv6 interface. Notes: + /// - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address. + /// - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged. + /// + /// This function never blocks. It either immediately fails or immediately returns successfully with a `resolve-address-stream` + /// that can be used to (asynchronously) fetch the results. + /// + /// At the moment, the stream never completes successfully with 0 items. Ie. the first call + /// to `resolve-next-address` never returns `ok(none)`. This may change in the future. + /// + /// # Typical errors + /// - `invalid-name`: `name` is a syntactically invalid domain name. + /// - `invalid-name`: `name` is an IP address. + /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) + /// + /// # References: + /// - + /// - + /// - + /// - + resolve-addresses: func(network: network, name: string, address-family: option, include-unavailable: bool) -> result + + + + type resolve-address-stream = u32 + + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// After which, you should release the stream with `drop-resolve-address-stream`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + resolve-next-address: func(this: resolve-address-stream) -> result, error-code> + + /// Dispose of the specified `resolve-address-stream`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-resolve-address-stream: func(this: resolve-address-stream) + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: resolve-address-stream) -> pollable +} diff --git a/examples/github/wit/deps/sockets/network.wit b/examples/github/wit/deps/sockets/network.wit new file mode 100644 index 0000000..9176e6b --- /dev/null +++ b/examples/github/wit/deps/sockets/network.wit @@ -0,0 +1,186 @@ + +default interface network { + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + /// + /// FYI, In the future this will be replaced by handle types. + type network = u32 + + /// Dispose of the specified `network`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-network: func(this: network) + + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + enum error-code { + // ### GENERAL ERRORS ### + + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + // ### IP ERRORS ### + + /// The specified address-family is not supported. + address-family-not-supported, + + /// An IPv4 address was passed to an IPv6 resource, or vice versa. + address-family-mismatch, + + /// The socket address is not a valid remote address. E.g. the IP address is set to INADDR_ANY, or the port is set to 0. + invalid-remote-address, + + /// The operation is only supported on IPv4 resources. + ipv4-only-operation, + + /// The operation is only supported on IPv6 resources. + ipv6-only-operation, + + + + // ### TCP & UDP SOCKET ERRORS ### + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// The socket is already attached to another network. + already-attached, + + /// The socket is already bound. + already-bound, + + /// The socket is already in the Connection state. + already-connected, + + /// The socket is not bound to any local address. + not-bound, + + /// The socket is not in the Connection state. + not-connected, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use. + address-in-use, + + /// A bind operation failed because there are no ephemeral ports available. + ephemeral-ports-exhausted, + + /// The remote address is not reachable + remote-unreachable, + + + // ### TCP SOCKET ERRORS ### + + /// The socket is already in the Listener state. + already-listening, + + /// The socket is already in the Listener state. + not-listening, + + /// The connection was forcefully rejected + connection-refused, + + /// The connection was reset. + connection-reset, + + + // ### UDP SOCKET ERRORS ### + datagram-too-large, + + + // ### NAME LOOKUP ERRORS ### + + /// The provided name is a syntactically invalid domain name. + invalid-name, + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + type ipv4-address = tuple + type ipv6-address = tuple + + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + record ipv4-socket-address { + port: u16, // sin_port + address: ipv4-address, // sin_addr + } + + record ipv6-socket-address { + port: u16, // sin6_port + flow-info: u32, // sin6_flowinfo + address: ipv6-address, // sin6_addr + scope-id: u32, // sin6_scope_id + } + + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + +} \ No newline at end of file diff --git a/examples/github/wit/deps/sockets/tcp-create-socket.wit b/examples/github/wit/deps/sockets/tcp-create-socket.wit new file mode 100644 index 0000000..6e948fa --- /dev/null +++ b/examples/github/wit/deps/sockets/tcp-create-socket.wit @@ -0,0 +1,27 @@ + +default interface tcp-create-socket { + use pkg.network.{network, error-code, ip-address-family} + use pkg.tcp.{tcp-socket} + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`listen`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The host does not support TCP sockets. (EOPNOTSUPP) + /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + create-tcp-socket: func(address-family: ip-address-family) -> result +} diff --git a/examples/github/wit/deps/sockets/tcp.wit b/examples/github/wit/deps/sockets/tcp.wit new file mode 100644 index 0000000..b871532 --- /dev/null +++ b/examples/github/wit/deps/sockets/tcp.wit @@ -0,0 +1,255 @@ + +default interface tcp { + use io.streams.{input-stream, output-stream} + use poll.poll.{pollable} + use pkg.network.{network, error-code, ip-socket-address, ip-address-family} + + /// A TCP socket handle. + type tcp-socket = u32 + + + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// When a socket is not explicitly bound, the first invocation to a listen or connect operation will + /// implicitly bind the socket. + /// + /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. + /// + /// # Typical `start` errors + /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) + /// - `already-bound`: The socket is already bound. (EINVAL) + /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// + /// # Typical `finish` errors + /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-bind: func(this: tcp-socket, network: network, local-address: ip-socket-address) -> result<_, error-code> + finish-bind: func(this: tcp-socket) -> result<_, error-code> + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the Connection state + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// # Typical `start` errors + /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `already-connected`: The socket is already in the Connection state. (EISCONN) + /// - `already-listening`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) + /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// + /// # Typical `finish` errors + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A `connect` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-connect: func(this: tcp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code> + finish-connect: func(this: tcp-socket) -> result, error-code> + + /// Start listening for new connections. + /// + /// Transitions the socket into the Listener state. + /// + /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. + /// + /// # Typical `start` errors + /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `listen` must be identical to the one passed to `bind`. + /// - `already-connected`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) + /// - `already-listening`: The socket is already in the Listener state. + /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) + /// + /// # Typical `finish` errors + /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A `listen` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-listen: func(this: tcp-socket, network: network) -> result<_, error-code> + finish-listen: func(this: tcp-socket) -> result<_, error-code> + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the Connection state. + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `not-listening`: Socket is not in the Listener state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// + /// Host implementations must skip over transient errors returned by the native accept syscall. + /// + /// # References + /// - + /// - + /// - + /// - + accept: func(this: tcp-socket) -> result, error-code> + + /// Get the bound local address. + /// + /// # Typical errors + /// - `not-bound`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + local-address: func(this: tcp-socket) -> result + + /// Get the bound remote address. + /// + /// # Typical errors + /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + remote-address: func(this: tcp-socket) -> result + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func(this: tcp-socket) -> ip-address-family + + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. + /// + /// Equivalent to the IPV6_V6ONLY socket option. + /// + /// # Typical errors + /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. + /// - `already-bound`: (set) The socket is already bound. + /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + ipv6-only: func(this: tcp-socket) -> result + set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error-code> + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// # Typical errors + /// - `already-connected`: (set) The socket is already in the Connection state. + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + set-listen-backlog-size: func(this: tcp-socket, value: u64) -> result<_, error-code> + + /// Equivalent to the SO_KEEPALIVE socket option. + /// + /// # Typical errors + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + keep-alive: func(this: tcp-socket) -> result + set-keep-alive: func(this: tcp-socket, value: bool) -> result<_, error-code> + + /// Equivalent to the TCP_NODELAY socket option. + /// + /// # Typical errors + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + no-delay: func(this: tcp-socket) -> result + set-no-delay: func(this: tcp-socket, value: bool) -> result<_, error-code> + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// # Typical errors + /// - `already-connected`: (set) The socket is already in the Connection state. + /// - `already-listening`: (set) The socket is already in the Listener state. + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + unicast-hop-limit: func(this: tcp-socket) -> result + set-unicast-hop-limit: func(this: tcp-socket, value: u8) -> result<_, error-code> + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. + /// In other words, after setting a value, reading the same setting back may return a different value. + /// + /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of + /// actual data to be sent/received by the application, because the kernel might also use the buffer space + /// for internal metadata structures. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `already-connected`: (set) The socket is already in the Connection state. + /// - `already-listening`: (set) The socket is already in the Listener state. + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + receive-buffer-size: func(this: tcp-socket) -> result + set-receive-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> + send-buffer-size: func(this: tcp-socket) -> result + set-send-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: tcp-socket) -> pollable + + /// Initiate a graceful shutdown. + /// + /// - receive: the socket is not expecting to receive any more data from the peer. All subsequent read + /// operations on the `input-stream` associated with this socket will return an End Of Stream indication. + /// Any data still in the receive queue at time of calling `shutdown` will be discarded. + /// - send: the socket is not expecting to send any more data to the peer. All subsequent write + /// operations on the `output-stream` associated with this socket will return an error. + /// - both: same effect as receive & send combined. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `not-connected`: The socket is not in the Connection state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + shutdown: func(this: tcp-socket, shutdown-type: shutdown-type) -> result<_, error-code> + + /// Dispose of the specified `tcp-socket`, after which it may no longer be used. + /// + /// Similar to the POSIX `close` function. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-tcp-socket: func(this: tcp-socket) +} diff --git a/examples/github/wit/deps/sockets/udp-create-socket.wit b/examples/github/wit/deps/sockets/udp-create-socket.wit new file mode 100644 index 0000000..2c987be --- /dev/null +++ b/examples/github/wit/deps/sockets/udp-create-socket.wit @@ -0,0 +1,27 @@ + +default interface udp-create-socket { + use pkg.network.{network, error-code, ip-address-family} + use pkg.udp.{udp-socket} + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The host does not support UDP sockets. (EOPNOTSUPP) + /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + create-udp-socket: func(address-family: ip-address-family) -> result +} diff --git a/examples/github/wit/deps/sockets/udp.wit b/examples/github/wit/deps/sockets/udp.wit new file mode 100644 index 0000000..271a2cb --- /dev/null +++ b/examples/github/wit/deps/sockets/udp.wit @@ -0,0 +1,211 @@ + +default interface udp { + use poll.poll.{pollable} + use pkg.network.{network, error-code, ip-socket-address, ip-address-family} + + + /// A UDP socket handle. + type udp-socket = u32 + + + record datagram { + data: list, // Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + remote-address: ip-socket-address, + + /// Possible future additions: + /// local-address: ip-socket-address, // IP_PKTINFO / IP_RECVDSTADDR / IPV6_PKTINFO + /// local-interface: u32, // IP_PKTINFO / IP_RECVIF + /// ttl: u8, // IP_RECVTTL + /// dscp: u6, // IP_RECVTOS + /// ecn: u2, // IP_RECVTOS + } + + + + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// When a socket is not explicitly bound, the first invocation to connect will implicitly bind the socket. + /// + /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. + /// + /// # Typical `start` errors + /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) + /// - `already-bound`: The socket is already bound. (EINVAL) + /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) + /// + /// # Typical `finish` errors + /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-bind: func(this: udp-socket, network: network, local-address: ip-socket-address) -> result<_, error-code> + finish-bind: func(this: udp-socket) -> result<_, error-code> + + /// Set the destination address. + /// + /// The local-address is updated based on the best network path to `remote-address`. + /// + /// When a destination address is set: + /// - all receive operations will only return datagrams sent from the provided `remote-address`. + /// - the `send` function can only be used to send to this destination. + /// + /// Note that this function does not generate any network traffic and the peer is not aware of this "connection". + /// + /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. + /// + /// # Typical `start` errors + /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `already-attached`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) + /// + /// # Typical `finish` errors + /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A `connect` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-connect: func(this: udp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code> + finish-connect: func(this: udp-socket) -> result<_, error-code> + + /// Receive a message. + /// + /// Returns: + /// - The sender address of the datagram + /// - The number of bytes read. + /// + /// # Typical errors + /// - `not-bound`: The socket is not bound to any local address. (EINVAL) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + /// - `would-block`: There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + receive: func(this: udp-socket) -> result + + /// Send a message to a specific destination address. + /// + /// The remote address option is required. To send a message to the "connected" peer, + /// call `remote-address` to get their address. + /// + /// # Typical errors + /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `already-connected`: The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) + /// - `not-bound`: The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// - `would-block`: The send buffer is currently full. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + send: func(this: udp-socket, datagram: datagram) -> result<_, error-code> + + /// Get the current bound address. + /// + /// # Typical errors + /// - `not-bound`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + local-address: func(this: udp-socket) -> result + + /// Get the address set with `connect`. + /// + /// # Typical errors + /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + remote-address: func(this: udp-socket) -> result + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func(this: udp-socket) -> ip-address-family + + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. + /// + /// Equivalent to the IPV6_V6ONLY socket option. + /// + /// # Typical errors + /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. + /// - `already-bound`: (set) The socket is already bound. + /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) + /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) + ipv6-only: func(this: udp-socket) -> result + set-ipv6-only: func(this: udp-socket, value: bool) -> result<_, error-code> + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// # Typical errors + /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) + unicast-hop-limit: func(this: udp-socket) -> result + set-unicast-hop-limit: func(this: udp-socket, value: u8) -> result<_, error-code> + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. + /// In other words, after setting a value, reading the same setting back may return a different value. + /// + /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of + /// actual data to be sent/received by the application, because the kernel might also use the buffer space + /// for internal metadata structures. + /// + /// Fails when this socket is in the Listening state. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) + receive-buffer-size: func(this: udp-socket) -> result + set-receive-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code> + send-buffer-size: func(this: udp-socket) -> result + set-send-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code> + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: udp-socket) -> pollable + + /// Dispose of the specified `udp-socket`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-udp-socket: func(this: udp-socket) +} diff --git a/examples/github/wit/deps/sockets/world.wit b/examples/github/wit/deps/sockets/world.wit new file mode 100644 index 0000000..854fac0 --- /dev/null +++ b/examples/github/wit/deps/sockets/world.wit @@ -0,0 +1,10 @@ + +default world example-world { + import instance-network: pkg.instance-network + import network: pkg.network + import udp: pkg.udp + import udp-create-socket: pkg.udp-create-socket + import tcp: pkg.tcp + import tcp-create-socket: pkg.tcp-create-socket + import ip-name-lookup: pkg.ip-name-lookup +} diff --git a/examples/github/wit/deps/sql/readwrite.wit b/examples/github/wit/deps/sql/readwrite.wit new file mode 100644 index 0000000..1a71940 --- /dev/null +++ b/examples/github/wit/deps/sql/readwrite.wit @@ -0,0 +1,12 @@ +default interface readwrite { + use pkg.types.{statement, row, error, connection} + + // query is optimized for querying data, and + // implementors can make use of that fact to optimize + // the performance of query execution (e.g., using + // indexes). + query: func(c: connection, q: statement) -> result, error> + + // exec is for modifying data in the database. + exec: func(c: connection, q: statement) -> result +} \ No newline at end of file diff --git a/examples/github/wit/deps/sql/sql.wit b/examples/github/wit/deps/sql/sql.wit new file mode 100644 index 0000000..0400879 --- /dev/null +++ b/examples/github/wit/deps/sql/sql.wit @@ -0,0 +1,3 @@ +default world sql { + import readwrite: pkg.readwrite +} \ No newline at end of file diff --git a/examples/github/wit/deps/sql/types.wit b/examples/github/wit/deps/sql/types.wit new file mode 100644 index 0000000..628cbc4 --- /dev/null +++ b/examples/github/wit/deps/sql/types.wit @@ -0,0 +1,43 @@ +default interface types { + // one single row item + record row { + field-name: string, + value: data-type, + } + + // common data types + variant data-type { + int32(s32), + int64(s64), + uint32(u32), + uint64(u64), + float(float64), + double(float64), + str(string), + boolean(bool), + date(string), + time(string), + timestamp(string), + binary(list), + null + } + + // allows parameterized queries + // e.g., prepare-statement("SELECT * FROM users WHERE name = ? AND age = ?", vec!["John Doe", "32"]) + type statement = u32 + drop-statement: func(s: statement) + prepare-statement: func(query: string, params: list) -> result + + /// An error resource type. + /// Currently, this provides only one function to return a string representation + /// of the error. In the future, this will be extended to provide more information. + // TODO: switch to `resource error { ... }` + type error = u32 + drop-error: func(e: error) + trace-error: func(e: error) -> string + + /// A connection to a sql store. + type connection = u32 + drop-connection: func(b: connection) + open-connection: func(name: string) -> result +} \ No newline at end of file diff --git a/examples/github/wit/world.wit b/examples/github/wit/world.wit index db95d71..c852556 100644 --- a/examples/github/wit/world.wit +++ b/examples/github/wit/world.wit @@ -1,5 +1,10 @@ default world github { + import clock: clocks.monotonic-clock import http: http.incoming-handler - import logging: logging.logging - import random: random.random + import messaging: messaging.consumer + import network: sockets.network + + // The interfaces below are incompatible with the rest due to `types` import conflict + //import keyvalue: keyvalue.readwrite + //import sql: sql.readwrite } diff --git a/src/bin/wit-deps/main.rs b/src/bin/wit-deps/main.rs index 6a0ac4a..e16ca15 100644 --- a/src/bin/wit-deps/main.rs +++ b/src/bin/wit-deps/main.rs @@ -32,17 +32,9 @@ struct Cli { #[derive(Debug, Subcommand)] enum Command { /// Lock dependencies - Lock { - /// Optional list of packages to lock - #[arg(short, long)] - package: Vec, - }, + Lock, /// Update dependencies - Update { - /// Optional list of packages to update - #[arg(short, long)] - package: Vec, - }, + Update, /// Write a deterministic tar containing the `wit` subdirectory for a package to stdout Tar { /// Package to archive @@ -80,21 +72,14 @@ async fn main() -> anyhow::Result<()> { } = Cli::parse(); match command { - None => wit_deps::lock_path(manifest_path, lock_path, deps_path, None) + None | Some(Command::Lock) => wit_deps::lock_path(manifest_path, lock_path, deps_path) + .await + .map(|_| ()), + Some(Command::Update) => wit_deps::update_path(manifest_path, lock_path, deps_path) .await .map(|_| ()), - Some(Command::Lock { package }) => { - wit_deps::lock_path(manifest_path, lock_path, deps_path, &package) - .await - .map(|_| ()) - } - Some(Command::Update { package }) => { - wit_deps::update_path(manifest_path, lock_path, deps_path, &package) - .await - .map(|_| ()) - } Some(Command::Tar { package, output }) => { - wit_deps::lock_path(manifest_path, lock_path, &deps_path, [&package]) + wit_deps::lock_path(manifest_path, lock_path, &deps_path) .await .map(|_| ())?; let package = deps_path.join(package); diff --git a/tests/build/build.rs b/tests/build/build.rs index 7324d48..1d30e67 100644 --- a/tests/build/build.rs +++ b/tests/build/build.rs @@ -10,9 +10,8 @@ fn main() -> anyhow::Result<()> { .with_writer(std::io::stderr), ) .with( - tracing_subscriber::EnvFilter::builder() - .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into()) - .from_env_lossy(), + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,wit_deps=trace")), ) .init(); diff --git a/tests/build/subcrate/build.rs b/tests/build/subcrate/build.rs index 3a4e8b0..f8857d6 100644 --- a/tests/build/subcrate/build.rs +++ b/tests/build/subcrate/build.rs @@ -10,9 +10,8 @@ fn main() -> anyhow::Result<()> { .with_writer(std::io::stderr), ) .with( - tracing_subscriber::EnvFilter::builder() - .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into()) - .from_env_lossy(), + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,wit_deps=trace")), ) .init(); diff --git a/tests/build/subcrate/wit/deps.lock b/tests/build/subcrate/wit/deps.lock index 6a00735..7d23cfe 100644 --- a/tests/build/subcrate/wit/deps.lock +++ b/tests/build/subcrate/wit/deps.lock @@ -1,29 +1,25 @@ [build] path = "../../wit" -sha256 = "ec29ec48fdb09b2efcb989bd821cc5c1aef318b41a1dd9908534c667591de680" -sha512 = "6317d43cbf66f99c7052c24d4cc2fb308b001acfe88964f3dcb6b976c77bc83cda127f8c671c71eb19f379f7fc37cb039eccae3e53e77ae5ca883d991d852d94" +sha256 = "62d044e05929f43a66ce337ec200a8d5f29297a35ce5e074dfc6b96117dc3b23" +sha512 = "c4612fab93029724ea95e05f3a93193661af095756de0a14910b0d7fe285e9df83b774612a574d18b0ab626b0a73a2e67c75ceace03c788ee42eddc0687ad580" +deps = ["http", "io", "logging", "poll", "random"] [http] -path = "../../wit/deps/http" sha256 = "1667921c63364722b0d23eb69023ede0cdace86ee9b4c6f7c4286b3a2dd8d635" sha512 = "9b2f225411d347d3e99276359d3ad98475e9fbee4b3fad0169060dc648a5efa3584a6a1db990607a4971c7e3c0026dc65f8e48fe2824f9553840bb46f3b389ae" [io] -path = "../../wit/deps/io" -sha256 = "50e907cde3a0b49b2358c564a10db26e3585a5466029baaecb00424dccea77a1" -sha512 = "096953066bc1e690b90e01d0a8ed92650052184173df8dac1e24d2a277a6f864a078eca0d555fa2de0b38be1ba8ed25db7949a396f60c9a35def7ea956a66ff1" +sha256 = "87d8bf336f37184edc9290af9a14f7cac38c8fe087a786e66d08d1a94e48d186" +sha512 = "cdb5b35860c46c637ad37d784dd17f6023cf1c7c689d37973ec7c4094718baee4a16864e64622173b5578eed3950ad73211d9fe48a5b61b951809fdd9242b607" [logging] -path = "../../wit/deps/logging" sha256 = "f378a2b6a36af096528ec5e7031da474990cecaec1949c663befc5066d719f6f" sha512 = "ba09307b12f5428c3058d878001c9fb15df21933c6203328f785d5009a4fc0cf304950271590d4146305a2d8b8988595f396d955961168cb6bfa7ea9af1cc362" [poll] -path = "../../wit/deps/poll" -sha256 = "065422b0ea6ccb2a9facc6b87b902eef110c53d76fc31f341a6bc8d0b0285b6a" -sha512 = "19a55cd3072a19ae6a1774723a4962e7a155a5ce89a27175e8c76020efb4d00bc92ebb78427d92bcb8effd4f0d03ebf0a0daa747ecd981b8d31d5abc2ad86423" +sha256 = "9f2e6b5ea1a017575f96a3c415c737fe31513b043d9b47aefeb21f6c27ab8425" +sha512 = "f65965395742a0290fd9e4e55408c2b5ce3a4eae0ee22be1ff012e3db75910110da21c5f0a61083e043b4c510967a0a73ff4491ae9719e9158543f512dbeea5f" [random] -path = "../../wit/deps/random" -sha256 = "79b6417bb1ab3c82c241c56021e717f258e2a0b3ab94db93907ca11d7f92ed66" -sha512 = "1e657ac56420e65bde75eabea920a605a0ff4adc6f2ba8b7cf1c37a603710449bc1a29568dda2b61c04ae9889d2b1a82b9935d4b9d573eb48a83e0ecf9cad591" +sha256 = "19d57f2262530016ec2b32991db3d8b6705b3a0bc5a7b6ae8fff3bcef0cf1088" +sha512 = "f9eba7dfd858d1edcaa868ce636f69545323bf010c3b0d4a2a1b23584d8027240c592d13e91882617e70a4dbf290754a8697724b5633147d11fa3db7869a2afe" diff --git a/tests/build/subcrate/wit/deps.toml b/tests/build/subcrate/wit/deps.toml index 11e93f3..8db95e0 100644 --- a/tests/build/subcrate/wit/deps.toml +++ b/tests/build/subcrate/wit/deps.toml @@ -1,6 +1 @@ build = "../../wit" -http = "../../wit/deps/http" -io = "../../wit/deps/io" -logging = "../../wit/deps/logging" -poll = "../../wit/deps/poll" -random = "../../wit/deps/random" diff --git a/tests/build/wit/build.wit b/tests/build/wit/build.wit index 6ae4dfa..f1c23d0 100644 --- a/tests/build/wit/build.wit +++ b/tests/build/wit/build.wit @@ -5,5 +5,4 @@ default interface foo { default world build { import http: http.incoming-handler import logging: logging.logging - import random: random.random } diff --git a/tests/build/wit/deps.lock b/tests/build/wit/deps.lock index 05688e2..30e4f12 100644 --- a/tests/build/wit/deps.lock +++ b/tests/build/wit/deps.lock @@ -2,11 +2,11 @@ url = "https://github.com/WebAssembly/wasi-http/archive/6c6855a7329fb040a48ecdbad1765be8e694416c.tar.gz" sha256 = "1667921c63364722b0d23eb69023ede0cdace86ee9b4c6f7c4286b3a2dd8d635" sha512 = "9b2f225411d347d3e99276359d3ad98475e9fbee4b3fad0169060dc648a5efa3584a6a1db990607a4971c7e3c0026dc65f8e48fe2824f9553840bb46f3b389ae" +deps = ["io", "poll", "random"] [io] -url = "https://github.com/rvolosatovs/wasi-io/archive/v0.1.0.tar.gz" -sha256 = "50e907cde3a0b49b2358c564a10db26e3585a5466029baaecb00424dccea77a1" -sha512 = "096953066bc1e690b90e01d0a8ed92650052184173df8dac1e24d2a277a6f864a078eca0d555fa2de0b38be1ba8ed25db7949a396f60c9a35def7ea956a66ff1" +sha256 = "87d8bf336f37184edc9290af9a14f7cac38c8fe087a786e66d08d1a94e48d186" +sha512 = "cdb5b35860c46c637ad37d784dd17f6023cf1c7c689d37973ec7c4094718baee4a16864e64622173b5578eed3950ad73211d9fe48a5b61b951809fdd9242b607" [logging] url = "https://github.com/WebAssembly/wasi-logging/archive/d106e59b25297d0496e6a5d221ad090e19c3aaa3.tar.gz" @@ -14,11 +14,9 @@ sha256 = "f378a2b6a36af096528ec5e7031da474990cecaec1949c663befc5066d719f6f" sha512 = "ba09307b12f5428c3058d878001c9fb15df21933c6203328f785d5009a4fc0cf304950271590d4146305a2d8b8988595f396d955961168cb6bfa7ea9af1cc362" [poll] -url = "https://github.com/WebAssembly/wasi-poll/archive/3ff76670b0d43bc7c8a224c2e65880a963416835.tar.gz" -sha256 = "065422b0ea6ccb2a9facc6b87b902eef110c53d76fc31f341a6bc8d0b0285b6a" -sha512 = "19a55cd3072a19ae6a1774723a4962e7a155a5ce89a27175e8c76020efb4d00bc92ebb78427d92bcb8effd4f0d03ebf0a0daa747ecd981b8d31d5abc2ad86423" +sha256 = "9f2e6b5ea1a017575f96a3c415c737fe31513b043d9b47aefeb21f6c27ab8425" +sha512 = "f65965395742a0290fd9e4e55408c2b5ce3a4eae0ee22be1ff012e3db75910110da21c5f0a61083e043b4c510967a0a73ff4491ae9719e9158543f512dbeea5f" [random] -url = "https://github.com/WebAssembly/wasi-random/archive/28970c50c3797c0087fa75a15e88bfa39b91e0a0.tar.gz" -sha256 = "79b6417bb1ab3c82c241c56021e717f258e2a0b3ab94db93907ca11d7f92ed66" -sha512 = "1e657ac56420e65bde75eabea920a605a0ff4adc6f2ba8b7cf1c37a603710449bc1a29568dda2b61c04ae9889d2b1a82b9935d4b9d573eb48a83e0ecf9cad591" +sha256 = "19d57f2262530016ec2b32991db3d8b6705b3a0bc5a7b6ae8fff3bcef0cf1088" +sha512 = "f9eba7dfd858d1edcaa868ce636f69545323bf010c3b0d4a2a1b23584d8027240c592d13e91882617e70a4dbf290754a8697724b5633147d11fa3db7869a2afe" diff --git a/tests/build/wit/deps.toml b/tests/build/wit/deps.toml index 5572ee9..a7d5be1 100644 --- a/tests/build/wit/deps.toml +++ b/tests/build/wit/deps.toml @@ -1,5 +1,2 @@ http = "https://github.com/WebAssembly/wasi-http/archive/6c6855a7329fb040a48ecdbad1765be8e694416c.tar.gz" -io = "https://github.com/rvolosatovs/wasi-io/archive/v0.1.0.tar.gz" # this fork renames `streams` interface for compatiblity with wasi-snapshot-preview1 logging = "https://github.com/WebAssembly/wasi-logging/archive/d106e59b25297d0496e6a5d221ad090e19c3aaa3.tar.gz" -poll = "https://github.com/WebAssembly/wasi-poll/archive/3ff76670b0d43bc7c8a224c2e65880a963416835.tar.gz" -random = "https://github.com/WebAssembly/wasi-random/archive/28970c50c3797c0087fa75a15e88bfa39b91e0a0.tar.gz"