calog/PORTING.md

390 lines
27 KiB
Markdown

# Porting calog
calog targets **Linux, macOS, and Windows**. Its core -- values, broker, the actor/threading
model, and every engine adapter -- is portable C11. The only operating-system-specific surface is
small and centralized:
- **Threads**: POSIX `pthreads`. Native on Linux and macOS; on Windows via mingw-w64's `winpthreads`.
- **Sockets + DNS**: BSD sockets + `getaddrinfo`, used only by the net/ssh/http libraries. BSD
sockets on Linux/macOS, Winsock on Windows. All of this is behind `src/calogPlatform.h`.
There is no `epoll`/`eventfd`/`kqueue`, no `fork`/`exec`, and no `dlopen` in calog's own code (the
actor model uses blocking sockets on per-context threads), so a port is a thin shim, not a rewrite.
## The platform abstraction: `src/calogPlatform.h`
The net/ssh/http libraries use these instead of raw socket calls, so the same source compiles on all
three platforms:
| Abstraction | POSIX (Linux/macOS) | Windows |
|---|---|---|
| `CalogSocketT` | `int` | `SOCKET` |
| `CALOG_INVALID_SOCKET` | `-1` | `INVALID_SOCKET` |
| `calogSockClose(fd)` | `close` | `closesocket` |
| `calogSockErrno()` / `calogSockErrStr()` | `errno` / `strerror` | `WSAGetLastError` / `FormatMessage` |
| `calogSockSetNonblock(fd, on)` | `fcntl(O_NONBLOCK)` | `ioctlsocket(FIONBIO)` |
| `calogSockInProgress(err)` | `EINPROGRESS` | `WSAEWOULDBLOCK`/`WSAEINPROGRESS` |
| `calogPoll(...)` | `poll` | `WSAPoll` |
| `calogPlatformNetInit()` / `Shutdown()` | no-op | `WSAStartup` / `WSACleanup` |
| `CALOG_MSG_NOSIGNAL` | `MSG_NOSIGNAL` | `0` (Windows has no `SIGPIPE`) |
A socket `fd` is unsigned on Windows, so the code compares against `CALOG_INVALID_SOCKET` (never
`fd < 0`). The abstraction is behavior-identical on POSIX (it maps to the same names), so the Linux
and macOS builds are unchanged by it.
### What the core assumes beyond C11
Sockets are the only surface that needed abstracting, but the core is not pure C11 either -- it
leans on three POSIX facilities that a new target must supply, none of them behind a wrapper:
| Facility | Used for | On Windows |
|---|---|---|
| `pthread_*` (threads, mutexes, condvars) | one thread per context, the message queues, the context registry | vendored **winpthreads** |
| `clock_gettime(CLOCK_MONOTONIC)` | `calogMonotonicMillis`: timer deadlines and the sandbox wall-clock budget | mingw-w64 |
| `nanosleep` | the host pump's park between drains, and the teardown's wait for an in-flight context close | mingw-w64 |
All three already resolve on the mingw-w64 / zig toolchain: `src/calogMain.c` calls `nanosleep`
unconditionally from its pump loop and is part of the Windows CLI that cross-built successfully, so
the teardown's use of it adds no new requirement. A target lacking any of them needs a shim in
`calogPlatform.h` rather than a change to the core.
## Build matrix
| Target | Command | Runtime dependencies | DNS |
|---|---|---|---|
| Linux dev | `make` | glibc, libstdc++, libgcc, ASan/UBSan (dev only) | yes |
| Linux release | `make release` | **glibc only** (libstdc++/libgcc linked static) | yes |
| Linux static | `make static` on Alpine/musl | **none** (fully static) | yes (musl resolver) |
| Linux static | `make static` on glibc | none, but see caveat | **no** (glibc NSS needs dlopen) |
| macOS | `make` (Darwin auto-detected) | libSystem (always present) | yes |
| Windows | mingw-w64 build | core OS DLLs (`kernel32`, `ws2_32`) | yes |
The Makefile auto-detects the platform (`PLATFORM` = `linux`/`darwin`/`windows`/`posix`) and adjusts
the link line: `DLLIB` is `-ldl` on Linux (empty on macOS, where `dlopen` is in libSystem) and
`SOCKETLIBS` is `-lws2_32` on Windows.
### Feature and verification parity
The **full `calog` CLI -- all ten engines and every capability library** (net/tls/http-https/db
[SQLite+PostgreSQL+MariaDB]/archive/regex/ssh/xml + the core libs) -- builds for all three operating
systems. What differs across ports is not features but how thoroughly each is verified, and the scope
of the fully-static route.
| Port | Build path | Full CLI (10 engines + all libs) | Verified |
|---|---|---|---|
| Linux glibc (dev / release) | `make` / `make release` | yes | **runtime** (the test suite runs) |
| Windows x86_64 (PE) | `tools/crossWinFull.sh` (zig cross) | yes | build + link only |
| macOS x86_64 | `tools/crossMacFull.sh mac-x64` (zig cross) | yes | build + link only |
| macOS arm64 | `tools/crossMacFull.sh mac-arm64` (zig cross) | yes | build + link only |
| Linux static musl (Alpine) | `make static` on Alpine | no -- Lua-only demo (`examples/staticDemo.c`) | runtime (per component) |
| Linux static glibc | `make static` on glibc | no -- Lua-only demo | build only |
So engine and library parity across Linux, macOS, and Windows is **complete**. The one open gap is
**run-verification on Windows and macOS**: both are cross-built and linked with a single zig toolchain
but have not yet been executed on the target OS (a real Windows/wine or Mac run is the remaining step).
The fully-static route is a deliberately narrower, minimal-dependency demo (Lua only, not the full
CLI), and static **glibc loses DNS** (its NSS resolver needs `dlopen`, which a static binary cannot do)
-- so Alpine/musl is the path to a zero-dependency binary that still resolves names.
## Linux
- **`make`** -- the sanitized development build (ASan + UBSan + strict warnings).
- **`make release`** -- the distribution build: sanitizers off, `-static-libstdc++ -static-libgcc`,
glibc dynamic. `ldd` collapses to just `libc`/`libm` + the loader. Because glibc stays dynamic,
`getaddrinfo` can still `dlopen` its NSS modules, so **DNS works**. Runs on any mainstream
glibc distro with nothing to install.
- **`make static` on Alpine (musl)** -- a fully static binary with **zero** runtime dependencies and
**working DNS** (musl has a built-in resolver, no NSS/dlopen). This is the minimal-dependency
artifact. Because calog builds every dependency from source, and Alpine's default toolchain is
musl, no per-dependency changes are needed -- build it in an Alpine container.
- **`make static` on glibc** -- fully static, but name resolution and `dlopen`-based Lua C modules
do not work (glibc NSS requires runtime `dlopen`). Connect by IP, or use the musl route above.
## macOS (Darwin)
The C is already POSIX-compatible (pthreads, BSD sockets, `getaddrinfo` are all native), so the port
is build configuration, not code -- confirmed by `tools/crossBuild.sh`, which links valid Mach-O
executables for **both x86_64 (Intel) and arm64 (Apple Silicon)** using zig's bundled `libSystem`
stub (calog only needs `libSystem` -- libc/pthreads/sockets -- so no Apple SDK/frameworks are
required for the core). Apple does not support statically linking libc, but `libSystem` is part of
the OS and always present, so "static everything except libSystem" is the natural minimal-deps build.
Each vendored dependency builds its own way and needs a macOS configuration:
- OpenSSL: `./Configure darwin64-<arch>-cc`
- Tcl: its `configure` detects Darwin
- mruby / Lua / SQLite / ENet / QuickJS / Squirrel / Berry / s7 / Wren: compile as-is with clang
- libssh2 / MariaDB: CMake with the macOS toolchain
Link against dynamic `libSystem` (the default); `-ldl` is dropped automatically (`DLLIB` is empty on
Darwin) and the C++ runtime is `libc++` (`CXXLIB = -lc++`). SIGPIPE is ignored at CLI startup, since
macOS `send()` has no `MSG_NOSIGNAL`. ENet's `unix.c` backend is correct on macOS (BSD sockets).
## Windows (mingw-w64)
calog's pthread code compiles unchanged on Windows against **winpthreads** (POSIX threads over the
Win32 API), which is **vendored** at `vendor/winpthreads/` and built from source like every other
dependency (see its `NOTICE.calog`). `src/calogPlatform.h` handles the Winsock differences. Building
uses a **mingw-w64** toolchain (not MSVC) for its POSIX-ish environment; `tools/crossBuild.sh` cross-
compiles from Linux with a single zig toolchain and is the tested path. Remaining work is build
configuration:
- Build each vendored dependency for mingw (OpenSSL `mingw64`, Tcl's `win/` makefile or configure,
libssh2/MariaDB via CMake with the mingw toolchain, mruby with a mingw build_config).
- ENet selects its `win32.c` backend automatically (`ENETBACKEND`); the link adds `-lws2_32 -lwinmm`
(handled by `SOCKETLIBS`).
- Static-link the runtime (`-static -static-libgcc -static-libstdc++`) so the binary depends only on
core Windows DLLs, which are always present. DNS works through Winsock's `getaddrinfo`.
Remaining code review for Windows: `calogFs.c` and `calogHttp.c` use POSIX headers (`dirent.h`,
`sys/stat.h`, `sys/time.h`) that mingw-w64 mostly provides, but path handling (`/` vs `\`) and a few
calls may need adjustment; `calogSsh.c` already guards `sys/select.h` (Winsock supplies
`fd_set`/`select`), and `httpSetTimeouts` already branches on the Winsock `SO_RCVTIMEO` DWORD form.
## Cross-compiling from Linux with zig (`tools/crossBuild.sh`)
A single [zig](https://ziglang.org/download/) toolchain cross-compiles C and C++ to musl-static and
Windows without installing per-target toolchains, so the ports can be exercised on a plain Linux box:
```
ZIG=/path/to/zig ./tools/crossBuild.sh
```
It builds a representative set -- core + one engine of each kind (Lua, QuickJS, Squirrel/C++,
my-basic) + the socket library (`calogNet`) -- because the crypto/db/http/ssh libraries pull in
OpenSSL/MariaDB/PostgreSQL/libssh2, each needing its own per-target build. What it does:
- **musl**: fully static Linux binaries. These **run on the build host**, so the script builds AND
runs them -- verifying the core, all four engine kinds, C++, and the socket abstraction on musl.
- **Windows**: builds `vendor/winpthreads` into `libwinpthreads.a`, then links real `.exe`s
(including the Winsock + ENet-win32 socket path). Verified as valid PE executables; running them
needs wine.
Verified results (this repo): musl -- Lua, QuickJS, my-basic, Squirrel, and `testNet` all build and
**pass** (fully static); Windows -- `testEngineLua.exe` and `testNet.exe` build against vendored
winpthreads. Since the Windows binaries are the *same source* that passes on musl, the build is strong
evidence; full run-verification needs a Windows host or wine.
## Cross-compiling the archive stack with zig (`tools/crossArchive.sh`)
The compression/archive library (libarchive + its five codecs: zlib, bzip2, lz4, zstd, xz/liblzma)
is the one vendored stack that leans on CMake, so it gets its own cross-build script. Same idea as
`crossBuild.sh`, same single zig toolchain:
```
ZIG=/path/to/zig ./tools/crossArchive.sh
```
It builds all five codecs plus libarchive for **musl** (fully static, RUN on the host) and **Windows
x64** (a `testArchive.exe` verified as a PE), driving each through calog's real `testArchive`
(compress/decompress round-trips through every codec). CMake cross-compilation with zig works by
wrapping `zig cc` as a single-binary compiler (`build/cross/bin/zcc-*`) plus a toolchain file. The
key asymmetry:
- **musl toolchain does NOT set `CMAKE_SYSTEM_NAME`.** A musl-static binary runs on this Linux host,
so the build stays "native" (`CMAKE_CROSSCOMPILING` false) and CMake's `try_run` feature-detection
executes normally. zig supplies its own musl sysroot, so no host headers leak in.
- **Windows toolchain sets `CMAKE_SYSTEM_NAME Windows`** -> cross mode. libarchive's single
`CHECK_C_SOURCE_RUNS` is already guarded on `CMAKE_CROSSCOMPILING` (it degrades to a warning) and
xz has no run-checks, so nothing needs pre-seeding.
Two target-specific gotchas the script handles:
- **zlib on musl** needs `-DHAVE_UNISTD_H` so it includes `<unistd.h>` for `lseek`. zlib's
`./configure` normally sets this; the object-rule build (no configure) must pass it, and musl --
unlike glibc -- does not declare `lseek` without the include. Only zlib is affected.
- **libarchive on Windows** pulls in system libraries the POSIX build does not: CNG crypto
(`-lbcrypt`) for its AES/PBKDF2, and XmlLite + OLE (`-lxmllite -lole32`) for XAR, which libarchive
auto-substitutes for the libxml2/expat we disable. The link adds
`-lbcrypt -lxmllite -lole32 -ladvapi32 -lcrypt32 -lshlwapi`.
Everything lands under `build/cross/` (gitignored); the vendored sources and the native
`vendor/*/build` / `_cmk` trees the normal `make` uses are never touched.
## Status
### Cross coverage as of 2026-07-24 (zig 0.16.0)
`tools/crossBuild.sh` now reports **39 ok, 0 failed**, and covers **all ten engines on all four
targets** rather than four engines on some:
| Target | What runs |
|---|---|
| musl static | all ten engine suites **RUN** (fully static, on this host), plus `testNet` and `testHttps` |
| Windows x64 PE | all ten engine suites + `testNet` build and link as PE32+ |
| macOS x86_64 + arm64 | all ten engine suites + `testNet` build and link as Mach-O |
Two of those are new kinds of evidence, not just more of the same:
- **TLS is exercised at runtime**, not merely linked. `testHttps` cross-builds fully static for musl
and runs here (6/6): an in-process RSA key + self-signed cert, a loopback TLS server, a rejected
untrusted cert, and the pinned-trust path -- all through the cross-built OpenSSL.
- **Every musl case is run strictly**: a nonzero exit is a failure, and the binary's last line (its
check counts) is echoed. The harness used to count a nonzero exit as a pass -- "some tests exit
nonzero by design", which none of them do -- and the Squirrel case discarded its exit code
entirely. That leniency is how `testEngineMyBasic` reported success on musl while 13 of its 20
checks were failing. There is one runner now, with no lenient variant to drift back to.
- **`tools/crossDeps.sh musl` is a full target**: every dep except `winpthreads` (Windows-only) and
`libarchive` (musl goes through `tools/crossArchive.sh`, which handles its iconv gotcha). Tcl and
mruby for musl are what let the last two engines into the matrix. Tcl needed two musl-specific
turns: configure picks the epoll notifier from the *host's* headers and its `tclEpollNotfy.c` wants
`<sys/queue.h>`, which musl does not ship (so the select notifier is selected instead), and with no
system zlib it must be pointed at the vendored one.
And the drift that started all of this is now caught mechanically: **`make cross-lint`** is a
zig-free, sub-second parity check wired into `make test`. It fails if a calog source the native build
compiles never reached both full-CLI cross scripts, or if an ABI-visible define (`MRB_USE_DEBUG_HOOK`
and friends) is missing from one of them -- exactly the class of drift that broke the Windows CLI for
weeks. `make cross` runs the real thing; `make cross-smoke`, `cross-win`, `cross-mac` and
`cross-deps-*` are the pieces. None of them are in `make test`, which stays hermetic and zig-free.
### Re-verified 2026-07-24 (zig 0.16.0)
The cross-builds were re-run after the teardown work. **The core changes port cleanly --
`src/context.c`, `src/value.c` and `src/broker.c` compile, link and run on every target** -- but the
run exposed three problems that all predate that work. All three are now fixed:
| Target | Result |
|---|---|
| musl static (built + run) | Lua, JavaScript, Squirrel, my-basic, **and `testNet` (10/10)** all pass |
| Windows x64 PE | `testEngineLua.exe` and `testNet.exe` **build**; the **full `calog.exe` builds** |
| macOS x86_64 + arm64 | `testEngineLua` and `testNet` **build** (both arches) |
`tools/crossBuild.sh` reports **11 ok, 0 failed**.
- **`testNet` on every target -- was failing, now fixed.** `libs/calogNet.c` has included
`<openssl/bio.h>` since the tcp transport gained TLS, and `tools/crossBuild.sh` passed no OpenSSL
flags: its `testNet` case predated that include. The test itself uses no TLS -- only the library it
links does -- so the fix is to link the per-target OpenSSL that `tools/crossDeps.sh` already
produces for win/mac, plus a new **`musl` target** in that script (OpenSSL only -- the full musl
CLI is `make static` on Alpine, not a cross build). `crossBuild.sh` now links it, and when a
target's OpenSSL has not been built it SKIPS with the exact command to produce it rather than
reporting a failure it cannot fix. The musl `testNet` is fully static and **runs 10/10 on the
build host**, matching the native run.
- **`testEngineMyBasic` on musl -- was 13 of 20 failing, now fixed (20/20).** The fork decided "this
symbol is a number" from `strtoll`/`strtod` alone reaching the string terminator. That is not
portable: **musl's `strtoll` advances `endptr` past leading whitespace and a sign even when no
conversion happens**, while glibc leaves it at the start. So on musl `+` and `-` (the operators)
and `\n` (the statement separator) each classified as the integer 0 -- every expression containing
an operator failed to parse, and every numeric assignment failed to run, both reported as
"Operator expected". Reproduced with a pure my-basic program containing no calog code at all
(`x = 1` -> `MB_FUNC_ERR` on musl, `MB_FUNC_OK` on glibc). Fixed in `vendor/ourbasic/ourBasic.c`
by rejecting a consumed span that is nothing but whitespace and sign; see that fork's CHANGELOG.
Worth remembering when porting: **a failed `strtol` does not leave `endptr` where you assume.**
- **The full CLI on Windows and macOS -- was broken, now fixed.** `tools/crossWinFull.sh` stopped at
`mrubyAdapter.c: no member named 'code_fetch_hook' in 'struct mrb_state'`. `src/mruby/build_config.rb`
defines `MRB_USE_DEBUG_HOOK` (the per-instruction hook the sandbox wall-clock budget needs) and the
Makefile passes the same macro to the adapter compile, but the cross build_config that
`tools/crossDeps.sh` generated defined only `MRB_INT64`, and neither `cross*Full.sh` passed the
macro -- so the cross `libmruby.a` had a different `mrb_state` layout than the adapter expected.
It had been broken since the sandbox-parity work landed (the previous `calog.exe` dated from just
before it), and failing loudly at compile time was the good outcome: the alternative was an ABI
mismatch. The macro is now in the generated cross config **and** in both `cross*Full.sh` adapter
compiles, and mruby was rebuilt for all three targets. **Rebuilt and re-verified:**
| Artifact | Result |
|---|---|
| `build/cross/win/calog.exe` | PE32+ x86-64, imports only Windows system DLLs |
| `build/cross/mac-x64/calog` | Mach-O 64-bit x86_64, PIE |
| `build/cross/mac-arm64/calog` | Mach-O 64-bit arm64, PIE |
Each links all ten engines and every library, and each carries this session's runtime changes
(the abort message is in the binaries). Still build-verified only -- running them
needs a Windows/wine or Mac host.
The bullets below record the state at the time each port was done; where they disagree with the
table above, the table is current.
- **Platform abstraction** (`calogPlatform.h`) and the net/ssh/http conversion: **done**, verified
behavior-identical on Linux (`make test` 30/30, socket + HTTP loopback smoke, ASan- and TSan-clean).
- **Linux release** target: **done and verified** -- `ldd bin/calog` shows only `libc`/`libm` + loader.
- **Linux musl-static**: **run-verified** via `tools/crossBuild.sh` (zig) -- core + Lua + QuickJS +
my-basic + Squirrel + `testNet` all build fully static and pass on the host. Also works via
`make static` in an Alpine container (musl default toolchain).
- **Windows**: **build-verified** via `tools/crossBuild.sh` -- `testEngineLua.exe` and `testNet.exe`
(core + engine + Winsock socket abstraction) link against vendored `winpthreads` into real PE
executables. Run-verification needs a Windows host or wine; the binaries are the same source that
passes on musl.
- **Build system**: OS-aware (`PLATFORM`, `DLLIB`, `CXXLIB`, `SOCKETLIBS`, `ENETBACKEND`, vendored
`winpthreads`); the Linux values are identical to before, so the dev build is unchanged.
- **macOS**: **build-verified** via `tools/crossBuild.sh` -- core + Lua + `testNet` link into valid
Mach-O executables for both **x86_64 and arm64** (zig's libSystem stub; no Apple SDK needed for
calog's libc surface). Run-verification needs a Mac. The C code and Makefile hooks are all in place
(`libc++`, no `-ldl`, SIGPIPE, native pthreads + BSD sockets).
- **Archive stack** (libarchive + zlib/bzip2/lz4/zstd/xz): **musl run-verified, Windows
build-verified** via `tools/crossArchive.sh` (zig). The musl `testArchive` is fully static and
passes all 22 checks on the host -- real compress/decompress through every codec **and an xar
read+write round-trip**: the musl target now cross-builds OpenSSL (xar's MD5/SHA1) and libxml2 (its
XML TOC), with iconv coming from musl's own libc, so its libarchive has full xar. The Windows build
is a valid `testArchive.exe` (PE32+) linked against vendored winpthreads + the CNG/XmlLite system
libs. This was the one CMake-heavy vendored stack, so it is the strongest cross-build evidence
after the core. On **Windows**, xar needs no vendored XML/crypto/iconv at all: libarchive
auto-uses the system **XmlLite** (its TOC) + **CNG/bcrypt** (its MD5/SHA1), both in the mingw
sysroot, so the Windows `testArchive.exe` already has full xar. One musl gotcha worth noting:
libarchive's `FIND_PATH(iconv.h)` returns the host `/usr/include` (glibc), whose headers break the
musl compile, so the script pins `ICONV_INCLUDE_DIR` to an empty dir and lets zig cc resolve musl's
own `iconv.h` implicitly.
- **Full Windows CLI cross-built (build-verified).** Every heavy vendored dependency now
cross-compiles for `x86_64-windows-gnu` via zig -- OpenSSL, libxml2, PCRE2, libssh2, MariaDB, and
the three hard ones **PostgreSQL/libpq, Tcl, mruby** (libpq needed a patched out-of-tree copy;
see `tools/crossWinFull.sh`). The complete `calog.exe` links **all 10 engines + all 20 libraries**
into a valid PE32+ that imports only Windows system DLLs (no vendored DLLs). Behavior-neutral
`#ifdef _WIN32` guards in calog's own source cover the small OS differences -- `calogFs` (mingw
1-arg `mkdir`, `O_BINARY`), `calogHttp` (no `SIGPIPE`), `calogInternal` (a `memmem` fallback),
`calogPlatform` (`__declspec(thread)` -> `_Thread_local`, which also fixes a latent Windows
thread-safety bug clang had been silently dropping), and `calogNet` (`SO_EXCLUSIVEADDRUSE` for
listeners instead of the hijack-prone `SO_REUSEADDR`). Two libraries have a full Windows backend
rather than a POSIX one: **`procRun`** spawns with `CreateProcess` + pipes (stdin fed on a helper
thread while stdout/stderr drain, since anonymous pipes have no `poll`), and the **HTTPS client**
reads trust anchors from the Windows `ROOT`/`CA` system certificate stores via CryptoAPI
(`CertOpenSystemStore`) instead of a Unix CA-bundle path. All are behavior-neutral off Windows; the
Linux suite stays green. Run-verification still needs a Windows host or wine.
- **Full macOS CLI cross-built for both archs (build-verified).** Every heavy vendored dependency
now cross-compiles for `x86_64-apple-darwin` and `arm64-apple-darwin` via zig, whose libSystem
stub supplies libc / pthreads / BSD sockets / getaddrinfo / dlopen / iconv -- so no Apple SDK is
needed for calog's OS surface (see `tools/crossMacFull.sh`). The complete `calog` links **all 10
engines + all 20 libraries** into a valid Mach-O per arch (`build/cross/mac-x64/calog` x86_64,
`build/cross/mac-arm64/calog` arm64), importing only libSystem -- no vendored dylibs, no `-ldl`.
Two additive `#if defined(__APPLE__)` guards are in calog's own source: `calogTimer` waits with a
**relative** `pthread_cond_timedwait_relative_np` timeout on Darwin, because macOS pthreads have no
`pthread_condattr_setclock` to pin the condvar to `CLOCK_MONOTONIC` (the relative wait preserves the
monotonic scheduling intent across wall-clock changes); and `calogHttp`'s HTTPS client reads trust
anchors from the **macOS keychain** via the Security framework (`SecTrustCopyAnchorCertificates`).
Both are behavior-neutral off Darwin, and the Linux suite stays green. Run-verification still needs a
Mac.
- **macOS Keychain HTTPS trust needs the Apple SDK (optional).** calog's OS surface (libc / pthreads /
BSD sockets / getaddrinfo / dlopen / iconv) is all in zig's libSystem stub, so the *runner* needs no
SDK. But the keychain trust path uses the **Security** + **CoreFoundation** frameworks, whose headers
zig does not ship. When an SDK is present, `tools/crossMacFull.sh` compiles `calogHttp` with
`-DCALOG_MAC_KEYCHAIN_TRUST` against the SDK framework headers; without one it omits keychain trust
and HTTPS falls back to `SSL_CERT_FILE` / `CALOG_CA_BUNDLE`. Point `CALOG_MACSDK` at a `MacOSX*.sdk`
directory to enable it. **Linker gotcha:** zig 0.16's Mach-O linker *segfaults* parsing the real
multi-target SDK `.tbd` files, so calog links against hand-written minimal stubs
(`tools/macStubs/{CoreFoundation,Security}.tbd`) that export only the handful of symbols the keychain
code calls -- the binary still imports the real system frameworks by install-name at runtime. Both
arch binaries are valid Mach-O importing `Security.framework` + `CoreFoundation.framework`
(build-verified; run-verification still needs a Mac).
- **Reproducible dependency builds (`tools/crossDeps.sh`).** The full-CLI scripts above consume
heavy pinned deps under `build/cross/<target>/`; `crossDeps.sh` rebuilds them from `vendor/` source
with a single zig toolchain, so the cross build is reproducible from a clean checkout rather than
depending on ad-hoc pins. Usage: `[ZIG=/path/to/zig] ./tools/crossDeps.sh <win|mac-x64|mac-arm64>
[dep ...]` (it regenerates the zig wrappers + CMake toolchain file first, so nothing under
`build/cross/` need pre-exist). It builds, in dependency order: the codecs (zlib/bzip2/lz4/zstd),
xz, OpenSSL (mac also gets a BSD-`ar` **repack**, since zig's Mach-O linker cannot read OpenSSL's
GNU-`ar` archives), libxml2, PCRE2, libssh2, MariaDB, PostgreSQL/libpq (the patched out-of-tree
copy: mingw `.obj`->`.o`, a real `ar` rule for `libpq.a`, and `pg_pthread_*` symbol localization on
Windows; libSystem pthreads and no patch on mac), Tcl, mruby, winpthreads (Windows only), and
libarchive (mac = xar via the pinned OpenSSL+libxml2; Windows = codecs only, xar via system
XmlLite/CNG). **VERIFIED build-clean from vendor source for ALL THREE targets, all 12 deps** (`OUT=`
builds into a scratch dir without clobbering the pins): `mac-x64` and `mac-arm64` each build the full
set in one clean invocation (OpenSSL confirmed Mach-O + `!<arch>` BSD repack); `win` builds the full
set including the mingw-only bits -- winpthreads, the patched libpq (`.obj`->`.o`, real `ar` rule,
`pg_pthread_*` localization, plus the Windows syslibs `-lws2_32 -lcrypt32 -lsecur32 -lbcrypt ...` so
configure's `-lcrypto` link test passes), and the two `.rc` resources (libxml2's + libpq's
`win32ver.rc`) compiled via a `zig rc` **windres shim** that handles both CMake's positional args and
PostgreSQL's `-i/-o/--include-dir` form. The win CMake toolchain sets `CMAKE_RC_COMPILER` to that
shim.
- **Tcl re-vendored to close the last gap (2026-07-06).** `vendor/tcl` had been trimmed to the native
unix/linux build set (no `win/`, no `macosx/`), so cross-Tcl could not build from vendored source.
The official tcl9.0.4 `win/` + `macosx/` subdirs are now restored, so all three ports build Tcl from
`vendor/`; the native Linux build is unchanged (still `testEngineTcl` 13/0). All ports now match.