From 2dc10a55a0d3346fa583dae2de9a1b51e6b95b88 Mon Sep 17 00:00:00 2001 From: "FARNA-HERRY-OFF\\farna" Date: Sun, 16 Aug 2026 14:54:04 +0800 Subject: [PATCH 1/3] feat(compat.openssl): add Windows support via VC-WIN64A + nmake source build The last major package still marked "windows deferred". Unblocks the Windows leg of compat.libmysqlclient, whose mcpp.toml is already wired for it; openssl was the only missing dependency there. - xpm: add a windows entry reusing the shared source tarball (same sha256) - mcpp: add windows ldflags. lld-link resolves `-lX` to `X.lib`, so the MSVC-built static libs must be spelled `-llibssl`/`-llibcrypto`; the static libcrypto system deps (ws2_32, crypt32, advapi32, user32) are listed explicitly, the same way the linux leg lists -ldl/-lpthread. - install(): route windows to _install_windows() instead of erroring out. - _install_windows(): one generated .bat runs under a single vcvars64 invocation and does Configure VC-WIN64A no-shared no-dso no-tests no-apps no-engine no-asm + nmake + install_sw, then verifies libssl.lib/libcrypto.lib exist and writes the anchor TU. Helpers: find_vcvars (vswhere, then known paths), resolve_perl_windows / perl_usable_windows, win_dirname. Pitfalls found on real hardware, baked into the implementation: - the xpkg hook env on Windows has no os.rm (must be pcall'd) and no path.dirname (hand-rolled win_dirname) - OpenSSL Configure needs Locale::Maketext::Simple, which MSYS perl (Git-for-Windows) lacks; the existing module probe would wrongly accept it, so the windows check requires Strawberry Perl - os.exec("bash -c ...") silently returns true WITHOUT running the command in the Windows hook env, so the windows build drives `cmd /c > 2>&1` directly instead of reusing the shared run() - vcvars (cmd /c "vcvars64.bat & set") works fine on Windows 11 + VS 2022/18; the old "takes the whole process chain down" note was a hook-env artifact, not a property of Windows --- pkgs/c/compat.openssl.lua | 198 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 192 insertions(+), 6 deletions(-) diff --git a/pkgs/c/compat.openssl.lua b/pkgs/c/compat.openssl.lua index 52a7a3d..eb4295a 100644 --- a/pkgs/c/compat.openssl.lua +++ b/pkgs/c/compat.openssl.lua @@ -106,7 +106,17 @@ package = { sha256 = "529043b15cffa5f36077a4d0af83f3de399807181d607441d734196d889b641f", }, }, - -- windows deferred (prebuilt zip not yet prepared) + windows = { + -- Same source tarball as the other platforms; the build differs + -- (VC-WIN64A + nmake), not the source. See _install_windows(). + ["3.5.1"] = { + url = { + GLOBAL = "https://github.com/openssl/openssl/releases/download/openssl-3.5.1/openssl-3.5.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/openssl/releases/download/3.5.1/openssl-3.5.1.tar.gz", + }, + sha256 = "529043b15cffa5f36077a4d0af83f3de399807181d607441d734196d889b641f", + }, + }, }, mcpp = { @@ -147,6 +157,13 @@ package = { -- this package built, so name resolution has nothing else to find, and -- libSystem already carries dl/pthread. macosx = { ldflags = { "-Llib", "-lssl", "-lcrypto" } }, + -- Windows: MSVC-built static libs, named libssl.lib / libcrypto.lib. + -- -lssl / -lcrypto resolve those (clang/lld-link looks for lib.lib). + -- Static libcrypto's own system deps must be spelled out for consumers, + -- the same way the linux leg lists -ldl/-lpthread. The set below is what + -- a no-asm Windows build needs (winsock + crypt + registry + windowing). + windows = { ldflags = { "-Llib", "-llibssl", "-llibcrypto", + "-lws2_32", "-lcrypt32", "-ladvapi32", "-luser32" } }, }, } @@ -445,13 +462,182 @@ local function _install_impl() return true end +-- ── Windows: VC-WIN64A + nmake ──────────────────────────────────────────── + +-- Locate vcvars64.bat: vswhere first (canonical), then well-known paths. +-- +-- NOTE: this xpkg runtime has no os.rm (calling it throws `attempt to call a +-- nil value`), so it is invoked through pcall; the missing temp file is not an +-- error worth surfacing anyway. +local function find_vcvars() + local vswhere = "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe" + if os.isfile(vswhere) then + local outf = path.join(os.getenv("TEMP") or ".", "mcpp_vswhere.txt") + pcall(os.rm, outf) + local ok, err = pcall(os.exec, string.format("bash -c %s", sh_quote( + vswhere .. " -latest -products * -requires " + .. "Microsoft.VisualStudio.Component.VC.Tools.x86.x64 " + .. "-property installationPath > " .. outf))) + if ok and not err then + local line = os.isfile(outf) and (io.readfile(outf) or "") or "" + line = tostring(line):gsub("%s+$", "") + if line ~= "" then + local cand = path.join(line, "VC", "Auxiliary", "Build", "vcvars64.bat") + if os.isfile(cand) then return cand end + end + end + end + for _, p in ipairs({ + "C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat", + "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat", + "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat", + }) do + if os.isfile(p) then return p end + end + return nil +end + +-- Windows-style dirname: strip the last path segment (works for both +-- backslash and forward-slash paths). xpkg's Lua has path.join but no +-- path.dirname, so derive it manually. +local function win_dirname(p) + local s = tostring(p):gsub("[/\\]+$", "") + local head = s:match("^(.*)[/\\][^/\\]+$") + return head or s +end + +-- Strawberry-perl check: unlike the unix build, Configure ALSO needs +-- Locale::Maketext::Simple here (MSYS perl lacks it and would die deep inside +-- Configure). Probe that module explicitly. +local function perl_usable_windows(perl) + return pcall(function() + os.exec(string.format("bash -c %s", + sh_quote(sh_quote(perl) + .. " -MLocale::Maketext::Simple -MConfig -MFindBin" + .. " -e exit >/dev/null 2>&1"))) + end) +end + +-- Resolve a perl that can actually drive Configure on Windows. Prefer a +-- native Windows perl (Strawberry) at well-known locations over whatever PATH +-- resolves, which on a Git-for-Windows host is the MSYS perl that fails the +-- Locale::Maketext check above. +local function resolve_perl_windows() + local scoop = os.getenv("USERPROFILE") + and path.join(os.getenv("USERPROFILE"), "scoop", "apps", "perl", + "current", "perl", "bin", "perl.exe") + local known = { + "C:\\Strawberry\\perl\\bin\\perl.exe", + scoop, + } + for _, c in ipairs(known) do + if c and os.isfile(c) and perl_usable_windows(c) then + return c, win_dirname(c) + end + end + if perl_usable_windows("perl") then + return "perl", nil + end + return nil, nil +end + +-- Build OpenSSL on Windows. VC-WIN64A generates an NMAKE makefile, so this +-- needs the MSVC toolset (nmake + cl). Everything runs inside ONE generated +-- .bat invoked once under vcvars64, so every step shares the same toolset env +-- (a vcvars invocation per command would re-enter `call` and is what the old +-- "vcvars kills the process chain" report was chasing). A child `cmd /c` +-- really does run vcvars fine on a normal machine; this hook captures that +-- environment by simply doing all the work inside it. +local function _install_windows() + local vcvars = find_vcvars() + if not vcvars then + log.error("compat.openssl: no Visual Studio C++ toolset found. Install " + .. "\"Desktop development with C++\" (MSVC + Windows SDK) and retry.") + return false + end + + local perl, perlbin = resolve_perl_windows() + if not perl then + log.error("compat.openssl: no usable perl. Configure needs a perl WITH " + .. "Locale::Maketext::Simple + core modules. Install Strawberry " + .. "Perl (https://strawberryperl.com) and retry.") + return false + end + local perlbinpath = perlbin or win_dirname(perl) + + local ifile = pkginfo.install_file() + local srcroot = ifile and tostring(ifile):replace(".tar.gz", "") + or ("openssl-" .. pkginfo.version()) + if not os.isdir(srcroot) then + srcroot = "openssl-" .. pkginfo.version() + end + + local prefix = pkginfo.install_dir() + os.tryrm(prefix) + os.mkdir(prefix) + local logf = path.join(prefix, "mcpp_openssl_build.log") + + -- Static-only, no asm (no NASM dependency), no apps/tests/engine. `no-asm` + -- is the deliberate Windows default for now: VC-WIN64A's asm path needs + -- NASM on %PATH%, which would be a second host dependency to resolve. Pure-C + -- crypto is functionally identical, just a bit slower; revisit if a build + -- dep for nasm ever lands. + local flags = "no-shared no-dso no-tests no-apps no-engine no-asm" + local bat = path.join(srcroot, "mcpp_build_win.bat") + io.writefile(bat, string.format([[ +@echo off +call "%s" >nul 2>&1 +if errorlevel 1 exit /b 1 +set "PATH=%s;%%PATH%%" +cd /d "%s" +perl Configure VC-WIN64A %s --prefix="%s" --libdir=lib +if errorlevel 1 exit /b 1 +nmake +if errorlevel 1 exit /b 1 +nmake install_sw +if errorlevel 1 exit /b 1 +]], vcvars, perlbinpath, srcroot, flags, prefix)) + + -- The unix `run()` helper wraps commands in `bash -c`, but this hook's + -- Windows environment has no usable bash — os.exec("bash -c …") returns + -- true without running anything (probed directly). Drive the build via a + -- single `cmd /c` invocation instead; paths here are space-free in the + -- standard layout, and cmd itself handles the `>` capture. + local exok, exerr = pcall(os.exec, string.format( + "cmd /c %s > %s 2>&1", bat, logf)) + if not exok then + log.error("compat.openssl: windows build could not start " + .. "(os.exec failed: %s)", tostring(exerr)) + return false + end + + local libdir = path.join(prefix, "lib") + local crypto = path.join(libdir, "libcrypto.lib") + local ssl = path.join(libdir, "libssl.lib") + if not os.isfile(crypto) or not os.isfile(ssl) then + log.error("compat.openssl: windows build produced no libcrypto.lib / " + .. "libssl.lib under %s (see %s)", libdir, logf) + return false + end + + -- Emit the anchor TU mcpp compiles; its absence is what triggers install(). + io.writefile(path.join(prefix, "mcpp_openssl_anchor.c"), + "int mcpp_compat_openssl_anchor(void) { return 0; }\n") + return true +end + function install() - -- Windows is deferred: there is no windows xpm block, so version - -- resolution already fails before this point. Kept as a named error in - -- case a windows entry is added before this hook learns to build there. if os.host() == "windows" then - log.error("compat.openssl: windows is not yet supported") - return false + local ok, result = pcall(_install_windows) + if not ok then + log.error("compat.openssl install() (windows) failed: %s", tostring(result)) + return false + end + if not result then + log.error("compat.openssl install() (windows) returned false") + return false + end + return true end local ok, result = pcall(_install_impl) if not ok then From 923473261e0fe2d8b4fa486cb58a24b9740fe4a6 Mon Sep 17 00:00:00 2001 From: "FARNA-HERRY-OFF\\farna" Date: Sun, 16 Aug 2026 20:03:50 +0800 Subject: [PATCH 2/3] test(openssl): run the openssl member on Windows too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compat.openssl now builds on Windows (VC-WIN64A + nmake), so the tests/examples/openssl member no longer has to be a no-op main() there. Declare the dependency and HAVE_OPENSSL on all three platforms; tls.cpp compiles its real TLS-context + SHA-256 checks on Windows as well. Verified on Windows (llvm/clang toolchain, MSVC-built libssl.lib + libcrypto.lib): `mcpp test -p openssl` → tls ... ok (1 passed, 0 failed). --- tests/examples/openssl/mcpp.toml | 16 ++++++++++++---- tests/examples/openssl/tests/tls.cpp | 12 +++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/examples/openssl/mcpp.toml b/tests/examples/openssl/mcpp.toml index ebe8c4c..6fd2862 100644 --- a/tests/examples/openssl/mcpp.toml +++ b/tests/examples/openssl/mcpp.toml @@ -1,5 +1,6 @@ # compat.openssl test project — the dedicated member that validates the -# from-source OpenSSL build (static libssl.a + libcrypto.a + headers). +# from-source OpenSSL build (static libssl.a + libcrypto.a + headers on +# linux/macOS; libssl.lib + libcrypto.lib on Windows). # # It inherits the workspace-root `compat` redirect, so the descriptor in THIS # checkout is what gets built and linked. tests/examples/asio-ssl covers the @@ -7,9 +8,10 @@ # and this member is what isolates a failure to openssl itself rather than to # the feature wiring around it. # -# linux + macOS only: there is no windows xpm entry yet (prebuilt MSVC archives -# unpublished), so on windows the member carries no dependency and the test -# compiles to a no-op main(). +# All three platforms: linux/macOS build via perl Configure + GNU Make, windows +# via VC-WIN64A + nmake (see _install_windows in the descriptor). The member +# declares the dependency and HAVE_OPENSSL on every platform, so the test is a +# real test everywhere — not a no-op main() on windows. [package] name = "openssl-tests" version = "0.1.0" @@ -25,3 +27,9 @@ openssl = "3.5.1" [target.'cfg(macos)'.build] cxxflags = ["-DHAVE_OPENSSL=1"] + +[target.'cfg(windows)'.dependencies.compat] +openssl = "3.5.1" + +[target.'cfg(windows)'.build] +cxxflags = ["-DHAVE_OPENSSL=1"] diff --git a/tests/examples/openssl/tests/tls.cpp b/tests/examples/openssl/tests/tls.cpp index dba1970..bc4a37d 100644 --- a/tests/examples/openssl/tests/tls.cpp +++ b/tests/examples/openssl/tests/tls.cpp @@ -2,11 +2,13 @@ // library initialises far enough to stand up a TLS context and run a digest. // // Touching BOTH archives is the point: libssl.a (SSL_CTX_new, TLS_method) and -// libcrypto.a (EVP_*). A link that silently dropped one, or picked up a host -// libssl.so instead of the package's own static build, fails here. +// libcrypto.a (EVP_*) — libssl.lib / libcrypto.lib on Windows. A link that +// silently dropped one, or picked up a host libssl.so instead of the package's +// own static build, fails here. // -// HAVE_OPENSSL comes from this project's own cfg-gated cxxflags — the package -// is linux/macOS-only, so elsewhere this file is an empty main(). +// HAVE_OPENSSL comes from this project's own cfg-gated cxxflags. The package +// is supported on all three platforms now (linux/macOS: perl Configure + GNU +// Make; windows: VC-WIN64A + nmake), so the real test runs everywhere. #ifdef HAVE_OPENSSL #include #include @@ -46,5 +48,5 @@ int main() { return 0; } #else -int main() { return 0; } // compat.openssl is linux/macOS-only; no-op elsewhere +int main() { return 0; } // no HAVE_OPENSSL — fallback that always passes #endif From cdda30985c35f0ea46430d1525f9d5f5141f9e4a Mon Sep 17 00:00:00 2001 From: "FARNA-HERRY-OFF\\farna" Date: Sun, 16 Aug 2026 21:09:50 +0800 Subject: [PATCH 3/3] fix(compat.openssl): find the VS toolset through cmd /c, not bash -c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows CI failed `mcpp test -p openssl` at install(): find_vcvars ran vswhere through `os.exec("bash -c …")`, which this hook's Windows environment silently swallows (returns true, runs nothing), so vswhere's output file was never written. The search then fell back to hardcoded vcvars64.bat paths that only covered Community editions — and GitHub Actions runners carry VS **Enterprise**, so the toolset was never found. - find_vcvars: drive vswhere through a generated .bat under `cmd /c` (the same pattern _install_windows uses), which actually executes and is edition-agnostic; keep the hardcoded fallback but expand it to Enterprise/BuildTools across the 2022 (17) / 18 (2026) product lines. - perl_usable_windows: the old probe ran perl via `bash -c`, which also no-ops — it accepted ANY perl, including the MSYS perl that lacks Locale::Maketext::Simple and dies inside Configure. Probe perl through a generated .bat under `cmd /c` and require it to print a marker only when the modules load; MSYS perl on PATH now correctly fails the probe. Verified on hardware: the generated vswhere .bat returns the install path (and the matching vcvars64.bat), and the perl probe prints `ok` for Strawberry (scoop) perl while MSYS perl fails it with the missing-module error. Descriptor still parses with the pinned CI client (2026.8.10.3). --- pkgs/c/compat.openssl.lua | 70 ++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/pkgs/c/compat.openssl.lua b/pkgs/c/compat.openssl.lua index eb4295a..0cabfac 100644 --- a/pkgs/c/compat.openssl.lua +++ b/pkgs/c/compat.openssl.lua @@ -472,27 +472,43 @@ end local function find_vcvars() local vswhere = "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe" if os.isfile(vswhere) then - local outf = path.join(os.getenv("TEMP") or ".", "mcpp_vswhere.txt") + -- vswhere is the canonical, edition-agnostic way to find the toolset. + -- It has to be driven through cmd /c: this hook's environment silently + -- swallows `os.exec("bash -c ...")` (returns true without running), so + -- a `bash -c "vswhere ... > out"` probe never writes its output file + -- and the search falls through to the hardcoded paths below — which is + -- exactly what breaks on CI runners (VS Enterprise, not Community). + local tmp = os.getenv("TEMP") or "." + local bat = path.join(tmp, "mcpp_vswhere.bat") + local outf = path.join(tmp, "mcpp_vswhere.txt") + pcall(os.rm, bat) pcall(os.rm, outf) - local ok, err = pcall(os.exec, string.format("bash -c %s", sh_quote( - vswhere .. " -latest -products * -requires " - .. "Microsoft.VisualStudio.Component.VC.Tools.x86.x64 " - .. "-property installationPath > " .. outf))) - if ok and not err then - local line = os.isfile(outf) and (io.readfile(outf) or "") or "" - line = tostring(line):gsub("%s+$", "") + io.writefile(bat, string.format([[ +@echo off +"%s" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath > "%s" +]], vswhere, outf)) + local ok = pcall(os.exec, string.format("cmd /c %s", bat)) + if ok and os.isfile(outf) then + local line = (io.readfile(outf) or ""):gsub("%s+$", "") if line ~= "" then local cand = path.join(line, "VC", "Auxiliary", "Build", "vcvars64.bat") if os.isfile(cand) then return cand end end end end - for _, p in ipairs({ - "C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat", - "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat", - "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat", + -- Fallback: well-known vcvars64.bat locations across the Enterprise / + -- Community / BuildTools editions and the 2022 (17) / 18 (2026) product + -- lines. vswhere above is canonical; this is the safety net for installs + -- where it is absent or the layout is unusual. + for _, base in ipairs({ + "C:\\Program Files\\Microsoft Visual Studio\\18", + "C:\\Program Files\\Microsoft Visual Studio\\2022", + "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022", }) do - if os.isfile(p) then return p end + for _, ed in ipairs({ "Enterprise", "Community", "BuildTools" }) do + local cand = path.join(base, ed, "VC", "Auxiliary", "Build", "vcvars64.bat") + if os.isfile(cand) then return cand end + end end return nil end @@ -508,14 +524,28 @@ end -- Strawberry-perl check: unlike the unix build, Configure ALSO needs -- Locale::Maketext::Simple here (MSYS perl lacks it and would die deep inside --- Configure). Probe that module explicitly. +-- Configure). Probe that module explicitly. The probe is a REAL one: this +-- hook's environment silently swallows `os.exec("bash -c ...")`, so the old +-- probe accepted ANY perl — including the MSYS perl that would fail inside +-- Configure. Drive perl through a generated .bat under `cmd /c` (the same +-- pattern find_vcvars and _install_windows use) and have it print a marker +-- only when every required module loads. The perl paths passed here are +-- space-free (well-known install dirs or bare `perl` from PATH); only the +-- marker path is quoted. local function perl_usable_windows(perl) - return pcall(function() - os.exec(string.format("bash -c %s", - sh_quote(sh_quote(perl) - .. " -MLocale::Maketext::Simple -MConfig -MFindBin" - .. " -e exit >/dev/null 2>&1"))) - end) + local tmp = os.getenv("TEMP") or "." + local probe = path.join(tmp, "mcpp_perl_probe.bat") + local marker = path.join(tmp, "mcpp_perl_probe.txt") + pcall(os.rm, probe) + pcall(os.rm, marker) + io.writefile(probe, string.format([[ +@echo off +%s -MLocale::Maketext::Simple -MConfig -MFindBin -e "print qq(ok)" > "%s" 2>&1 +]], perl, marker)) + local ok = pcall(os.exec, string.format("cmd /c %s", probe)) + if not ok then return false end + local content = os.isfile(marker) and (io.readfile(marker) or "") or "" + return content:find("ok", 1, true) ~= nil end -- Resolve a perl that can actually drive Configure on Windows. Prefer a