---
name: wally-e2e
description: Verify a built wally binary against a pinned C++ desktop kit on macOS and Windows. Use when CI smoke/e2e is red, backends are missing, DLLs fail to load, or the Apple MLX host fails to link.
---

# Wally e2e

Entry: `scripts/test/e2e.sh <path-to-wally>`. Always runs `scripts/test/smoke.sh`, then
`scripts/test/e2e-modalities.sh` (engine-agnostic primitives). Public CI leaves
modality knobs unset so every round-trip **skips**. Device runs set
`WALLY_E2E_<MOD>` / `WALLY_E2E_MODEL_ROOTS` / `WALLY_E2E_AUTO=1`. See
`wally-device-e2e` for ANE/NPU.

| Env | Primitive | Example |
|---|---|---|
| `WALLY_E2E_LLM` / `WALLY_E2E_MODEL` | llm | `mlx-qwen3` or a `*_HNPU` dir |
| `WALLY_E2E_STT` | stt | `whisper-tiny` or `whisper_base_HNPU` |
| `WALLY_E2E_TTS` | tts | `piper` or `kitten_micro_0_8_HNPU` |
| `WALLY_E2E_VLM` | vlm | `smolvlm2` (SDK inserts the media marker) |
| `WALLY_E2E_EMBED` | embed | `minilm` or `embeddinggemma_300m_HNPU` |
| `WALLY_E2E_IMAGE` | image | compiled SD1.5 tree / `sd15` |
| `WALLY_E2E_NEURT_MODEL` | classified by path | `sd15`, a Parakeet ANE tree, or `lfm2-230m-ane` |
| `WALLY_E2E_VAD` | vad | `silero` |
| `WALLY_E2E_RERANK` | rerank | `bge-reranker` |
| `WALLY_E2E_SEGMENT` | segment | `segformer` (P6 PPM) |
| `WALLY_E2E_ENGINE` | override only | `qhexrt` / `neurt` / `mlx` |

Legacy `WALLY_E2E_MLX_MODEL` / `WALLY_E2E_NEURT_MODEL` / `WALLY_E2E_QHEXRT_MODEL`
are classified by path/id into a primitive (not always image). Do not add new
engine-named knobs.

`scripts/test/assert-binary-backends.sh` greps `nm`/`llvm-nm`/`dumpbin`/`strings`
for registrar symbols (`raMLXRegisterRuntime`, `rac_plugin_entry_neurt`,
`rac_plugin_entry_qhexrt`, …) so a backends() listing cannot pass without
the engine actually being linked into the bottle.

Pass `WALLY_SDK_KIT` so overlay backends (`neurt` / `qhexrt`) are required
when those libs are in the kit. `CMAKE_PREFIX_PATH` is only used for
`HAS_*` flags and Windows DLL staging — an ambient overlay prefix must
not make a public OSS bottle fail for missing NeuRT.

## What "green" means

`scripts/test/assert-backends.sh` requires every engine the kit actually ships:

| Condition | Required `wally --json backends` name |
|---|---|
| no kit Config (public OSS bottle) | `llamacpp` + `onnx` + `sherpa` |
| kit `RunAnywhere_HAS_LLAMACPP` TRUE | `llamacpp` |
| kit `RunAnywhere_HAS_ONNX` TRUE | `onnx` |
| kit `RunAnywhere_HAS_SHERPA` TRUE | `sherpa` |
| Darwin arm64 product binary `wally` (not `wally-cxx`) | `mlx` |
| overlay `lib/librac_backend_neurt.a` / `rac_backend_neurt.lib` | `neurt` |
| overlay `lib/librac_backend_qhexrt.a` / `rac_backend_qhexrt.lib` | `qhexrt` |

Do **not** drop sherpa from the expected list to make 0.20.26 Windows green
while `HAS_SHERPA` is TRUE. That kit compiled sherpa with speech ops off
(`RAC_SHERPA_ROUTABLE=0`): `rac_backend_sherpa_register()` returned SUCCESS,
`capability_check` returned `BACKEND_UNAVAILABLE`, the registry refused the
plugin. The fix is pin a routable kit (0.20.28+), not weaken the assertion.

## `backends` must walk every live primitive

`src/commands/cmd_backends.rs` iterates `1 .. RAC_PRIMITIVE_COUNT-1`, skipping
retired wire value 6. ONNX without RAG only advertises SEGMENT / DIARIZE. A
hardcoded GENERATE_TEXT / TRANSCRIBE / EMBED list made onnx invisible even when
the plugin was registered. Do not reintroduce a primitive allow-list.

