|
| 1 | +# Windows Runtime-DLL Deployment & `compat.openblas` Windows Support (Design) |
| 2 | + |
| 3 | +Date: 2026-06-29 |
| 4 | +Status: **Design — not yet implemented.** Proposed as the staged follow-up to the |
| 5 | +feature/capability work (mcpp 0.0.72) and the `compat.openblas` package |
| 6 | +(mcpp-index #54). Scope: `src/manifest.cppm`, `src/build/{plan,flags,ninja_backend}.cppm` |
| 7 | +(mcpp); `pkgs/c/compat.openblas.lua`, `.github/workflows/validate.yml` (mcpp-index). |
| 8 | + |
| 9 | +## 1. Problem |
| 10 | + |
| 11 | +`compat.openblas` is a first-class BLAS **library**, not merely an Eigen backend: |
| 12 | +a consumer can depend on it directly, `#include <cblas.h>`, and call |
| 13 | +`cblas_dgemm` (proven — a standalone consumer links and runs, producing the |
| 14 | +correct `[19 22; 43 50]`). It therefore deserves cross-platform support. |
| 15 | + |
| 16 | +It currently ships **linux/macosx only**. The two platforms build OpenBLAS from |
| 17 | +source through its GNU Make system, driven by the xpkg `install()` hook |
| 18 | +(build-dep `xim:make`, `CC=gcc`), producing a fully static `libopenblas.a` that |
| 19 | +links with no runtime artifact. Windows has no equivalent path: |
| 20 | + |
| 21 | +- mcpp links Windows with **MSVC-ABI Clang** (msvcrt / msvc-stl / MSVC C++ ABI). |
| 22 | +- OpenBLAS's prebuilt `OpenBLAS-<ver>-x64.zip` contains a **mingw** static |
| 23 | + `libopenblas.a` (Itanium ABI + libgcc/mingw-libc) that does not link cleanly |
| 24 | + into an MSVC-ABI binary, plus an MSVC import library `libopenblas.lib` and a |
| 25 | + runtime `libopenblas.dll`. The import-lib path links, but then the produced |
| 26 | + executable needs `libopenblas.dll` present at launch. |
| 27 | +- Building OpenBLAS statically with clang-cl/CMake inside the xim sandbox is not |
| 28 | + available (no MSVC toolchain there) and would be a heavy CMake build. |
| 29 | + |
| 30 | +So the only viable Windows route is **import-lib + runtime DLL**, and that is |
| 31 | +blocked by a genuine mcpp capability gap: **mcpp does not deploy a dependency's |
| 32 | +runtime DLL alongside the produced executable.** A Windows `compat.openblas` |
| 33 | +would link but fail to launch (missing DLL). |
| 34 | + |
| 35 | +## 2. Background — mcpp's existing runtime model |
| 36 | + |
| 37 | +mcpp already models "what a built binary needs at launch" via the `[runtime]` |
| 38 | +section (`manifest::RuntimeConfig`): |
| 39 | + |
| 40 | +- `library_dirs` — directories (relative to the package root) holding the |
| 41 | + package's runtime shared libraries. These flow into |
| 42 | + `BuildPlan::depRuntimeLibraryDirs`. |
| 43 | +- `dlopen_libs`, `capabilities`, `provides`, provider overrides. |
| 44 | + |
| 45 | +On platforms where `mcpp::platform::supports_rpath` is true (ELF / Mach-O — |
| 46 | +Linux, macOS), `src/build/flags.cppm` turns each runtime library dir into |
| 47 | +`-Wl,-rpath,<dir>` so the loader finds the `.so`/`.dylib` at runtime. `mcpp run` |
| 48 | +additionally exports the platform runtime path variable |
| 49 | +(`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) from the same list. |
| 50 | + |
| 51 | +On Windows (PE), `supports_rpath` is **false**. There is no general RPATH |
| 52 | +mechanism, so `depRuntimeLibraryDirs` is currently a no-op at link time: a |
| 53 | +directly-launched `.exe` cannot find a dependency's DLL. |
| 54 | + |
| 55 | +## 3. Design decision |
| 56 | + |
| 57 | +**Reuse `[runtime].library_dirs`; add a Windows deployment backend. No new schema |
| 58 | +key, no top-level per-OS block.** |
| 59 | + |
| 60 | +The recipe schema already carries platform structure at three layers, each with a |
| 61 | +distinct responsibility: |
| 62 | + |
| 63 | +| Layer | Responsibility | Platform split | |
| 64 | +|---|---|---| |
| 65 | +| `xpm.<os>` | download source (url / sha256) | `xpm.{linux,macosx,windows}` | |
| 66 | +| `mcpp.<os>` | compile/link keys (`cflags`/`sources`/`ldflags`) | `mcpp.{linux,macosx,windows}` (cf. compat.glfw) | |
| 67 | +| `[runtime]` | launch-time requirements (`library_dirs`, `dlopen_libs`, …) | global today | |
| 68 | + |
| 69 | +A dependency's runtime DLL is a **launch-time requirement**, so it belongs to |
| 70 | +`[runtime].library_dirs` (pointing at the directory that holds the DLL, e.g. |
| 71 | +`bin/`). The abstraction *already exists*; Windows is simply the platform on |
| 72 | +which its deployment backend is unimplemented. |
| 73 | + |
| 74 | +The implementation mirrors the existing RPATH branch. Where `flags.cppm` today |
| 75 | +does, in effect: |
| 76 | + |
| 77 | +``` |
| 78 | +if constexpr (supports_rpath) // ELF / Mach-O |
| 79 | + for dir in depRuntimeLibraryDirs: ldflags += "-Wl,-rpath," + dir |
| 80 | +``` |
| 81 | + |
| 82 | +it gains the symmetric PE branch: |
| 83 | + |
| 84 | +``` |
| 85 | +else if constexpr (is_windows) // PE |
| 86 | + for dir in depRuntimeLibraryDirs: deploy *.dll from dir → <output>/bin/ |
| 87 | +``` |
| 88 | + |
| 89 | +The same declaration ("this dependency's runtime libraries live in dir X") is |
| 90 | +dispatched, at compile time, to the platform-appropriate mechanism: RPATH on |
| 91 | +ELF/Mach-O, copy-beside-executable on PE. Rejected alternatives: |
| 92 | + |
| 93 | +- **A top-level `windows = {}` recipe block** — redundant; `mcpp.<os>` and |
| 94 | + `[runtime]` already provide the platform and runtime axes. |
| 95 | +- **A new `runtime_libs` key** — redundant with `[runtime].library_dirs`; adds a |
| 96 | + second way to say the same thing. |
| 97 | + |
| 98 | +## 4. Architecture evaluation |
| 99 | + |
| 100 | +| Dimension | Rating | Rationale | |
| 101 | +|---|---|---| |
| 102 | +| Soundness | High | Completes an abstraction already designed but unimplemented on PE; RPATH and copy are both "make the runtime library locatable" — semantically unified. | |
| 103 | +| Compatibility | High | Purely additive, dispatched by `if constexpr`; non-Windows behavior is byte-for-byte unchanged; no new schema key, so existing recipes are untouched. | |
| 104 | +| Generality | High | Benefits every Windows compat library that ships a DLL (future fftw / hdf5 / any prebuilt-DLL package), not an OpenBLAS-specific patch. | |
| 105 | +| Complexity | Low–Medium | One ninja `copy` edge plus exposing the DLLs of `depRuntimeLibraryDirs` to the backend. No new concept, no new key. ninja handles incrementality/timestamps. | |
| 106 | +| Elegance | High | Symmetric with the RPATH path; reuses `[runtime]`; zero new abstraction. | |
| 107 | +| Stability | High (deploy side) | The link side is unchanged (still import-lib); the addition is an order-only file copy at the end of the build — small failure surface. | |
| 108 | + |
| 109 | +The residual risk is **not** in this feature's deploy mechanism but in the |
| 110 | +Windows-specific link/run that only a Windows host exercises (see §7). |
| 111 | + |
| 112 | +## 5. Implementation phases |
| 113 | + |
| 114 | +### Phase A — mcpp: Windows runtime-DLL deployment (the core feature) |
| 115 | + |
| 116 | +1. **Schema** (`manifest.cppm`): allow `[runtime].library_dirs` to be declared |
| 117 | + per-OS under `mcpp.<os>` (today `mcpp.<os>` parses `cflags`/`sources`/`ldflags`; |
| 118 | + extend it to also read a runtime library-dir list), so a recipe can scope the |
| 119 | + directory to Windows. No new key is introduced. |
| 120 | +2. **Propagation** (`prepare.cppm` / `plan.cppm`): the runtime library dirs of a |
| 121 | + dependency in an executable's link closure already reach |
| 122 | + `BuildPlan::depRuntimeLibraryDirs`; ensure they are resolved to absolute |
| 123 | + source paths (reuse the `-L<rel>` → `<depRoot>/<rel>` normalization) and |
| 124 | + de-duplicated. |
| 125 | +3. **Backend** (`flags.cppm` + `ninja_backend.cppm`): add the PE branch — for |
| 126 | + each runtime library dir of a linked dependency, emit |
| 127 | + `build <output>/bin/<dll> : copy <abs-src>` and make the executable target |
| 128 | + take an order-only dependency on it. Guard by `is_windows` so other platforms |
| 129 | + are unaffected. |
| 130 | +4. **Packaging** (`mcpp pack`): include the deployed `bin/*.dll` so a packaged |
| 131 | + artifact is self-contained off the build machine. |
| 132 | +5. **Local test** (achievable without Windows): an e2e on Linux using a *dummy* |
| 133 | + shared library — a dependency declares a runtime library dir; assert |
| 134 | + `mcpp build` deploys the file beside the executable. This validates the deploy |
| 135 | + mechanism mechanically. The Windows-specific link/run is verified in Phase D. |
| 136 | + |
| 137 | +### Phase B — release mcpp 0.0.73 |
| 138 | + |
| 139 | +The feature must ship in a released mcpp before the recipe can rely on it (an |
| 140 | +older mcpp would link the import lib but never deploy the DLL → runtime failure). |
| 141 | +Run the established pipeline: version bump (`mcpp.toml` + `fingerprint.cppm`), |
| 142 | +design-doc + tests, PR → CI (5 suites) → squash-merge → `release.yml` (4 |
| 143 | +platforms) → mirror `xlings-res/mcpp` (GitHub + GitCode) → bump |
| 144 | +`xim-pkgindex/pkgs/m/mcpp.lua` → `xlings install mcpp@0.0.73` verification → bump |
| 145 | +the workspace bootstrap pin. |
| 146 | + |
| 147 | +### Phase C — `compat.openblas` Windows recipe |
| 148 | + |
| 149 | +1. `xpm.windows`: `url` → `OpenBLAS-0.3.33-x64.zip` (GLOBAL = OpenMathLib GitHub; |
| 150 | + CN = gitcode `mcpp-res/openblas`); `sha256`. The zip unpacks to |
| 151 | + `bin/ lib/ include/` (no wrapper directory). |
| 152 | +2. `mcpp.windows`: |
| 153 | + - `include_dirs` resolves the shipped `include/` (carries `cblas.h`). |
| 154 | + - `ldflags` link the import library `libopenblas.lib` (clang-cl form to be |
| 155 | + finalized via CI). |
| 156 | + - `[runtime] library_dirs = ["bin"]` (the existing key) so Phase A deploys |
| 157 | + `libopenblas.dll` beside the consumer's executable. |
| 158 | + - anchor strategy: Windows has no build step, so the anchor TU is supplied via |
| 159 | + `generated_files` (mcpp writes it; `install()` does not run). Linux/macOS keep |
| 160 | + the "`install()` produces the anchor → triggers the Make build" trigger. |
| 161 | +3. `install()` branches on `os.host()`: Windows returns immediately (prebuilt — no |
| 162 | + Make); linux/macosx run the existing source build. |
| 163 | +4. CN mirror: upload `OpenBLAS-0.3.33-x64.zip` (~40 MB) to the gitcode |
| 164 | + `mcpp-res/openblas` 0.3.33 release; verify byte-equality via a GET download. |
| 165 | + |
| 166 | +The linux/windows asymmetry is intentional and semantically natural: Linux/macOS |
| 167 | +link a fully static `libopenblas.a` (no runtime artifact, so no runtime dir is |
| 168 | +declared); Windows uses the prebuilt import-lib + DLL (MSVC-ABI Clang has no |
| 169 | +static option from the upstream zip), so only Windows declares a runtime library |
| 170 | +directory. |
| 171 | + |
| 172 | +### Phase D — verification via CI (the only Windows-host validation) |
| 173 | + |
| 174 | +The Windows runtime half (clang-cl linking `libopenblas.lib`, the DLL loading at |
| 175 | +launch, `cblas_dgemm` producing the correct result) can be verified **only** on a |
| 176 | +real Windows runner. The current mcpp-index CI does **not** cover it as-is: |
| 177 | + |
| 178 | +- `smoke-examples` is **ubuntu-only** and does build **and** run |
| 179 | + (`tests/run_example.sh` = `mcpp build` + `mcpp run`), but never on Windows. |
| 180 | +- `smoke-windows` runs on `windows-latest` but is **build-only** |
| 181 | + (`smoke_compat_*.sh` invoke `mcpp build`, not `mcpp run`) and is **pinned to |
| 182 | + mcpp 0.0.68**. |
| 183 | + |
| 184 | +So Phase D must, in `validate.yml`: |
| 185 | + |
| 186 | +1. Bump the Windows smoke job's mcpp pin to ≥ 0.0.73 (the release carrying the |
| 187 | + deployment feature). |
| 188 | +2. Add a Windows **build-and-run** step for an OpenBLAS example that asserts the |
| 189 | + computed result — only `mcpp run` exercises DLL loading; a build-only check |
| 190 | + would pass even if the DLL were never deployed. |
| 191 | + |
| 192 | +Iterate against the runner logs until green (no interactive debugging is possible |
| 193 | +on the runner; correction is log-driven). |
| 194 | + |
| 195 | +## 6. Verification boundary |
| 196 | + |
| 197 | +| Item | Local (Linux host) | CI (`windows-latest`) | |
| 198 | +|---|---|---| |
| 199 | +| recipe parse / schema / Linux deploy mechanism | yes (dummy-lib e2e) | — | |
| 200 | +| clang-cl linking `libopenblas.lib` | no (no MSVC env) | yes | |
| 201 | +| DLL deployed beside the exe + loaded + `cblas_dgemm` runs | no | yes — once the Windows smoke is bumped to ≥ 0.0.73 and extended to run+assert (Phase D) | |
| 202 | + |
| 203 | +## 7. Open questions / risks |
| 204 | + |
| 205 | +- **Whole-directory `*.dll` vs an explicit list.** OpenBLAS's `bin/` holds a |
| 206 | + single DLL, so copying `*.dll` from each runtime library dir is sufficient and |
| 207 | + simple (preferred — YAGNI). If a future package's `bin/` mixes unrelated DLLs, |
| 208 | + an optional explicit list can narrow it later. |
| 209 | +- **clang-cl link form and symbol decoration.** The exact flag to link |
| 210 | + `libopenblas.lib` and the cdecl `cblas_*` symbol names must match what the |
| 211 | + consumer references; only CI confirms this. |
| 212 | +- **`mcpp.<os>` runtime parsing.** If the parser reads `[runtime]` only at global |
| 213 | + scope, allowing `library_dirs` under `mcpp.windows` is a small parser addition |
| 214 | + (alternatively declare it globally — harmless on Linux, which links statically |
| 215 | + and ships no DLL). |
0 commit comments