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/.github/dependabot.yml b/.github/dependabot.yml index 5c1266c..8ec1844 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,10 @@ updates: directory: "/" schedule: interval: daily +- package-ecosystem: cargo + directory: "/tools" + schedule: + interval: daily - package-ecosystem: rust-toolchain directory: "/" schedule: diff --git a/.github/workflows/bump-version.yaml b/.github/workflows/bump-version.yaml new file mode 100644 index 0000000..88dcb77 --- /dev/null +++ b/.github/workflows/bump-version.yaml @@ -0,0 +1,159 @@ +name: Bump version + +on: + workflow_dispatch: + inputs: + version: + description: The new version of the interface package and crates, e.g. 0.1.0 or 0.2.0-dev + required: true + type: string + default: "0.1.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 pull-request 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: + - 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 + echo "${PWD}/target/tools/bin" >> "${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 + + # opens the pull request using only first party actions and the gh cli + pull-request: + needs: + - bump + runs-on: ubuntu-latest + # the branch and pull request are created 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 + 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 + permission-pull-requests: write + # the bump changes the default version in this workflow + permission-workflows: 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 crates' workspace version is the current version + run: | + current=$( sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"$/\1/p' Cargo.toml ) + 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: | + branch="bump-version/${VERSION}" + 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 and signed off (DCO) by the user who triggered the workflow, with their GitHub + # noreply email so the commit is attributed to them without exposing their email address. + # Committed by the custodian app's bot, which made the commit on their behalf. The commit is + # unsigned, GitHub only signs commits it attributes entirely to the app. + name=$( gh api "users/${GITHUB_ACTOR}" --jq '.name // .login' ) + name="${name:-${GITHUB_ACTOR}}" + email="${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" + bot="${APP_SLUG}[bot]" + bot_email="$( gh api "users/${bot}" --jq .id )+${bot}@users.noreply.github.com" + commit=$( jq -n \ + --arg message "$( printf 'Bump version from %s to %s\n\nSigned-off-by: %s <%s>' "${CURRENT_VERSION}" "${VERSION}" "${name}" "${email}" )" \ + --arg tree "${tree}" --arg parent "${base}" --arg name "${name}" --arg email "${email}" \ + --arg bot "${bot}" --arg bot_email "${bot_email}" \ + '{message: $message, tree: $tree, parents: [$parent], author: {name: $name, email: $email}, committer: {name: $bot, email: $bot_email}}' \ + | gh api --method POST "${api}/git/commits" --input - --jq .sha ) + echo "created commit ${commit}" + + # points the branch at the commit, replacing the branch left by an earlier run for the same version + if gh api "${api}/git/ref/heads/${branch}" --silent 2> /dev/null ; then + gh api --method PATCH "${api}/git/refs/heads/${branch}" -f sha="${commit}" -F force=true --silent + else + gh api --method POST "${api}/git/refs" -f ref="refs/heads/${branch}" -f sha="${commit}" --silent + fi + - name: Open pull request + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + branch="bump-version/${VERSION}" + if [ -n "$( gh pr list --head "${branch}" --state open --json number --jq '.[].number' )" ] ; then + echo "A pull request for ${branch} is already open, updated by the new commit" + exit 0 + fi + gh pr create \ + --base "${GITHUB_REF_NAME}" \ + --head "${branch}" \ + --title "Bump version from \`${CURRENT_VERSION}\` to \`${VERSION}\`" \ + --body "Bumps the wit package and crates from \`${CURRENT_VERSION}\` to \`${VERSION}\`. + + Triggered by @${GITHUB_ACTOR} from the [Bump version](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) workflow." diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8cf9672..160bb08 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,12 +17,11 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v2 - name: Install cargo binstall uses: cargo-bins/cargo-binstall@main - - name: Install wasmtime - run: cargo binstall --force wasmtime-cli - - 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 + echo "${PWD}/target/tools/bin" >> "${GITHUB_PATH}" - name: Sync wit run: make wit - name: Check for drift in generated wit @@ -30,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: @@ -41,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: @@ -63,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 + echo "${PWD}/target/tools/bin" >> "${GITHUB_PATH}" - name: Install cosign uses: sigstore/cosign-installer@v4.1.2 - name: Download components.tar @@ -74,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 @@ -101,7 +108,8 @@ jobs: with: draft: true files: | - lib/*.wasm + target/components/*.wasm + target/components/*/*.wasm components.tar fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b546111..f3027d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /components.tar /target .DS_Store +/tools/Cargo.lock +# wit dependencies, fetched by `make wit` from the wkg.toml and wkg.lock files +**/wit/deps/ diff --git a/Cargo.toml b/Cargo.toml index 246ea56..7603948 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,7 @@ [workspace] resolver = "2" members = [ - "components/*", -] -exclude = [ - "components/wit", + "components/client", ] [workspace.dependencies] diff --git a/Makefile b/Makefile index 25db4ec..9c2a844 100644 --- a/Makefile +++ b/Makefile @@ -1,63 +1,143 @@ SHELL := /bin/bash +WKG_CONFIG := $(CURDIR)/.config/wasm-pkg/config.toml + export RUST_BACKTRACE ?= 1 export WASMTIME_BACKTRACE_DETAILS ?= 1 -COMPONENTS = $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard components/*/Cargo.toml))))) +COMPONENTS_DIR := $(abspath target/components) +TOOLS_DIR := $(abspath target/tools) +export PATH := $(TOOLS_DIR)/bin:$(PATH) + +# 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) + +COMPONENTS = $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard $(addprefix components/*/,*.properties *.wac *.wkg Cargo.toml)))))) +TOOLS := static-config wac-cli wasm-tools wkg .PHONY: all all: components .PHONY: clean -clean: +clean: clean-wit cargo clean - rm -rf lib/*.wasm - rm -rf lib/*.wasm.md + +.PHONY: clean-components +clean-components: clean-wit + rm -rf ${COMPONENTS_DIR} + +.PHONY: clean-wit ## Remove the fetched wit dependencies, fetched again by `make wit` +clean-wit: + rm -rf wit/deps components/wit/deps components/*/wit/deps .PHONY: test -test: - @echo "TODO add tests" +test: components + cargo test --workspace + + +tool_version = $(shell sed -n 's/^$(1) = "=\(.*\)"$$/\1/p' tools/Cargo.toml) +# a stamp naming the version of a tool installed in target/tools/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))) + +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)))) .PHONY: components -components: lib/interface.wasm $(foreach component,$(COMPONENTS),lib/$(component).wasm $(foreach component,$(COMPONENTS),lib/$(component).debug.wasm)) +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: lib/$1.wasm lib/$1.debug.wasm +components/$1: ${COMPONENTS_DIR}/$1/$1.wasm ${COMPONENTS_DIR}/$1/$1.debug.wasm + +ifneq ($(wildcard components/$1/$1.properties),) + +${COMPONENTS_DIR}/$1/$1.wasm: components/$1/$1.properties ${COMPONENTS_DIR}/$1/README.md | $(call tool,static-config) + static-config -f components/$1/$1.properties -o ${COMPONENTS_DIR}/$1/$1.wasm -lib/$1.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) +${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/$1.properties ${COMPONENTS_DIR}/$1/README.md | $(call tool,static-config) + 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 | $(call tool,wac-cli) + wac compose $$(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 | $(call tool,wac-cli) + wac compose $$(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 lib/$1.wasm - cp components/$1/README.md lib/$1.wasm.md + wasm-tools component new target/wasm32-unknown-unknown/release/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.wasm -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 +${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 --target wasm32-unknown-unknown -p $1 + wasm-tools component new target/wasm32-unknown-unknown/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)))) -lib/interface.wasm: wit/deps README.md - wkg build -o lib/interface.wasm - cp README.md lib/interface.wasm.md +${COMPONENTS_DIR}/interface.wasm: wit/deps README.md | $(call tool,wkg) + @mkdir -p ${COMPONENTS_DIR} + wkg build -o ${COMPONENTS_DIR}/interface.wasm + @cp README.md ${COMPONENTS_DIR}/README.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 +wit/deps: wkg.toml $(shell find wit -type f -name "*.wit" -not -path "deps") | $(call tool,wkg) + wkg fetch --config $(WKG_CONFIG) + +components/wit/deps: wit/deps components/wkg.toml $(shell find components/wit -type f -name "*.wit" -not -path "deps") | $(call tool,wkg) + ( cd components && wkg fetch --config $(WKG_CONFIG) ) + +# sign published components with cosign, `SIGN=false` to push without signing, e.g. to a local registry +SIGN ?= true -components/wit/deps: wit/deps components/wkg.toml $(shell find components/wit -type f -name "*.wit" -not -path "deps") - ( cd components && wkg fetch ) +# the files that can be published, e.g. gate.wasm, published from target/components/gate/gate.wasm +PUBLISH_FILES := interface.wasm $(foreach component,$(filter-out dep-% test-%,$(COMPONENTS)),$(component).wasm $(component).debug.wasm) -.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: publish ## Publish each component in the target/components directory +publish: $(addprefix publish-,$(PUBLISH_FILES)) -.PHONY: publish-% -publish-%: +.PHONY: $(addprefix publish-,$(PUBLISH_FILES)) +$(addprefix publish-,$(PUBLISH_FILES)): publish-%: | $(call tool,wkg) ifndef VERSION $(error VERSION is undefined) endif @@ -65,9 +145,12 @@ ifndef REPOSITORY $(error REPOSITORY is undefined) endif @$(eval FILE := $(@:publish-%=%)) - @$(eval COMPONENT := $(if $(filter %.debug.wasm,$(FILE)),$(FILE:%.debug.wasm=%),$(FILE:%.wasm=%))) + @$(eval COMPONENT := $(patsubst %.wasm,%,$(patsubst %.debug.wasm,%,$(FILE)))) +# components are in a directory of their own, the interface is not, e.g. gate/gate.wasm and interface.wasm + @$(eval COMPONENT_FILE := $(if $(filter interface.wasm,$(FILE)),$(FILE),$(COMPONENT)/$(FILE))) + @$(eval README := ${COMPONENTS_DIR}/$(dir $(COMPONENT_FILE))README.md) @$(eval TITLE := $(subst /,:,$(GITHUB_REPOSITORY))$(if $(filter interface,$(COMPONENT)),,-$(COMPONENT))$(if $(filter %.debug.wasm,$(FILE)), (debug))) - @$(eval DESCRIPTION := $(shell head -n 3 "lib/${FILE}.md" | tail -n 1)) + @$(eval DESCRIPTION := $(shell head -n 3 "$(README)" | tail -n 1)) @$(eval COMMIT := $(shell git rev-parse HEAD)) @$(eval README_DIR := $(if $(wildcard components/$(COMPONENT)/README.md),/components/$(COMPONENT))) @$(eval URL := https://github.com/${GITHUB_REPOSITORY}/tree/${COMMIT}${README_DIR}) @@ -77,7 +160,8 @@ endif @$(eval IMAGE := $(if $(filter interface.wasm,$(FILE)),${REPOSITORY}:${TAG},${REPOSITORY}/${COMPONENT}:${TAG})) @echo "::group::${FILE} -> ${IMAGE}" - @DIGEST=$$( \ + @set -o pipefail ; \ + DIGEST=$$( \ wkg oci push \ --annotation "org.opencontainers.image.title=${TITLE}" \ --annotation "org.opencontainers.image.description=${DESCRIPTION}" \ @@ -87,10 +171,10 @@ endif --annotation "org.opencontainers.image.revision=${REVISION}" \ --annotation "org.opencontainers.image.licenses=Apache-2.0" \ "${IMAGE}" \ - "lib/${FILE}" \ + "${COMPONENTS_DIR}/${COMPONENT_FILE}" \ 2>&1 \ | tee /dev/stderr \ | grep -o 'sha256:[a-f0-9]\{64\}' \ - ) ; \ - cosign sign --yes "${IMAGE}@$${DIGEST}" + ) && \ + $(if $(filter true,$(SIGN)),cosign sign --yes "${IMAGE}@$${DIGEST}",echo "Not signing ${IMAGE}@$${DIGEST}, SIGN=${SIGN}") @echo "::endgroup::" diff --git a/README.md b/README.md index 6f8b330..9e52bea 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ A collection of utility components that remix wasi:http types and interfaces. +- [Components](#components) - [Build](#build) - - [Components](#components) - [Community](#community) - [Code of Conduct](#code-of-conduct) - [Communication](#communication) @@ -12,6 +12,10 @@ A collection of utility components that remix wasi:http types and interfaces. - [License](#license) +## Components + +- [`client`](./components/client/) + ## Build A [dev container](https://containers.dev) is available that contains the necessary tools and configuration out of the box. @@ -25,9 +29,17 @@ Prereqs: make components ``` -### Components +The build creates each component in [`components`](./components) into `target/components`, e.g. the client at `target/components/client/client.wasm`, along with `target/components/interface.wasm`, the `componentized:http` WIT package. Each component is also built with debug info, e.g. `target/components/client/client.debug.wasm`. -- [`client`](./components/client/) +```sh +make test +``` + +The WIT dependencies in each `wit/deps` directory are fetched rather than committed, pinned by the `wkg.lock` files. The make targets fetch them as needed. To fetch or update them directly, e.g. before building the Rust components with `cargo`, whose bindings are generated from the WIT: + +```sh +make wit +``` ## Community diff --git a/components/wit/deps/componentized-http-0.1.0-dev/package.wit b/components/wit/deps/componentized-http-0.1.0-dev/package.wit deleted file mode 100644 index 38d3bb1..0000000 --- a/components/wit/deps/componentized-http-0.1.0-dev/package.wit +++ /dev/null @@ -1,71 +0,0 @@ -package componentized:http@0.1.0-dev; - -interface client { - enum method { - get, - post, - put, - delete, - patch, - head, - options, - trace, - query, - } - - variant error-code { - other(option), - } - - /// Per-request options. None fields fall through to host defaults. - record request-options { - connect-timeout-ms: option, - first-byte-timeout-ms: option, - between-bytes-timeout-ms: option, - } - - /// A streaming HTTP response. The status and headers are available - /// immediately; the body streams as `body`, and trailers (if any) resolve - /// via `trailers` once the body stream is fully consumed. - record http-response { - status: u16, - headers: list>, - body: stream, - trailers: future>, error-code>>, - } - - /// Send an HTTP request with an explicit method. Both the request body and - /// the response body stream. - request: async func(method: method, url: string, headers: list>, body: option>, options: option) -> result; - - /// HTTP GET request. - get: async func(url: string, headers: list>, options: option) -> result; - - /// HTTP POST request. - post: async func(url: string, headers: list>, body: stream, options: option) -> result; - - /// HTTP PUT request. - put: async func(url: string, headers: list>, body: stream, options: option) -> result; - - /// HTTP DELETE request. - delete: async func(url: string, headers: list>, options: option) -> result; - - /// HTTP PATCH request. - patch: async func(url: string, headers: list>, body: stream, options: option) -> result; - - /// HTTP HEAD request. - head: async func(url: string, headers: list>, options: option) -> result; - - /// HTTP OPTIONS request. - options: async func(url: string, headers: list>, options: option) -> result; - - /// HTTP TRACE request. - trace: async func(url: string, headers: list>, options: option) -> result; - - /// HTTP QUERY request. - query: async func(url: string, headers: list>, body: stream, options: option) -> result; -} - -world imports { - import client; -} diff --git a/components/wit/deps/wasi-cli-0.3.0/package.wit b/components/wit/deps/wasi-cli-0.3.0/package.wit deleted file mode 100644 index d0b02bb..0000000 --- a/components/wit/deps/wasi-cli-0.3.0/package.wit +++ /dev/null @@ -1,28 +0,0 @@ -package wasi:cli@0.3.0; - -interface types { - enum error-code { - io, - illegal-byte-sequence, - pipe, - } -} - -interface stdout { - use types.{error-code}; - - write-via-stream: func(data: stream) -> future>; -} - -interface stderr { - use types.{error-code}; - - write-via-stream: func(data: stream) -> future>; -} - -interface stdin { - use types.{error-code}; - - read-via-stream: func() -> tuple, future>>; -} - 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 871adf0..0000000 --- a/components/wit/deps/wasi-clocks-0.3.0/package.wit +++ /dev/null @@ -1,43 +0,0 @@ -package wasi:clocks@0.3.0; - -interface types { - type duration = u64; -} - -interface monotonic-clock { - use types.{duration}; - - type mark = u64; - - now: func() -> mark; - - get-resolution: func() -> duration; - - wait-until: async func(when: mark); - - wait-for: async func(how-long: duration); -} - -interface system-clock { - use types.{duration}; - - record instant { - seconds: s64, - nanoseconds: u32, - } - - now: func() -> instant; - - get-resolution: func() -> duration; -} - -interface timezone { - use system-clock.{instant}; - - iana-id: func() -> option; - - utc-offset: func(when: instant) -> option; - - to-debug-string: func() -> string; -} - diff --git a/components/wit/deps/wasi-http-0.3.0/package.wit b/components/wit/deps/wasi-http-0.3.0/package.wit deleted file mode 100644 index 08458f7..0000000 --- a/components/wit/deps/wasi-http-0.3.0/package.wit +++ /dev/null @@ -1,509 +0,0 @@ -package wasi:http@0.3.0; - -/// This interface defines all of the types and methods for implementing HTTP -/// Requests and Responses, as well as their headers, trailers, and bodies. -@since(version = 0.3.0) -interface types { - use wasi:clocks/types@0.3.0.{duration}; - - /// This type corresponds to HTTP standard Methods. - @since(version = 0.3.0) - variant method { - get, - head, - post, - put, - delete, - connect, - options, - trace, - patch, - other(string), - } - - /// This type corresponds to HTTP standard Related Schemes. - @since(version = 0.3.0) - variant scheme { - HTTP, - HTTPS, - other(string), - } - - /// Defines the case payload type for `DNS-error` above: - @since(version = 0.3.0) - record DNS-error-payload { - rcode: option, - info-code: option, - } - - /// Defines the case payload type for `TLS-alert-received` above: - @since(version = 0.3.0) - record TLS-alert-received-payload { - alert-id: option, - alert-message: option, - } - - /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: - @since(version = 0.3.0) - record field-size-payload { - field-name: option, - field-size: option, - } - - /// These cases are inspired by the IANA HTTP Proxy Error Types: - /// - @since(version = 0.3.0) - variant error-code { - DNS-timeout, - DNS-error(DNS-error-payload), - destination-not-found, - destination-unavailable, - destination-IP-prohibited, - destination-IP-unroutable, - connection-refused, - connection-terminated, - connection-timeout, - connection-read-timeout, - connection-write-timeout, - connection-limit-reached, - TLS-protocol-error, - TLS-certificate-error, - TLS-alert-received(TLS-alert-received-payload), - HTTP-request-denied, - HTTP-request-length-required, - HTTP-request-body-size(option), - HTTP-request-method-invalid, - HTTP-request-URI-invalid, - HTTP-request-URI-too-long, - HTTP-request-header-section-size(option), - HTTP-request-header-size(option), - HTTP-request-trailer-section-size(option), - HTTP-request-trailer-size(field-size-payload), - HTTP-response-incomplete, - HTTP-response-header-section-size(option), - HTTP-response-header-size(field-size-payload), - HTTP-response-body-size(option), - HTTP-response-trailer-section-size(option), - HTTP-response-trailer-size(field-size-payload), - HTTP-response-transfer-coding(option), - HTTP-response-content-coding(option), - HTTP-response-timeout, - HTTP-upgrade-failed, - HTTP-protocol-error, - loop-detected, - configuration-error, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. It also includes an optional string for an - /// unstructured description of the error. Users should not depend on the - /// string for diagnosing errors, as it's not required to be consistent - /// between implementations. - internal-error(option), - } - - /// This type enumerates the different kinds of errors that may occur when - /// setting or appending to a `fields` resource. - @since(version = 0.3.0) - variant header-error { - /// This error indicates that a `field-name` or `field-value` was - /// syntactically invalid when used with an operation that sets headers in a - /// `fields`. - invalid-syntax, - /// This error indicates that a forbidden `field-name` was used when trying - /// to set a header in a `fields`. - forbidden, - /// This error indicates that the operation on the `fields` was not - /// permitted because the fields are immutable. - immutable, - /// This error indicates that the operation would exceed an - /// implementation-defined limit on field sizes. This may apply to - /// an individual `field-value`, a single `field-name` plus all its - /// values, or the total aggregate size of all fields. - size-exceeded, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. Implementations can use this to extend the error - /// type without breaking existing code. It also includes an optional - /// string for an unstructured description of the error. Users should not - /// depend on the string for diagnosing errors, as it's not required to be - /// consistent between implementations. - other(option), - } - - /// This type enumerates the different kinds of errors that may occur when - /// setting fields of a `request-options` resource. - @since(version = 0.3.0) - variant request-options-error { - /// Indicates the specified field is not supported by this implementation. - not-supported, - /// Indicates that the operation on the `request-options` was not permitted - /// because it is immutable. - immutable, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. Implementations can use this to extend the error - /// type without breaking existing code. It also includes an optional - /// string for an unstructured description of the error. Users should not - /// depend on the string for diagnosing errors, as it's not required to be - /// consistent between implementations. - other(option), - } - - /// Field names are always strings. - /// - /// Field names should always be treated as case insensitive by the `fields` - /// resource for the purposes of equality checking. - @since(version = 0.3.0) - type field-name = string; - - /// Field values should always be ASCII strings. However, in - /// reality, HTTP implementations often have to interpret malformed values, - /// so they are provided as a list of bytes. - @since(version = 0.3.0) - type field-value = list; - - /// This following block defines the `fields` resource which corresponds to - /// HTTP standard Fields. Fields are a common representation used for both - /// Headers and Trailers. - /// - /// A `fields` may be mutable or immutable. A `fields` created using the - /// constructor, `from-list`, or `clone` will be mutable, but a `fields` - /// resource given by other means (including, but not limited to, - /// `request.headers`) might be be immutable. In an immutable fields, the - /// `set`, `append`, and `delete` operations will fail with - /// `header-error.immutable`. - /// - /// A `fields` resource should store `field-name`s and `field-value`s in their - /// original casing used to construct or mutate the `fields` resource. The `fields` - /// resource should use that original casing when serializing the fields for - /// transport or when returning them from a method. - /// - /// Implementations may impose limits on individual field values and on total - /// aggregate field section size. Operations that would exceed these limits - /// fail with `header-error.size-exceeded` - @since(version = 0.3.0) - resource fields { - /// Construct an empty HTTP Fields. - /// - /// The resulting `fields` is mutable. - constructor(); - /// Construct an HTTP Fields. - /// - /// The resulting `fields` is mutable. - /// - /// The list represents each name-value pair in the Fields. Names - /// which have multiple values are represented by multiple entries in this - /// list with the same name. - /// - /// The tuple is a pair of the field name, represented as a string, and - /// Value, represented as a list of bytes. In a valid Fields, all names - /// and values are valid UTF-8 strings. However, values are not always - /// well-formed, so they are represented as a raw list of bytes. - /// - /// An error result will be returned if any header or value was - /// syntactically invalid, if a header was forbidden, or if the - /// entries would exceed an implementation size limit. - from-list: static func(entries: list>) -> result; - /// Get all of the values corresponding to a name. If the name is not present - /// in this `fields`, an empty list is returned. However, if the name is - /// present but empty, this is represented by a list with one or more - /// empty field-values present. - get: func(name: field-name) -> list; - /// Returns `true` when the name is present in this `fields`. If the name is - /// syntactically invalid, `false` is returned. - has: func(name: field-name) -> bool; - /// Set all of the values for a name. Clears any existing values for that - /// name, if they have been set. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.size-exceeded` if the name or values would - /// exceed an implementation-defined size limit. - set: func(name: field-name, value: list) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name - /// exist. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - delete: func(name: field-name) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name - /// exist. - /// - /// Returns all values previously corresponding to the name, if any. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - get-and-delete: func(name: field-name) -> result, header-error>; - /// Append a value for a name. Does not change or delete any existing - /// values for that name. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.size-exceeded` if the value would exceed - /// an implementation-defined size limit. - append: func(name: field-name, value: field-value) -> result<_, header-error>; - /// Retrieve the full set of names and values in the Fields. Like the - /// constructor, the list represents each name-value pair. - /// - /// The outer list represents each name-value pair in the Fields. Names - /// which have multiple values are represented by multiple entries in this - /// list with the same name. - /// - /// The names and values are always returned in the original casing and in - /// the order in which they will be serialized for transport. - copy-all: func() -> list>; - /// Make a deep copy of the Fields. Equivalent in behavior to calling the - /// `fields` constructor on the return value of `copy-all`. The resulting - /// `fields` is mutable. - clone: func() -> fields; - } - - /// Headers is an alias for Fields. - @since(version = 0.3.0) - type headers = fields; - - /// Trailers is an alias for Fields. - @since(version = 0.3.0) - type trailers = fields; - - /// Represents an HTTP Request. - @since(version = 0.3.0) - resource request { - /// Construct a new `request` with a default `method` of `GET`, and - /// `none` values for `path-with-query`, `scheme`, and `authority`. - /// - /// `headers` is the HTTP Headers for the Request. - /// - /// `contents` is the optional body content stream with `none` - /// representing a zero-length content stream. - /// Once it is closed, `trailers` future must resolve to a result. - /// If `trailers` resolves to an error, underlying connection - /// will be closed immediately. - /// - /// `options` is optional `request-options` resource to be used - /// if the request is sent over a network connection. - /// - /// It is possible to construct, or manipulate with the accessor functions - /// below, a `request` with an invalid combination of `scheme` - /// and `authority`, or `headers` which are not permitted to be sent. - /// It is the obligation of the `handler.handle` implementation - /// to reject invalid constructions of `request`. - /// - /// The returned future resolves to result of transmission of this request. - new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; - /// Get the Method for the Request. - get-method: func() -> method; - /// Set the Method for the Request. Fails if the string present in a - /// `method.other` argument is not a syntactically valid method. - set-method: func(method: method) -> result; - /// Get the combination of the HTTP Path and Query for the Request. When - /// `none`, this represents an empty Path and empty Query. - get-path-with-query: func() -> option; - /// Set the combination of the HTTP Path and Query for the Request. When - /// `none`, this represents an empty Path and empty Query. Fails is the - /// string given is not a syntactically valid path and query uri component. - set-path-with-query: func(path-with-query: option) -> result; - /// Get the HTTP Related Scheme for the Request. When `none`, the - /// implementation may choose an appropriate default scheme. - get-scheme: func() -> option; - /// Set the HTTP Related Scheme for the Request. When `none`, the - /// implementation may choose an appropriate default scheme. Fails if the - /// string given is not a syntactically valid uri scheme. - set-scheme: func(scheme: option) -> result; - /// Get the authority of the Request's target URI. A value of `none` may be used - /// with Related Schemes which do not require an authority. The HTTP and - /// HTTPS schemes always require an authority. - get-authority: func() -> option; - /// Set the authority of the Request's target URI. A value of `none` may be used - /// with Related Schemes which do not require an authority. The HTTP and - /// HTTPS schemes always require an authority. Fails if the string given is - /// not a syntactically valid URI authority. - set-authority: func(authority: option) -> result; - /// Get the `request-options` to be associated with this request - /// - /// The returned `request-options` resource is immutable: `set-*` operations - /// will fail if invoked. - /// - /// This `request-options` resource is a child: it must be dropped before - /// the parent `request` is dropped, or its ownership is transferred to - /// another component by e.g. `handler.handle`. - get-options: func() -> option; - /// Get the headers associated with the Request. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - get-headers: func() -> headers; - /// Get body of the Request. - /// - /// Stream returned by this method represents the contents of the body. - /// Once the stream is reported as closed, callers should await the returned - /// future to determine whether the body was received successfully. - /// The future will only resolve after the stream is reported as closed. - /// - /// This function takes a `res` future as a parameter, which can be used to - /// communicate an error in handling of the request. - /// - /// Note that function will move the `request`, but references to headers or - /// request options acquired from it previously will remain valid. - consume-body: static func(this: request, res: future>) -> tuple, future, error-code>>>; - } - - /// Parameters for making an HTTP Request. Each of these parameters is - /// currently an optional timeout applicable to the transport layer of the - /// HTTP protocol. - /// - /// These timeouts are separate from any the user may use to bound an - /// asynchronous call. - @since(version = 0.3.0) - resource request-options { - /// Construct a default `request-options` value. - constructor(); - /// The timeout for the initial connect to the HTTP Server. - get-connect-timeout: func() -> option; - /// Set the timeout for the initial connect to the HTTP Server. An error - /// return value indicates that this timeout is not supported or that this - /// handle is immutable. - set-connect-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving the first byte of the Response body. - get-first-byte-timeout: func() -> option; - /// Set the timeout for receiving the first byte of the Response body. An - /// error return value indicates that this timeout is not supported or that - /// this handle is immutable. - set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving subsequent chunks of bytes in the Response - /// body stream. - get-between-bytes-timeout: func() -> option; - /// Set the timeout for receiving subsequent chunks of bytes in the Response - /// body stream. An error return value indicates that this timeout is not - /// supported or that this handle is immutable. - set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; - /// Make a deep copy of the `request-options`. - /// The resulting `request-options` is mutable. - clone: func() -> request-options; - } - - /// This type corresponds to the HTTP standard Status Code. - @since(version = 0.3.0) - type status-code = u16; - - /// Represents an HTTP Response. - @since(version = 0.3.0) - resource response { - /// Construct a new `response`, with a default `status-code` of `200`. - /// If a different `status-code` is needed, it must be set via the - /// `set-status-code` method. - /// - /// `headers` is the HTTP Headers for the Response. - /// - /// `contents` is the optional body content stream with `none` - /// representing a zero-length content stream. - /// Once it is closed, `trailers` future must resolve to a result. - /// If `trailers` resolves to an error, underlying connection - /// will be closed immediately. - /// - /// The returned future resolves to result of transmission of this response. - new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; - /// Get the HTTP Status Code for the Response. - get-status-code: func() -> status-code; - /// Set the HTTP Status Code for the Response. Fails if the status-code - /// given is not a valid http status code. - set-status-code: func(status-code: status-code) -> result; - /// Get the headers associated with the Response. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - get-headers: func() -> headers; - /// Get body of the Response. - /// - /// Stream returned by this method represents the contents of the body. - /// Once the stream is reported as closed, callers should await the returned - /// future to determine whether the body was received successfully. - /// The future will only resolve after the stream is reported as closed. - /// - /// This function takes a `res` future as a parameter, which can be used to - /// communicate an error in handling of the response. - /// - /// Note that function will move the `response`, but references to headers - /// acquired from it previously will remain valid. - consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; - } -} - -/// This interface defines a handler of HTTP Requests. -/// -/// In a `wasi:http/service` this interface is exported to respond to an -/// incoming HTTP Request with a Response. -/// -/// In `wasi:http/middleware` this interface is both exported and imported as -/// the "downstream" and "upstream" directions of the middleware chain. -@since(version = 0.3.0) -interface handler { - use types.{request, response, error-code}; - - /// This function may be called with either an incoming request read from the - /// network or a request synthesized or forwarded by another component. - handle: async func(request: request) -> result; -} - -/// This interface defines an HTTP client for sending "outgoing" requests. -/// -/// Most components are expected to import this interface to provide the -/// capability to send HTTP requests to arbitrary destinations on a network. -/// -/// The type signature of `client.send` is the same as `handler.handle`. This -/// duplication is currently necessary because some Component Model tooling -/// (including WIT itself) is unable to represent a component importing two -/// instances of the same interface. A `client.send` import may be linked -/// directly to a `handler.handle` export to bypass the network. -@since(version = 0.3.0) -interface client { - use types.{request, response, error-code}; - - /// This function may be used to either send an outgoing request over the - /// network or to forward it to another component. - send: async func(request: request) -> result; -} - -/// The `wasi:http/service` world captures a broad category of HTTP services -/// including web applications, API servers, and proxies. It may be `include`d -/// in more specific worlds such as `wasi:http/middleware`. -@since(version = 0.3.0) -world service { - import wasi:cli/types@0.3.0; - import wasi:cli/stdout@0.3.0; - import wasi:cli/stderr@0.3.0; - import wasi:cli/stdin@0.3.0; - import wasi:clocks/types@0.3.0; - import types; - import client; - import wasi:clocks/monotonic-clock@0.3.0; - import wasi:clocks/system-clock@0.3.0; - @unstable(feature = clocks-timezone) - import wasi:clocks/timezone@0.3.0; - import wasi:random/random@0.3.0; - import wasi:random/insecure@0.3.0; - import wasi:random/insecure-seed@0.3.0; - - export handler; -} -/// The `wasi:http/middleware` world captures HTTP services that forward HTTP -/// Requests to another handler. -/// -/// Components may implement this world to allow them to participate in handler -/// "chains" where a `request` flows through handlers on its way to some terminal -/// `service` and corresponding `response` flows in the opposite direction. -@since(version = 0.3.0) -world middleware { - import wasi:clocks/types@0.3.0; - import types; - import handler; - import wasi:cli/types@0.3.0; - import wasi:cli/stdout@0.3.0; - import wasi:cli/stderr@0.3.0; - import wasi:cli/stdin@0.3.0; - import client; - import wasi:clocks/monotonic-clock@0.3.0; - import wasi:clocks/system-clock@0.3.0; - @unstable(feature = clocks-timezone) - import wasi:clocks/timezone@0.3.0; - import wasi:random/random@0.3.0; - import wasi:random/insecure@0.3.0; - import wasi:random/insecure-seed@0.3.0; - - export handler; -} diff --git a/components/wit/deps/wasi-random-0.3.0/package.wit b/components/wit/deps/wasi-random-0.3.0/package.wit deleted file mode 100644 index f6cfb81..0000000 --- a/components/wit/deps/wasi-random-0.3.0/package.wit +++ /dev/null @@ -1,18 +0,0 @@ -package wasi:random@0.3.0; - -interface random { - get-random-bytes: func(max-len: u64) -> list; - - get-random-u64: func() -> u64; -} - -interface insecure { - get-insecure-random-bytes: func(max-len: u64) -> list; - - get-insecure-random-u64: func() -> u64; -} - -interface insecure-seed { - get-insecure-seed: func() -> tuple; -} - diff --git a/lib/.gitignore b/lib/.gitignore deleted file mode 100644 index ac4a381..0000000 --- a/lib/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.wasm -*.md diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 0000000..a3a9666 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +# Bump the version of the wit interface package, and of the crates. +# +# scripts/bump-version.sh +# +# Updates the package declaration and every reference to the package in tracked files, then +# refreshes the generated wit dependencies. The crates share the interface's version: the +# workspace version the crates inherit, and the workspace's requirement on the library, move to the +# new version too. 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))}" +# the library crate, the workspace's requirement on it moves to the new version +LIBRARY="${LIBRARY:-componentized-constants}" +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:constants/%u8@0.1.0` +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 + +# the version in the [workspace.package] section, inherited by the crates +perl -pi -e 'if (/^\[workspace\.package\]/ .. /^\[(?!workspace\.package\])/) { s/^version = "[^"]*"/version = "'"${new}"'"/ }' Cargo.toml +echo "updated the workspace version in Cargo.toml" +perl -pi -e 's/^(\Q'"${LIBRARY}"'\E = \{.*\bversion = ")[^"]*(")/${1}'"${new}"'${2}/' Cargo.toml +echo "updated the ${LIBRARY} requirement in Cargo.toml" + +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}" \ No newline at end of file diff --git a/tools/Cargo.toml b/tools/Cargo.toml new file mode 100644 index 0000000..514cd37 --- /dev/null +++ b/tools/Cargo.toml @@ -0,0 +1,22 @@ +# 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" + +[dependencies] +static-config = "=0.2.0" +wac-cli = "=0.11.0" +wasm-tools = "=1.259.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/http.wit b/wit/http.wit index 5dfbcb6..e659997 100644 --- a/wit/http.wit +++ b/wit/http.wit @@ -1,6 +1,7 @@ -package componentized:http@0.1.0-dev; - +@since(version = 0.1.0-dev) interface client { + + @since(version = 0.1.0-dev) enum method { get, post, @@ -13,11 +14,13 @@ interface client { query, } + @since(version = 0.1.0-dev) variant error-code { other(option), } /// Per-request options. None fields fall through to host defaults. + @since(version = 0.1.0-dev) record request-options { connect-timeout-ms: option, first-byte-timeout-ms: option, @@ -27,6 +30,7 @@ interface client { /// A streaming HTTP response. The status and headers are available /// immediately; the body streams as `body`, and trailers (if any) resolve /// via `trailers` once the body stream is fully consumed. + @since(version = 0.1.0-dev) record http-response { status: u16, headers: list>, @@ -36,32 +40,42 @@ interface client { /// Send an HTTP request with an explicit method. Both the request body and /// the response body stream. + @since(version = 0.1.0-dev) request: async func(method: method, url: string, headers: list>, body: option>, options: option) -> result; /// HTTP GET request. + @since(version = 0.1.0-dev) get: async func(url: string, headers: list>, options: option) -> result; /// HTTP POST request. + @since(version = 0.1.0-dev) post: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP PUT request. + @since(version = 0.1.0-dev) put: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP DELETE request. + @since(version = 0.1.0-dev) delete: async func(url: string, headers: list>, options: option) -> result; /// HTTP PATCH request. + @since(version = 0.1.0-dev) patch: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP HEAD request. + @since(version = 0.1.0-dev) head: async func(url: string, headers: list>, options: option) -> result; /// HTTP OPTIONS request. + @since(version = 0.1.0-dev) options: async func(url: string, headers: list>, options: option) -> result; /// HTTP TRACE request. + @since(version = 0.1.0-dev) trace: async func(url: string, headers: list>, options: option) -> result; /// HTTP QUERY request. + @since(version = 0.1.0-dev) query: async func(url: string, headers: list>, body: stream, options: option) -> result; } diff --git a/wit/worlds.wit b/wit/worlds.wit index e520f1f..e5320cd 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -1,3 +1,5 @@ +package componentized:http@0.1.0-dev; + world imports { import client; }