## Windows DLLs

Win32 `LoadLibrary` searches the exe directory, then PATH. `e2e.sh` copies
`third_party` / `bin` / `lib` `*.dll` next to `wally.exe` and prepends those
dirs to PATH **before** smoke. Skipping that produces "llamacpp only" even when
the kit contains `rac_backend_onnx.lib` + `onnxruntime.dll`.

Never pass `onnxruntime.dll` to `link.exe` (LNK1107) — link the import lib;
stage the DLL at runtime.

GitHub Windows: `GITHUB_WORKSPACE` is `D:\a\...`; msys `tar -C` needs
`cygpath -u` (`fetch-kit.sh` already does).

## Apple MLX host link

`cmake/WallyRust.cmake` gets the kit's link line without hand-parsing Ninja: it
queries the CMake file API (`cmake_file_api(QUERY API_VERSION 1 CODEMODEL 2)`)
against `wally_link_probe`, a target configured
but never built that carries the same kit closure the old C++ `wally`
executable had. `build.rs` (`link_native`/`probe_link_args`) reads that reply,
drops compile-only fragments (`-D`/`-I`/`-O`/…), hands the rest to `cargo` as
`rustc-link-arg`s for every artifact it links, and — when
`WALLY_NATIVE_LINK_ARGS_OUT` is set (CMake sets it) — writes the same list to
`build/wally-native-link-args.txt`. `scripts/build/build-mlx.sh` reads that
file and turns each fragment into an `xcodebuild` `OTHER_LDFLAGS` token, then
links `build/cargo/release/libwally.a` (the crate's staticlib, built with the
same fragments) against it. No manual `ninja -t commands` harvesting, no
`bundle-core.sh` merge step — both are gone.

- swiftc (Xcode 27) rejects raw `-Wl,` options and ignores bare archive paths
  in `OTHER_LDFLAGS`, so `build-mlx.sh` sends everything aimed at `ld` through
  `-Xlinker`; `-l`/`-L`/`-F` and `-framework` are swiftc options and pass
  through as-is.

`scripts/build/build-mlx.sh` must dump the xcodebuild log on failure (`Undefined
symbols` does not contain `error:`). Do not grep bare `error:` — every
CompileC line contains `-Werror=`. Observed CI `32786359915`: grep
`error:|Metal|BUILD` left only `clang: error: linker command failed`.

Never put `#` comments in a `\`-continued `xcodebuild` invocation. Bash
cuts the command there, so `OTHER_LDFLAGS` and the log redirect never
run (empty `xcodebuild-mlx.log`, status taken from a later assignment).

Link flags that must survive the Swift host:

- The kit's plugin backends (static registrars) arrive already force-loaded in
  the fragments CMake recorded for `wally_link_probe` — `build-mlx.sh` does not
  re-derive which archives need `-force_load`, it replays what CMake linked.
- `-L$KIT/third_party -lonnxruntime` and `-Wl,-rpath,$KIT/third_party` must
  both survive the flag rewrite, or the Swift host abort-traps at launch
  (`Library not loaded: @rpath/libonnxruntime.dylib`).
- The crate's own native dependencies (Rust std, native-tls's
  `Security.framework`, `-liconv`) and the C++ runtime (`-lc++`, the kit links
  through the C++ driver) go on the link line after the kit's own flags
  (`build-mlx.sh`'s `rust_native` array).
- Canonicalize `WALLY_SDK_SWIFT_PATH` with `cd && pwd`. SwiftPM's local package
  identity is the **directory name**, so `…/EXTERNAL/Wally/../..` registers as
  `..`. Nested checkouts named `sdks1` must use that name in
  `.product(..., package:)`.

Do not point `WALLY_SDK_SWIFT_PATH` at an unreleased `Package.swift` whose
`sdkVersion` zips 404 (`v0.20.28` before publish). CI checks out the **tagged**
SDK tree (`ref: v$SDK`) whose binaryTargets already exist.

CI macOS runner is **macos-26** (Xcode 26 / Swift 6.2). The MLX host resolves
`RunanywhereAI/runanywhere-sdks` `Package.swift`, which is
`swift-tools-version: 6.2`. macos-15 is Xcode 16.4 / Swift 6.1 and fails after
a successful libtool merge with `package 'runanywhere-sdks' is using Swift
tools version 6.2.0 but the installed version is 6.1.0`. macos-14 is Swift
5.10. `release.yml` must use the same runner as `ci.yml`.

## Linux

Linux bottles are not a v1 merge blocker. Windows x64 and macOS arm64 are.
`scripts/test/e2e-linux.sh` exists for later.

## Private engines

NeuRT / QHexRT only appear in `backends` when the overlay was applied.
`scripts/test/e2e.sh` requires `neurt` / `qhexrt` when
`lib/librac_backend_neurt.a` or `lib/rac_backend_qhexrt.lib` exists — not by
grepping packaged `HAS_NEURT FALSE` (that stays false; find_package flips it
when the archive is present). Public CI must pass without overlays. Image gen
(`cmd_image.rs`, gated `#[cfg(wally_has_neurt)]`) is compiled out unless NeuRT is present.

