Skip to content

Commit 457567d

Browse files
FarnaHerryFARNA-HERRY-OFF\farna
andauthored
feat(compat.openssl): add Windows support via VC-WIN64A + nmake sourc… (#211)
* 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 <bat> > <log> 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 * test(openssl): run the openssl member on Windows too 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). * fix(compat.openssl): find the VS toolset through cmd /c, not bash -c 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). --------- Co-authored-by: FARNA-HERRY-OFF\farna <farnaherryoffical@outlook.com>
1 parent 0dd1e5d commit 457567d

3 files changed

Lines changed: 241 additions & 15 deletions

File tree

pkgs/c/compat.openssl.lua

Lines changed: 222 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,17 @@ package = {
106106
sha256 = "529043b15cffa5f36077a4d0af83f3de399807181d607441d734196d889b641f",
107107
},
108108
},
109-
-- windows deferred (prebuilt zip not yet prepared)
109+
windows = {
110+
-- Same source tarball as the other platforms; the build differs
111+
-- (VC-WIN64A + nmake), not the source. See _install_windows().
112+
["3.5.1"] = {
113+
url = {
114+
GLOBAL = "https://github.com/openssl/openssl/releases/download/openssl-3.5.1/openssl-3.5.1.tar.gz",
115+
CN = "https://gitcode.com/mcpp-res/openssl/releases/download/3.5.1/openssl-3.5.1.tar.gz",
116+
},
117+
sha256 = "529043b15cffa5f36077a4d0af83f3de399807181d607441d734196d889b641f",
118+
},
119+
},
110120
},
111121

112122
mcpp = {
@@ -147,6 +157,13 @@ package = {
147157
-- this package built, so name resolution has nothing else to find, and
148158
-- libSystem already carries dl/pthread.
149159
macosx = { ldflags = { "-Llib", "-lssl", "-lcrypto" } },
160+
-- Windows: MSVC-built static libs, named libssl.lib / libcrypto.lib.
161+
-- -lssl / -lcrypto resolve those (clang/lld-link looks for lib<name>.lib).
162+
-- Static libcrypto's own system deps must be spelled out for consumers,
163+
-- the same way the linux leg lists -ldl/-lpthread. The set below is what
164+
-- a no-asm Windows build needs (winsock + crypt + registry + windowing).
165+
windows = { ldflags = { "-Llib", "-llibssl", "-llibcrypto",
166+
"-lws2_32", "-lcrypt32", "-ladvapi32", "-luser32" } },
150167
},
151168
}
152169

@@ -445,13 +462,212 @@ local function _install_impl()
445462
return true
446463
end
447464

