diff --git a/.dockerignore b/.dockerignore index dd97eb3..97a1453 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,7 @@ **/target/ **/.DS_Store services/ws-wasm-agent/pkg/ +services/ws-modules/pywasm1/pkg/ services/ws-server/static/models/ **/.zig-cache/ **/zig-out/ diff --git a/.editorconfig b/.editorconfig index 5276445..bb26645 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,7 +19,7 @@ indent_style = tab indent_size = unset # License files use the canonical upstream formatting (centred headers, odd -# indent widths, etc.) — leave them alone. +# indent widths, etc.) -- leave them alone. [LICENSE-*] indent_size = unset indent_style = unset @@ -43,7 +43,7 @@ indent_style = unset max_line_length = unset trim_trailing_whitespace = unset -# Upstream WIT packages fetched verbatim — their formatting is whatever the +# Upstream WIT packages fetched verbatim -- their formatting is whatever the # upstream repos use and shouldn't be normalised on our side. [generated/specs/wit/deps/wasi-*/**] charset = unset @@ -55,22 +55,18 @@ trim_trailing_whitespace = unset max_line_length = unset # openapi-python-client emits framework-boilerplate docstrings (e.g. the -# "errors.UnexpectedStatus: If the server returns an undocumented status code -# …" line in every operation) that exceed 120 chars, and continuation lines -# inside the generated function signatures use 3-space indentation that -# clashes with the workspace `indent_size = 2`. ruff format doesn't reflow -# plain-text docstrings or re-indent the templates, so we drop both checks -# for this tree. +# "errors.UnexpectedStatus: If the server returns an undocumented status code ..." line in every operation) that exceed +# 120 chars, and continuation lines inside the generated function signatures use 3-space indentation that +# clashes with the workspace `indent_size = 2`. ruff format doesn't reflow plain-text docstrings or +# re-indent the templates, so we drop both checks for this tree. [generated/python-rest/**] indent_size = unset max_line_length = unset -# datamodel-code-generator renders each Pydantic field's `description=` -# from the schemars `description` (i.e. the source enum's `///` doc -# comment) as a single string literal — `\n`-joined, no implicit -# concatenation. ruff format won't break string literals, so any -# multi-paragraph Rust doc comment on `ClientMessage` / `ServerMessage` -# blows past 120 chars in the generated Python. +# datamodel-code-generator renders each Pydantic field's `description=` from the schemars `description` +# (i.e. the source enum's `///` doc comment) as a single string literal -- `\n`-joined, no implicit +# concatenation. ruff format won't break string literals, so any multi-paragraph Rust doc comment on +# `ClientMessage` / `ServerMessage` blows past 120 chars in the generated Python. [generated/python-ws/**] max_line_length = unset @@ -79,3 +75,6 @@ max_line_length = unset # The generator is the source of truth; we don't edit its output by hand. [generated/zig-rest/**] max_line_length = unset + +[config/upstream-cache/data.toml] +max_line_length = 200 diff --git a/.gitattributes b/.gitattributes index 36be331..91c502e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,10 +3,10 @@ # Windows runners default to `core.autocrlf=true`, which rewrites LF to # CRLF at checkout. Multiple formatters in this repo (zig fmt, dprint) # read CRLF as a diff against their LF-normalized output and report -# every file as "would be reformatted" — with no diff, just paths. +# every file as "would be reformatted" -- with no diff, just paths. # Pinning `eol=lf` workspace-wide makes the checked-out tree match the # committed bytes on every OS, so the formatters behave identically. * text=auto eol=lf -# Binaries tracked in this repo — never run text conversion on these. +# Binaries tracked in this repo -- never run text conversion on these. *.png binary diff --git a/.github/actions/free-disk-space-windows/action.yaml b/.github/actions/free-disk-space-windows/action.yaml new file mode 100644 index 0000000..887e8ab --- /dev/null +++ b/.github/actions/free-disk-space-windows/action.yaml @@ -0,0 +1,124 @@ +--- +name: Free disk space +description: >- + Remove safely-unused preinstalled software from the GHA Windows runner's + `C:\` to give downstream `docker build` (which stages HCS layer scratch + on `C:\Windows\SystemTemp`) more headroom. Counterpart to + jlumbroso/free-disk-space on Linux, which doesn't have a Windows path. + Logs disk-free before + after so the reclaimed space is visible. + + Enabling all makes approximately 10Gb free. + + Caller is responsible for the `if: runner.os == 'Windows'` guard -- + every step here uses Git Bash with `C:\` mounted at `/c`; running this + on a non-Windows runner will silently no-op (no /c mount) at best. + +inputs: + android: + description: "Remove Android SDK trees (~12 GB)." + required: false + default: "true" + hostedtoolcache-old-pythons: + description: >- + Remove Python 3.9/3.10/3.11/3.12 versions from `hostedtoolcache` + (~2 GB). The repo uses mise's pinned 3.13. + required: false + default: "true" + hostedtoolcache-old-jdks: + description: >- + Remove JDK 8/11/17/21 versions from `hostedtoolcache\Java` + (~1 GB). The repo uses mise's pinned Java 26. + required: false + default: "true" + old-dotnet-sdks: + description: >- + Remove .NET SDK 6.0.x and 7.0.x trees (~1-2 GB). The repo + installs current dotnet via mise. + required: false + default: "true" + misc-sdks: + description: >- + Remove unused preinstalled SDKs (Heroku, MongoDB, PostgreSQL, + Strawberry Perl) -- ~1 GB combined. + required: false + default: "true" + keep-image: + description: >- + Docker image (repo:tag) to preserve during the docker prune. + Empty (default) removes every image. + required: false + default: "" + +runs: + using: composite + steps: + - name: Disk usage before + shell: bash --noprofile --norc -euo pipefail {0} + run: df -h /c + + - name: Remove Android SDK + if: inputs.android == 'true' + shell: bash --noprofile --norc -euo pipefail {0} + run: | + for p in "/c/Android" "/c/Program Files (x86)/Android"; do + [ -e "$p" ] && { echo "removing $p"; rm -rf -- "$p"; } || true + done + + - name: Remove hostedtoolcache old Pythons + if: inputs.hostedtoolcache-old-pythons == 'true' + shell: bash --noprofile --norc -euo pipefail {0} + run: | + shopt -s nullglob + for p in /c/hostedtoolcache/Python/3.{9,10,11,12}*; do + echo "removing $p" + rm -rf -- "$p" + done + + - name: Remove hostedtoolcache old JDKs + if: inputs.hostedtoolcache-old-jdks == 'true' + shell: bash --noprofile --norc -euo pipefail {0} + run: | + shopt -s nullglob + for p in /c/hostedtoolcache/Java_Adopt_jdk/{8,11,17,21}.*; do + echo "removing $p" + rm -rf -- "$p" + done + + - name: Remove old .NET SDKs + if: inputs.old-dotnet-sdks == 'true' + shell: bash --noprofile --norc -euo pipefail {0} + run: | + shopt -s nullglob + for p in "/c/Program Files/dotnet/sdk/"{6,7}.0.*; do + echo "removing $p" + rm -rf -- "$p" + done + + - name: Remove misc SDKs + if: inputs.misc-sdks == 'true' + shell: bash --noprofile --norc -euo pipefail {0} + run: | + pf="/c/Program Files" + pf86="/c/Program Files (x86)" + for p in "$pf/MongoDB" "$pf/PostgreSQL" "$pf86/Heroku" "/c/Strawberry"; do + [ -e "$p" ] && { echo "removing $p"; rm -rf -- "$p"; } || true + done + + - name: Prune docker images and builder cache + shell: bash --noprofile --norc -euo pipefail {0} + env: + KEEP_IMAGE: ${{ inputs.keep-image }} + run: | + docker images + for img in $(docker images --format '{{.Repository}}:{{.Tag}}'); do + if [ "$img" != "$KEEP_IMAGE" ]; then + docker rmi -f "$img" || true + fi + done + docker container prune -f + docker network prune -f + docker builder prune -af + + - name: Disk usage after + shell: bash --noprofile --norc -euo pipefail {0} + run: df -h /c diff --git a/.github/actions/install-mise-tools/action.yaml b/.github/actions/install-mise-tools/action.yaml new file mode 100644 index 0000000..017b1c9 --- /dev/null +++ b/.github/actions/install-mise-tools/action.yaml @@ -0,0 +1,74 @@ +--- +name: Install mise tools +description: >- + Runs `install-mise` action, then installs all of the tools in the per-language + toolchains named by the workflow's MISE_ENV (with one retry on transient + HTTP flakes). Shared by check.yaml and test.yaml; the caller must run + `actions/checkout` before this so .mise/config*.toml exists for `mise trust`. + +inputs: + github-token: + description: >- + GitHub token forwarded as `$GITHUB_TOKEN` to the mise install steps. + required: true + install-action-tools: + description: >- + Comma-separated tools to co-install via taiki-e/install-action in the + same step as mise. Forwarded to install-mise's `extra-tools` input. + NOT used for `aube` -- install-action's manifest expects an `aubr` + binary that recent aube releases don't ship, so the install resolves + to a cargo source-build that flakes on crates.io SSL. The aube path + below uses the original `mise run setup-aube` (npm-backed, allowed + to fail) which has been the reliable install method. + required: false + default: "" + +runs: + using: composite + steps: + - name: Install mise binary + uses: ./.github/actions/install-mise + with: + install-action: ${{ inputs.install-action-tools }} + + # Optional npm backend, installed before the main `mise install`. + # Only useful when js env is loaded (it's the backend for npm:* tools, all of which live in config.js.toml); skip + # otherwise to avoid the install cost on workflows that don't need any npm: install. See [tasks.setup-aube] in + # .mise/config.toml for the full rationale. + - name: Install aube (optional npm backend, allowed to fail) + if: contains(env.MISE_ENV, 'js') + continue-on-error: true + shell: bash --noprofile --norc -euo pipefail {0} + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: mise run setup-aube + + - name: Install mise tools + id: install-mise-tools + continue-on-error: true + shell: bash --noprofile --norc -euo pipefail {0} + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: | + mise run preinstall + mise install + + - name: Install mise tools (retry) + if: steps.install-mise-tools.outcome == 'failure' + shell: bash --noprofile --norc -euo pipefail {0} + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: | + sleep 20 + mise install + + # Delete mise's auto-generated `mise.cmd` shim on Windows. + # This lets cmd's PATH search fall through to the real `mise.exe` at `~/.cargo/bin/mise.exe` (staged by + # taiki-e/install-action). Must run AFTER `mise install` -- that's the step that creates the shims. + - name: Remove self-recursive mise.cmd shim on Windows + if: runner.os == 'Windows' + shell: bash --noprofile --norc -euo pipefail {0} + run: | + shims="$LOCALAPPDATA/mise/shims" + rm -fv "$shims/mise.cmd" "$shims/mise" + ls "$shims" | grep -i '^mise' || true diff --git a/.github/actions/install-mise/action.yaml b/.github/actions/install-mise/action.yaml new file mode 100644 index 0000000..97f1875 --- /dev/null +++ b/.github/actions/install-mise/action.yaml @@ -0,0 +1,54 @@ +--- +name: Install mise +description: >- + Install the mise binary via taiki-e/install-action, and on Windows add mise's + file-shim dir to PATH so subsequent steps can resolve mise-managed tools by + bare name. + +inputs: + install-action-tools: + description: >- + Comma-separated tools to co-install via taiki-e/install-action. + required: false + default: "" + +runs: + using: composite + steps: + - name: Verify actions/checkout has run + shell: bash --noprofile --norc -euo pipefail {0} + run: git rev-parse --is-inside-work-tree >/dev/null + + - name: Install mise + uses: taiki-e/install-action@v2 + with: + tool: cargo-binstall,mise@2026.6.5 + + - name: Install extra tools via install-action + if: inputs.install-action-tools != '' + uses: taiki-e/install-action@v2 + with: + tool: ${{ inputs.install-action-tools }} + + # Append mise's shims dir to PATH for subsequent steps on Windows. + # taiki-e/install-action's mise manifest stages only `mise.exe`; mise's per-tool `.cmd` "file" shims under + # `%LOCALAPPDATA%\mise\shims` aren't on PATH by default, so tool lookups inside `mise run` fail with + # `'dart' / 'mvn' / 'recur' / 'wasm-pack' / 'zig' is not recognized...`. The companion `mise-shim.exe` + # (single-binary shim) ALSO ships in the Windows zip, but staging it triggers a CreateProcess + # `ERROR_FILENAME_EXCED_RANGE` (os error 206) when mise-shim re-execs mise.exe with the runner's already-long + # PATH inherited -- the file-shim fallback doesn't have this problem (each .cmd shim invokes `mise exec --` + # plainly via cmd). + - name: Add mise shims dir to PATH on Windows + if: runner.os == 'Windows' + shell: bash --noprofile --norc -euo pipefail {0} + # The github-env audit's arbitrary-code-execution shape doesn't apply here. + # The appended value is a hardcoded mise install-dir constant under our control -- no user input flows in. + run: echo "$LOCALAPPDATA\mise\shims" >> "$GITHUB_PATH" # zizmor: ignore[github-env] + + - name: Trust mise config + shell: bash --noprofile --norc -euo pipefail {0} + run: mise trust + + - name: Show MISE_ENV + shell: bash --noprofile --norc -euo pipefail {0} + run: echo "MISE_ENV=$MISE_ENV" diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index e18059f..ba13f09 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -16,7 +16,10 @@ concurrency: defaults: run: - shell: bash + shell: bash --noprofile --norc -euo pipefail {0} + +env: + MISE_ENV: dart,dotnet,java,js,python,rust,zig jobs: check: @@ -29,35 +32,10 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Install mise - uses: taiki-e/install-action@v2 + - name: Install mise + tools + uses: ./.github/actions/install-mise-tools with: - tool: cargo-binstall,mise@2026.6.5 - - - name: Select all language envs - run: echo "MISE_ENV=$(mise run print-all-langs)" >> "$GITHUB_ENV" - - # Optional npm backend, installed before the main `mise install`. - # See [tasks.setup-aube] in .mise/config.toml for the full rationale. - - name: Install aube (optional npm backend, allowed to fail) - continue-on-error: true - run: | - mise settings experimental=true - mise run setup-aube - env: - GITHUB_TOKEN: ${{ github.token }} - MISE_HTTP_TIMEOUT: "120" - - - name: Install mise tools - run: | - mise run preinstall - mise install - env: - GITHUB_TOKEN: ${{ github.token }} - # GitHub release downloads occasionally take longer than mise's - # default 30s HTTP timeout; bump it so transient network slowness - # doesn't fail the whole `mise install` step. - MISE_HTTP_TIMEOUT: "120" + github-token: ${{ github.token }} - name: Prefetch Rust dependencies run: mise run prefetch:rust diff --git a/.github/workflows/dependencies.yaml b/.github/workflows/dependencies.yaml index eea0754..cab0ccf 100644 --- a/.github/workflows/dependencies.yaml +++ b/.github/workflows/dependencies.yaml @@ -10,6 +10,7 @@ name: dependencies - config/deny.toml - config/osv-scanner.toml - .github/workflows/dependencies.yaml + - .github/actions/install-mise/** workflow_dispatch: permissions: @@ -21,10 +22,18 @@ concurrency: defaults: run: - shell: bash + shell: bash --noprofile --norc -euo pipefail {0} + +env: + MISE_ENV: dart,dotnet,java,js,python,rust,zig jobs: dependencies: + # Use `ubuntu-latest`, not `ubuntu-slim`, for this workflow. + # Although every step here is metadata/network-bound (no Rust compile), `taiki-e/install-action` itself assumes + # a standard runner-image FHS (e.g. ~/.cargo/bin) and fails on slim's bare container with + # `I/O Error: No such file or directory` while staging cargo-unmaintained. Slim only fits workflows whose first + # step doesn't need that scaffolding. runs-on: ubuntu-latest timeout-minutes: 25 steps: @@ -34,13 +43,10 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Install tools - uses: taiki-e/install-action@v2 + - name: Install mise + dependency-scan tools + uses: ./.github/actions/install-mise with: - tool: cargo-deny,cargo-unmaintained,coreutils,mise@2026.6.5,osv-scanner,ripgrep - - - name: Trust mise config - run: mise trust + install-action-tools: cargo-deny,cargo-unmaintained,coreutils,osv-scanner,ripgrep - name: Generate config/osv-scanner.toml from config/deny.toml run: mise run gen:osv-scanner @@ -51,13 +57,12 @@ jobs: - name: osv-scanner run: mise run osv-scanner - # `cargo unmaintained` persists per-repository archival/last-commit - # lookups under `$XDG_CACHE_HOME/cargo-unmaintained` (default - # `~/.cache/cargo-unmaintained`, with a versioned `v2/` subdir). On a - # cold runner the per-dependency GitHub round-trips take ~12 min, which - # nearly blew the old 15-min timeout; restoring this cache keeps reruns - # fast. Key on Cargo.lock so a dependency change refreshes; the prefix - # restore-key reuses the previous cache for crates that didn't change. + # Restore cached `cargo unmaintained` lookups to keep reruns fast. + # `cargo unmaintained` persists per-repository archival/last-commit lookups under + # `$XDG_CACHE_HOME/cargo-unmaintained` (default `~/.cache/cargo-unmaintained`, with a versioned `v2/` subdir). + # On a cold runner the per-dependency GitHub round-trips take ~12 min, which nearly blew the old 15-min + # timeout. Key on Cargo.lock so a dependency change refreshes; the prefix restore-key reuses the previous + # cache for crates that didn't change. - name: Cache cargo-unmaintained lookups uses: actions/cache@v4 with: @@ -66,10 +71,10 @@ jobs: restore-keys: | cargo-unmaintained- - # `cargo unmaintained` queries the GitHub API to check whether each - # declared upstream repository is archived. Without a token the - # archival check is skipped with a warning, leaving an unmaintained - # archived crate undetected. + # Provide a token so `cargo unmaintained` can run its archival check. + # `cargo unmaintained` queries the GitHub API to check whether each declared upstream repository is + # archived. Without a token the archival check is skipped with a warning, leaving an unmaintained archived + # crate undetected. - name: cargo unmaintained env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/docker-linux.yaml b/.github/workflows/docker-linux.yaml index cd77e89..1ea771a 100644 --- a/.github/workflows/docker-linux.yaml +++ b/.github/workflows/docker-linux.yaml @@ -17,7 +17,10 @@ concurrency: defaults: run: - shell: bash + shell: bash --noprofile --norc -euo pipefail {0} + +env: + MISE_ENV: dart,dotnet,java,js,python,rust,zig jobs: build: @@ -27,13 +30,15 @@ jobs: fail-fast: false matrix: base: - - ubuntu:22.04 - - ubuntu:24.04 - - ubuntu:26.04 + - amazonlinux:2023 + - azurelinux v3.0 - debian:bookworm - debian:trixie - fedora:42 - - mcr.microsoft.com/azurelinux/base/core:3.0 + - opensuse/leap:15.6 + - ubuntu:22.04 + - ubuntu:24.04 + - ubuntu:26.04 name: build (${{ matrix.base }}) steps: - name: Checkout @@ -42,12 +47,16 @@ jobs: fetch-depth: 1 persist-credentials: false - # The image is huge (every language toolchain + prefetched models + a full - # debug build, incl. aws-lc-sys's large C objects), and its peak `target/` - # overruns a single runner disk. Reclaim the unused preinstalled SDKs and - # concatenate the freed root space with /mnt into one LVM volume mounted at - # Docker's data dir, then restart Docker so the build uses the combined space. - - name: Maximize build space (combine root + /mnt for Docker) + - name: Show MISE_ENV + run: echo "MISE_ENV=$MISE_ENV" + + # Relocate Docker's data dir onto an LVM volume to maximize build space. + # jlumbroso/free-disk-space (which test.yaml uses) only frees space on the existing root partition. This + # workflow needs more: the image build writes to /var/lib/docker, so we have to relocate Docker's data dir + # onto an LVM volume that concatenates the freed root space with /mnt. easimon is the only maintained action + # that does that LVM remount, so we stay on it here despite its rougher edges (hard-fails on runners without + # /mnt or without a Docker daemon -- not a concern for this workflow's ubuntu-latest matrix, which has both). + - name: Maximize build space uses: easimon/maximize-build-space@v10 with: root-reserve-mb: 4096 @@ -63,25 +72,34 @@ jobs: - name: Restart Docker on the maximized volume run: sudo systemctl restart docker + - name: Resolve BASE_IMAGE + env: + MATRIX_BASE: ${{ matrix.base }} + run: | + if [ "$MATRIX_BASE" = "azurelinux v3.0" ]; then + echo "BASE_IMAGE=mcr.microsoft.com/azurelinux/base/core:3.0" >> "$GITHUB_ENV" + else + echo "BASE_IMAGE=$MATRIX_BASE" >> "$GITHUB_ENV" + fi + - name: Build stage test env: - BASE_IMAGE: ${{ matrix.base }} DOCKER_BUILDKIT: "1" GITHUB_TOKEN: ${{ github.token }} - run: docker build --target test --build-arg BASE_IMAGE --secret id=gh_token,env=GITHUB_TOKEN -t et-test . - - # The check stage needs `.git/` + every tracked Dockerfile. The main - # `.dockerignore` excludes both (`**/Dockerfile*` so Dockerfiles in - # subdirs don't bloat the build context), so we pre-stage them into a - # directory and hand it to docker as a named build context (`extras`). - # The check stage does `COPY --from=extras . ./` to land them in - # /workspace. `cp --parents` preserves the subdir path of each Dockerfile - # (e.g. services/ws-server/Dockerfile), so hadolint's `git ls-files - # '*Dockerfile' '*Dockerfile.*' | xargs hadolint` finds every entry on - # disk -- a top-level cp would have left subdir Dockerfiles unfindable. + run: | + args="--target test --build-arg BASE_IMAGE --build-arg MISE_ENV" + args="$args --secret id=gh_token,env=GITHUB_TOKEN -t et-test" + docker build $args . + + # The check stage needs `.git/` + every tracked Dockerfile. + # The main `.dockerignore` excludes both (`**/Dockerfile*` so Dockerfiles in subdirs don't bloat the build + # context), so we pre-stage them into a directory and hand it to docker as a named build context (`extras`). + # The check stage does `COPY --from=extras . ./` to land them in /workspace. `cp --parents` preserves the + # subdir path of each Dockerfile (e.g. services/ws-server/Dockerfile), so hadolint's + # `git ls-files '*Dockerfile' '*Dockerfile.*' | xargs hadolint` finds every entry on disk -- a top-level cp + # would have left subdir Dockerfiles unfindable. - name: Build stage check env: - BASE_IMAGE: ${{ matrix.base }} CTX: target/check-ctx DOCKER_BUILDKIT: "1" TAG: et-check @@ -90,10 +108,51 @@ jobs: mkdir -p "$CTX" cp -r .git "$CTX/" git ls-files '*Dockerfile' '*Dockerfile.*' | xargs -I{} cp --parents {} "$CTX/" - docker build --target check --build-arg BASE_IMAGE --build-context extras=$CTX --secret id=gh_token -t $TAG . + args="--target check --build-arg BASE_IMAGE --build-arg MISE_ENV" + args="$args --build-context extras=$CTX --secret id=gh_token -t $TAG" + docker build $args . + + # Capture image + intermediate stage sizes after the builds. + # This also runs after the test/check runs leave any -rm'd container layers behind. `always()` so we still + # get the breakdown when a downstream step ran the disk dry; the typical failure mode (test-ws-web-runner + # compile inside `docker run`) leaves the host's image catalogue intact and this step is what surfaces + # which layer ballooned. -a includes intermediate/dangling layers each stage produces; `system df -v` + # totals per cache type (Images, Build Cache, Containers, Volumes). + - name: Docker disk usage (debug) + if: always() + run: | + echo "::group::docker images -a" + docker images -a + echo "::endgroup::" + echo "::group::docker system df -v" + docker system df -v + echo "::endgroup::" + echo "::group::df -h /var/lib/docker" + df -h /var/lib/docker || true + echo "::endgroup::" + # Report which paths inside the et-test image are eating space. + # `du --threshold=10M --max-depth=2` for a top-level subtree map (catches install dirs, tool caches, + # /var stragglers); `find ... -size +50M` for the individual heavyweights. Run in a throwaway container + # so the host's writable layer doesn't tilt the numbers; find/du stderr noise (unreadable /proc entries + # etc.) prints to the log but doesn't affect the sort pipeline. + echo "::group::et-test: largest dirs (depth 2, >=10M)" + docker run --rm et-test sh -c "du -h --threshold=10M --max-depth=2 / | sort -h | tail -50" + echo "::endgroup::" + echo "::group::et-test: individual files >50M" + docker run --rm et-test sh -c "find / -xdev -type f -size +50M -exec du -h {} + | sort -h | tail -50" + echo "::endgroup::" + + # Run the test phases as two separate `docker run` invocations. + # Each starts a fresh container with an empty writable layer, so the per-phase compile target/ doesn't share + # disk with the other phase. Splits cargo-test's two halves (workspace minus et-ws-web-runner, then + # et-ws-web-runner serialized) for clearer attribution when a tight-disk runner fails. test-ws-web-runner + # already skips itself on hosts that can't satisfy its Deno-runtime requirements, so this step is a no-op on + # those platforms. + - name: Run cargo-test-other + run: docker run --rm et-test mise run cargo-test-other - - name: Run the test suite - run: docker run --rm et-test + - name: Run test-ws-web-runner + run: docker run --rm et-test mise run test-ws-web-runner - name: Run mise check run: docker run --rm et-check diff --git a/.github/workflows/docker-windows.yaml b/.github/workflows/docker-windows.yaml index fa62d52..4f43288 100644 --- a/.github/workflows/docker-windows.yaml +++ b/.github/workflows/docker-windows.yaml @@ -4,8 +4,10 @@ name: docker-windows "on": pull_request: paths: + - .github/actions/free-disk-space-windows/** - .github/workflows/docker-windows.yaml - Dockerfile.nanoserver + - Dockerfile.windows workflow_dispatch: permissions: @@ -17,7 +19,10 @@ concurrency: defaults: run: - shell: bash + shell: bash --noprofile --norc -euo pipefail {0} + +env: + MISE_ENV: dart,dotnet,java,js,python,rust,zig jobs: build: @@ -27,14 +32,24 @@ jobs: matrix: include: - runner: windows-2022 + base: nanoserver + windows_version: ltsc2022 + - runner: windows-2025 + base: nanoserver + windows_version: ltsc2025 + - runner: windows-2022 + base: servercore + windows_version: ltsc2022 - runner: windows-2025 - build_arg: "--build-arg WINDOWS_VERSION=ltsc2025" - name: build (${{ matrix.runner }}) + base: servercore + windows_version: ltsc2025 + name: build (${{ matrix.runner }}, ${{ matrix.base }}) timeout-minutes: 120 env: - # The classic Windows builder on windows-2022 can't substitute build-args into the Dockerfile's RUN, - # and mise's prebuilt "latest" zip is stale (2026.3.0, too old for the config). - # 2026.6.5 is the first release with auto_env (loads .mise/config.windows.toml). + # Pin the mise version because the prebuilt "latest" zip is too old. + # The classic Windows builder on windows-2022 can't substitute build-args into the Dockerfile's RUN, and + # mise's prebuilt "latest" zip is stale (2026.3.0, too old for the config). 2026.6.5 is the first release + # with auto_env (loads .mise/config.windows.toml). MISE_VERSION: "2026.6.5" steps: - name: Checkout @@ -43,19 +58,108 @@ jobs: fetch-depth: 1 persist-credentials: false - # Hosted Windows runners don't reliably leave the Docker daemon running, so - # the build can fail connecting to the docker_engine pipe. Start it (no-op - # if already running) and confirm connectivity before building. + - name: Show MISE_ENV + if: matrix.base == 'servercore' + run: echo "MISE_ENV=$MISE_ENV" + + # Start the Docker daemon and confirm connectivity before building. + # Hosted Windows runners don't reliably leave the Docker daemon running, so the build can fail connecting + # to the docker_engine pipe. Start it (no-op if already running) and confirm connectivity before building. - name: Start the Docker daemon run: | sc query docker | grep -q RUNNING || net start docker docker version + - name: Free disk space on Windows runner + uses: ./.github/actions/free-disk-space-windows + with: + keep-image: mcr.microsoft.com/windows/${{ matrix.base }}:${{ matrix.windows_version }} + - name: Prepare mise and Github token for the build context run: | v="${{ env.MISE_VERSION }}" curl -fsSL -o mise.zip "https://github.com/jdx/mise/releases/download/v$v/mise-v$v-windows-x64.zip" printf '%s' "${{ github.token }}" > gh_token - - name: Build stage precompile - run: docker build -f Dockerfile.nanoserver ${{ matrix.build_arg }} --target precompile . + # Pick which Dockerfile each matrix lane builds. + # The build is split into `build-minimal` -> diagnostics -> `precompile` so the et-rp (rustpython) + # diagnostic runs between the two stages. Both `docker build` calls share BuildKit's layer cache; the + # second one rebuilds only the layers above `build-minimal`, so the split adds the diagnostic step's + # runtime, not a second full compile. + - name: Resolve Dockerfile + image tag + id: dockerfile + run: | + case "${{ matrix.base }}" in + nanoserver) dockerfile=Dockerfile.nanoserver ;; + servercore) dockerfile=Dockerfile.windows ;; + *) echo "unknown matrix.base: ${{ matrix.base }}" >&2; exit 2 ;; + esac + tag="et-windows:${{ matrix.runner }}-${{ matrix.base }}-build-minimal" + echo "dockerfile=$dockerfile" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Build stage build-minimal + env: + DOCKERFILE: ${{ steps.dockerfile.outputs.dockerfile }} + IMAGE_TAG: ${{ steps.dockerfile.outputs.tag }} + run: | + args="-f $DOCKERFILE --build-arg MISE_ENV" + if [ "${{ matrix.runner }}" != "windows-2022" ]; then + args="$args --build-arg WINDOWS_VERSION=${{ matrix.windows_version }}" + fi + args="$args --target build-minimal -t $IMAGE_TAG" + docker build $args . + + # Diagnose whether the et-rp (rustpython) binary actually runs inside this base image. + # servercore expected: works. nanoserver expected: fails with 0xc0000139 (STATUS_ENTRYPOINT_NOT_FOUND) + # when rustpython.exe tries to resolve symbols from Nano-stripped shell32/winmm/propsys. Captured here + # rather than only via the preinstall fallback so the comparison sits side-by-side in the same workflow + # run. continue-on-error so a Nano failure here doesn't pre-empt the precompile build that follows. + - name: Diagnose et-rp (rustpython) in build-minimal + continue-on-error: true + env: + IMAGE_TAG: ${{ steps.dockerfile.outputs.tag }} + run: | + echo "::group::which rustpython" + docker run --rm "$IMAGE_TAG" cmd /c "mise where http:et-rp & where rustpython" + echo "::endgroup::" + echo "::group::rustpython --version" + docker run --rm "$IMAGE_TAG" cmd /c "mise exec -- rustpython --version" + echo "::endgroup::" + echo "::group::rustpython -c hello" + docker run --rm "$IMAGE_TAG" cmd /c "mise exec -- rustpython -c \"print('et-rp ok')\"" + echo "::endgroup::" + + # The build target depends on the base image. + # nanoserver stops at `precompile` (no python/dotnet/test-runtime; the test stage doesn't exist there), + # servercore goes all the way through `test` so the next two steps can `docker run` the suite. Both targets + # compose with the precompile-stage layer cache from the build-minimal step above. + - name: Build stage precompile (nanoserver) / test (servercore) + env: + DOCKERFILE: ${{ steps.dockerfile.outputs.dockerfile }} + run: | + if [ "${{ matrix.base }}" = "servercore" ]; then + target=test + tag="-t et-windows-test" + else + target=precompile + tag="" + fi + args="-f $DOCKERFILE --build-arg MISE_ENV" + if [ "${{ matrix.runner }}" != "windows-2022" ]; then + args="$args --build-arg WINDOWS_VERSION=${{ matrix.windows_version }}" + fi + args="$args --target $target $tag" + docker build $args . + + # Run the test suite split the same way docker-linux does. + # cargo-test-other (workspace minus et-ws-web-runner) and test-ws-web-runner (Deno-runtime gated, self-skips + # on hosts that can't satisfy it). Each runs in a fresh container so the per-phase compile target/ doesn't + # share the writable layer with the other phase. servercore-only -- Nano has no test stage. + - name: Run cargo-test-other + if: matrix.base == 'servercore' + run: docker run --rm et-windows-test mise run cargo-test-other + + - name: Run test-ws-web-runner + if: matrix.base == 'servercore' + run: docker run --rm et-windows-test mise run test-ws-web-runner diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1a09c89..c059a6f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,10 +12,12 @@ name: test options: - all - ubuntu-latest + - ubuntu-26.04 - ubuntu-24.04-arm - macos-latest - macos-26-intel - windows-latest + - windows-11-arm permissions: contents: read @@ -26,19 +28,35 @@ concurrency: defaults: run: - shell: bash + shell: bash --noprofile --norc -euo pipefail {0} + +env: + MISE_ENV: "dart,dotnet,java,js,python,rust,zig" jobs: rust: runs-on: ${{ matrix.os }} timeout-minutes: ${{ matrix.timeout }} + # Override the workflow-level `shell: bash` default for Windows runs. + # This uses the busybox-w32 ash that mise installs in `_setup_all` (the same shell `MISE_BASH_PATH` already + # points at for `shell = "bash"` tasks on the Windows Docker images). Avoids Git Bash's MSYS fork-emulation + # cygheap pathology on long-running mise task chains. The path matches the version pin in + # `.mise/config.windows.toml`'s [tools."http:busybox"] entry. + defaults: + run: + shell: >- + ${{ startsWith(matrix.os, 'windows') && + 'C:\Users\runneradmin\AppData\Local\mise\installs\http-busybox\1.37.0\ash.exe -euo pipefail {0}' + || 'bash --noprofile --norc -euo pipefail {0}' }} strategy: fail-fast: false + # Compute the OS list here in the strategy block, where `github.*` IS in scope. # `matrix` context isn't available in job-level `if:`, so we compute the - # OS list right here in the strategy block (where `github.*` IS in scope): - # pull_request → ubuntu-latest + ubuntu-24.04-arm + macos-latest - # (skip slow Intel Mac & Windows) - # workflow_dispatch → the OS picked in the dropdown, or all of them + # OS list right here: + # pull_request -> ubuntu-latest + ubuntu-24.04-arm + macos-latest + # + windows-latest (skip slow Intel Mac; + # ubuntu-26.04 + windows-11-arm are dispatch-only) + # workflow_dispatch -> the OS picked in the dropdown, or all of them # The combos are emitted as a fully-formed list of `{os, timeout}` # objects so the `include:` "unmatched entries append as new jobs" # behavior doesn't bring back the OSes we just filtered out. @@ -46,38 +64,29 @@ jobs: include: ${{ fromJSON( github.event_name == 'workflow_dispatch' && ( github.event.inputs.os == 'all' && format( - '[{0},{1},{2},{3},{4}]', - '{"os":"ubuntu-latest","timeout":30}', - '{"os":"ubuntu-24.04-arm","timeout":30}', + '[{0},{1},{2},{3},{4},{5},{6}]', + '{"os":"ubuntu-latest","timeout":40}', + '{"os":"ubuntu-26.04","timeout":40}', + '{"os":"ubuntu-24.04-arm","timeout":40}', '{"os":"macos-latest","timeout":45}', '{"os":"macos-26-intel","timeout":60}', - '{"os":"windows-latest","timeout":60}' + '{"os":"windows-latest","timeout":60}', + '{"os":"windows-11-arm","timeout":60}' ) - || github.event.inputs.os == 'ubuntu-latest' && '[{"os":"ubuntu-latest","timeout":30}]' - || github.event.inputs.os == 'ubuntu-24.04-arm' && '[{"os":"ubuntu-24.04-arm","timeout":30}]' + || github.event.inputs.os == 'ubuntu-latest' && '[{"os":"ubuntu-latest","timeout":40}]' + || github.event.inputs.os == 'ubuntu-26.04' && '[{"os":"ubuntu-26.04","timeout":40}]' + || github.event.inputs.os == 'ubuntu-24.04-arm' && '[{"os":"ubuntu-24.04-arm","timeout":40}]' || github.event.inputs.os == 'macos-latest' && '[{"os":"macos-latest","timeout":45}]' || github.event.inputs.os == 'macos-26-intel' && '[{"os":"macos-26-intel","timeout":60}]' || github.event.inputs.os == 'windows-latest' && '[{"os":"windows-latest","timeout":60}]' + || github.event.inputs.os == 'windows-11-arm' && '[{"os":"windows-11-arm","timeout":60}]' ) - || format( - '[{0},{1},{2}]', - '{"os":"ubuntu-latest","timeout":30}', - '{"os":"ubuntu-24.04-arm","timeout":30}', - '{"os":"macos-latest","timeout":45}' - ) + || format('[{0},{1},{2},{3}]', + '{"os":"ubuntu-latest","timeout":40}', + '{"os":"ubuntu-24.04-arm","timeout":40}', + '{"os":"macos-latest","timeout":45}', + '{"os":"windows-latest","timeout":60}') ) }} - env: - # Windows-only: skip the two cargo: source-builds that need a fully-set-up - # gnullvm toolchain at install time. The Nano docker (Dockerfile.nanoserver) - # sets the same flag; on the windows-latest runner here the gnullvm - # rust-std isn't reliably in place when cargo install runs them, leading to - # the rustc diagnostic: - # error[E0463]: can't find crate for `core` - # = note: the `x86_64-pc-windows-gnullvm` target may not be installed - # Both are dev-only tools (cargo-expand: macro debugging; dart-typegen: - # used only by `mise run gen:dart-ws`, which the dart config skips on - # Windows anyway); neither is in the test suite's hot path. - MISE_DISABLE_TOOLS: ${{ startsWith(matrix.os, 'windows') && 'cargo:cargo-expand,cargo:dart-typegen' || '' }} steps: - name: Checkout uses: actions/checkout@v4 @@ -85,57 +94,52 @@ jobs: fetch-depth: 1 persist-credentials: false - # Reclaim the large preinstalled toolchains we never use (~30 GB). - # `|| true` so a missing dir (e.g. on the arm image) is not fatal. - - name: Free up disk space (Linux) + - name: Free disk space on Linux if: runner.os == 'Linux' - run: | - sudo rm -rf /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL /usr/share/swift || true - sudo docker image prune --all --force || true - df -h / + uses: jlumbroso/free-disk-space@v1.3.1 + + - name: Free disk space on Windows + if: runner.os == 'Windows' + uses: ./.github/actions/free-disk-space-windows - name: Install Mesa Vulkan drivers (lavapipe) if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y mesa-vulkan-drivers + sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers - - name: Install mise - uses: taiki-e/install-action@v2 + - name: Install mise + tools + uses: ./.github/actions/install-mise-tools + timeout-minutes: 20 with: - tool: cargo-binstall,mise@2026.6.5 - - - name: Select all language envs - run: echo "MISE_ENV=$(mise run print-all-langs)" >> "$GITHUB_ENV" - - # Optional npm backend, installed before the main `mise install`. - # See [tasks.setup-aube] in .mise/config.toml for the full rationale. - - name: Install aube (optional npm backend, allowed to fail) - continue-on-error: true - run: | - mise settings experimental=true - mise run setup-aube - env: - GITHUB_TOKEN: ${{ github.token }} - MISE_HTTP_TIMEOUT: "120" - - - name: Install mise tools - run: | - mise run preinstall - mise install - env: - GITHUB_TOKEN: ${{ github.token }} - # Match check.yaml: bump mise's 30s HTTP timeout so GitHub-release - # downloads don't fail the install on transient slowness. - MISE_HTTP_TIMEOUT: "120" + github-token: ${{ github.token }} + # GITHUB_TOKEN raises the rate-limit ceiling on mise's fetch of the rp-v rustpython_wasm tarball. + # (The fetch is [tools."http:rp-wasm"], via mise's http backend against github.com release assets.) - name: Prefetch dependencies + timeout-minutes: 15 env: GITHUB_TOKEN: ${{ github.token }} run: mise run prefetch-ci + # MISE_JOBS=1 on Windows serializes mise's task scheduler so parallel `build-ws-*-module` deps don't race. + # The contention is on cargo's `.cargo/registry/index` lock -- on Linux/macOS cargo waits gracefully, but + # on Windows it fails fast with + # `cargo metadata exited with an error: Blocking waiting for file lock on package cache` + # (observed in job 82448665293, where build-ws-graphics-info-module + build-ws-nfc-module + + # build-ws-har1-module all kicked off in parallel). Conditional via a separate step rather than a step-level + # `env:` because mise rejects an empty MISE_JOBS value ("cannot parse integer from empty string"), and GHA + # has no `if:` on individual env entries. + - name: Serialize mise tasks on Windows + if: runner.os == 'Windows' + run: echo "MISE_JOBS=1" >> "$GITHUB_ENV" + - name: Build WASM modules + timeout-minutes: 25 + env: + GITHUB_TOKEN: ${{ github.token }} run: mise run build-modules - name: Run tests + timeout-minutes: 30 run: mise run test diff --git a/.github/workflows/upstream-cache.yaml b/.github/workflows/upstream-cache.yaml new file mode 100644 index 0000000..69bd5c6 --- /dev/null +++ b/.github/workflows/upstream-cache.yaml @@ -0,0 +1,682 @@ +--- +name: upstream-cache + +# Manually-dispatched maintainer workflow that builds + publishes upstream-sourced binary artifacts. +# Artifacts go to GitHub releases owned by this repo, so consumers (CI + workstations) can `mise install` prebuilts +# instead of compiling from source. One job per cached upstream; add more jobs as new upstreams need caching. +# +# - rustpython: native binary per host triple + the platform-agnostic +# wasm tarball, published to the rp-v release. Forward-stocked -- +# we publish even if [tools."http:et-rp".platforms.] isn't +# wired up yet, so consumers can migrate off cargo:rustpython +# whenever it's convenient. + +"on": + pull_request: + paths: + - .github/workflows/upstream-cache.yaml + - .github/actions/install-mise/** + - .github/actions/install-mise-tools/** + - config/upstream-cache/data.toml + workflow_dispatch: + inputs: + os: + description: "OS(es) to publish for" + type: choice + default: all + options: + - all + - ubuntu-latest + - ubuntu-24.04-arm + - macos-latest + - macos-26-intel + - windows-latest + +permissions: + contents: read + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +defaults: + run: + shell: bash --noprofile --norc -euo pipefail {0} + +env: + MISE_ENV: dart,dotnet,java,js,python,rust,zig + +jobs: + rustpython: + runs-on: ${{ matrix.os }} + # Job-level write so gh release upload can push assets. + # The workflow default is contents:read. + permissions: + contents: write + timeout-minutes: 60 + env: + MISE_ENV: maint,rust + strategy: + fail-fast: false + matrix: + # Pick the matrix from the trigger: a specific dispatched OS, else every platform. + # workflow_dispatch with a specific OS -> just that one; everything + # else (workflow_dispatch=all, push, pull_request) -> every platform. + os: ${{ fromJSON( + (github.event_name == 'workflow_dispatch' + && github.event.inputs.os != 'all' + && format('["{0}"]', github.event.inputs.os)) + || '["ubuntu-latest","ubuntu-24.04-arm","macos-latest","macos-26-intel","windows-latest"]' + ) }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + persist-credentials: false + + # Cheap host-only detection of whether this matrix entry has anything to publish. + # Reads the pinned short SHA from config.toml via sed, derives the triple from matrix.os, and queries rp-v1 + # with the runner's preinstalled gh CLI. Runs before any install so the typical case (asset already + # present) skips disk-free, mise install, RustPython clone, and the cargo source build below entirely. + # `work=yes` when this matrix entry has something to do: a missing native asset for its triple, or (only + # on ubuntu-latest) a missing wasm asset. Subsequent steps gate on `work`. Logic mirrors the + # `rp-detect-missing` mise task (kept for local maintainer use, where mise's tools are already there). + - name: Detect missing rp-v1 assets + id: rp_check + env: + GITHUB_TOKEN: ${{ github.token }} + # Pass MATRIX_OS via env, not `${{ matrix.os }}` inline, to satisfy zizmor. + # This way zizmor's template-injection rule doesn't flag matrix expansion inside the bash run block. + MATRIX_OS: ${{ matrix.os }} + run: | + case "$MATRIX_OS" in + ubuntu-latest) triple=x86_64-unknown-linux-gnu ;; + ubuntu-24.04-arm) triple=aarch64-unknown-linux-gnu ;; + macos-latest) triple=aarch64-apple-darwin ;; + macos-26-intel) triple=x86_64-apple-darwin ;; + windows-latest) triple=x86_64-pc-windows-msvc ;; + *) echo "unknown matrix.os: $MATRIX_OS" >&2; exit 1 ;; + esac + # Extract the pinned version with awk (portable across BSD/macOS + GNU/Linux). + # Enter the [tools."http:et-rp"] section, capture its `version = "..."`, stop on the next `[` table + # header. BSD sed rejects the `/range/{s/...p}` one-liner -- "bad flag in substitute". + short=$(awk ' + /^\[tools\."http:et-rp"\]$/ { in_section=1; next } + in_section && /^\[/ { exit } + in_section && /^version = / { gsub(/^version = "|"$/, ""); print; exit } + ' .mise/config.toml) + native_asset="rustpython-${short}-${triple}.tar.gz" + wasm_asset="rustpython-wasm-${short}.tar.gz" + repo=edge-toolkit/core + assets=$(gh release view rp-v1 --repo "$repo" --json assets --jq '.assets[].name' 2>/dev/null || true) + native_missing=true; wasm_missing=true + if echo "$assets" | grep -Fxq "$native_asset"; then native_missing=false; fi + if echo "$assets" | grep -Fxq "$wasm_asset"; then wasm_missing=false; fi + work= + [ "$native_missing" = "true" ] && work=yes + [ "$wasm_missing" = "true" ] && [ "$MATRIX_OS" = "ubuntu-latest" ] && work=yes + { + echo "short=$short" + echo "triple=$triple" + echo "native_missing=$native_missing" + echo "wasm_missing=$wasm_missing" + echo "work=$work" + } >> "$GITHUB_OUTPUT" + echo "[rp-check] triple=$triple short=$short" + echo "[rp-check] native_missing=$native_missing wasm_missing=$wasm_missing work=${work:-no}" + if [ -z "$work" ]; then + echo "::notice::All rp-v1 assets present for $triple; skipping install + publish." + fi + + # Everything below this point runs only when there's actually something to upload. + # `repo.fork != true` skips fork PRs (GHA gives them a read-only GITHUB_TOKEN, so the upload would fail; + # building the asset would be wasted compute). + - name: Free up disk space (Linux) + if: |- + steps.rp_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + && runner.os == 'Linux' + uses: jlumbroso/free-disk-space@v1.3.1 + + - name: Install mise + tools + if: |- + steps.rp_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + uses: ./.github/actions/install-mise-tools + with: + github-token: ${{ github.token }} + + # RP_SHORT comes via env, not `${{ }}` inside the run block, to keep zizmor happy. + # This keeps zizmor's template-injection rule happy -- step outputs are treated as potentially + # attacker-controllable when expanded inside bash. `repo.fork != true` skips fork PRs (GHA gives them a + # read-only GITHUB_TOKEN, so the upload would fail; building the asset would be wasted compute). + - name: Clone RustPython at pinned SHA + if: |- + github.event.pull_request.head.repo.fork != true + && (steps.rp_check.outputs.native_missing == 'true' + || (steps.rp_check.outputs.wasm_missing == 'true' && matrix.os == 'ubuntu-latest')) + env: + RP_SHORT: ${{ steps.rp_check.outputs.short }} + run: | + git clone https://github.com/RustPython/RustPython.git "$HOME/rust/RustPython" + git -C "$HOME/rust/RustPython" checkout "$RP_SHORT" + + # Windows-only step that sources the MSVC dev env for the rustpython build. + # This puts `rc.exe` (Windows Resource Compiler) on PATH for the rustpython build. The `embed_resource` + # crate in rustpython's build.rs targets msvc and otherwise warns: + # warning: rustpython@0.5.0: Failed to compile Windows resources: + # The system cannot find the path specified. (os error 3) + # The resulting .exe still runs (resource embedding is best-effort), but the icon + version metadata get + # dropped. ilammy/msvc-dev-cmd is the maintained equivalent of `call vcvars64.bat` for GHA. + - name: Set up MSVC dev env for rc.exe (Windows) + if: |- + matrix.os == 'windows-latest' + && github.event.pull_request.head.repo.fork != true + && steps.rp_check.outputs.native_missing == 'true' + uses: ilammy/msvc-dev-cmd@v1 + + # Native rustpython is per-triple. + # Each matrix entry publishes its own host's binary if missing. + - name: Build + publish native rustpython for this host + if: |- + github.event.pull_request.head.repo.fork != true + && steps.rp_check.outputs.native_missing == 'true' + env: + GITHUB_TOKEN: ${{ github.token }} + run: mise run publish-rp-native-to-release + + # Wasm rustpython is platform-agnostic. + # Gate to a single canonical runner so multiple matrix jobs don't race-clobber the same asset. + - name: Build + publish wasm rustpython (ubuntu-latest only) + if: |- + github.event.pull_request.head.repo.fork != true + && steps.rp_check.outputs.wasm_missing == 'true' + && matrix.os == 'ubuntu-latest' + env: + GITHUB_TOKEN: ${{ github.token }} + run: mise run publish-rp-wasm-to-release + + # Windows-only augeas build. + # Augeas has no upstream Windows binaries anywhere (not in MSYS2/Cygwin/aqua/conda); the only way to get a + # native gpg-style PE binary is to build it ourselves under MSYS2. This job mirrors the rustpython pattern: + # detect missing asset, build from source, tar the install dir, and upload to the augeas-v1 GitHub release + # (which mise's `http:augeas` tool entry points at, per platforms.windows-x64). + # + # Source is the edge-toolkit augeas fork's `win` branch rather than the upstream 1.14.1 release tarball: the + # Windows port needs changes across many files (vendored glob, _WIN32 guards in augtool/transform, gnulib + # fsync, libtool -no-undefined, MIN/MAX fallbacks, a .gitattributes LF fix, etc.) that the released tarball + # doesn't carry. The fork builds green on its own CI across Linux/macOS/Windows, so we build it directly + # instead of maintaining a sprawling patch against the tarball. + augeas: + # All five platforms build from the same jayvdb/augeas `win` branch source. + # One fork, one set of portability patches, identical binaries across Linux/macOS/Windows. The fork's own + # build.yml is the recipe template (./autogen.sh --disable-gnulib-tests on Linux/macOS; MSYS2 + mingw-w64 + # CFLAGS workarounds on Windows). + runs-on: ${{ matrix.runner }} + permissions: + contents: write + timeout-minutes: 60 + env: + MISE_ENV: maint + AUG_VERSION: "1.14.1" + AUG_RELEASE_TAG: "augeas-v1" + AUG_FORK_REPO: "https://github.com/jayvdb/augeas.git" + AUG_REF: "win" + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + triple: x86_64-unknown-linux-gnu + archive_ext: tar.xz + - runner: ubuntu-24.04-arm + triple: aarch64-unknown-linux-gnu + archive_ext: tar.xz + - runner: macos-latest + triple: aarch64-apple-darwin + archive_ext: tar.xz + - runner: macos-26-intel + triple: x86_64-apple-darwin + archive_ext: tar.xz + - runner: windows-latest + triple: x86_64-pc-windows-mingw + archive_ext: tar.gz + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Detect missing augeas asset + id: aug_check + env: + GITHUB_TOKEN: ${{ github.token }} + MATRIX_TRIPLE: ${{ matrix.triple }} + MATRIX_EXT: ${{ matrix.archive_ext }} + run: | + # The filename carries no "augeas-" project prefix. + # The release tag + # (`augeas-v1`) already encodes the project. Saves URL length and + # keeps the `-.` shape uniform with the + # dart-typegen + gnupg-w32 assets. + asset="${AUG_VERSION}-${MATRIX_TRIPLE}.${MATRIX_EXT}" + repo=edge-toolkit/core + jq='.assets[].name' + assets=$(gh release view "$AUG_RELEASE_TAG" --repo "$repo" --json assets --jq "$jq" 2>/dev/null || true) + if echo "$assets" | grep -Fxq "$asset"; then + echo "::notice::$asset already in $AUG_RELEASE_TAG; skipping" + echo "work=" >> "$GITHUB_OUTPUT" + else + echo "work=yes" >> "$GITHUB_OUTPUT" + echo "asset=$asset" >> "$GITHUB_OUTPUT" + fi + + # On Linux, apt-get the build deps, identical set to the fork's build.yml `linux` job. + # Skipped on the macOS/Windows runners. + - name: Install Linux build deps + if: |- + steps.aug_check.outputs.work == 'yes' + && runner.os == 'Linux' + run: | + sudo apt-get update + pkgs="autoconf automake bison flex libtool-bin libxml2-dev libreadline-dev pkg-config" + sudo apt-get install -y --no-install-recommends $pkgs + + # On macOS, brew the build deps, mirroring the fork's build.yml `macos` job. + # CPPFLAGS/LDFLAGS point configure at GNU readline (libedit lacks + # rl_crlf/rl_replace_line/rl_char_is_quoted_p); modern bison required. + - name: Install macOS build deps + if: |- + steps.aug_check.outputs.work == 'yes' + && runner.os == 'macOS' + run: | + brew install autoconf automake libtool readline libxml2 pkg-config bison + + # On Windows, set up the MSYS2 toolchain + autotools, mirroring the fork's `windows` job. + # `msystem: MINGW64` targets x86_64-w64-mingw32. + - name: Set up MSYS2 (Windows) + if: |- + steps.aug_check.outputs.work == 'yes' + && runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: |- + base-devel + git + autoconf + automake + libtool + bison + flex + mingw-w64-x86_64-toolchain + mingw-w64-x86_64-libxml2 + mingw-w64-x86_64-readline + mingw-w64-x86_64-pkgconf + + # Unix build (Linux + macOS). + # Both use the same ./autogen.sh recipe with --disable-gnulib-tests (gnulib's bundled test harness fails + # to build against modern toolchains; unrelated to augeas itself). macOS needs the homebrew bison/readline + # overrides; Linux uses system libs. + - name: Build + package augeas (Unix) + if: |- + steps.aug_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + && runner.os != 'Windows' + env: + ASSET: ${{ steps.aug_check.outputs.asset }} + run: | + set -euo pipefail + srcdir="$RUNNER_TEMP/augeas-src" + installdir="$RUNNER_TEMP/install" + mkdir -p "$installdir" + clone_args="--depth 1 --branch $AUG_REF --recurse-submodules" + git clone $clone_args "$AUG_FORK_REPO" "$srcdir" + cd "$srcdir" + if [ "$RUNNER_OS" = "macOS" ]; then + bison_prefix=$(brew --prefix bison) + readline_prefix=$(brew --prefix readline) + libxml2_prefix=$(brew --prefix libxml2) + export PATH="$bison_prefix/bin:$PATH" + export PKG_CONFIG_PATH="$libxml2_prefix/lib/pkgconfig:$readline_prefix/lib/pkgconfig" + export CPPFLAGS="-I$readline_prefix/include" + export LDFLAGS="-L$readline_prefix/lib" + fi + autogen_args="--prefix=$installdir --disable-static --disable-gnulib-tests" + autogen_args="$autogen_args --disable-dependency-tracking" + ./autogen.sh $autogen_args + make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)" + make install + # Relocate the install tree to a portable form. + # libtool bakes the absolute --prefix into binaries and dylibs: Linux + # RUNPATH=/home/runner/work/_temp/install/lib and macOS LC_ID_DYLIB/LC_LOAD_DYLIB = + # /Users/runner/work/_temp/install/lib/libaugeas.0.dylib. Both break the moment we ship the binary + # elsewhere (dyld error "Library not loaded: /Users/runner/work/_temp/install/lib/libaugeas.0.dylib"). + # Post-process to relocatable form so the tarball is consumable from any directory. + cd "$installdir" + if [ "$RUNNER_OS" = "Linux" ]; then + sudo apt-get install -y --no-install-recommends patchelf + for f in bin/* lib/lib*.so*; do + [ -f "$f" ] && [ ! -L "$f" ] || continue + patchelf --set-rpath '$ORIGIN/../lib' "$f" 2>/dev/null || true + done + else + # On macOS, make the install tree relocatable and self-contained. + # Relocate internal lib install_names + cross-refs to + # @rpath, add @loader_path/../lib rpath to binaries, and + # bundle the Homebrew deps (readline + libxml2 + their own + # deps) so consumers don't need brew installed. + our_libs="" + for lib in lib/libaugeas*.dylib lib/libfa*.dylib; do + [ -f "$lib" ] && [ ! -L "$lib" ] || continue + base=$(basename "$lib") + install_name_tool -id "@rpath/$base" "$lib" + our_libs="$our_libs $base" + done + for f in lib/libaugeas*.dylib lib/libfa*.dylib bin/*; do + [ -f "$f" ] && [ ! -L "$f" ] || continue + file "$f" | grep -q "Mach-O" || continue + for old in $(otool -L "$f" 2>/dev/null | awk 'NR>1 {print $1}'); do + base=$(basename "$old") + for ours in $our_libs; do + if [ "$base" = "$ours" ]; then + install_name_tool -change "$old" "@rpath/$base" "$f" + fi + done + done + done + for f in bin/*; do + [ -f "$f" ] && [ ! -L "$f" ] || continue + file "$f" | grep -q "Mach-O" || continue + install_name_tool -add_rpath @loader_path/../lib "$f" 2>/dev/null || true + done + # Bundle the Homebrew dylib closure. + # otool -L on augtool tells us + # which absolute brew paths got baked in; copy each, rewrite + # its install_name + nested refs, and rewrite consumers. + brew_filter='NR>1 && $1 ~ /^\/(usr\/local|opt\/homebrew)\// {print $1}' + brew_deps_otool=$(otool -L bin/augtool lib/libaugeas*.dylib lib/libfa*.dylib 2>/dev/null) + queue=$(echo "$brew_deps_otool" | awk "$brew_filter" | sort -u) + seen="" + while [ -n "$queue" ]; do + next="" + for dep in $queue; do + base=$(basename "$dep") + # space-delimited seen list; membership = substring with sentinels + case " $seen " in *" $base "*) continue ;; esac + seen="$seen $base" + [ -f "$dep" ] || continue + cp -L "$dep" "lib/$base" + chmod +w "lib/$base" + install_name_tool -id "@rpath/$base" "lib/$base" + # add new transitive brew deps to the queue + nested_otool=$(otool -L "lib/$base" 2>/dev/null) + nested_refs=$(echo "$nested_otool" | awk "$brew_filter") + for nested in $nested_refs; do + next="$next $nested" + done + done + queue=$(echo "$next" | tr ' ' '\n' | sort -u | grep -v '^$' || true) + done + # Rewrite every Mach-O file's refs to brew paths -> @rpath/ + for f in lib/*.dylib bin/*; do + [ -f "$f" ] && [ ! -L "$f" ] || continue + file "$f" | grep -q "Mach-O" || continue + f_otool=$(otool -L "$f" 2>/dev/null) + for old in $(echo "$f_otool" | awk "$brew_filter"); do + install_name_tool -change "$old" "@rpath/$(basename "$old")" "$f" + done + done + # Bundled dylibs in lib/ need their own rpath so they can resolve each other. + # This rpath is the $ORIGIN equivalent. + for lib in lib/*.dylib; do + [ -f "$lib" ] && [ ! -L "$lib" ] || continue + install_name_tool -add_rpath @loader_path "$lib" 2>/dev/null || true + done + fi + # tar.xz: smaller than .gz, mise's http backend handles both. + (cd "$installdir" && tar -cJf "$RUNNER_TEMP/$ASSET" .) + ls -lh "$RUNNER_TEMP/$ASSET" + + # Windows build under MSYS2, kept as a separate step for several reasons. + # The shell differs (msys2 {0}), tar parses Windows-form paths as `host:path` (workarounds via cygpath -m + # + --force-local), and we bundle mingw runtime DLLs so the tarball is self-contained on a vanilla Windows + # host. + - name: Build + package augeas (Windows) + if: |- + steps.aug_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + && runner.os == 'Windows' + shell: msys2 {0} + env: + ASSET: ${{ steps.aug_check.outputs.asset }} + run: | + set -euo pipefail + # Convert $RUNNER_TEMP to a forward-slash Windows path with cygpath -m. + # MSYS2 bash interprets `D:\a\_temp` (Windows-form $RUNNER_TEMP) as escape sequences -- libtool's + # `-rpath must be absolute` check then rejects the mangled result. cygpath -m gives the Windows-form + # path with forward slashes (e.g. `D:/a/_temp`). + runner_temp_m=$(cygpath -m "$RUNNER_TEMP") + srcdir="$runner_temp_m/augeas-src" + installdir="$runner_temp_m/install" + mkdir -p "$installdir" + clone_args="--depth 1 --branch $AUG_REF --recurse-submodules" + git clone $clone_args "$AUG_FORK_REPO" "$srcdir" + cd "$srcdir" + # CFLAGS, mirroring the fork's build.yml windows job. + # -Wno-implicit-function-declaration: mingw-w64 gcc 14+ promoted + # this from warning to error; some autoconf probes still trip it. + # -Duint=unsigned: augeas headers use the BSD-ism `uint`, which + # mingw-w64's doesn't define unconditionally. + export CFLAGS="-Wno-implicit-function-declaration -Duint=unsigned" + autogen_args="--prefix=$installdir --disable-static --disable-debug" + autogen_args="$autogen_args --disable-dependency-tracking" + ./autogen.sh $autogen_args + make -j"$(nproc)" + make install + # Bundle the mingw runtime DLLs augeas links against. + # This makes the tarball self-contained on a vanilla Windows host. + dlls="libxml2-2.dll libreadline8.dll libncursesw6.dll" + dlls="$dlls libwinpthread-1.dll libgcc_s_seh-1.dll libstdc++-6.dll" + dlls="$dlls libiconv-2.dll libtermcap-0.dll zlib1.dll" + for dll in $dlls; do + src="/mingw64/bin/$dll" + if [ -f "$src" ]; then + cp -v "$src" "$installdir/bin/" + fi + done + # Disable tar's remote-path parsing with --force-local. + # tar parses any colon in the output path as `host:path` ("Cannot connect to D: resolve failed"); + # --force-local disables that. cd into installdir keeps the input arg as `.`. + (cd "$installdir" && tar --force-local -czf "$runner_temp_m/$ASSET" .) + ls -lh "$runner_temp_m/$ASSET" + + - name: Publish augeas asset to release + if: |- + steps.aug_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + env: + GITHUB_TOKEN: ${{ github.token }} + ASSET: ${{ steps.aug_check.outputs.asset }} + run: | + gh release upload "$AUG_RELEASE_TAG" --repo edge-toolkit/core --clobber "$RUNNER_TEMP/$ASSET" + + # Windows-only dart-typegen cargo build. + # dart-typegen has no prebuilt on crates.io; Linux/macOS source-build it via `cargo:dart-typegen` in + # config.dart.toml (os-scoped to those two). Windows can't take that path: cargo source-build against the + # gnullvm rust host trips `error[E0463]: can't find crate for 'core'`. Workaround: pre-build the binary here + # under the runner's stable-msvc toolchain (already on the windows-latest GHA image) and publish it so the + # Windows lane `http:` installs it (`http:dart-typegen` in config.windows.toml) instead of source-compiling. + # Same shape as the augeas job above. + dart-typegen: + runs-on: windows-latest + permissions: + contents: write + timeout-minutes: 30 + env: + MISE_ENV: maint,rust + DTG_VERSION: "0.1.13" + DTG_RELEASE_TAG: "dart-typegen-v1" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Detect missing dart-typegen asset + id: dtg_check + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + triple=x86_64-pc-windows-msvc + # No "dart-typegen-" project prefix; release tag already names it. + asset="${DTG_VERSION}-${triple}.tar.gz" + repo=edge-toolkit/core + jq='.assets[].name' + assets=$(gh release view "$DTG_RELEASE_TAG" --repo "$repo" --json assets --jq "$jq" 2>/dev/null || true) + if echo "$assets" | grep -Fxq "$asset"; then + echo "::notice::$asset already in $DTG_RELEASE_TAG; skipping" + echo "work=" >> "$GITHUB_OUTPUT" + else + echo "work=yes" >> "$GITHUB_OUTPUT" + echo "triple=$triple" >> "$GITHUB_OUTPUT" + echo "asset=$asset" >> "$GITHUB_OUTPUT" + fi + + # No explicit setup-rust step is needed for this build. + # The GHA windows-latest image ships rustup with the stable-x86_64-pc-windows-msvc toolchain pre-installed. + - name: Build dart-typegen via cargo install + if: |- + steps.dtg_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + env: + ASSET: ${{ steps.dtg_check.outputs.asset }} + run: | + set -euo pipefail + installdir="$RUNNER_TEMP/install" + mkdir -p "$installdir" + # Force the msvc target. + # The runner default-host is msvc, but a stray default-toolchain (e.g. on a custom runner) could + # otherwise pick gnullvm and trip the same build failure we're working around. + cargo_args="--version $DTG_VERSION --root $installdir" + cargo_args="$cargo_args --target x86_64-pc-windows-msvc --locked" + cargo install dart-typegen $cargo_args + ls "$installdir/bin" + # `cd` into the dir and tar a relative path to avoid the colon. + # Git Bash's bundled tar parses `D:\a\_temp/...` as `host:path` (the colon) and then errors out with + # `Cannot connect to D: resolve failed`. Relative paths avoid the colon entirely. + (cd "$installdir/bin" && tar czf "../../$ASSET" dart-typegen.exe) + ls -lh "$RUNNER_TEMP/$ASSET" + + # The release tag must already exist before this step runs. + # Run `MISE_ENV=maint mise run bootstrap-dart-typegen-release` locally once before the first dispatch. The + # bootstrap task is idempotent. + - name: Publish dart-typegen asset to release + if: |- + steps.dtg_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + env: + GITHUB_TOKEN: ${{ github.token }} + ASSET: ${{ steps.dtg_check.outputs.asset }} + run: | + gh release upload "$DTG_RELEASE_TAG" --repo edge-toolkit/core --clobber "$RUNNER_TEMP/$ASSET" + + # Re-package GnuPG's "Simple installer for Windows" as a flat tarball mise can extract. + # The installer is an NSIS-wrapped `.exe`; the flat tarball lets mise's http: backend extract it natively. The + # upstream installer at https://gnupg.org/ftp/gcrypt/binary/gnupg-w32-.exe is a 5.5 MB NSIS installer; + # mise's built-in extraction (`sevenz_rust2`) handles canonical .7z but not the NSIS wrapper, so we extract + # once in CI via the `7z` binary (p7zip-full, preinstalled on ubuntu-latest) and host the resulting tarball. + # Output is a flat `bin/`+`share/`+`lib/`+`include/` layout -- a self-contained native-PE + # gpg/gpg-agent/dirmngr/gpgsm suite with no `Library/usr/bin/` nesting, no msys2-runtime autoload trap + # (every binary is PE32+ x86-64, not Cygwin). Runs on ubuntu-latest because 7zz extraction is fully + # cross-platform and ubuntu jobs are cheaper than windows ones. + gnupg-w32: + runs-on: ubuntu-latest + permissions: + contents: write + timeout-minutes: 15 + env: + MISE_ENV: maint + # Bump these in lockstep with upstream's "Simple installer" page. + # See https://gnupg.org/download/ -- the .exe filename embeds both the version and a build date stamp. + # Keep the date stamp here to pin to the exact upstream build (a re-release at the same version would + # otherwise silently drift our cached asset). + GNUPG_VERSION: "2.5.20" + GNUPG_BUILD_STAMP: "20260513" + GNUPG_RELEASE_TAG: "gnupg-w32-v1" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Detect missing gnupg-w32 asset + id: gpg_check + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + # No "gnupg-w32-" project prefix; release tag already names it. + asset="${GNUPG_VERSION}_${GNUPG_BUILD_STAMP}-x86_64-pc-windows.tar.gz" + repo=edge-toolkit/core + jq='.assets[].name' + assets=$(gh release view "$GNUPG_RELEASE_TAG" --repo "$repo" --json assets --jq "$jq" 2>/dev/null || true) + if echo "$assets" | grep -Fxq "$asset"; then + echo "::notice::$asset already in $GNUPG_RELEASE_TAG; skipping" + echo "work=" >> "$GITHUB_OUTPUT" + else + echo "work=yes" >> "$GITHUB_OUTPUT" + echo "asset=$asset" >> "$GITHUB_OUTPUT" + fi + + # No mise needed for this job. + # curl, 7z (p7zip-full), tar, and gh are all preinstalled on ubuntu-latest. We tried + # `MISE_ENV=maint mise exec -- xh` first (per the "use mise tools" preference) but mise 2026.6.5 on the + # runner failed to resolve xh from its aqua install dir layout (`/xh-v0.25.3-/xh`); the + # newer 2026.6.10 mise locally handles it, but pinning mise just for this job is more setup than the tool + # savings warrant. + + - name: Extract + repackage upstream installer + if: |- + steps.gpg_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + env: + ASSET: ${{ steps.gpg_check.outputs.asset }} + run: | + set -euo pipefail + work="$RUNNER_TEMP/gnupg-w32" + mkdir -p "$work" "$work/extracted" + cd "$work" + installer="gnupg-w32-${GNUPG_VERSION}_${GNUPG_BUILD_STAMP}.exe" + url="https://gnupg.org/ftp/gcrypt/binary/${installer}" + curl -sLO "$url" + 7z x -y -o"extracted" "$installer" + # Strip NSIS install-staging cruft. + # `$PLUGINSDIR/` is NSIS's own internal use, and the `*.tmp` files in bin/ are NSIS's mid-install + # copies of binaries (cleaned up by a working installer run, redundant in our extracted form). + rm -rf "extracted/\$PLUGINSDIR" + find extracted/bin -name '*.tmp' -delete + ls extracted/bin + # Flatten the extracted tree into the tarball. + # bin/ + lib/ + share/ + include/ keep the install layout consumers expect; mise's http backend + # respects strip_components so we set the tar root to that. + tar czf "$RUNNER_TEMP/$ASSET" -C extracted . + ls -lh "$RUNNER_TEMP/$ASSET" + sha256sum "$RUNNER_TEMP/$ASSET" + + # The release tag must already exist before this step runs. + # Run `MISE_ENV=maint mise run bootstrap-gnupg-w32-release` locally once before the first dispatch. The + # bootstrap task is idempotent. + - name: Publish gnupg-w32 asset to release + if: |- + steps.gpg_check.outputs.work == 'yes' + && github.event.pull_request.head.repo.fork != true + env: + GITHUB_TOKEN: ${{ github.token }} + ASSET: ${{ steps.gpg_check.outputs.asset }} + run: | + gh release upload "$GNUPG_RELEASE_TAG" --repo edge-toolkit/core --clobber "$RUNNER_TEMP/$ASSET" diff --git a/.gitignore b/.gitignore index 9284041..c94bd3d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ target/ .DS_Store services/ws-wasm-agent/pkg/ +services/ws-modules/pywasm1/pkg/ services/ws-server/static/models/ .zig-cache/ zig-out/ diff --git a/.mise/config.dart.toml b/.mise/config.dart.toml index 5934e59..0523c3f 100644 --- a/.mise/config.dart.toml +++ b/.mise/config.dart.toml @@ -1,19 +1,24 @@ # Dart toolchain + tasks. Loaded when MISE_ENV includes `dart`. [vars] -# Google dart-archive base URLs — the release tree and the bucket listing — -# referenced by [tools.dart] below so its url/version_list_url stay one line. +# Google dart-archive base URLs, split out so [tools.dart] below stays one line. +# The release tree and the bucket listing are referenced by its url/version_list_url. dart_bucket = "https://storage.googleapis.com/storage/v1/b/dart-archive/o" dart_release = "https://storage.googleapis.com/dart-archive/channels/stable/release" [tools] -# cargo: backend -- dart-typegen has no prebuilt binary (crates.io only). -"cargo:dart-typegen" = "latest" +"cargo:dart-typegen" = { version = "latest", os = ["linux", "macos"] } -# dart isn't an aqua/registry tool — install it straight from Google's dart -# archive, discovering the latest stable from the bucket listing. `{{ version }}` +# dart isn't an aqua/registry tool, so install it straight from Google's dart archive. +# The latest stable is discovered from the bucket listing. `{{ version }}` # etc. resolve in the tool-url context; the static host/path prefix is a var. +# +# `strip_components = 1` makes mise drop the `dart-sdk/` wrapper every dartsdk zip uses (Windows/macOS/Linux +# ship the same `dart-sdk/` shape). mise normally auto-detects single-top-level-dir archives and strips, +# but that auto-detect doesn't fire reliably on Windows ZIPs, leaving the binary at `/dart-sdk/bin/dart.exe` +# instead of the `/bin/dart.exe` mise's PATH activation expects. Explicit beats auto. [tools.dart] +strip_components = 1 url = "{{ vars.dart_release }}/{{ version }}/sdk/dartsdk-{{ os() }}-{{ arch() }}-release.zip" version = "latest" version_expr = ''' @@ -24,13 +29,12 @@ fromJSON(body).prefixes version_list_url = "{{ vars.dart_bucket }}?prefix=channels/stable/release/&delimiter=/" [tasks.dart-pub-get] -# `dart format` and `dart analyze` walk up to find pubspec.yaml *and* read -# `.dart_tool/package_config.json` if present. With package_config.json, the -# formatter picks the package's exact language version; without it, it -# falls back to a different default — same source file, different output. -# That mismatch is invisible locally (we accumulate .dart_tool/ over time) -# but bites CI's fresh checkout. Run `dart pub get` for every Dart package -# before any dart-* task so .dart_tool/ exists and the format is stable. +# Run `dart pub get` for every Dart package before any dart-* task. +# This makes .dart_tool/ exist so the format is stable. `dart format` and `dart analyze` walk up to find +# pubspec.yaml *and* read `.dart_tool/package_config.json` if present. With package_config.json, the formatter +# picks the package's exact language version; without it, it falls back to a different default -- same source +# file, different output. That mismatch is invisible locally (we accumulate .dart_tool/ over time) but bites +# CI's fresh checkout. run = """ dart pub get --directory generated/dart-ws dart pub get --directory services/ws-modules/dart-comm1 @@ -47,8 +51,8 @@ run = "dart format services/ws-modules/dart-comm1/ generated/dart-ws/" [tasks.dart-fmt-check] depends = ["dart-pub-get"] -# Parallel of cargo-fmt-check: fail if any tracked Dart source would be -# reformatted, so CI enforces the same formatting `dart-fmt` applies. +# Parallel of cargo-fmt-check: fail if any tracked Dart source would be reformatted. +# This lets CI enforce the same formatting `dart-fmt` applies. run = "dart format --output=none --set-exit-if-changed services/ws-modules/dart-comm1/ generated/dart-ws/" # Namespaced aggregators picked up by the default config's globbed `check`/`fmt`. @@ -76,10 +80,9 @@ run = "dart pub get --directory services/ws-modules/dart-comm1" [tasks."gen:dart-ws"] depends = ["gen:ws-spec"] description = "Emit the Dart sealed-class WS client via dart-typegen (consumes generated/specs/ws.kdl from gen:ws-spec)" -# dart-typegen exits 0 on Windows but writes an empty .dart file, which -# would clobber the committed source and fail gen-specs-check. Skip the -# regen there; the committed file (regenerated on Linux/macOS) stays -# canonical. +# Skip the regen on Windows; the committed file stays canonical. +# That committed file is regenerated on Linux/macOS. dart-typegen exits 0 on Windows but writes an empty .dart +# file, which would clobber the committed source and fail gen-specs-check. run = """ [ "${OS:-}" = "Windows_NT" ] && { echo "gen:dart-ws: skipped on Windows (dart-typegen empty output)"; exit 0; } mkdir -p generated/dart-ws/lib diff --git a/.mise/config.dotnet.toml b/.mise/config.dotnet.toml index 93dec23..4ec7ef6 100644 --- a/.mise/config.dotnet.toml +++ b/.mise/config.dotnet.toml @@ -27,44 +27,74 @@ description = "Format .NET sources" description = "Build the dotnet-data1 C# WASM workflow module" dir = "services/ws-modules/dotnet-data1" run = ''' +# The Windows lane is skipped: no lever the .NET WebAssembly SDK exposes works. +# Neither routes bare-`emcc` in the link to the workload-installed pack. MSBuild's BrowserWasmApp.targets +# link step's `` does NOT inherit the bash-level PATH prepend (validated empirically +# -- the emcc-debug step in test.yaml shows `cmd.exe %PATH%` sees the emscripten dir after the prepend, yet the +# link still trips `'emcc' is not recognized` / MSB3073 exit 9009), and `-p:EmscriptenLocation=...\Sdk` fixes +# the compile target but not the link. tests/modules.rs's `dotnet_data1_pkg_built()` matches this skip so the +# downstream test on Windows logs `skipping ...: pkg/ not built` instead of failing. Drop this guard once +# .NET's SDK either honors PATH or documents a property that overrides the link command. +if [ "${OS:-}" = "Windows_NT" ]; then + echo "build-ws-dotnet-data1-module: skipped on Windows (MSBuild link ignores our PATH; emcc unresolvable)" + exit 0 +fi + +# `set -x` so every command is echoed to the log. +# That covers the find / dirname chain that resolves $(EmscriptenLocation), the final dotnet publish invocation +# with its -p:EmscriptenLocation=... arg, and the cp of the publish artefacts. The Windows path of this task is +# fiddly and the CI log is the only way we get to see it; leave the trace on until the Windows build is +# reliably green. +set -x + dotnet workload install wasm-tools --skip-manifest-update -# Windows-only: `dotnet workload install` drops the Emscripten SDK pack under +# Windows-only: surface the Emscripten SDK pack path to the build ourselves. +# `dotnet workload install` drops the pack under # %LOCALAPPDATA%/mise/dotnet-root/packs/Microsoft.NET.Runtime.Emscripten.*/*/tools/emscripten/, -# but does not add that dir to PATH (Linux/macOS get it via dotnet's -# workload-install shell-rc setup). `dotnet publish`'s MSBuild targets then -# shell out to bare `emcc` and cmd reports verbatim: -# 'emcc' is not recognized as an internal or external command, -# operable program or batch file. -# Discover the pack dir and prepend to PATH so the publish subprocess finds -# emcc.bat. Glob via `find` so a future Emscripten / pack version bump -# rolls forward without touching this task. +# but does not surface that path to the build the way Linux/macOS do via +# dotnet's workload-install shell-rc setup. MSBuild's BrowserWasmApp.targets: +# - Compile target uses `$(EmscriptenLocation)\..\tools\emscripten\emcc` +# (absolute path; works as long as we set the property). +# - LINK target's `` calls bare `emcc @rsp` and relies on PATH, +# which we have to prepend in bash AND keep propagating through +# `dotnet publish` -> MSBuild -> cmd.exe. Without that the link errors: +# 'emcc' is not recognized as an internal or external command, +# operable program or batch file. +# error MSB3073: The command "emcc "@...emcc-default.rsp" ..." exited with code 9009. +# Belt-and-braces: do BOTH the `-p:EmscriptenLocation=...\Sdk` property (compile) AND the PATH prepend (link). +# The PATH prepend uses MSYS Unix form (`/c/Users/...`); when bash spawns dotnet.exe MSYS converts the whole +# `PATH` (`:`->`;`, `/c/Users/...` -> `C:\Users\...`) -- a `C:\Users\...` entry would otherwise escape the +# conversion and the trailing `:` would glue it to the next entry, leaving cmd.exe (run by MSBuild's ) +# without the emscripten dir. Glob via `find` so an Emscripten / pack version bump rolls forward. echo "[emcc-find] OS=${OS:-} MSYSTEM=${MSYSTEM:-}" +publish_args="" if [ "${OS:-}" = "Windows_NT" ]; then echo "[emcc-find] LOCALAPPDATA=${LOCALAPPDATA:-}" emcc_bat="$(find "$LOCALAPPDATA/mise/dotnet-root/packs" -name 'emcc.bat' 2>/dev/null | head -1)" echo "[emcc-find] emcc_bat=${emcc_bat:-}" - if [ -n "$emcc_bat" ]; then - emcc_dir="$(dirname "$emcc_bat")" - # `cygpath -w` (Git Bash) converts /c/Users/... to C:\Users\... so the - # prepended PATH entry resolves in cmd.exe when MSBuild's shells - # out to bare `emcc`. Without conversion, Git Bash hands Windows - # processes a PATH where this dir may not be recognised in the form - # cmd's resolver expects. - if command -v cygpath >/dev/null 2>&1; then - emcc_dir_win="$(cygpath -w "$emcc_dir")" - echo "[emcc-find] cygpath conversion: $emcc_dir -> $emcc_dir_win" - emcc_dir="$emcc_dir_win" - fi - export PATH="$emcc_dir:$PATH" - echo "[emcc-find] prepended to PATH: $emcc_dir" - else + if [ -z "$emcc_bat" ]; then echo "build-ws-dotnet-data1-module: emcc.bat not found under \$LOCALAPPDATA/mise/dotnet-root/packs" >&2 exit 1 fi + emcc_dir="$(dirname "$emcc_bat")" + # Resolve `$(EmscriptenLocation)`: the sibling `Sdk\` dir of the `tools\` dir. + # That `tools\` dir holds emcc.bat at `/tools/emscripten/emcc.bat`. So two dirnames up from emcc.bat, + # plus `/Sdk`, is `$(EmscriptenLocation)`. MSBuild wants Windows form. + emsdk_loc="$(dirname "$(dirname "$emcc_dir")")/Sdk" + if command -v cygpath >/dev/null 2>&1; then + emsdk_loc="$(cygpath -w "$emsdk_loc")" + emcc_dir="$(cygpath -u "$emcc_dir")" + fi + export PATH="$emcc_dir:$PATH" + publish_args="-p:EmscriptenLocation=$emsdk_loc" + echo "[emcc-find] prepended to PATH: $emcc_dir" + echo "[emcc-find] $publish_args" fi -dotnet publish -c Release +# $publish_args holds zero or one CLI flag; word-splitting is intentional. +# shellcheck disable=SC2086 +dotnet publish -c Release $publish_args PUBLISH=bin/Release/net10.0/publish/wwwroot/_framework cp "$PUBLISH"/*.js "$PUBLISH"/*.wasm "$PUBLISH"/*.dat pkg/ diff --git a/.mise/config.java.toml b/.mise/config.java.toml index d1d66d3..96f0142 100644 --- a/.mise/config.java.toml +++ b/.mise/config.java.toml @@ -1,26 +1,76 @@ # Java toolchain + tasks. Loaded when MISE_ENV includes `java`. [tools] -java = "latest" -maven = "latest" +# Pinned (not "latest") because the version is templated into install-dir segments in two places. +# The JAVA_HOME path below + config.windows.toml's maven_bin _.path entry both template install-dir +# segments with version. Bump in lockstep, AND on bump verify the Windows OpenJDK wrapper name in the +# java_home var below (build-number suffix changes with each GA release). +java = "26.0.1" +maven = "3.9.16" + +[vars] +# JDK install root for JAVA_HOME (see [env] below). +# mise's java plugin extracts the OpenJDK archive flat on every platform (Linux/macOS tarball and Windows zip +# alike -- `bin/java[.exe]` sits directly under the install dir; no `jdk-X.Y.Z+N\` wrapper). Default base +# directory is mise's per-platform data dir (`~/.local/share/mise` on Unix, `%LOCALAPPDATA%\mise` on Windows); +# MISE_DATA_DIR overrides on either. +# +# Windows-only slash normalization: %LOCALAPPDATA% arrives in mise's tera context with forward slashes +# (`C:/Users/.../AppData/Local`) but our literal segments use backslashes; the resulting mixed-slash path +# passes busybox `ls` but mvn.cmd's `if not exist "%JAVA_HOME%\bin\java.exe"` rejects it. The +# `| replace(from="/", to=`\`)` pass on the prefix normalizes everything to a single backslash. Note the +# backtick-quoted string for the replacement: tera 1.20's pest grammar treats strings verbatim (no `\\`-as-`\` +# escape), so backticks + a single literal backslash are the way to express a one-char `\` argument. +java_home = '''{% if os() == "windows" -%} +{%- set win_base = get_env(name="LOCALAPPDATA", default="") ~ "\mise" -%} +{{- get_env(name="MISE_DATA_DIR", default=win_base) | replace(from="/", to=`\`) -}} +\installs\java\26.0.1 +{%- else -%} +{{- get_env(name="MISE_DATA_DIR", default=env.HOME ~ "/.local/share/mise") -}} +/installs/java/26.0.1 +{%- endif %}''' [env] -# Silence Maven's per-artifact download/upload progress lines (the -# "Downloading from central:" / "Downloaded" spam). Build output and real -# errors still print. MAVEN_ARGS is honored by Maven 3.9+ and prepends to -# every `mvn` invocation. +# JAVA_HOME must be set in the env because mvn.cmd requires it. +# mvn.cmd's first action is `if "%JAVA_HOME%" == "" goto :error` -- it REQUIRES JAVA_HOME in the env +# (PATH-discovered java.exe is not enough). Linux/macOS mvn shells are more relaxed but still benefit from a +# pinned JAVA_HOME (otherwise java-toolchains.xml lookups vary by host). +JAVA_HOME = "{{ vars.java_home }}" +# Silence Maven's per-artifact download/upload progress lines. +# These are the "Downloading from central:" / "Downloaded" spam. Build output and real errors still print. +# MAVEN_ARGS is honored by Maven 3.9+ and prepends to every `mvn` invocation. MAVEN_ARGS = "--no-transfer-progress" +# Cross-platform mvn invocation. +# On Windows we must NOT let busybox bash resolve `mvn`: the maven distribution ships both files side-by-side +# in bin/ (`mvn` is a Unix shell script; `mvn.cmd` is the Windows wrapper), and busybox-w32's PATH lookup +# picks the bare `mvn` first via `file_is_executable`, parses its `#!/bin/sh` shebang, and runs it through +# busybox's own sh applet -- but the Unix script's JAVACMD composition (`JAVACMD="$JAVA_HOME/bin/java"`, no +# `.exe`) then fails its `[ -x "$JAVACMD" ]` test because on Windows the file is named `bin/java.exe`. Bare +# `mvn.cmd` (and `cmd.exe /c mvn`) both fail in the all-langs MISE_ENV with "'mvn[.cmd]' is not recognized" -- +# the wrapping cmd.exe cannot see maven\bin in its inherited PATH (likely a length/encoding issue introduced +# by the wider tool set; works fine with MISE_ENV=java alone). Use the absolute path to `mvn.cmd` instead: +# busybox-w32's spawnve detects `.cmd` and auto-wraps with cmd.exe, which runs the full path directly with no +# PATH lookup required. `vars.maven_bin` is defined in config.windows.toml; vars merge across all loaded +# configs. +MVN = '''{% if os() == "windows" -%} +{{- vars.maven_bin -}}\mvn.cmd +{%- else -%} +mvn +{%- endif %}''' [tasks.build-ws-java-data1-module] description = "Build the java-data1 workflow module" -run = "mvn package" +run = "$MVN package" +shell = "bash -euo pipefail -c" -# Namespaced aggregator picked up by the default config's globbed `check`. The -# compile triggers maven-compiler-plugin with -Xlint:all -Werror + Error Prone. +# Namespaced aggregator picked up by the default config's globbed `check`. +# The compile triggers maven-compiler-plugin with -Xlint:all -Werror + Error Prone. [tasks."check:java"] description = "Run Java checks (javac -Xlint:all -Werror, Error Prone)" -run = "mvn -q compile" +run = "$MVN -q compile" +shell = "bash -euo pipefail -c" [tasks."prefetch:java"] description = "Prefetch Java (Maven) dependencies" -run = "mvn dependency:resolve --quiet" +run = "$MVN dependency:resolve --quiet" +shell = "bash -euo pipefail -c" diff --git a/.mise/config.js.toml b/.mise/config.js.toml new file mode 100644 index 0000000..0b2afd1 --- /dev/null +++ b/.mise/config.js.toml @@ -0,0 +1,120 @@ +# JavaScript / npm env (MISE_ENV=js). +# +# Tools that resolve through mise's `npm:` backend live here so the modules that consume them (har1, +# face-detection -- both have `onnxruntime-web` as a Cargo dep and load the JS package at runtime via +# /modules/onnxruntime-web) only build when the `js` env is loaded. The aqua-backed `pnpm` row stays in the +# default config; the `npm:pnpm` row below is the macos/x64 fallback (no aqua darwin/amd64 prebuilt). oxlint + +# oxfmt also live here -- they only know JS/TS (the framework-file support is just the ` diff --git a/services/ws-test-server/Cargo.toml b/services/ws-test-server/Cargo.toml index fd6a4f3..1e5d810 100644 --- a/services/ws-test-server/Cargo.toml +++ b/services/ws-test-server/Cargo.toml @@ -17,8 +17,8 @@ et-modules-service.workspace = true et-storage-service.workspace = true et-ws-service.workspace = true tempfile.workspace = true -# Same TracingLogger setup as the real ws-server, so tests that init OTLP -# in-process see server-side spans parented on the propagated traceparent. +# Same TracingLogger setup as the real ws-server. +# So tests that init OTLP in-process see server-side spans parented on the propagated traceparent. tracing-actix-web.workspace = true [dev-dependencies] diff --git a/services/ws-wasi-runner/Cargo.toml b/services/ws-wasi-runner/Cargo.toml index c8d440f..59f9d72 100644 --- a/services/ws-wasi-runner/Cargo.toml +++ b/services/ws-wasi-runner/Cargo.toml @@ -17,8 +17,8 @@ path = "src/main.rs" [features] default = [] -# Enable wasmtime-wasi-nn's `onnx-cuda` feature, which flips the cfg in its -# ONNX backend so `ExecutionTarget::Gpu` dispatches to `CUDAExecutionProvider` +# Enable wasmtime-wasi-nn's `onnx-cuda` feature. +# It flips the cfg in its ONNX backend so `ExecutionTarget::Gpu` dispatches to `CUDAExecutionProvider` # instead of warning and falling back to CPU. `onnx-cuda` transitively turns # on `ort/cuda` too, so the EP is compiled into ort. Opt-in because # `ort/download-binaries` then pulls the CUDA-flavoured ONNX Runtime prebuilt, @@ -36,8 +36,8 @@ et-otlp.workspace = true et-rest-client = { workspace = true, features = ["tracing"] } et-ws-runner-common.workspace = true futures-util.workspace = true -# libc::_exit for the macOS test-only fast exit (see main.rs). std::process::exit -# still runs atexit handlers, which is exactly the path that races libc++ during +# libc::_exit for the macOS test-only fast exit (see main.rs). +# std::process::exit still runs atexit handlers, which is exactly the path that races libc++ during # ORT teardown -- _exit skips them. libc.workspace = true opentelemetry.workspace = true @@ -56,11 +56,11 @@ tracing-opentelemetry.workspace = true tracing-subscriber.workspace = true wasmtime.workspace = true wasmtime-wasi.workspace = true -# wasi-nn standardised ML inference. The workspace pin drops `openvino` + -# `winml` (which we don't use) and keeps `onnx`. +# wasi-nn standardised ML inference. +# The workspace pin drops `openvino` + `winml` (which we don't use) and keeps `onnx`. wasmtime-wasi-nn.workspace = true -# Workspace pins `ort = "=2.0.0-rc.10"` because wasmtime-wasi-nn 44.0.1 was -# built against that specific prerelease; rc.11+ moved +# Workspace pins `ort = "=2.0.0-rc.10"` because wasmtime-wasi-nn 44.0.1 was built against it. +# That is a specific prerelease; rc.11+ moved # `ort::session::{Input, Output}` and `ort::tensor`, and the cargo resolver # would otherwise pick the latest rc. ort.workspace = true diff --git a/services/ws-wasi-runner/tests/modules.rs b/services/ws-wasi-runner/tests/modules.rs index ecc376d..aace892 100644 --- a/services/ws-wasi-runner/tests/modules.rs +++ b/services/ws-wasi-runner/tests/modules.rs @@ -4,11 +4,9 @@ //! components rather than browser-targeted JS. #![cfg(test)] -#![expect( - clippy::expect_used, - reason = "test code: process spawn failure should fail the test" -)] +#![expect(clippy::expect_used, reason = "test code: process spawn failure fails the test")] +use edge_toolkit::config::{Language, mise_env_includes}; use rstest::rstest; // Skipped on Windows: the wasi runner gets a 404 fetching the module's @@ -16,11 +14,14 @@ use rstest::rstest; // under mise's cmd.exe default shell. Re-enable once the Windows task-shell // story is sorted. #[rstest] -#[case::wasi_comm1("et-ws-wasi-comm1")] -#[case::wasi_data1("et-ws-wasi-data1")] -#[case::wasi_graphics_info("et-ws-wasi-graphics-info")] +#[case::wasi_comm1("et-ws-wasi-comm1", Language::Rust)] +#[case::wasi_data1("et-ws-wasi-data1", Language::Rust)] +#[case::wasi_graphics_info("et-ws-wasi-graphics-info", Language::Python)] #[cfg_attr(windows, ignore = "pkg/package.json 404 on Windows -- see comment above")] -fn module_runs_successfully(#[case] module: &str) { +fn module_runs_successfully(#[case] module: &str, #[case] language: Language) { + if !mise_env_includes(language) { + return; + } let server = et_ws_test_server::start(); let bin = env!("CARGO_BIN_EXE_et-ws-wasi-runner"); diff --git a/services/ws-wasi-runner/tests/otel_propagation.rs b/services/ws-wasi-runner/tests/otel_propagation.rs index e07754b..83bf88d 100644 --- a/services/ws-wasi-runner/tests/otel_propagation.rs +++ b/services/ws-wasi-runner/tests/otel_propagation.rs @@ -25,13 +25,13 @@ clippy::expect_used, clippy::uninlined_format_args, clippy::needless_collect, - reason = "test code: assertions include captured span dumps in failure messages" + reason = "test code: expect failures fail the test; assertion-helper format/collect idioms" )] use std::collections::HashSet; use std::time::Duration; -use edge_toolkit::config::{OtlpConfig, OtlpProtocol}; +use edge_toolkit::config::{Language, OtlpConfig, OtlpProtocol, mise_env_includes}; // Skipped on Windows: this test spawns the runner against the wasi-data1 // module via ws-test-server, so it hits the same `pkg/package.json` 404 @@ -40,6 +40,11 @@ use edge_toolkit::config::{OtlpConfig, OtlpProtocol}; #[test] #[cfg_attr(windows, ignore = "pkg/package.json 404 on Windows -- see comment above")] fn trace_ids_propagate_between_runner_and_server() { + // wasi-data1 lives in the rust env; without it, build-ws-wasi-data1-module + // doesn't run and the runner's package.json fetch 404s. + if !mise_env_includes(Language::Rust) { + return; + } // 1. Start the mock collector. Both processes will export to it. let mock = int_otlp_mock::start(); diff --git a/services/ws-web-runner/Cargo.toml b/services/ws-web-runner/Cargo.toml index ef99d9b..23a88ce 100644 --- a/services/ws-web-runner/Cargo.toml +++ b/services/ws-web-runner/Cargo.toml @@ -25,8 +25,8 @@ edge-toolkit.workspace = true et-rest-client = { workspace = true, features = ["tracing"] } et-ws-runner-common.workspace = true futures-util.workspace = true -# Direct dep so we can configure the REST client's reqwest retry policy: the -# default `ProtocolNacks` policy doesn't cover the HTTP/1 keep-alive race (see +# Direct dep so we can configure the REST client's reqwest retry policy. +# The default `ProtocolNacks` policy doesn't cover the HTTP/1 keep-alive race (see # lib.rs `build_rest_client`). Features match et-rest-client's native build. reqwest = { workspace = true, features = ["json", "query", "rustls", "stream"] } serde.workspace = true @@ -37,11 +37,11 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tracing.workspace = true tracing-subscriber.workspace = true +# Depend on winapi with `std` here so cargo feature unification turns it on for the shared winapi. # deno_io (pulled via deno_runtime) declares `winapi` without its `std` feature, # so winapi defines its own `c_void` and its `HANDLE` stops matching std's # `RawHandle`, breaking the `from_raw_handle(GetStdHandle(..))` path on Windows -# (a missing-winapi-feature bug, cf. denoland/deno#24212). Depend on winapi with -# `std` here so cargo feature unification turns it on for the shared winapi. +# (a missing-winapi-feature bug, cf. denoland/deno#24212). [target.'cfg(windows)'.dependencies] winapi.workspace = true diff --git a/services/ws-web-runner/src/shim.js b/services/ws-web-runner/src/shim.js index 64219f2..ea694de 100644 --- a/services/ws-web-runner/src/shim.js +++ b/services/ws-web-runner/src/shim.js @@ -31,7 +31,9 @@ if (typeof globalThis.navigator === "object" && !globalThis.navigator.userAgent) value: "et-ws-web-runner/deno", configurable: true, }); - } catch (_) { /* navigator is read-only in some setups, ignore */ } + } catch { + /* navigator is read-only in some setups, ignore */ + } } // `document` stub -- enough for wasm-bindgen modules that probe DOM @@ -67,11 +69,13 @@ if (typeof globalThis.document === "undefined") { // A `