From 1650a002ba12f542b766d827afdb719838fcaeb5 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 16 Oct 2026 17:17:06 -0400 Subject: [PATCH] Update project structure to latest Signed-off-by: Scott Andrews --- .config/wasm-pkg/config.toml | 3 + .devcontainer/Dockerfile | 7 - .devcontainer/devcontainer.json | 15 +- .github/dependabot.yml | 36 +- .github/workflows/bump-version.yaml | 203 +++++++ .github/workflows/ci.yaml | 60 +- .gitignore | 6 + Cargo.toml | 22 +- Makefile | 99 +-- README.md | 7 +- components/chroot/src/lib.rs | 2 - components/readonly/src/lib.rs | 2 - components/tracing/src/lib.rs | 2 - .../package.wit | 8 - .../wit/deps/wasi-clocks-0.3.0/package.wit | 19 - .../deps/wasi-config-0.2.0-rc.1/package.wit | 33 - .../deps/wasi-filesystem-0.3.0/package.wit | 575 ------------------ .../deps/wasi-logging-0.1.0-draft/package.wit | 36 -- lib/.gitignore | 2 - make/components.mk | 83 +++ make/pinned.jq | 6 + make/publish.mk | 17 + make/tools.mk | 53 ++ make/wit.mk | 41 ++ scripts/bump-version.sh | 95 +++ scripts/init-devcontainer.sh | 12 + scripts/publish.sh | 106 ++++ scripts/wac-deps.sh | 46 ++ tools/Cargo.toml | 28 + tools/lib.rs | 1 + wit/deps/wasi-clocks-0.3.0/package.wit | 19 - wit/deps/wasi-filesystem-0.3.0/package.wit | 575 ------------------ 32 files changed, 808 insertions(+), 1411 deletions(-) create mode 100644 .config/wasm-pkg/config.toml delete mode 100644 .devcontainer/Dockerfile create mode 100644 .github/workflows/bump-version.yaml delete mode 100644 components/wit/deps/componentized-filesystem-0.3.0-dev/package.wit delete mode 100644 components/wit/deps/wasi-clocks-0.3.0/package.wit delete mode 100644 components/wit/deps/wasi-config-0.2.0-rc.1/package.wit delete mode 100644 components/wit/deps/wasi-filesystem-0.3.0/package.wit delete mode 100644 components/wit/deps/wasi-logging-0.1.0-draft/package.wit delete mode 100644 lib/.gitignore create mode 100644 make/components.mk create mode 100644 make/pinned.jq create mode 100644 make/publish.mk create mode 100644 make/tools.mk create mode 100644 make/wit.mk create mode 100755 scripts/bump-version.sh create mode 100755 scripts/init-devcontainer.sh create mode 100755 scripts/publish.sh create mode 100755 scripts/wac-deps.sh create mode 100644 tools/Cargo.toml create mode 100644 tools/lib.rs delete mode 100644 wit/deps/wasi-clocks-0.3.0/package.wit delete mode 100644 wit/deps/wasi-filesystem-0.3.0/package.wit diff --git a/.config/wasm-pkg/config.toml b/.config/wasm-pkg/config.toml new file mode 100644 index 0000000..ec58b31 --- /dev/null +++ b/.config/wasm-pkg/config.toml @@ -0,0 +1,3 @@ +[namespace_registries] +wasi = "wasi.dev" +componentized = "componentized.dev" diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index b46b2fb..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/rust:2-1-trixie -RUN sudo apt update && sudo apt upgrade -y -RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash -RUN cargo binstall -y wasm-tools -RUN cargo binstall -y wkg -RUN cargo binstall -y wac-cli -RUN cargo binstall -y wasmtime-cli diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b5e7302..39a7203 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,11 +1,8 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/rust { - "name": "filesystem", - "build": { - "dockerfile": "Dockerfile" - }, - + "name": "componentized-filesystem", + "image": "mcr.microsoft.com/devcontainers/rust", // Use 'mounts' to make the cargo cache persistent in a Docker Volume. "mounts": [ { @@ -19,15 +16,12 @@ "type": "volume" } ], - // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers/features/github-cli:1": {} }, - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "cargo check", - + "postCreateCommand": "./scripts/init-devcontainer.sh", // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. @@ -47,7 +41,6 @@ ] } } - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" -} +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5c1266c..fc87e11 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,18 +1,22 @@ version: 2 updates: -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: daily -- package-ecosystem: cargo - directory: "/" - schedule: - interval: daily -- package-ecosystem: rust-toolchain - directory: "/" - schedule: - interval: daily -- package-ecosystem: devcontainers - directory: "/" - schedule: - interval: weekly + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + - package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + - package-ecosystem: cargo + directory: "/tools" + schedule: + interval: daily + - package-ecosystem: rust-toolchain + directory: "/" + schedule: + interval: daily + - package-ecosystem: devcontainers + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/bump-version.yaml b/.github/workflows/bump-version.yaml new file mode 100644 index 0000000..8d18ae8 --- /dev/null +++ b/.github/workflows/bump-version.yaml @@ -0,0 +1,203 @@ +name: Bump version + +on: + workflow_dispatch: + inputs: + version: + description: The new version of the interface package, e.g. 0.3.0 or 0.4.0-dev + required: true + type: string + default: "0.3.0-dev" # the current version, kept current by scripts/bump-version.sh + +jobs: + # bumps the version with read only access, the changes are handed to the push job as a patch so + # the third party actions used to build never run with write access + bump: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check version + # a semver version without a leading `v`, the tag adds it. Build metadata is used to tag the + # debug builds of components, e.g. 0.1.0+debug, and isn't allowed in the version itself. + run: | + identifier='(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)' + semver="^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-${identifier}(\.${identifier})*)?$" + if ! [[ "${VERSION}" =~ ${semver} ]] ; then + echo "::error::the version '${VERSION}' is not a valid semver version, e.g. 0.3.0 or 0.4.0-dev" + exit 1 + fi + if [[ "${VERSION}" == *+* ]] ; then + echo "::error::the version '${VERSION}' must not contain build metadata" + exit 1 + fi + env: + VERSION: ${{ inputs.version }} + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions-rust-lang/setup-rust-toolchain@v2 + - name: Install cargo binstall + uses: cargo-bins/cargo-binstall@main + - name: Install tools + run: | + make tools + make -s tools-path >> "${GITHUB_PATH}" + - name: Bump version + # also fetches the wit dependencies for the new version, and builds and tests the components + run: scripts/bump-version.sh "${VERSION}" + env: + VERSION: ${{ inputs.version }} + - name: Collect changes + run: | + git add --all + git diff --cached --binary > bump-version.patch + - name: Upload changes + uses: actions/upload-artifact@v7 + with: + name: bump-version.patch + path: bump-version.patch + if-no-files-found: error + retention-days: 1 + + # pushes the bump to the branch the workflow was run on, using only first party actions and the gh + # cli + push: + needs: + - bump + # the bump is a new commit on the branch, a tag can't be moved forward + if: github.ref_type == 'branch' + runs-on: ubuntu-latest + # the commit is pushed with a token for the custodian GitHub App rather than the GITHUB_TOKEN, + # which can't change workflow files and doesn't trigger the CI workflow. The app must be allowed + # to bypass the branch's ruleset, the commit is pushed without the status checks it requires, + # the bump job built and tested the changes instead. + permissions: + contents: read + env: + VERSION: ${{ inputs.version }} + steps: + - name: Check custodian app credentials + run: | + if [ -z "${CLIENT_ID}" ] || [ -z "${PRIVATE_KEY}" ] ; then + echo "::error::the CUSTODIAN_CLIENT_ID and CUSTODIAN_PRIVATE_KEY secrets must be available to this repository, the private key of the custodian GitHub App is needed to create a token" + exit 1 + fi + env: + CLIENT_ID: ${{ secrets.CUSTODIAN_CLIENT_ID }} + PRIVATE_KEY: ${{ secrets.CUSTODIAN_PRIVATE_KEY }} + - name: Create custodian app token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.CUSTODIAN_CLIENT_ID }} + private-key: ${{ secrets.CUSTODIAN_PRIVATE_KEY }} + # only this repository, with only the permissions the bump needs + repositories: ${{ github.event.repository.name }} + permission-contents: write + # the bump changes the default version in this workflow + permission-workflows: write + # a release runs this workflow again, to bump to the next dev version + permission-actions: write + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Download changes + uses: actions/download-artifact@v8 + with: + name: bump-version.patch + path: ${{ runner.temp }} + - name: Read current version + # the checkout is before the bump, the wit package's version is the current version + run: | + current=$( sed -n 's/^package componentized:filesystem@\(.*\);$/\1/p' wit/worlds.wit ) + echo "CURRENT_VERSION=${current}" >> "${GITHUB_ENV}" + - name: Commit changes + # the commit is created with the REST API, as the app's token can't push. The patch is applied + # locally only to find the changed files and their modes. + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + api="repos/${GITHUB_REPOSITORY}" + base=$( git rev-parse HEAD ) + git apply --index "${RUNNER_TEMP}/bump-version.patch" + + # a blob for each changed file, or a null sha for a deleted file + entries="${RUNNER_TEMP}/tree-entries.json" + echo '[]' > "${entries}" + git diff --cached --no-renames --name-status "${base}" | while IFS=$'\t' read -r status path ; do + if [ "${status}" = "D" ] ; then + entry=$( jq -n --arg path "${path}" '{path: $path, mode: "100644", type: "blob", sha: null}' ) + else + mode=$( git ls-files --stage -- "${path}" | cut -d' ' -f1 ) + sha=$( base64 < "${path}" | tr -d '\n' | jq -Rs '{encoding: "base64", content: .}' | gh api --method POST "${api}/git/blobs" --input - --jq .sha ) + entry=$( jq -n --arg path "${path}" --arg mode "${mode}" --arg sha "${sha}" '{path: $path, mode: $mode, type: "blob", sha: $sha}' ) + fi + jq --argjson entry "${entry}" '. + [$entry]' "${entries}" > "${entries}.tmp" && mv "${entries}.tmp" "${entries}" + echo "${status} ${path}" + done + tree=$( jq --arg base "$( git rev-parse "${base}^{tree}" )" '{base_tree: $base, tree: .}' "${entries}" | gh api --method POST "${api}/git/trees" --input - --jq .sha ) + + # authored, committed and signed off (DCO) by the custodian app's bot. GitHub only signs + # commits it attributes entirely to the app, so the author and committer are left for GitHub + # to fill in. The user who triggered the workflow is credited as a co-author, with their + # GitHub noreply email so their email address isn't exposed. A run started by the app after a + # release has no one else to credit. + bot="${APP_SLUG}[bot]" + bot_email="$( gh api "users/${bot}" --jq .id )+${bot}@users.noreply.github.com" + message=$( printf 'Bump version from %s to %s\n\nSigned-off-by: %s <%s>' "${CURRENT_VERSION}" "${VERSION}" "${bot}" "${bot_email}" ) + if [ "${GITHUB_ACTOR}" != "${bot}" ] ; then + name=$( gh api "users/${GITHUB_ACTOR}" --jq '.name // .login' ) + name="${name:-${GITHUB_ACTOR}}" + email="${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" + message=$( printf '%s\nCo-authored-by: %s <%s>' "${message}" "${name}" "${email}" ) + fi + read -r commit verified < <( jq -n \ + --arg message "${message}" \ + --arg tree "${tree}" --arg parent "${base}" \ + '{message: $message, tree: $tree, parents: [$parent]}' \ + | gh api --method POST "${api}/git/commits" --input - --jq '"\(.sha) \(.verification.verified)"' ) + echo "created commit ${commit}" + # the branch requires signed commits, fail before pushing rather than after + if [ "${verified}" != "true" ] ; then + echo "::error::the commit '${commit}' was not created, or was not signed by GitHub" + exit 1 + fi + + # a release, rather than a pre-release, is tagged, e.g. v0.1.0. Tags are immutable, check the + # tag is free before pushing the branch, so a release isn't left without its tag + tag="" + if [[ "${VERSION}" != *-* ]] ; then + tag="v${VERSION}" + if gh api "${api}/git/ref/tags/${tag}" --silent 2> /dev/null ; then + echo "::error::the tag ${tag} already exists" + exit 1 + fi + fi + + # fast forwards the branch to the commit, failing rather than losing commits pushed to the + # branch since the workflow started + if ! gh api --method PATCH "${api}/git/refs/heads/${GITHUB_REF_NAME}" -f sha="${commit}" -F force=false --silent ; then + echo "::error::failed to push ${commit} to ${GITHUB_REF_NAME}, either the branch moved since ${base} and the workflow should be run again, or the custodian app is not allowed to bypass the branch's ruleset" + exit 1 + fi + echo "pushed ${commit} to ${GITHUB_REF_NAME}" + + # a lightweight tag, pushed with the app's token so the CI workflow runs for it and drafts + # the release + if [ -n "${tag}" ] ; then + gh api --method POST "${api}/git/refs" -f ref="refs/tags/${tag}" -f sha="${commit}" --silent + echo "tagged ${commit} as ${tag}" + fi + - name: Bump to the next dev version + # after a release, the branch moves on to a pre-release of the next patch version, e.g. 0.3.0 + # is followed by 0.3.1-dev. Run with the app's token, the GITHUB_TOKEN can't start workflows. + if: ${{ !contains(inputs.version, '-') }} + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + IFS=. read -r major minor patch <<< "${VERSION}" + next="${major}.${minor}.$(( patch + 1 ))-dev" + gh workflow run bump-version.yaml --repo "${GITHUB_REPOSITORY}" --ref "${GITHUB_REF_NAME}" -f version="${next}" + echo "started the ${GITHUB_WORKFLOW} workflow for ${next} on ${GITHUB_REF_NAME}" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a141571..64d6a83 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,10 +17,11 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v2 - name: Install cargo binstall uses: cargo-bins/cargo-binstall@main - - name: Install wkg - run: cargo binstall --force wkg - - name: Install wasm-tools - run: cargo binstall --force wasm-tools + - name: Install tools + # the versions pinned in tools/Cargo.toml, on the path for later steps + run: | + make tools + make -s tools-path >> "${GITHUB_PATH}" - name: Sync wit run: make wit - name: Check for drift in generated wit @@ -28,8 +29,8 @@ jobs: - name: Build components run: make components - name: Collect components.tar - run: tar -cvf ../components.tar *.wasm* - working-directory: ./lib + run: tar -cvf ../../components.tar . + working-directory: ./target/components - name: Upload components.tar uses: actions/upload-artifact@v7 with: @@ -39,12 +40,20 @@ jobs: - name: Test run: make test - name: Capture WIT - working-directory: ./lib + working-directory: ./target/components run: | - for component in *.wasm ; do - echo "::group::${component} ($(du -h ${component} | cut -f1 ))" - wasm-tools component wit "${component}" - echo "::endgroup::" + dump_wit() { + echo "::group::$(basename "$1") ($(du -h "$1" | awk '{print $1}' ))" + wasm-tools component wit "$1" + echo "::endgroup::" + } + + # print interface.wasm first + if [ -f interface.wasm ] ; then + dump_wit interface.wasm + fi + for component in $(find . -name '*.wasm' -not -name '*.debug.wasm' -not -name 'interface.wasm' | sort) ; do + dump_wit "${component}" done publish: @@ -61,10 +70,10 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v2 - name: Install cargo binstall uses: cargo-bins/cargo-binstall@main - - name: Install wkg - run: cargo binstall --force wkg - - name: Install wasm-tools - run: cargo binstall --force wasm-tools + - name: Install tools + run: | + make tools + make -s tools-path >> "${GITHUB_PATH}" - name: Install cosign uses: sigstore/cosign-installer@v4.1.2 - name: Download components.tar @@ -72,10 +81,10 @@ jobs: with: name: components.tar - name: Extract components - run: tar -xvf components.tar -C lib + run: mkdir -p target/components && tar -xvf components.tar -C target/components - name: Get interface version id: interface_version - run: echo "VERSION=$( wasm-tools component wit lib/interface.wasm --json | jq -r "[.packages[] | select(.name | contains(\"${GITHUB_REPOSITORY/\//:}@\"))][0].name" | cut -d'@' -f2 )" >> $GITHUB_OUTPUT + run: echo "VERSION=$( wasm-tools component wit target/components/interface.wasm --json | jq -r "[.packages[] | select(.name | contains(\"${GITHUB_REPOSITORY/\//:}@\"))][0].name" | cut -d'@' -f2 )" >> $GITHUB_OUTPUT - name: Get tag version if: startsWith(github.ref, 'refs/tags/') id: tag_version @@ -91,15 +100,30 @@ jobs: if: startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && contains(steps.interface_version.outputs.VERSION, '-')) run: make publish env: + PUBLISH_LOG: "${{ runner.temp }}/published.txt" REPOSITORY: "ghcr.io/${{ github.repository }}" VERSION: "${{ case(github.ref == 'refs/heads/main', steps.interface_version.outputs.VERSION, steps.tag_version.outputs.VERSION) }}" + - name: Draft release notes + if: startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && contains(steps.interface_version.outputs.VERSION, '-')) + run: | + { + echo "## Published components" + echo + echo "| File | Size | Image |" + echo "| --- | --: | --- |" + while read -r file bytes image ; do + size=$( numfmt --to=iec-i --suffix=B --format=%.1f "${bytes}" ) + echo "| \`${file}\` | ${size} | \`${image}\` |" + done < "${RUNNER_TEMP}/published.txt" + } > "${RUNNER_TEMP}/release-notes.md" + cat "${RUNNER_TEMP}/release-notes.md" - name: Draft GitHub Release if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v3 with: draft: true + body_path: ${{ runner.temp }}/release-notes.md files: | - lib/*.wasm components.tar fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b546111..deab1a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ /components.tar /target .DS_Store +/tools/Cargo.lock +# wit dependencies, fetched by `make wit` from the wkg.toml and wkg.lock files +**/wit/deps/ + +# a lock wkg rewrote, kept while `make` checks whether its content changed +wkg.lock.orig diff --git a/Cargo.toml b/Cargo.toml index b90e848..4429d78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,27 @@ [workspace] resolver = "2" members = [ - "components/*", -] -exclude = [ - "components/wit", + "components/chroot", + "components/readonly", + "components/tracing", ] [workspace.dependencies] chrono = { git = "https://github.com/chronotope/chrono.git", branch = "0.5.x" } heck = "0.5" wit-bindgen = "0.62.0" + +[profile.release] +opt-level = "s" +lto = true +codegen-units = 1 +strip = "debuginfo" + +[profile.component-debug] +inherits = "dev" +opt-level = 1 # most of the size win; locals mostly still visible +lto = true +codegen-units = 1 +incremental = false # no benefit with one codegen unit +debug = "full" # variables and types for lldb, file:line in backtraces +# kept from dev: debug-assertions, overflow-checks, no strip diff --git a/Makefile b/Makefile index f31a554..9e26af7 100644 --- a/Makefile +++ b/Makefile @@ -2,91 +2,30 @@ SHELL := /bin/bash export RUST_BACKTRACE ?= 1 export WASMTIME_BACKTRACE_DETAILS ?= 1 +export WKG_CONFIG_FILE := $(abspath .config/wasm-pkg/config.toml) -COMPONENTS = $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard components/*/Cargo.toml))))) +# `make` without a goal runs all, otherwise the first target of an included file would run +.DEFAULT_GOAL := all -.PHONY: all -all: components - -.PHONY: clean -clean: - cargo clean - rm -rf lib/*.wasm - rm -rf lib/*.wasm.md - -.PHONY: test -test: - @echo "TODO add tests" - -.PHONY: components -components: lib/interface.wasm $(foreach component,$(COMPONENTS),lib/$(component).wasm lib/$(component).debug.wasm) - -define BUILD_COMPONENT - -.PHONY: components/$1 -components/$1: lib/$1.wasm lib/$1.debug.wasm +# tools first, the others use the variables it defines for each tool when their rules are read +include make/tools.mk +include make/wit.mk +include make/components.mk +include make/publish.mk -lib/$1.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) - cargo build -p $1 --target wasm32-unknown-unknown --release - wasm-tools component new target/wasm32-unknown-unknown/release/$(subst -,_,$1).wasm -o lib/$1.wasm - cp components/$1/README.md lib/$1.wasm.md -lib/$1.debug.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) - cargo build -p $1 --target wasm32-unknown-unknown - wasm-tools component new target/wasm32-unknown-unknown/debug/$(subst -,_,$1).wasm -o lib/$1.debug.wasm - cp components/$1/README.md lib/$1.debug.wasm.md - -endef - -$(foreach component,$(COMPONENTS),$(eval $(call BUILD_COMPONENT,$(component)))) - -lib/interface.wasm: wit/deps README.md - wkg build -o lib/interface.wasm - cp README.md lib/interface.wasm.md - -.PHONY: wit -wit: wit/deps components/wit/deps - -wit/deps: wkg.toml $(shell find wit -type f -name "*.wit" -not -path "deps") - wkg fetch +.PHONY: all +all: tools wit components test -components/wit/deps: wit/deps components/wkg.toml $(shell find components/wit -type f -name "*.wit" -not -path "deps") - ( cd components && wkg fetch ) +.PHONY: clean +clean: clean-components clean-wit + @: -.PHONY: publish ## Publish each component in the lib directory -publish: $(shell find lib -maxdepth 1 -type f -name "*.wasm" | sed -e 's:^lib/:publish-:g') +.PHONY: clean-all +clean-all: clean-components clean-tools clean-wit + cargo clean -.PHONY: publish-% -publish-%: -ifndef VERSION - $(error VERSION is undefined) -endif -ifndef REPOSITORY - $(error REPOSITORY is undefined) -endif - @$(eval FILE := $(@:publish-%=%)) - @$(eval COMPONENT := $(if $(filter %.debug.wasm,$(FILE)),$(FILE:%.debug.wasm=%),$(FILE:%.wasm=%))) - @$(eval TITLE := $(if $(filter %.debug.wasm,$(FILE)),$(COMPONENT) (debug),$(COMPONENT))) - @$(eval DESCRIPTION := $(shell head -n 3 "lib/${FILE}.md" | tail -n 1)) - @$(eval REVISION := $(shell git rev-parse HEAD)$(shell git diff --quiet HEAD && echo "+dirty")) - @$(eval COMPONENT_VERSION := $(if $(filter %.debug.wasm,$(FILE)),${VERSION}+debug,${VERSION})) - @$(eval TAG := $(patsubst v%,%,$(subst +,_,$(COMPONENT_VERSION)))) - @$(eval IMAGE := $(if $(filter interface.wasm,$(FILE)),${REPOSITORY}:${TAG},${REPOSITORY}/${COMPONENT}:${TAG})) - @echo "::group::${FILE} -> ${IMAGE}" - @DIGEST=$$( \ - wkg oci push \ - --annotation "org.opencontainers.image.title=${TITLE}" \ - --annotation "org.opencontainers.image.description=${DESCRIPTION}" \ - --annotation "org.opencontainers.image.version=${COMPONENT_VERSION}" \ - --annotation "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}.git" \ - --annotation "org.opencontainers.image.revision=${REVISION}" \ - --annotation "org.opencontainers.image.licenses=Apache-2.0" \ - "${IMAGE}" \ - "lib/${FILE}" \ - 2>&1 \ - | tee /dev/stderr \ - | grep -o 'sha256:[a-f0-9]\{64\}' \ - ) ; \ - cosign sign --yes "${IMAGE}@$${DIGEST}" - @echo "::endgroup::" +.PHONY: test +test: components + cargo test --workspace diff --git a/README.md b/README.md index f07d925..1df174d 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,16 @@ A [dev container](https://containers.dev) is available that contains the necessa Prereqs: - a rust toolchain -- [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools) -- [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools) +- [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall), optional, to download prebuilt tools instead of building them ```sh make components ``` +The build creates each component in [`components`](./components) into `target/components`, e.g. the readonly component at `target/components/readonly/readonly.wasm`, along with `target/components/interface.wasm`, the `componentized:filesystem` WIT package. Each component is also built with debug info, e.g. `target/components/readonly/readonly.debug.wasm`. + +The cli tools the build uses, [`wac`](https://github.com/bytecodealliance/wac), [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools) and [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools), are pinned in [`tools/Cargo.toml`](./tools/Cargo.toml) and installed into `target/tools/`, e.g. `target/tools/aarch64-apple-darwin`, as needed, or ahead of time with `make tools`. Dependabot bumps the pinned versions. + ## Community ### Code of Conduct diff --git a/components/chroot/src/lib.rs b/components/chroot/src/lib.rs index 4d19753..11a1551 100644 --- a/components/chroot/src/lib.rs +++ b/components/chroot/src/lib.rs @@ -1,5 +1,3 @@ -#![no_main] - use std::path::{Component, Path, PathBuf}; use exports::wasi::filesystem::preopens::Guest as Preopens; diff --git a/components/readonly/src/lib.rs b/components/readonly/src/lib.rs index 6ee90b9..231c4b5 100644 --- a/components/readonly/src/lib.rs +++ b/components/readonly/src/lib.rs @@ -1,5 +1,3 @@ -#![no_main] - use std::path::PathBuf; use exports::wasi::filesystem::preopens::Guest as Preopens; diff --git a/components/tracing/src/lib.rs b/components/tracing/src/lib.rs index 4dc2b85..a9e7cb8 100644 --- a/components/tracing/src/lib.rs +++ b/components/tracing/src/lib.rs @@ -1,5 +1,3 @@ -#![no_main] - use std::fmt::Display; use std::path::PathBuf; diff --git a/components/wit/deps/componentized-filesystem-0.3.0-dev/package.wit b/components/wit/deps/componentized-filesystem-0.3.0-dev/package.wit deleted file mode 100644 index 51d830e..0000000 --- a/components/wit/deps/componentized-filesystem-0.3.0-dev/package.wit +++ /dev/null @@ -1,8 +0,0 @@ -package componentized:filesystem@0.3.0-dev; - -world imports { - import wasi:clocks/types@0.3.0; - import wasi:clocks/system-clock@0.3.0; - import wasi:filesystem/types@0.3.0; - import wasi:filesystem/preopens@0.3.0; -} diff --git a/components/wit/deps/wasi-clocks-0.3.0/package.wit b/components/wit/deps/wasi-clocks-0.3.0/package.wit deleted file mode 100644 index 2594e02..0000000 --- a/components/wit/deps/wasi-clocks-0.3.0/package.wit +++ /dev/null @@ -1,19 +0,0 @@ -package wasi:clocks@0.3.0; - -interface types { - type duration = u64; -} - -interface system-clock { - use types.{duration}; - - record instant { - seconds: s64, - nanoseconds: u32, - } - - now: func() -> instant; - - get-resolution: func() -> duration; -} - diff --git a/components/wit/deps/wasi-config-0.2.0-rc.1/package.wit b/components/wit/deps/wasi-config-0.2.0-rc.1/package.wit deleted file mode 100644 index d8950ee..0000000 --- a/components/wit/deps/wasi-config-0.2.0-rc.1/package.wit +++ /dev/null @@ -1,33 +0,0 @@ -package wasi:config@0.2.0-rc.1; - -interface store { - /// An error type that encapsulates the different errors that can occur fetching configuration values. - variant error { - /// This indicates an error from an "upstream" config source. - /// As this could be almost _anything_ (such as Vault, Kubernetes ConfigMaps, KeyValue buckets, etc), - /// the error message is a string. - upstream(string), - /// This indicates an error from an I/O operation. - /// As this could be almost _anything_ (such as a file read, network connection, etc), - /// the error message is a string. - /// Depending on how this ends up being consumed, - /// we may consider moving this to use the `wasi:io/error` type instead. - /// For simplicity right now in supporting multiple implementations, it is being left as a string. - io(string), - } - - /// Gets a configuration value of type `string` associated with the `key`. - /// - /// The value is returned as an `option`. If the key is not found, - /// `Ok(none)` is returned. If an error occurs, an `Err(error)` is returned. - get: func(key: string) -> result, error>; - - /// Gets a list of configuration key-value pairs of type `string`. - /// - /// If an error occurs, an `Err(error)` is returned. - get-all: func() -> result>, error>; -} - -world imports { - import store; -} diff --git a/components/wit/deps/wasi-filesystem-0.3.0/package.wit b/components/wit/deps/wasi-filesystem-0.3.0/package.wit deleted file mode 100644 index e4a778f..0000000 --- a/components/wit/deps/wasi-filesystem-0.3.0/package.wit +++ /dev/null @@ -1,575 +0,0 @@ -package wasi:filesystem@0.3.0; - -/// WASI filesystem is a filesystem API primarily intended to let users run WASI -/// programs that access their files on their existing filesystems, without -/// significant overhead. -/// -/// Paths are passed as interface-type `string`s, meaning they must consist of -/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain -/// paths which are not accessible by this API. -/// -/// The directory separator in WASI is always the forward-slash (`/`). -/// -/// All paths in WASI are relative paths, and are interpreted relative to a -/// `descriptor` referring to a base directory. If a `path` argument to any WASI -/// function starts with `/`, or if any step of resolving a `path`, including -/// `..` and symbolic link steps, reaches a directory outside of the base -/// directory, or reaches a symlink to an absolute or rooted path in the -/// underlying filesystem, the function fails with `error-code::not-permitted`. -/// -/// For more information about WASI path resolution and sandboxing, see -/// [WASI filesystem path resolution]. -/// -/// Though this package presents a portable interface modelled on POSIX, it -/// prioritizes compatibility over portability: allowing users to access their -/// files on their machine is more important than exposing a single semantics -/// across all platforms. Notably, depending on the underlying operating system -/// and file system: -/// * Paths may be case-folded or not. -/// * Deleting (unlinking) a file may fail if there are other file descriptors -/// open. -/// * Durability and atomicity of changes to underlying files when there are -/// concurrent writers. -/// -/// Users that need well-defined, portable semantics should use a key-value -/// store or a database instead. -/// -/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md -@since(version = 0.3.0) -interface types { - @since(version = 0.3.0) - use wasi:clocks/system-clock@0.3.0.{instant}; - - /// File size or length of a region within a file. - @since(version = 0.3.0) - type filesize = u64; - - /// The type of a filesystem object referenced by a descriptor. - /// - /// Note: This was called `filetype` in earlier versions of WASI. - @since(version = 0.3.0) - variant descriptor-type { - /// The descriptor refers to a block device inode. - block-device, - /// The descriptor refers to a character device inode. - character-device, - /// The descriptor refers to a directory inode. - directory, - /// The descriptor refers to a named pipe. - fifo, - /// The file refers to a symbolic link inode. - symbolic-link, - /// The descriptor refers to a regular file inode. - regular-file, - /// The descriptor refers to a socket. - socket, - /// The type of the descriptor or file is different from any of the - /// other types specified. - other(option), - } - - /// Descriptor flags. - /// - /// Note: This was called `fdflags` in earlier versions of WASI. - @since(version = 0.3.0) - flags descriptor-flags { - /// Read mode: Data can be read. - read, - /// Write mode: Data can be written to. - write, - /// Request that writes be performed according to synchronized I/O file - /// integrity completion. The data stored in the file and the file's - /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - file-integrity-sync, - /// Request that writes be performed according to synchronized I/O data - /// integrity completion. Only the data stored in the file is - /// synchronized. This is similar to `O_DSYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - data-integrity-sync, - /// Requests that reads be performed at the same level of integrity - /// requested for writes. This is similar to `O_RSYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - requested-write-sync, - /// Mutating directories mode: Directory contents may be mutated. - /// - /// When this flag is unset on a descriptor, operations using the - /// descriptor which would create, rename, delete, modify the data or - /// metadata of filesystem objects, or obtain another handle which - /// would permit any of those, shall fail with `error-code::read-only` if - /// they would otherwise succeed. - /// - /// This may only be set on directories. - mutate-directory, - } - - /// Flags determining the method of how paths are resolved. - @since(version = 0.3.0) - flags path-flags { - /// As long as the resolved path corresponds to a symbolic link, it is - /// expanded. - symlink-follow, - } - - /// Open flags used by `open-at`. - @since(version = 0.3.0) - flags open-flags { - /// Create file if it does not exist, similar to `O_CREAT` in POSIX. - create, - /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. - directory, - /// Fail if file already exists, similar to `O_EXCL` in POSIX. - exclusive, - /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. - truncate, - } - - /// Number of hard links to an inode. - @since(version = 0.3.0) - type link-count = u64; - - /// File attributes. - /// - /// Note: This was called `filestat` in earlier versions of WASI. - @since(version = 0.3.0) - record descriptor-stat { - /// File type. - %type: descriptor-type, - /// Number of hard links to the file. - link-count: link-count, - /// For regular files, the file size in bytes. For symbolic links, the - /// length in bytes of the pathname contained in the symbolic link. - size: filesize, - /// Last data access timestamp. - /// - /// If the `option` is none, the platform doesn't maintain an access - /// timestamp for this file. - data-access-timestamp: option, - /// Last data modification timestamp. - /// - /// If the `option` is none, the platform doesn't maintain a - /// modification timestamp for this file. - data-modification-timestamp: option, - /// Last file status-change timestamp. - /// - /// If the `option` is none, the platform doesn't maintain a - /// status-change timestamp for this file. - status-change-timestamp: option, - } - - /// When setting a timestamp, this gives the value to set it to. - @since(version = 0.3.0) - variant new-timestamp { - /// Leave the timestamp set to its previous value. - no-change, - /// Set the timestamp to the current time of the system clock associated - /// with the filesystem. - now, - /// Set the timestamp to the given value. - timestamp(instant), - } - - /// A directory entry. - @since(version = 0.3.0) - record directory-entry { - /// The type of the file referred to by this directory entry. - %type: descriptor-type, - /// The name of the object. - name: string, - } - - /// Error codes returned by functions, similar to `errno` in POSIX. - /// Not all of these error codes are returned by the functions provided by this - /// API; some are used in higher-level library layers, and others are provided - /// merely for alignment with POSIX. - @since(version = 0.3.0) - variant error-code { - /// Permission denied, similar to `EACCES` in POSIX. - access, - /// Connection already in progress, similar to `EALREADY` in POSIX. - already, - /// Bad descriptor, similar to `EBADF` in POSIX. - bad-descriptor, - /// Device or resource busy, similar to `EBUSY` in POSIX. - busy, - /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. - deadlock, - /// Storage quota exceeded, similar to `EDQUOT` in POSIX. - quota, - /// File exists, similar to `EEXIST` in POSIX. - exist, - /// File too large, similar to `EFBIG` in POSIX. - file-too-large, - /// Illegal byte sequence, similar to `EILSEQ` in POSIX. - illegal-byte-sequence, - /// Operation in progress, similar to `EINPROGRESS` in POSIX. - in-progress, - /// Interrupted function, similar to `EINTR` in POSIX. - interrupted, - /// Invalid argument, similar to `EINVAL` in POSIX. - invalid, - /// I/O error, similar to `EIO` in POSIX. - io, - /// Is a directory, similar to `EISDIR` in POSIX. - is-directory, - /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. - loop, - /// Too many links, similar to `EMLINK` in POSIX. - too-many-links, - /// Message too large, similar to `EMSGSIZE` in POSIX. - message-size, - /// Filename too long, similar to `ENAMETOOLONG` in POSIX. - name-too-long, - /// No such device, similar to `ENODEV` in POSIX. - no-device, - /// No such file or directory, similar to `ENOENT` in POSIX. - no-entry, - /// No locks available, similar to `ENOLCK` in POSIX. - no-lock, - /// Not enough space, similar to `ENOMEM` in POSIX. - insufficient-memory, - /// No space left on device, similar to `ENOSPC` in POSIX. - insufficient-space, - /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. - not-directory, - /// Directory not empty, similar to `ENOTEMPTY` in POSIX. - not-empty, - /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. - not-recoverable, - /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. - unsupported, - /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. - no-tty, - /// No such device or address, similar to `ENXIO` in POSIX. - no-such-device, - /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. - overflow, - /// Operation not permitted, similar to `EPERM` in POSIX. - not-permitted, - /// Broken pipe, similar to `EPIPE` in POSIX. - pipe, - /// Read-only file system, similar to `EROFS` in POSIX. - read-only, - /// Invalid seek, similar to `ESPIPE` in POSIX. - invalid-seek, - /// Text file busy, similar to `ETXTBSY` in POSIX. - text-file-busy, - /// Cross-device link, similar to `EXDEV` in POSIX. - cross-device, - /// A catch-all for errors not captured by the existing variants. - /// Implementations can use this to extend the error type without - /// breaking existing code. - other(option), - } - - /// File or memory access pattern advisory information. - @since(version = 0.3.0) - enum advice { - /// The application has no advice to give on its behavior with respect - /// to the specified data. - normal, - /// The application expects to access the specified data sequentially - /// from lower offsets to higher offsets. - sequential, - /// The application expects to access the specified data in a random - /// order. - random, - /// The application expects to access the specified data in the near - /// future. - will-need, - /// The application expects that it will not access the specified data - /// in the near future. - dont-need, - /// The application expects to access the specified data once and then - /// not reuse it thereafter. - no-reuse, - } - - /// A 128-bit hash value, split into parts because wasm doesn't have a - /// 128-bit integer type. - @since(version = 0.3.0) - record metadata-hash-value { - /// 64 bits of a 128-bit hash value. - lower: u64, - /// Another 64 bits of a 128-bit hash value. - upper: u64, - } - - /// A descriptor is a reference to a filesystem object, which may be a file, - /// directory, named pipe, special file, or other object on which filesystem - /// calls may be made. - @since(version = 0.3.0) - resource descriptor { - /// Return a stream for reading from a file. - /// - /// Multiple read, write, and append streams may be active on the same open - /// file and they do not interfere with each other. - /// - /// This function returns a `stream` which provides the data received from the - /// file, and a `future` providing additional error information in case an - /// error is encountered. - /// - /// If no error is encountered, `stream.read` on the `stream` will return - /// `read-status::closed` with no `error-context` and the future resolves to - /// the value `ok`. If an error is encountered, `stream.read` on the - /// `stream` returns `read-status::closed` with an `error-context` and the future - /// resolves to `err` with an `error-code`. - /// - /// Note: This is similar to `pread` in POSIX. - @since(version = 0.3.0) - read-via-stream: func(offset: filesize) -> tuple, future>>; - /// Return a stream for writing to a file, if available. - /// - /// May fail with an error-code describing why the file cannot be written. - /// - /// It is valid to write past the end of a file; the file is extended to the - /// extent of the write, with bytes between the previous end and the start of - /// the write set to zero. - /// - /// This function returns once either full contents of the stream are - /// written or an error is encountered. - /// - /// Note: This is similar to `pwrite` in POSIX. - @since(version = 0.3.0) - write-via-stream: func(data: stream, offset: filesize) -> future>; - /// Return a stream for appending to a file, if available. - /// - /// May fail with an error-code describing why the file cannot be appended. - /// - /// This function returns once either full contents of the stream are - /// written or an error is encountered. - /// - /// Note: This is similar to `write` with `O_APPEND` in POSIX. - @since(version = 0.3.0) - append-via-stream: func(data: stream) -> future>; - /// Provide file advisory information on a descriptor. - /// - /// This is similar to `posix_fadvise` in POSIX. - @since(version = 0.3.0) - advise: async func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>; - /// Synchronize the data of a file to disk. - /// - /// This function succeeds with no effect if the file descriptor is not - /// opened for writing. - /// - /// Note: This is similar to `fdatasync` in POSIX. - @since(version = 0.3.0) - sync-data: async func() -> result<_, error-code>; - /// Get flags associated with a descriptor. - /// - /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. - /// - /// Note: This returns the value that was the `fs_flags` value returned - /// from `fdstat_get` in earlier versions of WASI. - @since(version = 0.3.0) - get-flags: async func() -> result; - /// Get the dynamic type of a descriptor. - /// - /// Note: This returns the same value as the `type` field of the `fd-stat` - /// returned by `stat`, `stat-at` and similar. - /// - /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided - /// by `fstat` in POSIX. - /// - /// Note: This returns the value that was the `fs_filetype` value returned - /// from `fdstat_get` in earlier versions of WASI. - @since(version = 0.3.0) - get-type: async func() -> result; - /// Adjust the size of an open file. If this increases the file's size, the - /// extra bytes are filled with zeros. - /// - /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. - @since(version = 0.3.0) - set-size: async func(size: filesize) -> result<_, error-code>; - /// Adjust the timestamps of an open file or directory. - /// - /// Note: This is similar to `futimens` in POSIX. - /// - /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. - @since(version = 0.3.0) - set-times: async func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; - /// Read directory entries from a directory. - /// - /// On filesystems where directories contain entries referring to themselves - /// and their parents, often named `.` and `..` respectively, these entries - /// are omitted. - /// - /// This always returns a new stream which starts at the beginning of the - /// directory. Multiple streams may be active on the same directory, and they - /// do not interfere with each other. - /// - /// This function returns a future, which will resolve to an error code if - /// reading full contents of the directory fails. - @since(version = 0.3.0) - read-directory: func() -> tuple, future>>; - /// Synchronize the data and metadata of a file to disk. - /// - /// This function succeeds with no effect if the file descriptor is not - /// opened for writing. - /// - /// Note: This is similar to `fsync` in POSIX. - @since(version = 0.3.0) - sync: async func() -> result<_, error-code>; - /// Create a directory. - /// - /// Note: This is similar to `mkdirat` in POSIX. - @since(version = 0.3.0) - create-directory-at: async func(path: string) -> result<_, error-code>; - /// Return the attributes of an open file or directory. - /// - /// Note: This is similar to `fstat` in POSIX, except that it does not return - /// device and inode information. For testing whether two descriptors refer to - /// the same underlying filesystem object, use `is-same-object`. To obtain - /// additional data that can be used do determine whether a file has been - /// modified, use `metadata-hash`. - /// - /// Note: This was called `fd_filestat_get` in earlier versions of WASI. - @since(version = 0.3.0) - stat: async func() -> result; - /// Return the attributes of a file or directory. - /// - /// Note: This is similar to `fstatat` in POSIX, except that it does not - /// return device and inode information. See the `stat` description for a - /// discussion of alternatives. - /// - /// Note: This was called `path_filestat_get` in earlier versions of WASI. - @since(version = 0.3.0) - stat-at: async func(path-flags: path-flags, path: string) -> result; - /// Adjust the timestamps of a file or directory. - /// - /// Note: This is similar to `utimensat` in POSIX. - /// - /// Note: This was called `path_filestat_set_times` in earlier versions of - /// WASI. - @since(version = 0.3.0) - set-times-at: async func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; - /// Create a hard link. - /// - /// Fails with `error-code::no-entry` if the old path does not exist, - /// with `error-code::exist` if the new path already exists, and - /// `error-code::not-permitted` if the old path is not a file. - /// - /// Note: This is similar to `linkat` in POSIX. - @since(version = 0.3.0) - link-at: async func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; - /// Open a file or directory. - /// - /// If `flags` contains `descriptor-flags::mutate-directory`, and the base - /// descriptor doesn't have `descriptor-flags::mutate-directory` set, - /// `open-at` fails with `error-code::read-only`. - /// - /// If `flags` contains `write` or `mutate-directory`, or `open-flags` - /// contains `truncate` or `create`, and the base descriptor doesn't have - /// `descriptor-flags::mutate-directory` set, `open-at` fails with - /// `error-code::read-only`. - /// - /// Note: This is similar to `openat` in POSIX. - @since(version = 0.3.0) - open-at: async func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result; - /// Read the contents of a symbolic link. - /// - /// If the contents contain an absolute or rooted path in the underlying - /// filesystem, this function fails with `error-code::not-permitted`. - /// - /// Note: This is similar to `readlinkat` in POSIX. - @since(version = 0.3.0) - readlink-at: async func(path: string) -> result; - /// Remove a directory. - /// - /// Return `error-code::not-empty` if the directory is not empty. - /// - /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. - @since(version = 0.3.0) - remove-directory-at: async func(path: string) -> result<_, error-code>; - /// Rename a filesystem object. - /// - /// Note: This is similar to `renameat` in POSIX. - @since(version = 0.3.0) - rename-at: async func(old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; - /// Create a symbolic link (also known as a "symlink"). - /// - /// If `old-path` starts with `/`, the function fails with - /// `error-code::not-permitted`. - /// - /// Note: This is similar to `symlinkat` in POSIX. - @since(version = 0.3.0) - symlink-at: async func(old-path: string, new-path: string) -> result<_, error-code>; - /// Unlink a filesystem object that is not a directory. - /// - /// This is similar to `unlinkat(fd, path, 0)` in POSIX. - /// - /// Error returns are as specified by POSIX. - /// - /// If the filesystem object is a directory, `error-code::access` or - /// `error-code::is-directory` may be returned instead of the - /// POSIX-specified `error-code::not-permitted`. - @since(version = 0.3.0) - unlink-file-at: async func(path: string) -> result<_, error-code>; - /// Test whether two descriptors refer to the same filesystem object. - /// - /// In POSIX, this corresponds to testing whether the two descriptors have the - /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. - /// wasi-filesystem does not expose device and inode numbers, so this function - /// may be used instead. - @since(version = 0.3.0) - is-same-object: async func(other: borrow) -> bool; - /// Return a hash of the metadata associated with a filesystem object referred - /// to by a descriptor. - /// - /// This returns a hash of the last-modification timestamp and file size, and - /// may also include the inode number, device number, birth timestamp, and - /// other metadata fields that may change when the file is modified or - /// replaced. It may also include a secret value chosen by the - /// implementation and not otherwise exposed. - /// - /// Implementations are encouraged to provide the following properties: - /// - /// - If the file is not modified or replaced, the computed hash value should - /// usually not change. - /// - If the object is modified or replaced, the computed hash value should - /// usually change. - /// - The inputs to the hash should not be easily computable from the - /// computed hash. - /// - /// However, none of these is required. - @since(version = 0.3.0) - metadata-hash: async func() -> result; - /// Return a hash of the metadata associated with a filesystem object referred - /// to by a directory descriptor and a relative path. - /// - /// This performs the same hash computation as `metadata-hash`. - @since(version = 0.3.0) - metadata-hash-at: async func(path-flags: path-flags, path: string) -> result; - } -} - -@since(version = 0.3.0) -interface preopens { - @since(version = 0.3.0) - use types.{descriptor}; - - /// Return the set of preopened directories, and their paths. - @since(version = 0.3.0) - get-directories: func() -> list>; -} - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - import wasi:clocks/types@0.3.0; - @since(version = 0.3.0) - import wasi:clocks/system-clock@0.3.0; - @since(version = 0.3.0) - import types; - @since(version = 0.3.0) - import preopens; -} diff --git a/components/wit/deps/wasi-logging-0.1.0-draft/package.wit b/components/wit/deps/wasi-logging-0.1.0-draft/package.wit deleted file mode 100644 index 164cb5b..0000000 --- a/components/wit/deps/wasi-logging-0.1.0-draft/package.wit +++ /dev/null @@ -1,36 +0,0 @@ -package wasi:logging@0.1.0-draft; - -/// WASI Logging is a logging API intended to let users emit log messages with -/// simple priority levels and context values. -interface logging { - /// A log level, describing a kind of message. - enum level { - /// Describes messages about the values of variables and the flow of - /// control within a program. - trace, - /// Describes messages likely to be of interest to someone debugging a - /// program. - debug, - /// Describes messages likely to be of interest to someone monitoring a - /// program. - info, - /// Describes messages indicating hazardous situations. - warn, - /// Describes messages indicating serious errors. - error, - /// Describes messages indicating fatal errors. - critical, - } - - /// Emit a log message. - /// - /// A log message has a `level` describing what kind of message is being - /// sent, a context, which is an uninterpreted string meant to help - /// consumers group similar messages, and a string containing the message - /// text. - log: func(level: level, context: string, message: string); -} - -world imports { - import logging; -} diff --git a/lib/.gitignore b/lib/.gitignore deleted file mode 100644 index 902f891..0000000 --- a/lib/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.md -*.wasm diff --git a/make/components.mk b/make/components.mk new file mode 100644 index 0000000..208942a --- /dev/null +++ b/make/components.mk @@ -0,0 +1,83 @@ +COMPONENTS_DIR := target/components + +# the WIT packages of components/wit/deps, encoded where `wac compose` looks for a package a +# composition names without instantiating it, e.g. `import stderr: wasi:cli/stderr@0.3.0;` +WAC_DEPS_DIR := target/wac-deps + +COMPONENTS = $(sort $(foreach file,$(wildcard $(addprefix components/*/,wit/*.constants.wit *.properties *.wac *.wkg Cargo.toml)),$(word 2,$(subst /, ,$(file))))) + +.PHONY: clean-components +clean-components: clean-wit + rm -rf ${COMPONENTS_DIR} ${WAC_DEPS_DIR} + +.PHONY: components +components: ${COMPONENTS_DIR}/interface.wasm $(foreach component,$(COMPONENTS),${COMPONENTS_DIR}/$(component)/$(component).wasm ${COMPONENTS_DIR}/$(component)/$(component).debug.wasm) + +define BUILD_COMPONENT + +.PHONY: components/$1 +components/$1: ${COMPONENTS_DIR}/$1/$1.wasm ${COMPONENTS_DIR}/$1/$1.debug.wasm + +ifneq ($(wildcard components/$1/wit/$1.constants.wit),) + +${COMPONENTS_DIR}/$1/$1.wasm: components/$1/wit/deps ${COMPONENTS_DIR}/$1/README.md | $(call tool,componentized-constants-cli) + $(CONSTANTS) --wit components/$1/wit -o ${COMPONENTS_DIR}/$1/$1.wasm + +${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/wit/deps ${COMPONENTS_DIR}/$1/README.md | $(call tool,componentized-constants-cli) + $(CONSTANTS) --wit components/$1/wit -o ${COMPONENTS_DIR}/$1/$1.debug.wasm + +else ifneq ($(wildcard components/$1/$1.properties),) + +${COMPONENTS_DIR}/$1/$1.wasm: components/$1/$1.properties ${COMPONENTS_DIR}/$1/README.md | $(call tool,componentized-static-config-cli) + $(STATIC_CONFIG) -f components/$1/$1.properties -o ${COMPONENTS_DIR}/$1/$1.wasm + +${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/$1.properties ${COMPONENTS_DIR}/$1/README.md | $(call tool,componentized-static-config-cli) + $(STATIC_CONFIG) -f components/$1/$1.properties -o ${COMPONENTS_DIR}/$1/$1.debug.wasm + +else ifneq ($(wildcard components/$1/$1.wac),) + +# the local packages the composition instantiates, e.g. `new local:latch-n2 { ... }` +WAC_DEPS_$1 := $$(shell grep -v '^\s*//' components/$1/$1.wac | grep -oE 'local:[a-z0-9-]+' | sed 's/^local://' | sort -u) + +${COMPONENTS_DIR}/$1/$1.wasm: components/$1/$1.wac $$(foreach component,$$(WAC_DEPS_$1),$${COMPONENTS_DIR}/$$(component)/$$(component).wasm) ${COMPONENTS_DIR}/$1/README.md ${WAC_DEPS_DIR} | $(call tool,wac-cli) + $(WAC) compose --deps-dir ${WAC_DEPS_DIR} $$(foreach component,$$(WAC_DEPS_$1),-d local:$$(component)=$${COMPONENTS_DIR}/$$(component)/$$(component).wasm) -o ${COMPONENTS_DIR}/$1/$1.wasm components/$1/$1.wac + +${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/$1.wac $$(foreach component,$$(WAC_DEPS_$1),$${COMPONENTS_DIR}/$$(component)/$$(component).debug.wasm) ${COMPONENTS_DIR}/$1/README.md ${WAC_DEPS_DIR} | $(call tool,wac-cli) + $(WAC) compose --deps-dir ${WAC_DEPS_DIR} $$(foreach component,$$(WAC_DEPS_$1),-d local:$$(component)=$${COMPONENTS_DIR}/$$(component)/$$(component).debug.wasm) -o ${COMPONENTS_DIR}/$1/$1.debug.wasm components/$1/$1.wac + +else ifneq ($(wildcard components/$1/$1.wkg),) + +${COMPONENTS_DIR}/$1/$1.wasm: components/$1/$1.wkg ${COMPONENTS_DIR}/$1/README.md | $(call tool,wkg) + $(WKG) oci pull $(shell cat components/$1/$1.wkg 2> /dev/null | head -1) -o ${COMPONENTS_DIR}/$1/$1.wasm + +${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/$1.wkg ${COMPONENTS_DIR}/$1/README.md | $(call tool,wkg) + $(WKG) oci pull $(shell cat components/$1/$1.wkg 2> /dev/null | tail -1 2> /dev/null) -o ${COMPONENTS_DIR}/$1/$1.debug.wasm + +# cargo is checked last, other strategies may have a Cargo.toml for tests of non-rust sources +else ifneq ($(wildcard components/$1/Cargo.toml),) + +${COMPONENTS_DIR}/$1/$1.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) $(shell find crates -type f 2> /dev/null) ${COMPONENTS_DIR}/$1/README.md | $(call tool,wasm-tools) + cargo build -p $1 --target wasm32-unknown-unknown --release + $(WASM_TOOLS) component new target/wasm32-unknown-unknown/release/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.wasm + +${COMPONENTS_DIR}/$1/$1.debug.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) $(shell find crates -type f 2> /dev/null) ${COMPONENTS_DIR}/$1/README.md | $(call tool,wasm-tools) + cargo build -p $1 --target wasm32-unknown-unknown --profile component-debug + $(WASM_TOOLS) component new target/wasm32-unknown-unknown/component-debug/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.debug.wasm + +endif + +${COMPONENTS_DIR}/$1/README.md: components/$1/README.md + @mkdir -p ${COMPONENTS_DIR}/$1 + @cp components/$1/README.md ${COMPONENTS_DIR}/$1/README.md + +endef + +$(foreach component,$(COMPONENTS),$(eval $(call BUILD_COMPONENT,$(component)))) + +${WAC_DEPS_DIR}: components/wit/deps scripts/wac-deps.sh | $(call tool,wasm-tools) + WASM_TOOLS=$(WASM_TOOLS) scripts/wac-deps.sh components/wit/deps ${WAC_DEPS_DIR} + +${COMPONENTS_DIR}/interface.wasm: wit/deps README.md | $(call tool,wkg) + @mkdir -p ${COMPONENTS_DIR} + $(call wkg,.,build -o $(abspath ${COMPONENTS_DIR})/interface.wasm) + @cp README.md ${COMPONENTS_DIR}/README.md diff --git a/make/pinned.jq b/make/pinned.jq new file mode 100644 index 0000000..78fc559 --- /dev/null +++ b/make/pinned.jq @@ -0,0 +1,6 @@ +# generates target/tools/pinned.mk from `cargo metadata --no-deps` of tools/Cargo.toml, see make/tools.mk +.packages[0] as $tools +| ($tools.dependencies[].name | select($tools.metadata.bins[.] == null) | error("\(.) is missing from [package.metadata.bins] in tools/Cargo.toml")), + "TOOLS := \([$tools.dependencies[].name] | join(" "))", + "TOOLS_PINNED := \([$tools.dependencies[] | "\(.name)@\(.req | ltrimstr("="))"] | join(" "))", + ($tools.metadata.bins[][] | "\(ascii_upcase | gsub("-"; "_")) := $(TOOLS_BIN)/\(.)") diff --git a/make/publish.mk b/make/publish.mk new file mode 100644 index 0000000..87c2324 --- /dev/null +++ b/make/publish.mk @@ -0,0 +1,17 @@ +# sign published components with cosign, `SIGN=false` to push without signing, e.g. to a local registry +SIGN ?= true +# append each published file and its image to this file, e.g. `gate.wasm ghcr.io/componentized/http/gate:0.1.0@sha256:...` +PUBLISH_LOG ?= + +# the files that can be published, e.g. gate.wasm, published from target/components/gate/gate.wasm. +# Components are not published when they are pulled from elsewhere, `dep-`, built here for use by +# other components, `internal-`, or tests, `test-`. +PUBLISH_FILES := interface.wasm $(foreach component,$(filter-out dep-% internal-% test-%,$(COMPONENTS)),$(component).wasm $(component).debug.wasm) + +.PHONY: publish ## Publish each component in the target/components directory +publish: $(addprefix publish-,$(PUBLISH_FILES)) + +.PHONY: $(addprefix publish-,$(PUBLISH_FILES)) +$(addprefix publish-,$(PUBLISH_FILES)): publish-%: | $(call tool,wkg) + @VERSION="$(VERSION)" REPOSITORY="$(REPOSITORY)" COMPONENTS_DIR="$(COMPONENTS_DIR)" SIGN="$(SIGN)" PUBLISH_LOG="$(PUBLISH_LOG)" \ + scripts/publish.sh $* diff --git a/make/tools.mk b/make/tools.mk new file mode 100644 index 0000000..f408f38 --- /dev/null +++ b/make/tools.mk @@ -0,0 +1,53 @@ +TOOLS_DIR := target/tools/$(shell rustc --print host-tuple) +# absolute, tools also run from other directories, e.g. `cd components && wkg fetch` +TOOLS_BIN := $(abspath $(TOOLS_DIR))/bin +# for the scripts and tests the recipes run, they look up the tools on the PATH +export PATH := $(TOOLS_BIN):$(PATH) + +# the tools pinned in tools/Cargo.toml, generated when it changes, make restarts after generating it. +# Defines TOOLS, TOOLS_PINNED with the version of each tool, e.g. `wkg@0.16.1`, and a variable for +# each binary a tool installs, e.g. `WKG := $(TOOLS_BIN)/wkg`. Recipes run the pinned tools by path, +# make 3.81 runs a simple command without a shell and looks it up on its own PATH, before the export +# above, which would find a tool already on the system. +TOOLS_MK := target/tools/pinned.mk +include $(TOOLS_MK) + +# cargo binstall downloads prebuilt binaries, without it the tools are built with cargo install +CARGO_INSTALL := $(if $(shell command -v cargo-binstall 2> /dev/null),cargo binstall --no-confirm --disable-telemetry,cargo install) + +.PHONY: clean-tools +clean-tools: + rm -rf ${TOOLS_DIR} + +# each tool is a dependency of the tools package, the binaries it installs are listed in +# [package.metadata.bins] +$(TOOLS_MK): tools/Cargo.toml make/pinned.jq + @mkdir -p $(@D) + @set -o pipefail; cargo metadata --manifest-path tools/Cargo.toml --no-deps --format-version 1 | jq -r -f make/pinned.jq > $@.tmp + @mv $@.tmp $@ + +tool_version = $(patsubst $(1)@%,%,$(filter $(1)@%,$(TOOLS_PINNED))) +# a stamp naming the version of a tool installed in $(TOOLS_DIR)/bin, e.g. `wkg@0.16.1`, the binary +# does not say which version it is. Bumping the pinned version names a stamp that does not exist yet, +# so the tool is installed again. +tool = $(TOOLS_DIR)/.installed/$(1)@$(call tool_version,$(1)) + +.PHONY: tools ## Install the cli tools pinned in tools/Cargo.toml +tools: $(foreach name,$(TOOLS),$(call tool,$(name))) + +.PHONY: tools-path ## Print the directory of the installed tools for this platform, to add to the PATH +tools-path: + @echo $(TOOLS_BIN) + +define INSTALL_TOOL + +$(call tool,$1): + $(CARGO_INSTALL) --locked --root $(TOOLS_DIR) --version $(call tool_version,$1) $1 + @mkdir -p $$(@D) + @# only the installed version has a stamp, so going back to a previous version installs it again + @rm -f $$(@D)/$1@* + @touch $$@ + +endef + +$(foreach name,$(TOOLS),$(eval $(call INSTALL_TOOL,$(name)))) diff --git a/make/wit.mk b/make/wit.mk new file mode 100644 index 0000000..fc9ea0c --- /dev/null +++ b/make/wit.mk @@ -0,0 +1,41 @@ +# a path relative to the root of the repository, e.g. `wit` for `components/../wit` +relpath = $(if $(filter $(CURDIR),$(abspath $(1))),.,$(patsubst $(CURDIR)/%,%,$(abspath $(1)))) + +# directories with a wkg.toml, each fetches the dependencies of its wit directory into wit/deps, +# e.g. `.` and `components` +WKG_DIRS := $(sort $(patsubst ./%,%,$(patsubst %/,%,$(dir $(shell find . -name wkg.toml -not -path './target/*' -not -path '*/deps/*'))))) + +# the wit/deps directory of a directory with a wkg.toml, e.g. `wit/deps` for `.` +wit_deps = $(patsubst ./%,%,$(1)/wit/deps) + +WIT_DEPS := $(foreach dir,$(WKG_DIRS),$(call wit_deps,$(dir))) + +# runs wkg with the arguments in the directory with a wkg.toml, e.g. `$(call wkg,components,fetch)`. +# wkg rewrites wkg.lock even when nothing changed, which would leave everything made from the lock +# looking out of date, so the lock is put back as it was, with its time, when its content is unchanged +wkg = cp -p $(1)/wkg.lock $(1)/wkg.lock.orig && \ + (cd $(1) && $(WKG) $(2)); status=$$?; \ + if cmp -s $(1)/wkg.lock.orig $(1)/wkg.lock; then mv $(1)/wkg.lock.orig $(1)/wkg.lock; else rm $(1)/wkg.lock.orig; fi; \ + exit $$status + +.PHONY: clean-wit ## Remove the fetched wit dependencies, fetched again by `make wit` +clean-wit: + rm -rf $(WIT_DEPS) + +.PHONY: wit +wit: $(WIT_DEPS) + +define FETCH_WIT + +# the dependencies are touched after fetching, newer than the lock they were fetched with even when +# the fetch changed nothing in them +# +# a package overridden with a local path, e.g. `{ path = "../wit" }`, has its dependencies fetched first, +# an override without dependencies of its own is skipped, e.g. `{ path = "./wit/overrides/wasi-muppets" }` +$(call wit_deps,$1): $1/wkg.toml $1/wkg.lock $(shell find $1/wit -type f -name "*.wit" -not -path "*/deps/*") $(filter $(WIT_DEPS),$(foreach path,$(shell sed -n 's/.*path *= *"\(.*\)".*/\1/p' $1/wkg.toml),$(call relpath,$1/$(path))/deps)) | $(call tool,wkg) + $$(call wkg,$1,fetch) + @touch $(call wit_deps,$1) + +endef + +$(foreach dir,$(WKG_DIRS),$(eval $(call FETCH_WIT,$(dir)))) diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 0000000..7715c4b --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +# Bump the version of the wit interface package. +# +# scripts/bump-version.sh +# +# Updates the package declaration and every reference to the package in tracked files, then +# refreshes the generated wit dependencies. Items whose `@since` names an unreleased (prerelease) +# version move to the new version, since they were never published under the old one. Items +# released under the old version keep their `@since`. +# +# 0.1.0-dev -> 0.1.0 releases 0.1.0, `@since(version = 0.1.0-dev)` becomes 0.1.0 +# 0.1.0 -> 0.2.0-dev starts 0.2.0, `@since(version = 0.1.0)` is unchanged + +set -euo pipefail + +cd "$(dirname "$0")/.." + +PACKAGE="${PACKAGE:-componentized:$(basename $(git rev-parse --show-toplevel))}" +SEMVER='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' + +new="${1:-}" +if [[ ! "$new" =~ $SEMVER ]]; then + echo "usage: $0 , e.g. 0.1.0 or 0.2.0-dev" >&2 + exit 1 +fi + +old=$(sed -n "s/^package ${PACKAGE}@\(.*\);$/\1/p" wit/worlds.wit) +if [[ -z "$old" ]]; then + echo "unable to find the ${PACKAGE} package declaration in wit/worlds.wit" >&2 + exit 1 +fi + +# succeeds when version $1 is lower than version $2, a prerelease is lower than its release +version_lt() { + local a_core="${1%%-*}" b_core="${2%%-*}" + local a_pre="" b_pre="" + [[ "$1" == *-* ]] && a_pre="${1#*-}" + [[ "$2" == *-* ]] && b_pre="${2#*-}" + if [[ "$a_core" != "$b_core" ]]; then + local IFS=. + local -a a=($a_core) b=($b_core) + for i in 0 1 2; do + (( a[i] < b[i] )) && return 0 + (( a[i] > b[i] )) && return 1 + done + fi + [[ -n "$a_pre" && -z "$b_pre" ]] && return 0 + [[ -z "$a_pre" && -n "$b_pre" ]] && return 1 + [[ -n "$a_pre" && "$a_pre" < "$b_pre" ]] +} + +if ! version_lt "$old" "$new"; then + echo "the new version ${new} must be greater than the current version ${old}" >&2 + exit 1 +fi + +# the bump-version workflow offers the current version as the default for the next bump, checked +# before changing anything +workflow=.github/workflows/bump-version.yaml +workflow_default="default: \"${old}\" # the current version, kept current by scripts/bump-version.sh" +if ! grep -qF "$workflow_default" "$workflow"; then + echo "unable to find the current version as the default in ${workflow}, expected: ${workflow_default}" >&2 + exit 1 +fi + +old_re="${old//./\\.}" +# references to the package or one of its interfaces, an interface named for a keyword is escaped +# with `%`, e.g. `componentized:filesystem/imports@0.3.0-dev` +ref_re="${PACKAGE}(/%?[a-z0-9-]+)?" +# the fetched wit dependencies and the wkg.lock files are left to `make wit`, wkg replaces the +# dependencies and updates the locks for the new version +files=$(git grep --untracked -l -E "${ref_re}@${old_re}" -- ':(exclude,glob)**/wit/deps/**' ':(exclude,glob)**/wkg.lock' || true) +for file in $files; do + sed -i.bak -E "s#(${ref_re})@${old_re}#\1@${new}#g" "$file" + rm "$file.bak" + echo "updated ${file}" +done + +if [[ "$old" == *-* ]]; then + files=$(git grep --untracked -l -F "@since(version = ${old})" -- 'wit/*.wit' || true) + for file in $files; do + sed -i.bak "s/@since(version = ${old_re})/@since(version = ${new})/g" "$file" + rm "$file.bak" + echo "updated @since in ${file}" + done +fi + +perl -pi -e 's{^(\s+)\Q'"${workflow_default}"'\E$}{${1}'"${workflow_default/\"${old}\"/\"${new}\"}"'}' "$workflow" +echo "updated the default version in ${workflow}" + +# regenerate the wit dependencies for the new version +make wit components test + +echo "bumped ${PACKAGE} from ${old} to ${new}" diff --git a/scripts/init-devcontainer.sh b/scripts/init-devcontainer.sh new file mode 100755 index 0000000..59f0186 --- /dev/null +++ b/scripts/init-devcontainer.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Setup script run once for new devcontainers to init the environment. + +set -euo pipefail + +curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + +cargo check + +echo "export \"PATH=$(make -s tools-path):${PATH}\"" >> ~/.bashrc +make tools diff --git a/scripts/publish.sh b/scripts/publish.sh new file mode 100755 index 0000000..669c112 --- /dev/null +++ b/scripts/publish.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +# Publish a built component to an OCI registry, and sign it with cosign. +# +# VERSION= REPOSITORY= scripts/publish.sh +# +# The file is one of the files built into the components directory, components are in a directory +# of their own, the interface is not, e.g. gate.wasm from target/components/gate/gate.wasm and +# interface.wasm from target/components/interface.wasm. The interface is published to the +# repository, each component to a repository of its own under it, a debug build is tagged with a +# `_debug` suffix. +# +# interface.wasm -> ${REPOSITORY}:0.1.0 +# gate.wasm -> ${REPOSITORY}/gate:0.1.0 +# gate.debug.wasm -> ${REPOSITORY}/gate:0.1.0_debug +# +# VERSION the version to publish, a leading `v` is dropped from the tag, e.g. v0.1.0 +# REPOSITORY the repository to publish to, e.g. ghcr.io/componentized/http +# GITHUB_REPOSITORY the GitHub repository the components are built from, e.g. componentized/http +# COMPONENTS_DIR the directory the components are built into, defaults to target/components +# SIGN sign the published components with cosign, `false` to push without signing, +# e.g. to a local registry +# PUBLISH_LOG append the published file and its image to this file, e.g. +# `gate.wasm ghcr.io/componentized/http/gate:0.1.0@sha256:...` + +set -euo pipefail + +cd "$(dirname "$0")/.." + +file="${1:-}" +if [[ -z "$file" ]]; then + echo "usage: $0 , e.g. interface.wasm or gate.wasm" >&2 + exit 1 +fi +: "${VERSION:?VERSION is undefined}" +: "${REPOSITORY:?REPOSITORY is undefined}" +GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-componentized/$(basename "$(git rev-parse --show-toplevel)")}" +COMPONENTS_DIR="${COMPONENTS_DIR:-target/components}" +SIGN="${SIGN:-true}" +PUBLISH_LOG="${PUBLISH_LOG:-}" + +component="${file%.wasm}" +component="${component%.debug}" +debug="" +[[ "$file" == *.debug.wasm ]] && debug=true + +if [[ "$file" == interface.wasm ]]; then + component_file="$file" + title="${GITHUB_REPOSITORY/\//:}" +else + component_file="${component}/${file}" + title="${GITHUB_REPOSITORY/\//:}-${component}" +fi +[[ -n "$debug" ]] && title="${title} (debug)" + +# the description is the line following the title in the readme +readme="${COMPONENTS_DIR}/$(dirname "$component_file")/README.md" +description=$(sed -n 3p "$readme") + +commit=$(git rev-parse HEAD) +revision="$commit" +git diff --quiet HEAD || revision="${commit}+dirty" +url="https://github.com/${GITHUB_REPOSITORY}/tree/${commit}" +[[ -f "components/${component}/README.md" ]] && url="${url}/components/${component}" + +component_version="$VERSION" +[[ -n "$debug" ]] && component_version="${VERSION}+debug" +tag="${component_version#v}" +tag="${tag//+/_}" + +if [[ "$file" == interface.wasm ]]; then + image="${REPOSITORY}:${tag}" +else + image="${REPOSITORY}/${component}:${tag}" +fi + +echo "::group::${file} -> ${image}" + +digest=$( + wkg oci push \ + --annotation "org.opencontainers.image.title=${title}" \ + --annotation "org.opencontainers.image.description=${description}" \ + --annotation "org.opencontainers.image.version=${component_version}" \ + --annotation "org.opencontainers.image.url=${url}" \ + --annotation "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}.git" \ + --annotation "org.opencontainers.image.revision=${revision}" \ + --annotation "org.opencontainers.image.licenses=Apache-2.0" \ + "$image" \ + "${COMPONENTS_DIR}/${component_file}" \ + 2>&1 \ + | tee /dev/stderr \ + | grep -o 'sha256:[a-f0-9]\{64\}' +) + +if [[ -n "$PUBLISH_LOG" ]]; then + size=$(wc -c < "${COMPONENTS_DIR}/${component_file}" | tr -d ' ') + echo "${file} ${size} ${image}@${digest}" >> "$PUBLISH_LOG" +fi + +if [[ "$SIGN" == true ]]; then + cosign sign --yes "${image}@${digest}" +else + echo "Not signing ${image}@${digest}, SIGN=${SIGN}" +fi + +echo "::endgroup::" diff --git a/scripts/wac-deps.sh b/scripts/wac-deps.sh new file mode 100755 index 0000000..7e10af8 --- /dev/null +++ b/scripts/wac-deps.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +# Encode each WIT package of a wit/deps directory where `wac compose --deps-dir` looks for it, so a +# composition can name the interfaces of a package it does not instantiate, e.g. +# `import stderr: wasi:cli/stderr@0.3.0;`. +# +# WASM_TOOLS= scripts/wac-deps.sh +# +# wac looks for a versioned package at `//.wasm`, e.g. +# +# wit/deps/wasi-cli-0.3.0 -> /wasi/cli/0.3.0.wasm +# +# Each package is encoded with the other packages as its dependencies. + +set -euo pipefail + +deps=$1 +out=$2 +wasm_tools=${WASM_TOOLS:-wasm-tools} + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +rm -rf "$out" +mkdir -p "$out" + +for package in "$deps"/*/; do + package=${package%/} + rm -rf "$work/package" + mkdir -p "$work/package/deps" + cp "$package"/*.wit "$work/package/" + for dep in "$deps"/*/; do + dep=${dep%/} + [[ "$dep" == "$package" ]] || cp -R "$dep" "$work/package/deps/" + done + "$wasm_tools" component wit "$work/package" --wasm -o "$work/package.wasm" + + # e.g. `package wasi:cli@0.3.0;` + id=$("$wasm_tools" component wit "$work/package.wasm" | sed -n 's/^package \(.*\);$/\1/p' | head -n 1) + namespace=${id%%:*} + name=${id#*:} + name=${name%%@*} + version=${id#*@} + mkdir -p "$out/$namespace/$name" + cp "$work/package.wasm" "$out/$namespace/$name/$version.wasm" +done diff --git a/tools/Cargo.toml b/tools/Cargo.toml new file mode 100644 index 0000000..6e46084 --- /dev/null +++ b/tools/Cargo.toml @@ -0,0 +1,28 @@ +# The versions of the cli tools the build uses, installed by `make tools` with cargo binstall. +# +# This package is never built, it lists the tools as dependencies so dependabot bumps them. Each +# tool is pinned to an exact version with `=`, the Makefile installs that version. +[package] +name = "tools" +version = "0.0.0" +edition = "2024" +license = "Apache-2.0" +publish = false + +[lib] +path = "lib.rs" + +# the binaries each tool installs, the Makefile runs each by path from a variable named after it, +# e.g. `$(WASM_TOOLS)` for `wasm-tools` +[package.metadata.bins] +wac-cli = ["wac"] +wasm-tools = ["wasm-tools"] +wkg = ["wkg"] + +[dependencies] +wac-cli = "=0.12.0" +wasm-tools = "=1.261.0" +wkg = "=0.16.1" + +# not a member of the repository's workspace +[workspace] diff --git a/tools/lib.rs b/tools/lib.rs new file mode 100644 index 0000000..d3769d7 --- /dev/null +++ b/tools/lib.rs @@ -0,0 +1 @@ +//! Never built, see `Cargo.toml`. diff --git a/wit/deps/wasi-clocks-0.3.0/package.wit b/wit/deps/wasi-clocks-0.3.0/package.wit deleted file mode 100644 index 2594e02..0000000 --- a/wit/deps/wasi-clocks-0.3.0/package.wit +++ /dev/null @@ -1,19 +0,0 @@ -package wasi:clocks@0.3.0; - -interface types { - type duration = u64; -} - -interface system-clock { - use types.{duration}; - - record instant { - seconds: s64, - nanoseconds: u32, - } - - now: func() -> instant; - - get-resolution: func() -> duration; -} - diff --git a/wit/deps/wasi-filesystem-0.3.0/package.wit b/wit/deps/wasi-filesystem-0.3.0/package.wit deleted file mode 100644 index e4a778f..0000000 --- a/wit/deps/wasi-filesystem-0.3.0/package.wit +++ /dev/null @@ -1,575 +0,0 @@ -package wasi:filesystem@0.3.0; - -/// WASI filesystem is a filesystem API primarily intended to let users run WASI -/// programs that access their files on their existing filesystems, without -/// significant overhead. -/// -/// Paths are passed as interface-type `string`s, meaning they must consist of -/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain -/// paths which are not accessible by this API. -/// -/// The directory separator in WASI is always the forward-slash (`/`). -/// -/// All paths in WASI are relative paths, and are interpreted relative to a -/// `descriptor` referring to a base directory. If a `path` argument to any WASI -/// function starts with `/`, or if any step of resolving a `path`, including -/// `..` and symbolic link steps, reaches a directory outside of the base -/// directory, or reaches a symlink to an absolute or rooted path in the -/// underlying filesystem, the function fails with `error-code::not-permitted`. -/// -/// For more information about WASI path resolution and sandboxing, see -/// [WASI filesystem path resolution]. -/// -/// Though this package presents a portable interface modelled on POSIX, it -/// prioritizes compatibility over portability: allowing users to access their -/// files on their machine is more important than exposing a single semantics -/// across all platforms. Notably, depending on the underlying operating system -/// and file system: -/// * Paths may be case-folded or not. -/// * Deleting (unlinking) a file may fail if there are other file descriptors -/// open. -/// * Durability and atomicity of changes to underlying files when there are -/// concurrent writers. -/// -/// Users that need well-defined, portable semantics should use a key-value -/// store or a database instead. -/// -/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md -@since(version = 0.3.0) -interface types { - @since(version = 0.3.0) - use wasi:clocks/system-clock@0.3.0.{instant}; - - /// File size or length of a region within a file. - @since(version = 0.3.0) - type filesize = u64; - - /// The type of a filesystem object referenced by a descriptor. - /// - /// Note: This was called `filetype` in earlier versions of WASI. - @since(version = 0.3.0) - variant descriptor-type { - /// The descriptor refers to a block device inode. - block-device, - /// The descriptor refers to a character device inode. - character-device, - /// The descriptor refers to a directory inode. - directory, - /// The descriptor refers to a named pipe. - fifo, - /// The file refers to a symbolic link inode. - symbolic-link, - /// The descriptor refers to a regular file inode. - regular-file, - /// The descriptor refers to a socket. - socket, - /// The type of the descriptor or file is different from any of the - /// other types specified. - other(option), - } - - /// Descriptor flags. - /// - /// Note: This was called `fdflags` in earlier versions of WASI. - @since(version = 0.3.0) - flags descriptor-flags { - /// Read mode: Data can be read. - read, - /// Write mode: Data can be written to. - write, - /// Request that writes be performed according to synchronized I/O file - /// integrity completion. The data stored in the file and the file's - /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - file-integrity-sync, - /// Request that writes be performed according to synchronized I/O data - /// integrity completion. Only the data stored in the file is - /// synchronized. This is similar to `O_DSYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - data-integrity-sync, - /// Requests that reads be performed at the same level of integrity - /// requested for writes. This is similar to `O_RSYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - requested-write-sync, - /// Mutating directories mode: Directory contents may be mutated. - /// - /// When this flag is unset on a descriptor, operations using the - /// descriptor which would create, rename, delete, modify the data or - /// metadata of filesystem objects, or obtain another handle which - /// would permit any of those, shall fail with `error-code::read-only` if - /// they would otherwise succeed. - /// - /// This may only be set on directories. - mutate-directory, - } - - /// Flags determining the method of how paths are resolved. - @since(version = 0.3.0) - flags path-flags { - /// As long as the resolved path corresponds to a symbolic link, it is - /// expanded. - symlink-follow, - } - - /// Open flags used by `open-at`. - @since(version = 0.3.0) - flags open-flags { - /// Create file if it does not exist, similar to `O_CREAT` in POSIX. - create, - /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. - directory, - /// Fail if file already exists, similar to `O_EXCL` in POSIX. - exclusive, - /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. - truncate, - } - - /// Number of hard links to an inode. - @since(version = 0.3.0) - type link-count = u64; - - /// File attributes. - /// - /// Note: This was called `filestat` in earlier versions of WASI. - @since(version = 0.3.0) - record descriptor-stat { - /// File type. - %type: descriptor-type, - /// Number of hard links to the file. - link-count: link-count, - /// For regular files, the file size in bytes. For symbolic links, the - /// length in bytes of the pathname contained in the symbolic link. - size: filesize, - /// Last data access timestamp. - /// - /// If the `option` is none, the platform doesn't maintain an access - /// timestamp for this file. - data-access-timestamp: option, - /// Last data modification timestamp. - /// - /// If the `option` is none, the platform doesn't maintain a - /// modification timestamp for this file. - data-modification-timestamp: option, - /// Last file status-change timestamp. - /// - /// If the `option` is none, the platform doesn't maintain a - /// status-change timestamp for this file. - status-change-timestamp: option, - } - - /// When setting a timestamp, this gives the value to set it to. - @since(version = 0.3.0) - variant new-timestamp { - /// Leave the timestamp set to its previous value. - no-change, - /// Set the timestamp to the current time of the system clock associated - /// with the filesystem. - now, - /// Set the timestamp to the given value. - timestamp(instant), - } - - /// A directory entry. - @since(version = 0.3.0) - record directory-entry { - /// The type of the file referred to by this directory entry. - %type: descriptor-type, - /// The name of the object. - name: string, - } - - /// Error codes returned by functions, similar to `errno` in POSIX. - /// Not all of these error codes are returned by the functions provided by this - /// API; some are used in higher-level library layers, and others are provided - /// merely for alignment with POSIX. - @since(version = 0.3.0) - variant error-code { - /// Permission denied, similar to `EACCES` in POSIX. - access, - /// Connection already in progress, similar to `EALREADY` in POSIX. - already, - /// Bad descriptor, similar to `EBADF` in POSIX. - bad-descriptor, - /// Device or resource busy, similar to `EBUSY` in POSIX. - busy, - /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. - deadlock, - /// Storage quota exceeded, similar to `EDQUOT` in POSIX. - quota, - /// File exists, similar to `EEXIST` in POSIX. - exist, - /// File too large, similar to `EFBIG` in POSIX. - file-too-large, - /// Illegal byte sequence, similar to `EILSEQ` in POSIX. - illegal-byte-sequence, - /// Operation in progress, similar to `EINPROGRESS` in POSIX. - in-progress, - /// Interrupted function, similar to `EINTR` in POSIX. - interrupted, - /// Invalid argument, similar to `EINVAL` in POSIX. - invalid, - /// I/O error, similar to `EIO` in POSIX. - io, - /// Is a directory, similar to `EISDIR` in POSIX. - is-directory, - /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. - loop, - /// Too many links, similar to `EMLINK` in POSIX. - too-many-links, - /// Message too large, similar to `EMSGSIZE` in POSIX. - message-size, - /// Filename too long, similar to `ENAMETOOLONG` in POSIX. - name-too-long, - /// No such device, similar to `ENODEV` in POSIX. - no-device, - /// No such file or directory, similar to `ENOENT` in POSIX. - no-entry, - /// No locks available, similar to `ENOLCK` in POSIX. - no-lock, - /// Not enough space, similar to `ENOMEM` in POSIX. - insufficient-memory, - /// No space left on device, similar to `ENOSPC` in POSIX. - insufficient-space, - /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. - not-directory, - /// Directory not empty, similar to `ENOTEMPTY` in POSIX. - not-empty, - /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. - not-recoverable, - /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. - unsupported, - /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. - no-tty, - /// No such device or address, similar to `ENXIO` in POSIX. - no-such-device, - /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. - overflow, - /// Operation not permitted, similar to `EPERM` in POSIX. - not-permitted, - /// Broken pipe, similar to `EPIPE` in POSIX. - pipe, - /// Read-only file system, similar to `EROFS` in POSIX. - read-only, - /// Invalid seek, similar to `ESPIPE` in POSIX. - invalid-seek, - /// Text file busy, similar to `ETXTBSY` in POSIX. - text-file-busy, - /// Cross-device link, similar to `EXDEV` in POSIX. - cross-device, - /// A catch-all for errors not captured by the existing variants. - /// Implementations can use this to extend the error type without - /// breaking existing code. - other(option), - } - - /// File or memory access pattern advisory information. - @since(version = 0.3.0) - enum advice { - /// The application has no advice to give on its behavior with respect - /// to the specified data. - normal, - /// The application expects to access the specified data sequentially - /// from lower offsets to higher offsets. - sequential, - /// The application expects to access the specified data in a random - /// order. - random, - /// The application expects to access the specified data in the near - /// future. - will-need, - /// The application expects that it will not access the specified data - /// in the near future. - dont-need, - /// The application expects to access the specified data once and then - /// not reuse it thereafter. - no-reuse, - } - - /// A 128-bit hash value, split into parts because wasm doesn't have a - /// 128-bit integer type. - @since(version = 0.3.0) - record metadata-hash-value { - /// 64 bits of a 128-bit hash value. - lower: u64, - /// Another 64 bits of a 128-bit hash value. - upper: u64, - } - - /// A descriptor is a reference to a filesystem object, which may be a file, - /// directory, named pipe, special file, or other object on which filesystem - /// calls may be made. - @since(version = 0.3.0) - resource descriptor { - /// Return a stream for reading from a file. - /// - /// Multiple read, write, and append streams may be active on the same open - /// file and they do not interfere with each other. - /// - /// This function returns a `stream` which provides the data received from the - /// file, and a `future` providing additional error information in case an - /// error is encountered. - /// - /// If no error is encountered, `stream.read` on the `stream` will return - /// `read-status::closed` with no `error-context` and the future resolves to - /// the value `ok`. If an error is encountered, `stream.read` on the - /// `stream` returns `read-status::closed` with an `error-context` and the future - /// resolves to `err` with an `error-code`. - /// - /// Note: This is similar to `pread` in POSIX. - @since(version = 0.3.0) - read-via-stream: func(offset: filesize) -> tuple, future>>; - /// Return a stream for writing to a file, if available. - /// - /// May fail with an error-code describing why the file cannot be written. - /// - /// It is valid to write past the end of a file; the file is extended to the - /// extent of the write, with bytes between the previous end and the start of - /// the write set to zero. - /// - /// This function returns once either full contents of the stream are - /// written or an error is encountered. - /// - /// Note: This is similar to `pwrite` in POSIX. - @since(version = 0.3.0) - write-via-stream: func(data: stream, offset: filesize) -> future>; - /// Return a stream for appending to a file, if available. - /// - /// May fail with an error-code describing why the file cannot be appended. - /// - /// This function returns once either full contents of the stream are - /// written or an error is encountered. - /// - /// Note: This is similar to `write` with `O_APPEND` in POSIX. - @since(version = 0.3.0) - append-via-stream: func(data: stream) -> future>; - /// Provide file advisory information on a descriptor. - /// - /// This is similar to `posix_fadvise` in POSIX. - @since(version = 0.3.0) - advise: async func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>; - /// Synchronize the data of a file to disk. - /// - /// This function succeeds with no effect if the file descriptor is not - /// opened for writing. - /// - /// Note: This is similar to `fdatasync` in POSIX. - @since(version = 0.3.0) - sync-data: async func() -> result<_, error-code>; - /// Get flags associated with a descriptor. - /// - /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. - /// - /// Note: This returns the value that was the `fs_flags` value returned - /// from `fdstat_get` in earlier versions of WASI. - @since(version = 0.3.0) - get-flags: async func() -> result; - /// Get the dynamic type of a descriptor. - /// - /// Note: This returns the same value as the `type` field of the `fd-stat` - /// returned by `stat`, `stat-at` and similar. - /// - /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided - /// by `fstat` in POSIX. - /// - /// Note: This returns the value that was the `fs_filetype` value returned - /// from `fdstat_get` in earlier versions of WASI. - @since(version = 0.3.0) - get-type: async func() -> result; - /// Adjust the size of an open file. If this increases the file's size, the - /// extra bytes are filled with zeros. - /// - /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. - @since(version = 0.3.0) - set-size: async func(size: filesize) -> result<_, error-code>; - /// Adjust the timestamps of an open file or directory. - /// - /// Note: This is similar to `futimens` in POSIX. - /// - /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. - @since(version = 0.3.0) - set-times: async func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; - /// Read directory entries from a directory. - /// - /// On filesystems where directories contain entries referring to themselves - /// and their parents, often named `.` and `..` respectively, these entries - /// are omitted. - /// - /// This always returns a new stream which starts at the beginning of the - /// directory. Multiple streams may be active on the same directory, and they - /// do not interfere with each other. - /// - /// This function returns a future, which will resolve to an error code if - /// reading full contents of the directory fails. - @since(version = 0.3.0) - read-directory: func() -> tuple, future>>; - /// Synchronize the data and metadata of a file to disk. - /// - /// This function succeeds with no effect if the file descriptor is not - /// opened for writing. - /// - /// Note: This is similar to `fsync` in POSIX. - @since(version = 0.3.0) - sync: async func() -> result<_, error-code>; - /// Create a directory. - /// - /// Note: This is similar to `mkdirat` in POSIX. - @since(version = 0.3.0) - create-directory-at: async func(path: string) -> result<_, error-code>; - /// Return the attributes of an open file or directory. - /// - /// Note: This is similar to `fstat` in POSIX, except that it does not return - /// device and inode information. For testing whether two descriptors refer to - /// the same underlying filesystem object, use `is-same-object`. To obtain - /// additional data that can be used do determine whether a file has been - /// modified, use `metadata-hash`. - /// - /// Note: This was called `fd_filestat_get` in earlier versions of WASI. - @since(version = 0.3.0) - stat: async func() -> result; - /// Return the attributes of a file or directory. - /// - /// Note: This is similar to `fstatat` in POSIX, except that it does not - /// return device and inode information. See the `stat` description for a - /// discussion of alternatives. - /// - /// Note: This was called `path_filestat_get` in earlier versions of WASI. - @since(version = 0.3.0) - stat-at: async func(path-flags: path-flags, path: string) -> result; - /// Adjust the timestamps of a file or directory. - /// - /// Note: This is similar to `utimensat` in POSIX. - /// - /// Note: This was called `path_filestat_set_times` in earlier versions of - /// WASI. - @since(version = 0.3.0) - set-times-at: async func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; - /// Create a hard link. - /// - /// Fails with `error-code::no-entry` if the old path does not exist, - /// with `error-code::exist` if the new path already exists, and - /// `error-code::not-permitted` if the old path is not a file. - /// - /// Note: This is similar to `linkat` in POSIX. - @since(version = 0.3.0) - link-at: async func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; - /// Open a file or directory. - /// - /// If `flags` contains `descriptor-flags::mutate-directory`, and the base - /// descriptor doesn't have `descriptor-flags::mutate-directory` set, - /// `open-at` fails with `error-code::read-only`. - /// - /// If `flags` contains `write` or `mutate-directory`, or `open-flags` - /// contains `truncate` or `create`, and the base descriptor doesn't have - /// `descriptor-flags::mutate-directory` set, `open-at` fails with - /// `error-code::read-only`. - /// - /// Note: This is similar to `openat` in POSIX. - @since(version = 0.3.0) - open-at: async func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result; - /// Read the contents of a symbolic link. - /// - /// If the contents contain an absolute or rooted path in the underlying - /// filesystem, this function fails with `error-code::not-permitted`. - /// - /// Note: This is similar to `readlinkat` in POSIX. - @since(version = 0.3.0) - readlink-at: async func(path: string) -> result; - /// Remove a directory. - /// - /// Return `error-code::not-empty` if the directory is not empty. - /// - /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. - @since(version = 0.3.0) - remove-directory-at: async func(path: string) -> result<_, error-code>; - /// Rename a filesystem object. - /// - /// Note: This is similar to `renameat` in POSIX. - @since(version = 0.3.0) - rename-at: async func(old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; - /// Create a symbolic link (also known as a "symlink"). - /// - /// If `old-path` starts with `/`, the function fails with - /// `error-code::not-permitted`. - /// - /// Note: This is similar to `symlinkat` in POSIX. - @since(version = 0.3.0) - symlink-at: async func(old-path: string, new-path: string) -> result<_, error-code>; - /// Unlink a filesystem object that is not a directory. - /// - /// This is similar to `unlinkat(fd, path, 0)` in POSIX. - /// - /// Error returns are as specified by POSIX. - /// - /// If the filesystem object is a directory, `error-code::access` or - /// `error-code::is-directory` may be returned instead of the - /// POSIX-specified `error-code::not-permitted`. - @since(version = 0.3.0) - unlink-file-at: async func(path: string) -> result<_, error-code>; - /// Test whether two descriptors refer to the same filesystem object. - /// - /// In POSIX, this corresponds to testing whether the two descriptors have the - /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. - /// wasi-filesystem does not expose device and inode numbers, so this function - /// may be used instead. - @since(version = 0.3.0) - is-same-object: async func(other: borrow) -> bool; - /// Return a hash of the metadata associated with a filesystem object referred - /// to by a descriptor. - /// - /// This returns a hash of the last-modification timestamp and file size, and - /// may also include the inode number, device number, birth timestamp, and - /// other metadata fields that may change when the file is modified or - /// replaced. It may also include a secret value chosen by the - /// implementation and not otherwise exposed. - /// - /// Implementations are encouraged to provide the following properties: - /// - /// - If the file is not modified or replaced, the computed hash value should - /// usually not change. - /// - If the object is modified or replaced, the computed hash value should - /// usually change. - /// - The inputs to the hash should not be easily computable from the - /// computed hash. - /// - /// However, none of these is required. - @since(version = 0.3.0) - metadata-hash: async func() -> result; - /// Return a hash of the metadata associated with a filesystem object referred - /// to by a directory descriptor and a relative path. - /// - /// This performs the same hash computation as `metadata-hash`. - @since(version = 0.3.0) - metadata-hash-at: async func(path-flags: path-flags, path: string) -> result; - } -} - -@since(version = 0.3.0) -interface preopens { - @since(version = 0.3.0) - use types.{descriptor}; - - /// Return the set of preopened directories, and their paths. - @since(version = 0.3.0) - get-directories: func() -> list>; -} - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - import wasi:clocks/types@0.3.0; - @since(version = 0.3.0) - import wasi:clocks/system-clock@0.3.0; - @since(version = 0.3.0) - import types; - @since(version = 0.3.0) - import preopens; -}