Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 222 additions & 6 deletions pkgs/c/compat.openssl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<name>.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" } },
},
}

Expand Down Expand Up @@ -445,13 +462,212 @@ 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
-- 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)
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
-- 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
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

-- 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. 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)
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
-- 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
Expand Down
16 changes: 12 additions & 4 deletions tests/examples/openssl/mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# 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
# other direction — the same package pulled in as asio's `ssl` feature dep —
# 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"
Expand All @@ -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"]
12 changes: 7 additions & 5 deletions tests/examples/openssl/tests/tls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <openssl/ssl.h>
#include <openssl/evp.h>
Expand Down Expand Up @@ -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
Loading