465+
-- ── Windows: VC-WIN64A + nmake ────────────────────────────────────────────
466+
467+
-- Locate vcvars64.bat: vswhere first (canonical), then well-known paths.
468+
--
469+
-- NOTE: this xpkg runtime has no os.rm (calling it throws `attempt to call a
470+
-- nil value`), so it is invoked through pcall; the missing temp file is not an
471+
-- error worth surfacing anyway.
472+
local function find_vcvars()
473+
local vswhere = "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe"
474+
if os.isfile(vswhere) then
475+
-- vswhere is the canonical, edition-agnostic way to find the toolset.
476+
-- It has to be driven through cmd /c: this hook's environment silently
477+
-- swallows `os.exec("bash -c ...")` (returns true without running), so
478+
-- a `bash -c "vswhere ... > out"` probe never writes its output file
479+
-- and the search falls through to the hardcoded paths below — which is
480+
-- exactly what breaks on CI runners (VS Enterprise, not Community).
481+
local tmp = os.getenv("TEMP") or "."
482+
local bat = path.join(tmp, "mcpp_vswhere.bat")
483+
local outf = path.join(tmp, "mcpp_vswhere.txt")
484+
pcall(os.rm, bat)
485+
pcall(os.rm, outf)
486+
io.writefile(bat, string.format([[
487+
@echo off
488+
"%s" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath > "%s"
489+
]], vswhere, outf))
490+
local ok = pcall(os.exec, string.format("cmd /c %s", bat))
491+
if ok and os.isfile(outf) then
492+
local line = (io.readfile(outf) or ""):gsub("%s+$", "")
493+
if line ~= "" then
494+
local cand = path.join(line, "VC", "Auxiliary", "Build", "vcvars64.bat")
495+
if os.isfile(cand) then return cand end
496+
end
497+
end
498+
end
499+
-- Fallback: well-known vcvars64.bat locations across the Enterprise /
500+
-- Community / BuildTools editions and the 2022 (17) / 18 (2026) product
501+
-- lines. vswhere above is canonical; this is the safety net for installs
502+
-- where it is absent or the layout is unusual.
503+
for _, base in ipairs({
504+
"C:\\Program Files\\Microsoft Visual Studio\\18",
505+
"C:\\Program Files\\Microsoft Visual Studio\\2022",
506+
"C:\\Program Files (x86)\\Microsoft Visual Studio\\2022",
507+
}) do
508+
for _, ed in ipairs({ "Enterprise", "Community", "BuildTools" }) do
509+
local cand = path.join(base, ed, "VC", "Auxiliary", "Build", "vcvars64.bat")
510+
if os.isfile(cand) then return cand end
511+
end
512+
end
513+
return nil
514+
end
515+
516+
-- Windows-style dirname: strip the last path segment (works for both
517+
-- backslash and forward-slash paths). xpkg's Lua has path.join but no
518+
-- path.dirname, so derive it manually.
519+
local function win_dirname(p)
520+
local s = tostring(p):gsub("[/\\]+$", "")
521+
local head = s:match("^(.*)[/\\][^/\\]+$")
522+
return head or s
523+
end
524+
525+
-- Strawberry-perl check: unlike the unix build, Configure ALSO needs
526+
-- Locale::Maketext::Simple here (MSYS perl lacks it and would die deep inside
527+
-- Configure). Probe that module explicitly. The probe is a REAL one: this
528+
-- hook's environment silently swallows `os.exec("bash -c ...")`, so the old
529+
-- probe accepted ANY perl — including the MSYS perl that would fail inside
530+
-- Configure. Drive perl through a generated .bat under `cmd /c` (the same
531+
-- pattern find_vcvars and _install_windows use) and have it print a marker
532+
-- only when every required module loads. The perl paths passed here are
533+
-- space-free (well-known install dirs or bare `perl` from PATH); only the
534+
-- marker path is quoted.
535+
local function perl_usable_windows(perl)
536+
local tmp = os.getenv("TEMP") or "."
537+
local probe = path.join(tmp, "mcpp_perl_probe.bat")
538+
local marker = path.join(tmp, "mcpp_perl_probe.txt")
539+
pcall(os.rm, probe)
540+
pcall(os.rm, marker)
541+
io.writefile(probe, string.format([[
542+
@echo off
543+
%s -MLocale::Maketext::Simple -MConfig -MFindBin -e "print qq(ok)" > "%s" 2>&1
544+
]], perl, marker))
545+
local ok = pcall(os.exec, string.format("cmd /c %s", probe))
546+
if not ok then return false end
547+
local content = os.isfile(marker) and (io.readfile(marker) or "") or ""
548+
return content:find("ok", 1, true) ~= nil
549+
end
550+
551+
-- Resolve a perl that can actually drive Configure on Windows. Prefer a
552+
-- native Windows perl (Strawberry) at well-known locations over whatever PATH
553+
-- resolves, which on a Git-for-Windows host is the MSYS perl that fails the
554+
-- Locale::Maketext check above.
555+
local function resolve_perl_windows()
556+
local scoop = os.getenv("USERPROFILE")
557+
and path.join(os.getenv("USERPROFILE"), "scoop", "apps", "perl",
558+
"current", "perl", "bin", "perl.exe")
559+
local known = {
560+
"C:\\Strawberry\\perl\\bin\\perl.exe",
561+
scoop,
562+
}
563+
for _, c in ipairs(known) do
564+
if c and os.isfile(c) and perl_usable_windows(c) then
565+
return c, win_dirname(c)
566+
end
567+
end
568+
if perl_usable_windows("perl") then
569+
return "perl", nil
570+
end
571+
return nil, nil
572+
end
573+
574+
-- Build OpenSSL on Windows. VC-WIN64A generates an NMAKE makefile, so this
575+
-- needs the MSVC toolset (nmake + cl). Everything runs inside ONE generated
576+
-- .bat invoked once under vcvars64, so every step shares the same toolset env
577+
-- (a vcvars invocation per command would re-enter `call` and is what the old
578+
-- "vcvars kills the process chain" report was chasing). A child `cmd /c`
579+
-- really does run vcvars fine on a normal machine; this hook captures that
580+
-- environment by simply doing all the work inside it.
581+
local function _install_windows()
582+
local vcvars = find_vcvars()
583+
if not vcvars then
584+
log.error("compat.openssl: no Visual Studio C++ toolset found. Install "
585+
.. "\"Desktop development with C++\" (MSVC + Windows SDK) and retry.")
586+
return false
587+
end
588+
589+
local perl, perlbin = resolve_perl_windows()
590+
if not perl then
591+
log.error("compat.openssl: no usable perl. Configure needs a perl WITH "
592+
.. "Locale::Maketext::Simple + core modules. Install Strawberry "
593+
.. "Perl (https://strawberryperl.com) and retry.")
594+
return false
595+
end
596+
local perlbinpath = perlbin or win_dirname(perl)
597+
598+
local ifile = pkginfo.install_file()
599+
local srcroot = ifile and tostring(ifile):replace(".tar.gz", "")
600+
or ("openssl-" .. pkginfo.version())
601+
if not os.isdir(srcroot) then
602+
srcroot = "openssl-" .. pkginfo.version()
603+
end
604+
605+
local prefix = pkginfo.install_dir()
606+
os.tryrm(prefix)
607+
os.mkdir(prefix)
608+
local logf = path.join(prefix, "mcpp_openssl_build.log")
609+
610+
-- Static-only, no asm (no NASM dependency), no apps/tests/engine. `no-asm`
611+
-- is the deliberate Windows default for now: VC-WIN64A's asm path needs
612+
-- NASM on %PATH%, which would be a second host dependency to resolve. Pure-C
613+
-- crypto is functionally identical, just a bit slower; revisit if a build
614+
-- dep for nasm ever lands.
615+
local flags = "no-shared no-dso no-tests no-apps no-engine no-asm"
616+
local bat = path.join(srcroot, "mcpp_build_win.bat")
617+
io.writefile(bat, string.format([[
618+
@echo off
619+
call "%s" >nul 2>&1
620+
if errorlevel 1 exit /b 1
621+
set "PATH=%s;%%PATH%%"
622+
cd /d "%s"
623+
perl Configure VC-WIN64A %s --prefix="%s" --libdir=lib
624+
if errorlevel 1 exit /b 1
625+
nmake
626+
if errorlevel 1 exit /b 1
627+
nmake install_sw
628+
if errorlevel 1 exit /b 1
629+
]], vcvars, perlbinpath, srcroot, flags, prefix))
630+
631+
-- The unix `run()` helper wraps commands in `bash -c`, but this hook's
632+
-- Windows environment has no usable bash — os.exec("bash -c …") returns
633+
-- true without running anything (probed directly). Drive the build via a
634+
-- single `cmd /c` invocation instead; paths here are space-free in the
635+
-- standard layout, and cmd itself handles the `>` capture.
636+
local exok, exerr = pcall(os.exec, string.format(
637+
"cmd /c %s > %s 2>&1", bat, logf))
638+
if not exok then
639+
log.error("compat.openssl: windows build could not start "
640+
.. "(os.exec failed: %s)", tostring(exerr))
641+
return false
642+
end
643+
644+
local libdir = path.join(prefix, "lib")
645+
local crypto = path.join(libdir, "libcrypto.lib")
646+
local ssl = path.join(libdir, "libssl.lib")
647+
if not os.isfile(crypto) or not os.isfile(ssl) then
648+
log.error("compat.openssl: windows build produced no libcrypto.lib / "
649+
.. "libssl.lib under %s (see %s)", libdir, logf)
650+
return false
651+
end
652+
653+
-- Emit the anchor TU mcpp compiles; its absence is what triggers install().
654+
io.writefile(path.join(prefix, "mcpp_openssl_anchor.c"),
655+
"int mcpp_compat_openssl_anchor(void) { return 0; }\n")
656+
return true
657+
end
658+
448659
function install()
449-
-- Windows is deferred: there is no windows xpm block, so version
450-
-- resolution already fails before this point. Kept as a named error in
451-
-- case a windows entry is added before this hook learns to build there.
452660
if os.host() == "windows" then
453-
log.error("compat.openssl: windows is not yet supported")
454-
return false
661+
local ok, result = pcall(_install_windows)
662+
if not ok then
663+
log.error("compat.openssl install() (windows) failed: %s", tostring(result))
664+
return false
665+
end
666+
if not result then
667+
log.error("compat.openssl install() (windows) returned false")
668+
return false
669+
end
670+
return true
455671
end
456672
local ok, result = pcall(_install_impl)
457673
if not ok then

