Skip to content

Commit 42ad3a4

Browse files
committed
docs: design for Windows runtime-DLL deployment + compat.openblas Windows support
Reuse [runtime].library_dirs with a PE deployment backend symmetric to the existing RPATH path (no new schema key, no top-level per-OS block). Covers the mcpp feature (Phase A), release (B), compat.openblas Windows recipe (C), and the CI-verification gaps (D: smoke-windows is build-only + pinned to 0.0.68, so it must be bumped to >=0.0.73 and extended to run+assert).
1 parent 749a5d2 commit 42ad3a4

1 file changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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

Comments
 (0)