From 0de9490f9b0334752a3347dc197dd0aa254e6ce2 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Sun, 4 Oct 2026 17:06:57 -0400 Subject: [PATCH] Sync updates to project structure Also adapt to upstream changes to componentized:http/client. Signed-off-by: Scott Andrews --- .devcontainer/Dockerfile | 7 - .devcontainer/devcontainer.json | 8 +- .github/dependabot.yml | 4 + .github/workflows/bump-version.yaml | 159 +++++++++ .github/workflows/ci.yaml | 48 +-- .gitignore | 3 + Cargo.toml | 5 +- Makefile | 191 ++++++++--- README.md | 7 +- components/client/Cargo.toml | 2 +- components/client/src/lib.rs | 52 +-- .../componentized-http-0.1.0-dev/package.wit | 71 ----- .../componentized-oci-0.0.0-dev/package.wit | 301 ------------------ .../wit/deps/wasi-clocks-0.3.0/package.wit | 161 ---------- components/wit/worlds.wit | 2 +- components/wkg.lock | 2 +- lib/.gitignore | 2 - scripts/bump-version.sh | 105 ++++++ scripts/init-devcontainer.sh | 12 + tools/Cargo.toml | 24 ++ tools/lib.rs | 1 + wit/client.wit | 30 ++ wit/deps/wasi-clocks-0.3.0/package.wit | 161 ---------- wit/worlds.wit | 2 +- 24 files changed, 542 insertions(+), 818 deletions(-) delete mode 100644 .devcontainer/Dockerfile create mode 100644 .github/workflows/bump-version.yaml delete mode 100644 components/wit/deps/componentized-http-0.1.0-dev/package.wit delete mode 100644 components/wit/deps/componentized-oci-0.0.0-dev/package.wit delete mode 100644 components/wit/deps/wasi-clocks-0.3.0/package.wit delete mode 100644 lib/.gitignore create mode 100755 scripts/bump-version.sh create mode 100755 scripts/init-devcontainer.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 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 171dad6..a8291ce 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +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": "oci", - "build": { - "dockerfile": "Dockerfile" - }, + "name": "componentized-oci", + "image": "mcr.microsoft.com/devcontainers/rust", // Use 'mounts' to make the cargo cache persistent in a Docker Volume. "mounts": [ { @@ -23,7 +21,7 @@ "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. 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..ff1bfe5 --- /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.0.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 + 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 + + # 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..cd547f3 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 + make -s tools-path >> "${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 + make -s tools-path >> "${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 4da95f8..3411f01 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 a94f03c..8e35ec1 100644 --- a/Makefile +++ b/Makefile @@ -2,63 +2,172 @@ SHELL := /bin/bash export RUST_BACKTRACE ?= 1 export WASMTIME_BACKTRACE_DETAILS ?= 1 -WKG_CONFIG_FILE ?= $(dir $(abspath $(lastword $(MAKEFILE_LIST)))).config/wasm-pkg/config.toml -COMPONENTS = $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard components/*/Cargo.toml))))) +COMPONENTS_DIR := target/components +TOOLS_DIR := target/tools/$(shell rustc --print host-tuple) +# absolute, tools also run from other directories, e.g. `cd components && wkg fetch` +export PATH := $(abspath $(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 $(foreach file,$(wildcard $(addprefix components/*/,wit/*.constants.wit *.properties *.wac *.wkg Cargo.toml)),$(word 2,$(subst /, ,$(file))))) +TOOLS := componentized-constants-cli static-config wac-cli wasm-tools wasmtime-cli wkg + +export WKG_CONFIG_FILE := $(abspath .config/wasm-pkg/config.toml) + +# 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)))) + .PHONY: all -all: components test +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) .PHONY: test -test: wit - cargo test +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 $(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 $(abspath $(TOOLS_DIR))/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)))) .PHONY: components -components: lib/interface.wasm $(foreach component,$(COMPONENTS),lib/$(component).wasm 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/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,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 + +${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 -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 +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 + +# 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))) .PHONY: wit -wit: wit/deps components/wit/deps +wit: $(WIT_DEPS) + +define FETCH_WIT + +# a package overridden with a local path, e.g. `{ path = "../wit" }`, has its dependencies fetched first +$(call wit_deps,$1): $1/wkg.toml $1/wkg.lock $(shell find $1/wit -type f -name "*.wit" -not -path "*/deps/*") $(foreach path,$(shell sed -n 's/.*path *= *"\(.*\)".*/\1/p' $1/wkg.toml),$(call relpath,$1/$(path))/deps) | $(call tool,wkg) + $(if $(filter .,$1),,cd $1 && )wkg fetch + +endef + +$(foreach dir,$(WKG_DIRS),$(eval $(call FETCH_WIT,$(dir)))) -wit/deps: wkg.toml $(WKG_CONFIG_FILE) $(shell find wit -type f -name "*.wit" -not -path "deps") - wkg fetch --config $(WKG_CONFIG_FILE) +# 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 $(WKG_CONFIG_FILE) $(shell find components/wit -type f -name "*.wit" -not -path "deps") - ( cd components && wkg fetch --config $(WKG_CONFIG_FILE) ) +# 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 @@ -66,28 +175,36 @@ 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 := $(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 "$(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}) + @$(eval REVISION := ${COMMIT}$(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=$$( \ + @set -o pipefail ; \ + 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}" \ - "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 bc05b49..38527a7 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,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 client at `target/components/client/client.wasm`, along with `target/components/interface.wasm`, the `componentized:oci` WIT package. Each component is also built with debug info, e.g. `target/components/client/client.debug.wasm`. + +The cli tools the build uses, [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools), [`wac`](https://github.com/bytecodealliance/wac), [`wasmtime`](https://github.com/bytecodealliance/wasmtime) 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. + ### Components - [`client`](./components/client/) diff --git a/components/client/Cargo.toml b/components/client/Cargo.toml index b8e629c..17cb7e5 100644 --- a/components/client/Cargo.toml +++ b/components/client/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "client" version = "0.1.0" -edition = "2021" +edition = "2024" license = "Apache-2.0" [lib] diff --git a/components/client/src/lib.rs b/components/client/src/lib.rs index 17d0746..f183348 100644 --- a/components/client/src/lib.rs +++ b/components/client/src/lib.rs @@ -160,7 +160,7 @@ impl OCIClient { url: String, mut headers: Vec<(String, String)>, ) -> Result { - let response = Self::get_with_redirects(url.clone(), headers.clone()).await?; + let response = http::get(url.clone(), headers.clone(), None).await?; if response.status != 401 { return Ok(response); } @@ -184,47 +184,7 @@ impl OCIClient { headers.push(("Authorization".to_string(), format!("Bearer {token}"))); // a second 401 is returned to the caller and decoded as a transport error - Self::get_with_redirects(url, headers).await - } - - /// Issues a GET request, following redirects (e.g. registries redirecting - /// blob downloads to a CDN). The `Authorization` header is dropped when a - /// redirect leaves the original origin, as pre-signed storage URLs reject - /// unexpected credentials and the token must not leak to other hosts. - async fn get_with_redirects( - url: String, - mut headers: Vec<(String, String)>, - ) -> Result { - const MAX_REDIRECTS: usize = 10; - - let mut url = Url::parse(&url) - .map_err(|e| ErrorCode::Other(Some(format!("invalid url {url}: {e}"))))?; - for _ in 0..=MAX_REDIRECTS { - let response = http::get(url.to_string(), headers.clone(), None).await?; - if !matches!(response.status, 301 | 302 | 303 | 307 | 308) { - return Ok(response); - } - let Some(location) = response - .headers - .iter() - .find(|(k, _)| k.eq_ignore_ascii_case("location")) - .map(|(_, v)| v.clone()) - else { - return Ok(response); - }; - - let next = url.join(&location).map_err(|e| { - ErrorCode::Other(Some(format!("invalid redirect location {location}: {e}"))) - })?; - if next.origin() != url.origin() { - headers.retain(|(k, _)| !k.eq_ignore_ascii_case("authorization")); - } - url = next; - } - - Err(ErrorCode::Other(Some(format!( - "too many redirects, stopped at {url}" - )))) + Ok(http::get(url, headers, None).await?) } async fn fetch_token(params: &BTreeMap) -> Result { @@ -243,7 +203,7 @@ impl OCIClient { } let http::HttpResponse { status, body, .. } = - Self::get_with_redirects(url.to_string(), vec![]).await?; + http::get(url.to_string(), vec![], None).await?; let body = body.collect().await; if status != 200 { return Err(ErrorCode::Unauthorized(format!( @@ -1156,6 +1116,12 @@ struct TransportError { impl From for ErrorCode { fn from(value: http::ErrorCode) -> Self { match value { + http::ErrorCode::RedirectLimitExceeded((_, count)) => Self::Other(Some(format!( + "too many redirects, stopped after {count}" + ))), + http::ErrorCode::RedirectRequiresBody(_) => Self::Other(Some( + "redirect requires resending the request body".to_string(), + )), http::ErrorCode::Other(message) => Self::Other(message), } } 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/componentized-oci-0.0.0-dev/package.wit b/components/wit/deps/componentized-oci-0.0.0-dev/package.wit deleted file mode 100644 index 2630638..0000000 --- a/components/wit/deps/componentized-oci-0.0.0-dev/package.wit +++ /dev/null @@ -1,301 +0,0 @@ -package componentized:oci@0.0.0-dev; - -interface client { - use wasi:clocks/system-clock@0.3.0.{instant}; - - variant retry-after { - date(instant), - delay-seconds(u32), - } - - variant error-code { - /// blob unknown to registry (code-1 `BLOB_UNKNOWN`) - blob-unknown(string), - /// blob upload invalid (code-2 `BLOB_UPLOAD_INVALID`) - blob-upload-invalid(string), - /// blob upload unknown to registry (code-3`BLOB_UPLOAD_UNKNOWN`) - blob-upload-unknown(string), - /// provided digest did not match uploaded content (code-4 `DIGEST_INVALID`) - digest-invalid(string), - /// manifest references a manifest or blob unknown to registry (code-5 `MANIFEST_BLOB_UNKNOWN`) - manifest-blob-unknown(string), - /// manifest invalid (code-6 `MANIFEST_INVALID`) - manifest-invalid(string), - /// manifest unknown to registry (code-7 `MANIFEST_UNKNOWN`) - manifest-unknown(string), - /// invalid repository name (code-8 `NAME_INVALID`) - name-invalid(string), - /// repository name not known to registry (code-9 `NAME_UNKNOWN`) - name-unknown(string), - /// provided length did not match content length (code-10 `SIZE_INVALID`) - size-invalid(string), - /// authentication required (code-11 `UNAUTHORIZED`) - unauthorized(string), - /// requested access to the resource is denied (code-12 `DENIED`) - denied(string), - /// the operation is unsupported (code-13 `UNSUPPORTED`) - unsupported(string), - /// too many requests (code-14 `TOOMANYREQUESTS`) - toomanyrequests(option), - /// other - other(option), - } - - variant schema-version { - v2, - other(option), - } - - variant media-type-suffix { - /// json encoded - json, - /// compressed with gzip - gzip, - /// compressed with zstd - zstd, - /// other encoding - other(option), - } - - /// common media types for oci artifacts - variant media-type { - /// content descriptor - application-vnd-oci-descriptor-v1(media-type-suffix), - /// oci layout - application-vnd-oci-layout-header-v1(media-type-suffix), - /// image index - application-vnd-oci-image-index-v1(media-type-suffix), - /// image manifest - application-vnd-oci-image-manifest-v1(media-type-suffix), - /// image config - application-vnd-oci-image-config-v1(media-type-suffix), - /// layer, as a tar archive - application-vnd-oci-image-layer-v1-tar(media-type-suffix), - /// empty for unused descriptors - application-vnd-oci-empty-v1(media-type-suffix), - /// layer, as a tar archive - application-vnd-oci-image-layer-nondistributable-v1-tar(media-type-suffix), - /// wasm config - application-vnd-wasm-config-v0(media-type-suffix), - /// wasm - application-wasm, - /// other - other(string), - } - - record oci-image-index-manifest-v1-manifest-platform { - /// CPU architecture which the binaries in this image are built to run on - architecture: string, - /// name of the operating system which the image is built to run on - os: string, - /// version of the operating system - os-version: option, - /// mandatory OS features - os-features: option>, - /// variant of the CPU - %variant: option, - } - - record oci-image-config-v1-config { - /// username or UID which is a platform-specific structure that allows specific control over which user the process run as - user: option, - /// set of ports to expose from a container running this image - exposed-ports: option>, - /// values act as defaults and are merged with any specified when creating a container - env: option>, - /// arguments to use as the command to execute when the container starts - entrypoint: option>, - /// arguments to the entrypoint of the container - cmd: option>, - /// directories describing where the process is likely to write data specific to a container instance - volumes: option>, - /// current working directory of the entrypoint process in the container - working-dir: option, - /// arbitrary metadata for the container - labels: option>, - /// system call signal that will be sent to the container to exit - stop-signal: option, - /// deprecated: present only for legacy compatibility - args-escaped: option, - } - - record oci-image-config-v1-history-entry { - /// combined date and time at which the layer was created - created: option, - /// author of the build point - author: option, - /// command which created the layer - created-by: option, - /// custom message set when creating the layer - comment: option, - /// mark if the history item created a filesystem diff - empty-layer: option, - } - - record wasm-config-v0-component { - exports: list, - imports: list, - target: option, - } - - record digest { - algorithm: string, - encoded: string, - } - - record oci-descriptor-v1 { - /// media type of the referenced content - media-type: media-type, - /// digest of the targeted content - digest: digest, - /// size, in bytes, of the raw content - size: u64, - /// list of URIs from which this object MAY be downloaded - urls: option>, - /// arbitrary metadata for this descriptor - annotations: option>, - /// embedded representation of the referenced content - data: option, - /// type of an artifact when the descriptor points to an artifact - artifact-type: option, - } - - record oci-image-manifest-v1 { - /// manifest schema version - schema-version: schema-version, - /// must contain the media type 'application/vnd.oci.image.manifest.v1+json'. - media-type: media-type, - /// type of an artifact when the manifest is used for an artifact - artifact-type: option, - /// configuration object for a container - config: oci-descriptor-v1, - /// list of layer descriptors - layers: list, - /// weak association to another manifest - subject: option, - /// arbitrary metadata for the image manifest - annotations: option>, - } - - record oci-image-index-manifest-v1-manifest { - /// media type of the referenced content - media-type: media-type, - /// digest of the targeted content - digest: digest, - /// size, in bytes, of the raw content - size: u64, - /// list of URIs from which this object MAY be downloaded - urls: option>, - /// arbitrary metadata for this descriptor - annotations: option>, - /// embedded representation of the referenced content - data: option, - /// type of an artifact when the descriptor points to an artifact - artifact-type: option, - /// minimum runtime requirements of the image - platform: option, - /// descriptor of another manifest - subject: option, - } - - record oci-image-index-manifest-v1 { - /// manifest schema version - schema-version: schema-version, - /// must contain the media type 'application/vnd.oci.image.index.v1+json'. - media-type: media-type, - /// type of an artifact when the manifest is used for an artifact - artifact-type: option, - /// manifests for specific platforms - manifests: list, - /// weak association to another manifest - subject: option, - /// arbitrary metadata for the image manifest - annotations: option>, - } - - variant manifest { - /// oci image - oci-image-v1(oci-image-manifest-v1), - /// oci image index - oci-image-index-v1(oci-image-index-manifest-v1), - /// other - other(list), - } - - record oci-image-config-v1-content-addresses { - /// must be 'layers' - %type: string, - /// layer content hashes, in order from first to last - diff-ids: list, - } - - record oci-image-config-v1 { - /// combined date and time at which the image was created - created: option, - /// name and/or email address of the person or entity which created and is responsible for maintaining the image - author: option, - /// CPU architecture which the binaries in this image are built to run on - architecture: string, - /// name of the operating system which the image is built to run on - os: string, - /// version of the operating system - os-version: option, - /// mandatory OS features - os-features: option>, - /// variant of the specified CPU architecture - %variant: option, - /// execution parameters which should be used as a base when running a container using the image - config: option, - /// layer content addresses used by the image - rootfs: oci-image-config-v1-content-addresses, - /// history of each layer. The array is ordered from first to last - history: option>, - } - - record wasm-config-v0 { - /// combined date and time at which the image was created - created: option, - /// name and/or email address of the person or entity which created and is responsible for maintaining the image - author: option, - /// must match a GOARCH value - architecture: string, - /// must match a GOOS value - os: string, - /// digests of the layers in the same order - layer-digests: list, - /// component metadata - component: option, - } - - variant config { - /// oci iamge - oci-image-v1(oci-image-config-v1), - /// wasm module - wasm-v0(wasm-config-v0), - /// other - other(list), - } - - record reference { - registry: string, - repository: string, - tag: option, - digest: option, - } - - parse-reference: func(reference: string) -> result; - - resolve-digest: async func(reference: reference) -> result; - - get-blob: async func(reference: reference) -> result, error-code>; - - get-config: async func(reference: reference, default-media-type: option) -> result; - - get-manifest: async func(reference: reference) -> result; -} - -world imports { - import wasi:clocks/types@0.3.0; - import wasi:clocks/system-clock@0.3.0; - import client; -} 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 d8b8cfe..0000000 --- a/components/wit/deps/wasi-clocks-0.3.0/package.wit +++ /dev/null @@ -1,161 +0,0 @@ -package wasi:clocks@0.3.0; - -/// This interface common types used throughout wasi:clocks. -@since(version = 0.3.0) -interface types { - /// A duration of time, in nanoseconds. - @since(version = 0.3.0) - type duration = u64; -} - -/// WASI Monotonic Clock is a clock API intended to let users measure elapsed -/// time. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -/// -/// A monotonic clock is a clock which has an unspecified initial value, and -/// successive reads of the clock will produce non-decreasing values. -@since(version = 0.3.0) -interface monotonic-clock { - use types.{duration}; - - /// A mark on a monotonic clock is a number of nanoseconds since an - /// unspecified initial value, and can only be compared to instances from - /// the same monotonic-clock. - @since(version = 0.3.0) - type mark = u64; - - /// Read the current value of the clock. - /// - /// The clock is monotonic, therefore calling this function repeatedly will - /// produce a sequence of non-decreasing values. - /// - /// For completeness, this function traps if it's not possible to represent - /// the value of the clock in a `mark`. Consequently, implementations - /// should ensure that the starting time is low enough to avoid the - /// possibility of overflow in practice. - @since(version = 0.3.0) - now: func() -> mark; - - /// Query the resolution of the clock. Returns the duration of time - /// corresponding to a clock tick. - @since(version = 0.3.0) - get-resolution: func() -> duration; - - /// Wait until the specified mark has occurred. - @since(version = 0.3.0) - wait-until: async func(when: mark); - - /// Wait for the specified duration to elapse. - @since(version = 0.3.0) - wait-for: async func(how-long: duration); -} - -/// WASI System Clock is a clock API intended to let users query the current -/// time. The clock is not necessarily monotonic as it may be reset. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -/// -/// External references may be reset, so this clock is not necessarily -/// monotonic, making it unsuitable for measuring elapsed time. -/// -/// It is intended for reporting the current date and time for humans. -@since(version = 0.3.0) -interface system-clock { - use types.{duration}; - - /// An "instant", or "exact time", is a point in time without regard to any - /// time zone: just the time since a particular external reference point, - /// often called an "epoch". - /// - /// Here, the epoch is 1970-01-01T00:00:00Z, also known as - /// [POSIX's Seconds Since the Epoch], also known as [Unix Time]. - /// - /// Note that even if the seconds field is negative, incrementing - /// nanoseconds always represents moving forwards in time. - /// For example, `{ -1 seconds, 999999999 nanoseconds }` represents the - /// instant one nanosecond before the epoch. - /// For more on various different ways to represent time, see - /// https://tc39.es/proposal-temporal/docs/timezone.html - /// - /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 - /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time - @since(version = 0.3.0) - record instant { - seconds: s64, - nanoseconds: u32, - } - - /// Read the current value of the clock. - /// - /// This clock is not monotonic, therefore calling this function repeatedly - /// will not necessarily produce a sequence of non-decreasing values. - /// - /// The nanoseconds field of the output is always less than 1000000000. - @since(version = 0.3.0) - now: func() -> instant; - - /// Query the resolution of the clock. Returns the smallest duration of time - /// that the implementation permits distinguishing. - @since(version = 0.3.0) - get-resolution: func() -> duration; -} - -@unstable(feature = clocks-timezone) -interface timezone { - @unstable(feature = clocks-timezone) - use system-clock.{instant}; - - /// Return the IANA identifier of the currently configured timezone. This - /// should be an identifier from the IANA Time Zone Database. - /// - /// For displaying to a user, the identifier should be converted into a - /// localized name by means of an internationalization API. - /// - /// If the implementation does not expose an actual timezone, or is unable - /// to provide mappings from times to deltas between the configured timezone - /// and UTC, or determining the current timezone fails, or the timezone does - /// not have an IANA identifier, this returns nothing. - @unstable(feature = clocks-timezone) - iana-id: func() -> option; - - /// The number of nanoseconds difference between UTC time and the local - /// time of the currently configured timezone, at the exact time of - /// `instant`. - /// - /// The magnitude of the returned value will always be less than - /// 86,400,000,000,000 which is the number of nanoseconds in a day - /// (24*60*60*1e9). - /// - /// If the implementation does not expose an actual timezone, or is unable - /// to provide mappings from times to deltas between the configured timezone - /// and UTC, or determining the current timezone fails, this returns - /// nothing. - @unstable(feature = clocks-timezone) - utc-offset: func(when: instant) -> option; - - /// Returns a string that is suitable to assist humans in debugging whether - /// any timezone is available, and if so, which. This may be the same string - /// as `iana-id`, or a formatted representation of the UTC offset such as - /// `-04:00`, or something else. - /// - /// WARNING: The returned string should not be consumed mechanically! It may - /// change across platforms, hosts, or other implementation details. Parsing - /// this string is a major platform-compatibility hazard. - @unstable(feature = clocks-timezone) - to-debug-string: func() -> string; -} - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - import types; - @since(version = 0.3.0) - import monotonic-clock; - @since(version = 0.3.0) - import system-clock; - @unstable(feature = clocks-timezone) - import timezone; -} diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 72a232d..b09d9eb 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -2,5 +2,5 @@ package componentized:oci-components; world client { import componentized:http/client@0.1.0-dev; - export componentized:oci/client@0.0.0-dev; + export componentized:oci/client@0.1.0-dev; } diff --git a/components/wkg.lock b/components/wkg.lock index 00c46d5..ed7a74c 100644 --- a/components/wkg.lock +++ b/components/wkg.lock @@ -9,7 +9,7 @@ registry = "componentized.dev" [[packages.versions]] requirement = "=0.1.0-dev" version = "0.1.0-dev" -digest = "sha256:1c43656521dfd238a67af2288f7abcf67e2ebd950352a90c396dcf11b25c4440" +digest = "sha256:f74b53af4b5ecb4c77e8bd47887920e0626efb2e45e8a9386f2519910faf5e5c" [[packages]] name = "wasi:clocks" 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/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/tools/Cargo.toml b/tools/Cargo.toml new file mode 100644 index 0000000..ce7848a --- /dev/null +++ b/tools/Cargo.toml @@ -0,0 +1,24 @@ +# 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] +componentized-constants-cli = "=0.1.0-dev" +static-config = "=0.2.0" +wac-cli = "=0.12.0" +wasm-tools = "=1.260.0" +wasmtime-cli = "=49.0.2" +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/client.wit b/wit/client.wit index 9ca060f..7134f00 100644 --- a/wit/client.wit +++ b/wit/client.wit @@ -1,7 +1,9 @@ +@since(version = 0.1.0-dev) interface client { use wasi:clocks/system-clock@0.3.0.{ instant }; + @since(version = 0.1.0-dev) variant error-code { // blob unknown to registry (code-1 `BLOB_UNKNOWN`) blob-unknown(string), @@ -35,17 +37,20 @@ interface client { other(option), } + @since(version = 0.1.0-dev) variant retry-after { date(instant), delay-seconds(u32), } + @since(version = 0.1.0-dev) variant schema-version { v2, other(option), } // common media types for oci artifacts + @since(version = 0.1.0-dev) variant media-type { // content descriptor application-vnd-oci-descriptor-v1(media-type-suffix), @@ -71,6 +76,7 @@ interface client { other(string), } + @since(version = 0.1.0-dev) variant media-type-suffix { // json encoded json, @@ -82,6 +88,7 @@ interface client { other(option), } + @since(version = 0.1.0-dev) record oci-descriptor-v1 { // media type of the referenced content media-type: media-type, @@ -99,6 +106,7 @@ interface client { artifact-type: option, } + @since(version = 0.1.0-dev) variant manifest { // oci image oci-image-v1(oci-image-manifest-v1), @@ -108,6 +116,7 @@ interface client { other(list), } + @since(version = 0.1.0-dev) record oci-image-manifest-v1 { // manifest schema version schema-version: schema-version, @@ -125,6 +134,7 @@ interface client { annotations: option>, } + @since(version = 0.1.0-dev) record oci-image-index-manifest-v1 { // manifest schema version schema-version: schema-version, @@ -140,6 +150,7 @@ interface client { annotations: option>, } + @since(version = 0.1.0-dev) record oci-image-index-manifest-v1-manifest { // media type of the referenced content media-type: media-type, @@ -162,6 +173,7 @@ interface client { subject: option, } + @since(version = 0.1.0-dev) record oci-image-index-manifest-v1-manifest-platform { // CPU architecture which the binaries in this image are built to run on architecture: string, @@ -175,6 +187,7 @@ interface client { %variant: option, } + @since(version = 0.1.0-dev) variant config { // oci iamge oci-image-v1(oci-image-config-v1), @@ -184,6 +197,7 @@ interface client { other(list), } + @since(version = 0.1.0-dev) record oci-image-config-v1 { // combined date and time at which the image was created created: option, @@ -207,6 +221,7 @@ interface client { history: option>, } + @since(version = 0.1.0-dev) record oci-image-config-v1-config { // username or UID which is a platform-specific structure that allows specific control over which user the process run as user: option, @@ -230,6 +245,7 @@ interface client { args-escaped: option, } + @since(version = 0.1.0-dev) record oci-image-config-v1-content-addresses { // must be 'layers' %type: string, @@ -237,6 +253,7 @@ interface client { diff-ids: list, } + @since(version = 0.1.0-dev) record oci-image-config-v1-history-entry { // combined date and time at which the layer was created created: option, @@ -250,6 +267,7 @@ interface client { empty-layer: option, } + @since(version = 0.1.0-dev) record wasm-config-v0 { // combined date and time at which the image was created created: option, @@ -265,12 +283,14 @@ interface client { component: option, } + @since(version = 0.1.0-dev) record wasm-config-v0-component { exports: list, imports: list, target: option, } + @since(version = 0.1.0-dev) record reference { registry: string, repository: string, @@ -278,15 +298,25 @@ interface client { digest: option, } + @since(version = 0.1.0-dev) record digest { algorithm: string, encoded: string, } + @since(version = 0.1.0-dev) parse-reference: func(reference: string) -> result; + + @since(version = 0.1.0-dev) resolve-digest: async func(reference: reference) -> result; + + @since(version = 0.1.0-dev) get-blob: async func(reference: reference) -> result, error-code>; + + @since(version = 0.1.0-dev) get-config: async func(reference: reference, default-media-type: option) -> result; + + @since(version = 0.1.0-dev) get-manifest: async func(reference: reference) -> result; } 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 d8b8cfe..0000000 --- a/wit/deps/wasi-clocks-0.3.0/package.wit +++ /dev/null @@ -1,161 +0,0 @@ -package wasi:clocks@0.3.0; - -/// This interface common types used throughout wasi:clocks. -@since(version = 0.3.0) -interface types { - /// A duration of time, in nanoseconds. - @since(version = 0.3.0) - type duration = u64; -} - -/// WASI Monotonic Clock is a clock API intended to let users measure elapsed -/// time. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -/// -/// A monotonic clock is a clock which has an unspecified initial value, and -/// successive reads of the clock will produce non-decreasing values. -@since(version = 0.3.0) -interface monotonic-clock { - use types.{duration}; - - /// A mark on a monotonic clock is a number of nanoseconds since an - /// unspecified initial value, and can only be compared to instances from - /// the same monotonic-clock. - @since(version = 0.3.0) - type mark = u64; - - /// Read the current value of the clock. - /// - /// The clock is monotonic, therefore calling this function repeatedly will - /// produce a sequence of non-decreasing values. - /// - /// For completeness, this function traps if it's not possible to represent - /// the value of the clock in a `mark`. Consequently, implementations - /// should ensure that the starting time is low enough to avoid the - /// possibility of overflow in practice. - @since(version = 0.3.0) - now: func() -> mark; - - /// Query the resolution of the clock. Returns the duration of time - /// corresponding to a clock tick. - @since(version = 0.3.0) - get-resolution: func() -> duration; - - /// Wait until the specified mark has occurred. - @since(version = 0.3.0) - wait-until: async func(when: mark); - - /// Wait for the specified duration to elapse. - @since(version = 0.3.0) - wait-for: async func(how-long: duration); -} - -/// WASI System Clock is a clock API intended to let users query the current -/// time. The clock is not necessarily monotonic as it may be reset. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -/// -/// External references may be reset, so this clock is not necessarily -/// monotonic, making it unsuitable for measuring elapsed time. -/// -/// It is intended for reporting the current date and time for humans. -@since(version = 0.3.0) -interface system-clock { - use types.{duration}; - - /// An "instant", or "exact time", is a point in time without regard to any - /// time zone: just the time since a particular external reference point, - /// often called an "epoch". - /// - /// Here, the epoch is 1970-01-01T00:00:00Z, also known as - /// [POSIX's Seconds Since the Epoch], also known as [Unix Time]. - /// - /// Note that even if the seconds field is negative, incrementing - /// nanoseconds always represents moving forwards in time. - /// For example, `{ -1 seconds, 999999999 nanoseconds }` represents the - /// instant one nanosecond before the epoch. - /// For more on various different ways to represent time, see - /// https://tc39.es/proposal-temporal/docs/timezone.html - /// - /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 - /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time - @since(version = 0.3.0) - record instant { - seconds: s64, - nanoseconds: u32, - } - - /// Read the current value of the clock. - /// - /// This clock is not monotonic, therefore calling this function repeatedly - /// will not necessarily produce a sequence of non-decreasing values. - /// - /// The nanoseconds field of the output is always less than 1000000000. - @since(version = 0.3.0) - now: func() -> instant; - - /// Query the resolution of the clock. Returns the smallest duration of time - /// that the implementation permits distinguishing. - @since(version = 0.3.0) - get-resolution: func() -> duration; -} - -@unstable(feature = clocks-timezone) -interface timezone { - @unstable(feature = clocks-timezone) - use system-clock.{instant}; - - /// Return the IANA identifier of the currently configured timezone. This - /// should be an identifier from the IANA Time Zone Database. - /// - /// For displaying to a user, the identifier should be converted into a - /// localized name by means of an internationalization API. - /// - /// If the implementation does not expose an actual timezone, or is unable - /// to provide mappings from times to deltas between the configured timezone - /// and UTC, or determining the current timezone fails, or the timezone does - /// not have an IANA identifier, this returns nothing. - @unstable(feature = clocks-timezone) - iana-id: func() -> option; - - /// The number of nanoseconds difference between UTC time and the local - /// time of the currently configured timezone, at the exact time of - /// `instant`. - /// - /// The magnitude of the returned value will always be less than - /// 86,400,000,000,000 which is the number of nanoseconds in a day - /// (24*60*60*1e9). - /// - /// If the implementation does not expose an actual timezone, or is unable - /// to provide mappings from times to deltas between the configured timezone - /// and UTC, or determining the current timezone fails, this returns - /// nothing. - @unstable(feature = clocks-timezone) - utc-offset: func(when: instant) -> option; - - /// Returns a string that is suitable to assist humans in debugging whether - /// any timezone is available, and if so, which. This may be the same string - /// as `iana-id`, or a formatted representation of the UTC offset such as - /// `-04:00`, or something else. - /// - /// WARNING: The returned string should not be consumed mechanically! It may - /// change across platforms, hosts, or other implementation details. Parsing - /// this string is a major platform-compatibility hazard. - @unstable(feature = clocks-timezone) - to-debug-string: func() -> string; -} - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - import types; - @since(version = 0.3.0) - import monotonic-clock; - @since(version = 0.3.0) - import system-clock; - @unstable(feature = clocks-timezone) - import timezone; -} diff --git a/wit/worlds.wit b/wit/worlds.wit index ae104f5..e87cf1b 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -1,4 +1,4 @@ -package componentized:oci@0.0.0-dev; +package componentized:oci@0.1.0-dev; world imports { import client;