## Device / overlay gotchas (0.5.1 + kit 0.20.28)

Public bottles never list `neurt` or `qhexrt`. That is the product, not a
test gap. Overlay-rebuild the product binary (`WALLY_APPLE_MLX_HOST=ON` on
Mac; ARM64 MSVC + QHexRT overlay on Snapdragon).

- **`CMAKE_PREFIX_PATH` is not an overlay opt-in.** Only `WALLY_SDK_KIT`
  makes e2e require `neurt`/`qhexrt`. An ambient overlay prefix from a
  previous rebuild will otherwise fail a public-bottle run.
- **Binary assert:** never `nm | grep -q` under `pipefail` (SIGPIPE → false
  FAIL). Stream `strings -a` / `nm -a`. Darwin MLX proof is
  `mlx-swift_Cmlx.bundle` next to product `wally` (`nm -gU` misses Swift
  host symbols). First C++ `rac_plugin_register(mlx)` logs `-811`; Swift
  callbacks then register MLX — noisy, not a miss.
- **Windows ARM64 public/overlay kits have `HAS_LLAMACPP FALSE`.** Do not
  require `llamacpp` in e2e. Overlay `wally.exe` listing **only** `qhexrt`
  (priority 150) is correct. On-disk GGUF (`qwen3.5-2b`) cannot run there.
- **QHexRT generate needs QAIRT matching the device skel, not the overlay
  DLL set.** Snapdragon X2 Elite / Hexagon v81: `QNN_SDK_ROOT` +
  `ADSP_LIBRARY_PATH=%QNN_SDK_ROOT%\lib\hexagon-v81\unsigned`, copy
  `aarch64-windows-msvc` `QnnHtp.dll` / `QnnHtpPrepare.dll` /
  `QnnHtpV81Stub.dll` / `QnnHtpV81CalculatorStub.dll` / `QnnSystem.dll`
  next to `wally.exe`. Overlay 2.47 DLLs vs device 2.41 skels fail; QAIRT
  **2.48** worked. Pass the `*_HNPU` directory (`--engine qhexrt`), not a
  GGUF. FastRPC `openSession` timeouts (~90s) then user-driver fallback
  are normal; a second generate while DSP is wedged fails with
  `Skel failed to process context binary` / `0x3ea` — `taskkill wally.exe`
  and use a `.bat` with **fully expanded** `ADSP_LIBRARY_PATH` (nested
  `%QNN_SDK_ROOT%` in `cmd /c "set A=…&& set B=%A%\…"` does not expand).
- **VS on the ARM64 box may be 2026 / 18 Community**, not 2022:
  `C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsarm64.bat`.
  CMake/Ninja live under VS CMake extensions; they are not on default PATH.
- **v0.20.28 Windows ARM64 public kit omits `libcurl.lib`.** Copy from
  `arm64-windows-static` into the kit `lib/` before linking (fixed in the
  SDK packager for the *next* kit; do not retag 0.20.28). Wally already
  links kit `libcurl.lib` when present.
- Published product bottles: macOS `wally-$V-macos-arm64.tar.gz`, Windows
  **x86_64** zip, Windows **arm64** zip, and Linux **x86_64**
  `wally-$V-linux-x86_64.tar.gz`. NPU (NeuRT/QHexRT) is overlay-only on any
  platform.
