Date: 2026-06-29 Status: Implemented & shipped.
- Phase A (mcpp runtime-DLL deployment) — merged in mcpp #185, released as
v0.0.73, mirrored to
xlings-res/mcpp(GitHub + GitCode), indexed viaopenxlings/xim-pkgindex#326 (latest → 0.0.73),xlings install mcpp@0.0.73verified, workspace bootstrap pin bumped. - Phase C (
compat.openblasWindows recipe) + Phase D (Windows CI build-and-run) — mcpp-index #55; theOpenBLAS-0.3.33-x64.zipCN mirror is live on gitcodemcpp-res/openblas(byte-verified).
Scope: src/build/{plan,ninja_backend}.cppm (mcpp); pkgs/c/compat.openblas.lua,
tests/smoke_compat_portable.sh, .github/workflows/validate.yml (mcpp-index).
The implemented mechanism is gated by the *.dll file extension, not by
if constexpr(is_windows) and not by a schema change. During build planning,
each *.dll found in a linked dependency's [runtime] library_dirs is staged
into bin/ beside the produced executable via a ninja cp_bmi copy edge that
the executable target takes as an implicit dependency. Consequences:
- No
manifest.cppmschema change (the original Phase-A-step-1). A recipe declares[runtime] library_dirsglobally; on Linux/macOS the dependency ships.so/.dylib(never.dll), so the glob matches nothing and the build is byte-for-byte unchanged. This is exactly the §7 "declare it globally — harmless on Linux" option, made safe by the extension filter. Per-OS scoping undermcpp.<os>is therefore unnecessary (and already supported for free by the existing per-OS textual merge if a recipe ever wants it). - The deploy path is exercised on a Linux host (test
tests/e2e/84_runtime_dll_deploy.sh) by a dummy dependency shipping a stublibdummy.dll— the same code that runs on Windows, validated without a Windows runner. The Windows link/run half is Phase D (mcpp-index CI). mcpp packneeds no change here: Windows PE packaging is separately stubbed (src/pack/pack.cppm, see2026-05-19-pack-windows-design.md); when implemented it will pick up the stagedbin/*.dll. On Linuxmcpp packusesldd, which never sees a.dll, so the deploy is invisible to it.
compat.openblas is a first-class BLAS library, not merely an Eigen backend:
a consumer can depend on it directly, #include <cblas.h>, and call
cblas_dgemm (proven — a standalone consumer links and runs, producing the
correct [19 22; 43 50]). It therefore deserves cross-platform support.
It currently ships linux/macosx only. The two platforms build OpenBLAS from
source through its GNU Make system, driven by the xpkg install() hook
(build-dep xim:make, CC=gcc), producing a fully static libopenblas.a that
links with no runtime artifact. Windows has no equivalent path:
- mcpp links Windows with MSVC-ABI Clang (msvcrt / msvc-stl / MSVC C++ ABI).
- OpenBLAS's prebuilt
OpenBLAS-<ver>-x64.zipcontains a mingw staticlibopenblas.a(Itanium ABI + libgcc/mingw-libc) that does not link cleanly into an MSVC-ABI binary, plus an MSVC import librarylibopenblas.liband a runtimelibopenblas.dll. The import-lib path links, but then the produced executable needslibopenblas.dllpresent at launch. - Building OpenBLAS statically with clang-cl/CMake inside the xim sandbox is not available (no MSVC toolchain there) and would be a heavy CMake build.
So the only viable Windows route is import-lib + runtime DLL, and that is
blocked by a genuine mcpp capability gap: mcpp does not deploy a dependency's
runtime DLL alongside the produced executable. A Windows compat.openblas
would link but fail to launch (missing DLL).
mcpp already models "what a built binary needs at launch" via the [runtime]
section (manifest::RuntimeConfig):
library_dirs— directories (relative to the package root) holding the package's runtime shared libraries. These flow intoBuildPlan::depRuntimeLibraryDirs.dlopen_libs,capabilities,provides, provider overrides.
On platforms where mcpp::platform::supports_rpath is true (ELF / Mach-O —
Linux, macOS), src/build/flags.cppm turns each runtime library dir into
-Wl,-rpath,<dir> so the loader finds the .so/.dylib at runtime. mcpp run
additionally exports the platform runtime path variable
(LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH) from the same list.
On Windows (PE), supports_rpath is false. There is no general RPATH
mechanism, so depRuntimeLibraryDirs is currently a no-op at link time: a
directly-launched .exe cannot find a dependency's DLL.
Reuse [runtime].library_dirs; add a Windows deployment backend. No new schema
key, no top-level per-OS block.
The recipe schema already carries platform structure at three layers, each with a distinct responsibility:
| Layer | Responsibility | Platform split |
|---|---|---|
xpm.<os> |
download source (url / sha256) | xpm.{linux,macosx,windows} |
mcpp.<os> |
compile/link keys (cflags/sources/ldflags) |
mcpp.{linux,macosx,windows} (cf. compat.glfw) |
[runtime] |
launch-time requirements (library_dirs, dlopen_libs, …) |
global today |
A dependency's runtime DLL is a launch-time requirement, so it belongs to
[runtime].library_dirs (pointing at the directory that holds the DLL, e.g.
bin/). The abstraction already exists; Windows is simply the platform on
which its deployment backend is unimplemented.
The implementation mirrors the existing RPATH branch. Where flags.cppm today
does, in effect:
if constexpr (supports_rpath) // ELF / Mach-O
for dir in depRuntimeLibraryDirs: ldflags += "-Wl,-rpath," + dir
it gains the symmetric PE branch:
else if constexpr (is_windows) // PE
for dir in depRuntimeLibraryDirs: deploy *.dll from dir → <output>/bin/
The same declaration ("this dependency's runtime libraries live in dir X") is dispatched, at compile time, to the platform-appropriate mechanism: RPATH on ELF/Mach-O, copy-beside-executable on PE. Rejected alternatives:
- A top-level
windows = {}recipe block — redundant;mcpp.<os>and[runtime]already provide the platform and runtime axes. - A new
runtime_libskey — redundant with[runtime].library_dirs; adds a second way to say the same thing.
| Dimension | Rating | Rationale |
|---|---|---|
| Soundness | High | Completes an abstraction already designed but unimplemented on PE; RPATH and copy are both "make the runtime library locatable" — semantically unified. |
| 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. |
| Generality | High | Benefits every Windows compat library that ships a DLL (future fftw / hdf5 / any prebuilt-DLL package), not an OpenBLAS-specific patch. |
| 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. |
| Elegance | High | Symmetric with the RPATH path; reuses [runtime]; zero new abstraction. |
| 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. |
The residual risk is not in this feature's deploy mechanism but in the Windows-specific link/run that only a Windows host exercises (see §7).
- Schema (
manifest.cppm): allow[runtime].library_dirsto be declared per-OS undermcpp.<os>(todaymcpp.<os>parsescflags/sources/ldflags; extend it to also read a runtime library-dir list), so a recipe can scope the directory to Windows. No new key is introduced. - Propagation (
prepare.cppm/plan.cppm): the runtime library dirs of a dependency in an executable's link closure already reachBuildPlan::depRuntimeLibraryDirs; ensure they are resolved to absolute source paths (reuse the-L<rel>→<depRoot>/<rel>normalization) and de-duplicated. - Backend (
flags.cppm+ninja_backend.cppm): add the PE branch — for each runtime library dir of a linked dependency, emitbuild <output>/bin/<dll> : copy <abs-src>and make the executable target take an order-only dependency on it. Guard byis_windowsso other platforms are unaffected. - Packaging (
mcpp pack): include the deployedbin/*.dllso a packaged artifact is self-contained off the build machine. - Local test (achievable without Windows): an e2e on Linux using a dummy
shared library — a dependency declares a runtime library dir; assert
mcpp builddeploys the file beside the executable. This validates the deploy mechanism mechanically. The Windows-specific link/run is verified in Phase D.
The feature must ship in a released mcpp before the recipe can rely on it (an
older mcpp would link the import lib but never deploy the DLL → runtime failure).
Run the established pipeline: version bump (mcpp.toml + fingerprint.cppm),
design-doc + tests, PR → CI (5 suites) → squash-merge → release.yml (4
platforms) → mirror xlings-res/mcpp (GitHub + GitCode) → bump
xim-pkgindex/pkgs/m/mcpp.lua → xlings install mcpp@0.0.73 verification → bump
the workspace bootstrap pin.
xpm.windows:url→OpenBLAS-0.3.33-x64.zip(GLOBAL = OpenMathLib GitHub; CN = gitcodemcpp-res/openblas);sha256. The zip unpacks tobin/ lib/ include/(no wrapper directory).mcpp.windows:include_dirsresolves the shippedinclude/(carriescblas.h).ldflagslink the import librarylibopenblas.lib(clang-cl form to be finalized via CI).[runtime] library_dirs = ["bin"](the existing key) so Phase A deployslibopenblas.dllbeside the consumer's executable.- anchor strategy: Windows has no build step, so the anchor TU is supplied via
generated_files(mcpp writes it;install()does not run). Linux/macOS keep the "install()produces the anchor → triggers the Make build" trigger.
install()branches onos.host(): Windows returns immediately (prebuilt — no Make); linux/macosx run the existing source build.- CN mirror: upload
OpenBLAS-0.3.33-x64.zip(~40 MB) to the gitcodemcpp-res/openblas0.3.33 release; verify byte-equality via a GET download.
The linux/windows asymmetry is intentional and semantically natural: Linux/macOS
link a fully static libopenblas.a (no runtime artifact, so no runtime dir is
declared); Windows uses the prebuilt import-lib + DLL (MSVC-ABI Clang has no
static option from the upstream zip), so only Windows declares a runtime library
directory.
The Windows runtime half (clang-cl linking libopenblas.lib, the DLL loading at
launch, cblas_dgemm producing the correct result) can be verified only on a
real Windows runner. The current mcpp-index CI does not cover it as-is:
smoke-examplesis ubuntu-only and does build and run (tests/run_example.sh=mcpp build+mcpp run), but never on Windows.smoke-windowsruns onwindows-latestbut is build-only (smoke_compat_*.shinvokemcpp build, notmcpp run) and is pinned to mcpp 0.0.68.
So Phase D must, in validate.yml:
- Bump the Windows smoke job's mcpp pin to ≥ 0.0.73 (the release carrying the deployment feature).
- Add a Windows build-and-run step for an OpenBLAS example that asserts the
computed result — only
mcpp runexercises DLL loading; a build-only check would pass even if the DLL were never deployed.
Iterate against the runner logs until green (no interactive debugging is possible on the runner; correction is log-driven).
| Item | Local (Linux host) | CI (windows-latest) |
|---|---|---|
| recipe parse / schema / Linux deploy mechanism | yes (dummy-lib e2e 84_runtime_dll_deploy.sh) |
— |
clang linking libopenblas.lib |
no (no MSVC env) | yes — confirmed (-Llib -llibopenblas) |
DLL deployed beside the exe + loaded + cblas_dgemm runs |
no | yes — confirmed (mcpp-index #55 smoke-windows: mcpp run + direct-exe launch from a neutral CWD both green) |
- Whole-directory
*.dllvs an explicit list. OpenBLAS'sbin/holds a single DLL, so copying*.dllfrom each runtime library dir is sufficient and simple (preferred — YAGNI). If a future package'sbin/mixes unrelated DLLs, an optional explicit list can narrow it later. (Shipped as whole-directory.) - clang link form and symbol decoration. Resolved: mcpp links Windows with
the clang MSVC driver, and
-Llib -llibopenblasresolves tolib/libopenblas.lib; the cdeclcblas_*symbols matched andcblas_dgemmproduced[19 22; 43 50]on the runner. mcpp.<os>runtime parsing. Resolved without a parser change: the deploy is filtered by the*.dllextension (§"Implementation note"), so[runtime] library_dirsis declared per-OS undermcpp.windowsvia the existing per-OS textual merge — no new schema key. On Linux/macOS the static archive ships no DLL, so the declaration is inert.
The first mcpp-index #55 run's smoke-windows failed not in the feature but in
the test: the new openblas smoke ran the produced .exe from a path find target
returned relative, after cd / to a neutral CWD — so the launch resolved to a
nonexistent path. The build had already linked the import lib, deployed
bin/libopenblas.dll, and mcpp run had exited 0 (correct GEMM). Absolutising the
exe path (pwd + basename) fixed the assertion; the re-run is green. The lesson is
the value of the direct-exe launch from a neutral CWD: it is the only check that
proves the DLL loads beside the exe rather than via mcpp run's PATH prepend.