tests/examples/openssl/mcpp.toml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
# compat.openssl test project — the dedicated member that validates the
2-
# from-source OpenSSL build (static libssl.a + libcrypto.a + headers).
2+
# from-source OpenSSL build (static libssl.a + libcrypto.a + headers on
3+
# linux/macOS; libssl.lib + libcrypto.lib on Windows).
34
#
45
# It inherits the workspace-root `compat` redirect, so the descriptor in THIS
56
# checkout is what gets built and linked. tests/examples/asio-ssl covers the
67
# other direction — the same package pulled in as asio's `ssl` feature dep —
78
# and this member is what isolates a failure to openssl itself rather than to
89
# the feature wiring around it.
910
#
10-
# linux + macOS only: there is no windows xpm entry yet (prebuilt MSVC archives
11-
# unpublished), so on windows the member carries no dependency and the test
12-
# compiles to a no-op main().
11+
# All three platforms: linux/macOS build via perl Configure + GNU Make, windows
12+
# via VC-WIN64A + nmake (see _install_windows in the descriptor). The member
13+
# declares the dependency and HAVE_OPENSSL on every platform, so the test is a
14+
# real test everywhere — not a no-op main() on windows.
1315
[package]
1416
name = "openssl-tests"
1517
version = "0.1.0"
@@ -25,3 +27,9 @@ openssl = "3.5.1"
2527