- **The private QHexRT overlay tarball used to ship zero skel files** (only
  `.dll`/`.lib`, no `.so`/`.cat`) — `rac-cli`'s own overlay build could not
  run `qwen3.8-27b-1bit-npu` (the Bonsai/Maple ternary decoder) out of the
  box; validating it required hand-copying `librun_main_on_hexagon_skel.so`
  + `.cat` in from the `electron-qhexrt` npm package as a workaround. Fixed
  in `runanywhere-sdks`' `scripts/build/package-private-engine-overlay.sh`
  (widened the copy filter and added a pass for `dsp/win-arm64/`). **Wally
  itself never had the `ADSP_LIBRARY_PATH` bug the Electron binding had** —
  `fastrpc_win.cpp`'s `exe_dir()` fallback naturally resolves for `wally.exe`
  because dependent DLLs/skels are staged flat beside the executable by this
  repo's own packaging convention — but that protection is a property of the
  *packaging layout*, not of Wally's code, so it is not something to assume
  going forward. **Always build a fresh overlay from the actual release
  script and run the ternary model against it after any SDK kit-pin bump**
  that touches QHexRT — do not assume last time's manually-patched overlay
  is still representative of what a real user's overlay build produces.
- **The private overlay tarball must be EXTRACTED ON TOP OF `kit/`, merging
  into the same directory tree (`overlay/bin/*` → `kit/bin/`, `overlay/lib/*`
  → `kit/lib/`, `overlay/include/*` → `kit/include/`, `overlay/share/...` →
  `kit/share/...`) — never kept as a separate sibling `overlay/` directory
  fed to CMake via a second `CMAKE_PREFIX_PATH` entry.** `wally_stage_windows_runtime_dlls()`
  (`cmake/RunAnywhereSDK.cmake`) only ever copies from
  `${RunAnywhere_LIBRARY_DIR}/../bin` — i.e. `kit/bin` — so a same-named
  `overlay/bin` sitting next to `kit/` is silently never consulted. Worse,
  this fails **completely silently**: the build succeeds, `wally.exe` links,
  and `wally backends --json` returns `{"backends":[]}` with no error naming
  QHexRT at all (`find_library`-style detection in `RunAnywhereSDK.cmake`
  just doesn't find `kit/lib/rac_backend_qhexrt.lib` because it was never
  copied there). If a fresh overlay build reports zero backends, check this
  BEFORE suspecting the overlay tarball's contents.
- **`qwen3.8-27b-1bit-npu`'s `HostOpFailed` had THREE compounding causes,
  found and fixed one at a time — a kit-pin bump to v0.20.31 alone was NOT
  enough; Wally needed its own additional fix (below) even with a perfectly
  merged overlay.**
  1. The overlay-skel-files-never-shipped bug (above), fixed upstream in
     `runanywhere-sdks`' overlay packaging script.
  2. `qhexrt::qnn::Backend::profile()` — called (via the same
     `engines/qhexrt/qhexrt_session.cpp` this repo statically links, same as
     the Electron binding) to pick the `v75`/`v79`/`v81` manifest directory
     before the manifest is even parsed — shared its device query with the
     code path that opens a real QNN HTP device, so the ternary decoder's
     `host_only` manifest paid for a live QNN device it never needed. Fixed
     in `neurun` v0.20.31 (`Backend::profile()` no longer shares
     `ensure_device()` with `device()`) — see that repo's
     `qhexrt-profile-must-not-create-live-device` KB finding.
  3. **Wally-specific, and NOT fixed by the kit-pin bump alone**:
     `copy-overlay-dlls.cmake` globbed `*.dll` only, so even a correctly
     merged overlay (per the bullet above) left the Bonsai skel's `.so`/
     `.cat` sitting in `kit/bin/` and NEVER staged next to `wally.exe` — the
     one place `fastrpc_win.cpp`'s `ADSP_LIBRARY_PATH ∪ exe_dir()` search
     actually looks. Fixed by widening the glob to `*.dll *.so *.cat`.
  **`fastrpc_win.cpp`'s `SET_PATH`/`GET_PATH` both returning a non-zero rc
  (`0x14`/`AEE_EUNSUPPORTED`) is EXPECTED and HARMLESS on this driver
  (`libcdsprpc` 11.1.4 simply doesn't implement that control call — see that
  file's own header comment) — do not treat it as a symptom of anything.**
  This was chased as a diagnostic signal once and wasted real device time;
  the only signal that matters is whether `remote_handle64_open` for the
  skel itself returns non-zero (`0x80000406` = `AEE_EUNABLETOLOAD`, which
  that same file's header comment exhaustively catalogs the causes of —
  missing skel, missing/wrong/stale `.cat`, or — as this entry adds — the
  pair never being in the searched directory at all).
  Confirmed fixed end to end on a Snapdragon X2 Elite with all three fixes
  in place: `wally run --engine qhexrt` against `qwen3.8-27b-1bit-npu` opens
  the cDSP session and generates correctly ("The capital of France is
  **Paris**.", 0.105 tok/s, 12425 DSP linears).
