From 7532b3686b05523103ea5f79d4d0d656fe368a4d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 29 Jun 2026 19:13:07 +0800 Subject: [PATCH 1/2] feat(compat.openblas): Windows support via prebuilt import-lib + runtime DLL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Windows to compat.openblas. OpenBLAS has no upstream Make path for the MSVC-ABI Clang mcpp links with, so Windows uses the prebuilt x64 zip: link the MSVC import lib (lib/libopenblas.lib) and ship the runtime DLL (bin/libopenblas.dll) beside the consumer's .exe. mcpp >= 0.0.73 stages a dependency's [runtime] library_dirs *.dll next to the executable (PE has no RPATH), so the consumer launches standalone. Recipe: * xpm.windows: OpenBLAS-0.3.33-x64.zip (GLOBAL OpenMathLib GitHub; CN gitcode mcpp-res/openblas, uploaded + GET-verified byte-equal). Unpacks to bin/ lib/ include/ (no wrapper dir). * mcpp ldflags are now per-OS (the synthesized-manifest parser APPENDS ldflags, so a global -lopenblas would wrongly reach the Windows link): linux/macosx link the static -lopenblas; windows links -llibopenblas (clang MSVC maps it to libopenblas.lib). * mcpp.windows: [runtime] library_dirs=["bin"] (DLL deploy) + a generated_files anchor TU (no Make build on Windows). install() returns immediately on windows; linux/macosx keep the source build. CI (Phase D): smoke_compat_portable.sh gains a Windows-only OpenBLAS build-AND-run — cblas_dgemm must produce [19 22; 43 50], and the produced .exe is launched DIRECTLY from a neutral CWD so a clean exit proves the DLL was actually deployed beside it (not merely found via mcpp run's PATH prepend). MCPP_VERSION pinned 0.0.68 -> 0.0.73 (the release carrying DLL deployment). See mcpp .agents/docs/2026-06-29-windows-runtime-dll-deployment-and-openblas.md. --- .github/workflows/validate.yml | 12 ++++--- pkgs/c/compat.openblas.lua | 64 ++++++++++++++++++++++++++++++---- tests/smoke_compat_portable.sh | 49 ++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 11 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f065244..ddf95d5 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -11,7 +11,9 @@ on: workflow_dispatch: env: - MCPP_VERSION: "0.0.68" + # Bumped to 0.0.73: carries Windows runtime-DLL deployment, required by the + # compat.openblas Windows recipe (bin/libopenblas.dll staged beside the .exe). + MCPP_VERSION: "0.0.73" jobs: lint: @@ -238,14 +240,14 @@ jobs: include: - platform: macos os: macos-15 - archive: mcpp-0.0.68-macosx-arm64.tar.gz - root: mcpp-0.0.68-macosx-arm64 + archive: mcpp-0.0.73-macosx-arm64.tar.gz + root: mcpp-0.0.73-macosx-arm64 mcpp: bin/mcpp xlings: registry/bin/xlings - platform: windows os: windows-latest - archive: mcpp-0.0.68-windows-x86_64.zip - root: mcpp-0.0.68-windows-x86_64 + archive: mcpp-0.0.73-windows-x86_64.zip + root: mcpp-0.0.73-windows-x86_64 mcpp: bin/mcpp.exe xlings: registry/bin/xlings.exe steps: diff --git a/pkgs/c/compat.openblas.lua b/pkgs/c/compat.openblas.lua index c5b2468..b0cb64e 100644 --- a/pkgs/c/compat.openblas.lua +++ b/pkgs/c/compat.openblas.lua @@ -18,8 +18,18 @@ -- headers, and emits the anchor — then compiles the anchor and links the lib -- (`-Llib -lopenblas`, with `-Llib` rewritten to /lib). -- --- Platforms: linux/macosx build from source via Make. Windows (no upstream Make --- path; OpenBLAS ships prebuilt zips) is a follow-up. +-- Platforms: +-- * linux/macosx — build a fully static libopenblas.a from source via Make +-- (no runtime artifact; install() emits the anchor → triggers the build). +-- * windows — no upstream Make path with the MSVC-ABI Clang mcpp links with, so +-- use OpenBLAS's prebuilt x64 zip: link the MSVC import lib (lib/libopenblas.lib) +-- and ship the runtime DLL (bin/libopenblas.dll) beside the consumer's .exe. +-- mcpp >= 0.0.73 deploys a dependency's [runtime] library_dirs *.dll next to +-- the executable (PE has no RPATH); see +-- mcpp/.agents/docs/2026-06-29-windows-runtime-dll-deployment-and-openblas.md. +-- The Windows anchor is a generated_files TU (no Make build), so install() +-- returns immediately on windows. The asymmetry is intentional: only Windows +-- declares a [runtime] library_dir because only Windows ships a runtime DLL. package = { spec = "1", namespace = "compat", @@ -50,21 +60,57 @@ package = { sha256 = "6761af1d9f5d353ab4f0b7497be2643313b36c8f31caec0144bfef198e71e6ab", }, }, + windows = { + -- Prebuilt x64 zip (no Make build). Unpacks to bin/ lib/ include/ with + -- no wrapper dir: bin/libopenblas.dll, lib/libopenblas.lib (MSVC import + -- lib), include/cblas.h. + ["0.3.33"] = { + url = { + GLOBAL = "https://github.com/OpenMathLib/OpenBLAS/releases/download/v0.3.33/OpenBLAS-0.3.33-x64.zip", + CN = "https://gitcode.com/mcpp-res/openblas/releases/download/0.3.33/OpenBLAS-0.3.33-x64.zip", + }, + sha256 = "7ad797ef0c9a5c42e28903bf726eaaaade307dafe187ff0e923d90cd4002780c", + }, + }, }, mcpp = { language = "c++23", import_std = false, c_standard = "c11", - -- The anchor is NOT a generated_files entry: it is emitted by install() - -- so mcpp must run install() (which also builds the lib) before it can - -- compile this source. include/ + lib/ are produced by `make install`. + -- On linux/macosx the anchor is NOT a generated_files entry: it is emitted + -- by install() so mcpp must run install() (which also builds the lib) + -- before it can compile this source. include/ + lib/ are produced by + -- `make install`. On windows there is no Make build, so the anchor is a + -- generated_files TU (see the windows block) and install() is a no-op. sources = { "mcpp_openblas_anchor.c" }, targets = { ["openblas"] = { kind = "lib" } }, include_dirs = { "include" }, - ldflags = { "-Llib", "-lopenblas" }, provides = { "blas" }, deps = { }, + + -- ldflags are per-OS: the synthesized-manifest parser APPENDS ldflags, so + -- a global `-lopenblas` would also reach the Windows link (wrong: there + -- the lib is the import lib libopenblas.lib). linux/macosx link the static + -- archive; the per-OS merge picks exactly one of these blocks by host. + linux = { ldflags = { "-Llib", "-lopenblas" } }, + macosx = { ldflags = { "-Llib", "-lopenblas" } }, + windows = { + -- Link the MSVC import lib lib/libopenblas.lib (clang MSVC driver maps + -- `-llibopenblas` → libopenblas.lib). `-Llib` is rewritten to + -- /lib by mcpp. + ldflags = { "-Llib", "-llibopenblas" }, + -- bin/libopenblas.dll is staged beside the consumer .exe by mcpp's + -- runtime-DLL deployment (mcpp >= 0.0.73). On non-Windows the *.dll + -- filter makes this declaration inert. + runtime = { library_dirs = { "bin" } }, + -- No Make build on Windows: provide the anchor TU directly so mcpp is + -- self-sufficient and never triggers install() here. (linux/macosx get + -- their anchor from install(), which is what triggers the Make build.) + generated_files = { + ["mcpp_openblas_anchor.c"] = "int mcpp_compat_openblas_anchor(void) { return 0; }\n", + }, + }, }, } @@ -155,6 +201,12 @@ local function _install_impl() end function install() + -- Windows ships a prebuilt zip (import lib + DLL); nothing to build. The + -- anchor TU comes from mcpp `generated_files`, so mcpp never needs install() + -- to produce a source here — this is a no-op success. + if os.host() == "windows" then + return true + end local ok, err = pcall(_install_impl) if not ok then log.error("compat.openblas install() failed: %s", tostring(err)) diff --git a/tests/smoke_compat_portable.sh b/tests/smoke_compat_portable.sh index c257110..472e38c 100755 --- a/tests/smoke_compat_portable.sh +++ b/tests/smoke_compat_portable.sh @@ -414,4 +414,53 @@ EOF "$MCPP_BIN_POSIX" build "$MCPP_BIN_POSIX" run +# compat.openblas — Windows-only build-AND-run. This is the only place that +# exercises the mcpp >= 0.0.73 runtime-DLL deployment end to end: the consumer +# links the MSVC import lib (lib/libopenblas.lib) and, at `mcpp run`, loads the +# bin/libopenblas.dll that mcpp staged beside the .exe (a build-only check would +# pass even if the DLL were never deployed). cblas_dgemm computes a 2x2 GEMM and +# the program returns nonzero unless the result is exactly [19 22; 43 50], so a +# wrong/missing BLAS fails the run. Scoped to Windows: linux/macosx link a static +# libopenblas.a (no DLL, nothing new to validate here) and macosx would add a +# multi-minute source build to every portable run. +if [[ "$platform" == "Windows_NT" ]]; then + make_project "compat-portable-openblas-smoke" + cat >> mcpp.toml <<'EOF' + +[dependencies.compat] +openblas = "0.3.33" +EOF + cat > src/main.cpp <<'EOF' +#include + +int main() { + // A (2x2, row-major) * B (2x2) = C; expect [[19,22],[43,50]]. + const double A[4] = {1, 2, 3, 4}; + const double B[4] = {5, 6, 7, 8}; + double C[4] = {0, 0, 0, 0}; + cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, + 2, 2, 2, 1.0, A, 2, B, 2, 0.0, C, 2); + const double expected[4] = {19, 22, 43, 50}; + for (int i = 0; i < 4; ++i) { + if (C[i] != expected[i]) return 10 + i; // wrong/missing BLAS + } + return 0; +} +EOF + "$MCPP_BIN_POSIX" build + # `mcpp run` also prepends the source bin/ to PATH, so it alone would not + # prove the DLL was deployed. Run the produced .exe DIRECTLY from a neutral + # CWD: Windows searches the executable's own directory first, so a clean exit + # proves bin/libopenblas.dll was actually staged beside app.exe (the deploy). + "$MCPP_BIN_POSIX" run + exe="$(find target -type f -name '*.exe' | head -1)" + [ -n "$exe" ] || { echo "FAIL: openblas smoke produced no .exe"; exit 1; } + exedir="$(cd "$(dirname "$exe")" && pwd)" + [ -f "$exedir/libopenblas.dll" ] || { + echo "FAIL: libopenblas.dll was not deployed beside the .exe ($exedir)" + ls -la "$exedir"; exit 1; } + ( cd / && "$(to_native_path "$exe")" ) || { + echo "FAIL: direct .exe launch failed — deployed DLL not loadable"; exit 1; } +fi + echo "OK" From b3ad220b8691844b9501e08d17feeeb8830554b3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 29 Jun 2026 19:30:34 +0800 Subject: [PATCH 2/2] fix(smoke): openblas Windows direct-launch uses absolute exe path The Phase D direct-launch assertion ran the .exe found by `find target` (a path relative to the project dir) after `cd /`, so it resolved to a nonexistent path ("No such file or directory") and failed even though the build linked the import lib, deployed bin/libopenblas.dll beside the .exe (the DLL-presence check passed), and `mcpp run` succeeded. Absolutise the exe path (exedir via pwd + basename) before the neutral-CWD launch. --- tests/smoke_compat_portable.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/smoke_compat_portable.sh b/tests/smoke_compat_portable.sh index 472e38c..967ce6c 100755 --- a/tests/smoke_compat_portable.sh +++ b/tests/smoke_compat_portable.sh @@ -455,11 +455,15 @@ EOF "$MCPP_BIN_POSIX" run exe="$(find target -type f -name '*.exe' | head -1)" [ -n "$exe" ] || { echo "FAIL: openblas smoke produced no .exe"; exit 1; } + # Absolutise: `find` yields a path relative to the project dir, but the launch + # runs from a neutral CWD (so the DLL can only be found beside the .exe, not + # via CWD). pwd makes exedir absolute; build an absolute native exe path. exedir="$(cd "$(dirname "$exe")" && pwd)" + exeabs="$exedir/$(basename "$exe")" [ -f "$exedir/libopenblas.dll" ] || { echo "FAIL: libopenblas.dll was not deployed beside the .exe ($exedir)" ls -la "$exedir"; exit 1; } - ( cd / && "$(to_native_path "$exe")" ) || { + ( cd / && "$(to_native_path "$exeabs")" ) || { echo "FAIL: direct .exe launch failed — deployed DLL not loadable"; exit 1; } fi