2628
[target.'cfg(macos)'.build]
2729
cxxflags = ["-DHAVE_OPENSSL=1"]
30+
31+
[target.'cfg(windows)'.dependencies.compat]
32+
openssl = "3.5.1"
33+
34+
[target.'cfg(windows)'.build]
35+
cxxflags = ["-DHAVE_OPENSSL=1"]

tests/examples/openssl/tests/tls.cpp

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// library initialises far enough to stand up a TLS context and run a digest.
33
//
44
// Touching BOTH archives is the point: libssl.a (SSL_CTX_new, TLS_method) and
5-
// libcrypto.a (EVP_*). A link that silently dropped one, or picked up a host
6-
// libssl.so instead of the package's own static build, fails here.
5+
// libcrypto.a (EVP_*) — libssl.lib / libcrypto.lib on Windows. A link that
6+
// silently dropped one, or picked up a host libssl.so instead of the package's
7+
// own static build, fails here.
78
//
8-
// HAVE_OPENSSL comes from this project's own cfg-gated cxxflags — the package
9-
// is linux/macOS-only, so elsewhere this file is an empty main().
9+
// HAVE_OPENSSL comes from this project's own cfg-gated cxxflags. The package
10+
// is supported on all three platforms now (linux/macOS: perl Configure + GNU
11+
// Make; windows: VC-WIN64A + nmake), so the real test runs everywhere.
1012
#ifdef HAVE_OPENSSL
1113
#include <openssl/ssl.h>
1214
#include <openssl/evp.h>
@@ -46,5 +48,5 @@ int main() {
4648
return 0;
4749
}
4850
#else
49-
int main() { return 0; } // compat.openssl is linux/macOS-only; no-op elsewhere
51+
int main() { return 0; } // no HAVE_OPENSSL — fallback that always passes
5052
#endif

0 commit comments

Comments
 (0)