Portability
This document records which OS/libc/kernel/toolchain assumptions d6's native implementation actually depends on, where each one lives in the code, and what's been verified versus merely assumed. It exists because "should build on Linux" is not a claim worth making without checking what "Linux" is doing the load-bearing work -- glibc and musl, in particular, disagree on enough POSIX-adjacent surface area to matter here.
Validated platforms
- Fedora 44, Clang 22, x86_64 (glibc, local 2026-09 run) -- the full 1543-case suite and the 277-case LSP suite both passed. This supplements, but does not replace, the continuous Fedora/Ubuntu CI matrix below.
- Fedora and Ubuntu 26.04, GCC and Clang, x86_64 (glibc) -- CI runs the
full test suite (
make test-all) across this 2x2 matrix, split intotest-builds,test-integration, andtest-fuzzjobs with independent time budgets (.github/workflows/ci.yml,test-allmatrix). The workflow runs on every pull-request update, with manual dispatch available for any branch (see CI operation). - Ubuntu 26.04, GCC, arm64 (glibc) -- CI runs the same suites the x86-64 GCC leg of
test-alldoes, on native GitHub-hosted arm64 hardware:make testandmake test-release(test-arm64), the wholetest-integrationtarget with a liveredis-server(test-arm64-integration), ASan/UBSan (test-arm64-sanitize) and TSan (test-arm64-tsan). The MariaDB, MySQL and PostgreSQL dialect tests run against those servers' arm64 images (test-database-dialects), and the pinned Thread/Channel/Supervisor stress loop runs too (test-stress-threads-arm64). The x86-64-only JIT fixtures are excluded from the corpus; checks elsewhere measure whether the JIT compiled anything rather than assume it. Narrower thantest-all: no Clang, no Fedora, and no fuzz targets on arm64. - Alpine (musl), GCC, x86_64 -- CI runs
make testplus a curated set of focused targets (test-musljob, same workflow) on every pull-request update. Everytests/cases/*.d6corpus case passes except two known failures (bcrypt.d6,active_record_secure_password.d6) are excluded from the CI run itself rather than left to fail it -- both are the same real, documented libc limitation (BCrypt, below), not a bug. Narrower thantest-all: GCC only (Clang-on-musl has not been checked), and no sanitizer/tsan builds or package-specific tests (database, HTTP, GraphQL, ...), since those have not actually been validated against musl either -- see thetest-musljob's own comment for why extending its scope to matchtest-allisn't done casually. See "musl-specific findings" below for exactly what building there required. - FreeBSD 15.1, Clang, x86_64 -- CI runs
make testplus the same curated focused-target set as musl (test-freebsdjob, same workflow), via a real QEMU-booted VM (vmactions/freebsd-vm), on every pull-request update. Four known failures excluded the same way musl's two are:bcrypt.d6/active_record_secure_password.d6(BCrypt.hash needs libxcrypt'scrypt_gensalt_rn, unavailable with no<crypt.h>at all on FreeBSD -- see "FreeBSD/OpenBSD" below) andmath_exp_log(_tanh).d6(exp/log differ from glibc's own libm in the last representable bit -- expected cross-library variance, not a bug). Narrower thantest-allfor the same reason musl's own job is: no sanitizer/tsan builds or package-specific tests validated against FreeBSD either. GCC-on-FreeBSD has not been checked (CC=clang here, FreeBSD's own base compiler, to avoid an extra ports build). - macOS/Darwin, Clang, arm64 (Apple Silicon) -- CI runs
make teston a realmacos-latestGitHub-hosted runner (test-macosjob, same workflow) on every pull-request update, with GNU coreutils/sed and a real bash 5+ installed via Homebrew and shadowed ontoPATH(see "macOS/Darwin" below for why the test harness needs those specifically). Same one known BCrypt failure excluded the same way musl/FreeBSD's own are. Narrower thantest-allfor the same reason musl/FreeBSD's own jobs are: Clang only (no system GCC on macOS at all -- thegccbinary there is a symlink to clang), no sanitizer/tsan builds, no package- specific tests validated against macOS either.
Language-level pthreads explicitly request an 8 MiB stack on every platform.
That matches the stack against which the interpreter recursion guard is
calibrated instead of inheriting libc-dependent defaults (roughly 128 KiB on
musl and 512 KiB on Darwin), which are too small for run_chunk's fixed native
frame and can fault before d6 can report SystemStackError. The program's
own main thread runs on a 512 MiB pthread stack (DIAMOND_MAIN_STACK_MB), with
the call-depth limit scaled to match; if that allocation fails it halves the
request and finally falls back to the process's own stack.
Cross-target builds and validation
The entries below distinguish continuous CI checks from manual validation.
- aarch64-linux-gnu cross build --
make aot-kit-crosswith clang and a Fedora aarch64 sysroot, thend6 build --target=...; the resulting binaries ran underqemu-aarch64(classes, SQLite3, threads, Regexp, JSON, fibers, deep recursion, supervised workers). It reproduced an aarch64-only failure that CI later reported. CI cross-builds the kit and runs its binary under qemu on every pull-request update (test-cross-aarch64). See deployment. - x86_64 Linux musl, fully static -- the same kit with
STATIC=1 NO_MYSQL=1against an Alpine sysroot; the binary is statically linked and ran TLS, SQLite3, threads, Regexp, JSON and fibers on a glibc host.MySQL.openraises there. - wasm32-wasip1 --
wasm/build.shwith wasi-sdk; the module runs the interpreter and the REPL under Node's WASI and in Firefox. Fibers, threads, sockets, TLS, subprocesses, PostgreSQL and MySQL are unavailable; SQLite works in memory. See WebAssembly build. Thetest-wasmCI job builds the module with wasi-sdk 34 and tests it under Node through the site's JavaScript runner; Firefox validation is manual.
OpenBSD remains unvalidated (blocked on a real gap, not just an unwritten CI job -- see below). Portability claims should not extend past what's actually been checked -- see docs/roadmap.md's "Explicitly deferred". See "macOS/Darwin" and "FreeBSD/OpenBSD" below for what each platform's own job required.
Toolchain assumptions
- C23, via
-std=c23:nullptr,constexpr,#embed,typeof,thread_localas a keyword. Requires GCC 15+ or Clang 19+.#embedspecifically is load-bearing (src/prelude.c,src/compiled_prelude_data.c) -- there's no fallback path for a pre-C23 compiler. -fPIEis explicit (Makefile'sCFLAGS_COMMON), not left to the compiler's own default. Fedora/Ubuntu's gcc pair-fPIE-at-compile-time and-pie-at-link-time consistently either way, so this was invisible there. Alpine's musl-targeting gcc does not: it links-pieby default without defaulting to-fPIEat compile time, so any object compiled without it fails to link at all (relocation R_X86_64_32 against '.rodata' can not be used when making a PIE object). Forcing-fPIEunconditionally makes every object agree regardless of a given toolchain's own default pairing.reginold/libreginold.aneeds-fPIC(reginold/Makefile) -- found on Ubuntu, unrelated to this pass; already fixed, kept here since it's the same category of "PIC/PIE default varies by toolchain" issue.
Feature-test macros (glibc vs. musl)
Both libcs gate POSIX-beyond-C23 declarations behind feature-test macros
(_DEFAULT_SOURCE, _POSIX_C_SOURCE, _XOPEN_SOURCE, _GNU_SOURCE), but
their defaults differ in exactly the case that matters here:
- glibc, with no macro defined at all, still exposes a workable default
set regardless of
-std=cNN. - musl's
<features.h>only falls back to a working default (_BSD_SOURCE+_XOPEN_SOURCE=700) when__STRICT_ANSI__is not defined. GCC/Clang define__STRICT_ANSI__for any strict standard mode (-std=c23, not-std=gnu23) -- which this project uses -- so a translation unit with no explicit feature-test macro gets none of musl's fallback, and any POSIX declaration (ucontext_t, for one) is simply absent.
The practical effect: every .c file that reaches vm.h (directly or
transitively, since vm.h itself does #include <ucontext.h>) needs its
own #define _DEFAULT_SOURCE (or an equivalent macro) before its own
first #include -- not merely before its own #include "vm.h" if some
earlier header already reaches a libc header first, and not centralized
in vm.h itself. Both restrictions come from the same mechanism: a
feature-test macro's effect on a given header (<signal.h>'s own
#if defined(_BSD_SOURCE) || ...) is decided the first time that header
is processed in the translation unit, gated by its own separate include
guard -- defining the macro after that point, even from inside a
supposedly-shared header, has no effect. This project's own established
pattern (src/main.c, src/vm.c, src/repl.c, src/run_source.c,
src/compiler.c, src/loader.c each already defined their own macro,
before this pass, for their own reasons) turned out to be the only
correct place to put this, not a stylistic inconsistency to clean up.
Files fixed in this pass for the same reason: src/bignum.c,
src/disassemble.c, src/value.c, src/compiled_prelude.c,
tools/gen_compiled_prelude.c, lsp/completion.c, lsp/definition.c,
lsp/diagnostics.c, lsp/document_symbol.c, lsp/references.c,
lsp/receiver.c, tests/api_invalidation.c, tests/fiber_states.c,
tests/incremental_compile_test.c, fuzz/compile_fuzzer.c.
musl-specific findings
Building and testing on Alpine surfaced four real, distinct issues beyond the feature-test-macro gap above:
ucontext_t/swapcontext/getcontext/makecontextare declared by musl's headers but not implemented in Alpine's shippedlibc.so-- confirmed directly (nm -Donlibc.soshows none of the three functions). Fibers (src/vm.c, see docs/internal/concurrency-internals.md) are built entirely on this POSIX API, andrun_chunk's own yield-opcode handler references it unconditionally, so the whole binary fails to link, not merely to run fibers, once the feature-test-macro gap above is fixed. Resolved by linkinglibucontext(apk add libucontext libucontext-dev, then-lucontext), a small compatibility library providing all four under their standard names as weak symbols -- no source change needed once linked, and confirmed to actually work (not just link): a real yield/resume fiber program runs correctly under it. This is a real, external build-time dependency for musl specifically, not something to route around in d6's own code.crypt_gensalt_rn/CRYPT_GENSALT_OUTPUT_SIZEdon't exist on musl at all -- these are libxcrypt-specific convenience functions (Fedora and Ubuntu both link against libxcrypt, not a bare glibccrypt) for generating a bcrypt salt string.BCrypt.hash(src/vm.c,bcrypt_hash_helper) used them directly. Worse than a missing symbol, though: musl's owncrypt_rdoesn't implement the bcrypt algorithm ($2b$) at all, confirmed directly (crypt_r("x", "$2b$04$...", &data)returns"*", libcrypt's own unsupported-algorithm signal, while the identical call with$6$(SHA-512) succeeds) -- so a portable salt generator alone would not have been enough.bcrypt_hash_helpernow checks forCRYPT_GENSALT_IMPLEMENTS_AUTO_ENTROPY(a capability macro libxcrypt's owncrypt.hdefines, and the same one this function's own comment already used for a different purpose) at compile time, and returns a clear runtime error ("BCrypt.hash is not supported on this platform's crypt() implementation") when it's absent, rather than failing to compile.BCrypt.verifyneeded no equivalent change: it was already written to treat anycrypt_rfailure as an ordinary non-match rather than an exception, so it degrades safely (always reports "no match") without modification -- confirmed correct, not just convenient, since verifying against an unsupported hash format is exactly the case that existing behavior was already meant to cover. A fully portable BCrypt would need to bundle its own algorithm implementation instead of delegating to the systemcrypt(3); not attempted here (see docs/roadmap.md).- Reassigning the global
stdout(src/repl.c, output capture during REPL candidate evaluation) doesn't even compile on musl:stdoutis a plain, reassignableFILE *global on glibc, but POSIX only guarantees it names someFILE *expression, and musl's own<stdio.h>defines it as a non-assignable macro. Fixed by redirecting the underlying file descriptor withdup2(fileno(capture), STDOUT_FILENO)instead of touching thestdoutvariable at all -- every write through the untouchedstdoutFILE*(including inside the user's own running code) still targets fd 1, which now happens to point at the capture file; restored the same way afterward. Portable by construction, not glibc-specific behavior relied on elsewhere. - Alpine's base image ships no
tzdata, no GNUsed(BusyBox's ownsedchokes on this project's own{...}block syntax with "unterminated {"), and nobash/coreutils/procpsby default -- all external environment-setup gaps, not code bugs, exactly like the Fedora-vs-Ubuntu MariaDB/PostgreSQL header-path differences already handled inMakefile's ownCPPFLAGS. Installed viaapk add tzdata sed bash coreutils procpsfor this validation; would need the same treatment in any CI job targeting musl. Also worth noting for whoever next validates this on a container specifically (not just a musl host): a container with no real init process (PID 1 that reaps orphans) can make an unrelated, environment-only "zombie processes left behind" test failure look like a d6 bug -- confirmed by re-running the identical test underpodman run --init, which passed cleanly.docker run --init/podman run --init(or an equivalent minimal reaper) is a real prerequisite for that specific test, unrelated to musl itself.
macOS/Darwin: a real, green CI job
Confirmed against a real GitHub-hosted macos-latest runner (arm64, Apple
Silicon, Xcode 26.6, Darwin 25). src/vm.c compiles cleanly with the real
project headers, and the test harness's own GNU-coreutils dependencies
(below) are addressed the same way FreeBSD's gsed already is: install
the real GNU tools (plus a real bash 5+) via Homebrew and shadow them onto
PATH under their normal names, rather than rewriting every call site --
see the test-macos job (.github/workflows/ci.yml), now real and green,
not just a build-level check.
Two
Makefilefixes, independent of whether a Darwin CI job ever lands:CFLAGS_RELEASE's-march=x86-64is an x86-only flag gcc/clang reject outright on arm64 -- what every current GitHub-hostedmacos-*runner is -- now guarded byuname -mrather than assumed (this also matters on an arm64 Linux box, not just macOS).LDLIBS's-lcryptfails to link on macOS (crypt()lives in libSystem directly, no matching.dylib) -- confirmed directly;-ldlwas wrongly assumed to fail the same way and doesn't (it links fine on this real runner -- an earlier version of this doc asserted otherwise from general knowledge, not a real check, and was wrong). Both are their own overridableLDLIBS_DL/LDLIBS_CRYPTvariables regardless, alongsideCPPFLAGS_EXTRA/LDFLAGS_EXTRAoverride points for Homebrew's keg-only OpenSSL/libpq/MariaDB Connector paths (/opt/homebrewon Apple Silicon).<ucontext.h>is not actually absent (unlike OpenBSD) -- Apple's own SDK header gatesgetcontext/makecontext/swapcontext/ucontext_tbehind an explicit#errorunless_XOPEN_SOURCEis defined ("The deprecated ucontext routines require _XOPEN_SOURCE to be defined"), confirmed directly. Fixed the same way this codebase already handles_DEFAULT_SOURCE/_GNU_SOURCE(Feature-test macrosabove): added to the same 22 files. Still compiles with deprecation warnings (not fatal, no-Werrorcovering them) -- Apple discourages these routines but hasn't removed them.Defining
_XOPEN_SOURCEalone breaks FreeBSD, confirmed the hard way: it landed onmainonce, broketest-freebsdimmediately (MAP_ANONYMOUS/SO_REUSEPORT/_SC_NPROCESSORS_ONLN/crypt_r/struct crypt_dataall became undeclared -- FreeBSD's<sys/cdefs.h>disables__BSD_VISIBLEby default once_XOPEN_SOURCEis defined), and was reverted the same day.__BSD_VISIBLE=1restores it on FreeBSD; it means nothing to Apple's headers, which needed their own equivalent,_DARWIN_C_SOURCE, to restore the identical set of hidden declarations there (plustimegm/NI_MAXHOST/NI_MAXSERV). All three macros --_XOPEN_SOURCE 700,__BSD_VISIBLE 1,_DARWIN_C_SOURCE-- now sit alongside_DEFAULT_SOURCEin the same 22 files; each is a no-op where its own libc doesn't recognize the name (glibc/musl ignore the BSD/Darwin ones the same way FreeBSD/macOS ignore_DEFAULT_SOURCEitself).struct crypt_data/crypt_rdon't exist on macOS at all -- neither declared in any header, confirmed directly ("incomplete type 'struct crypt_data'"). No portable feature test predicts this the way__has_include(<crypt.h>)did for the top-of-file include: FreeBSD also lacks<crypt.h>but does declare both (in<unistd.h>), so that same check would have wrongly disabled this on FreeBSD too.bcrypt_verify_helper's use of them is now gated on__APPLE__specifically -- the one deliberate exception in this codebase to the "no OS-name branching" pattern documented below, because no feature test exists for this particular gap. Degrades the same already- documented way an unsupported algorithm does on musl:BCrypt.verifynever raises, so an unresolvable digest is just reported as no match. (BCrypt.hashwas already correctly excluded from ever reaching this code on macOS -- no<crypt.h>there either meansCRYPT_GENSALT_IMPLEMENTS_AUTO_ENTROPYis never defined, same as FreeBSD/musl.)Plain
crypt()on macOS silently mishandles an unrecognized bcrypt salt rather than returningNULLthe honest way FreeBSD/OpenBSD/musl all do -- confirmed directly:crypt("password", "$2b$04$...")returned a 13-character traditional-DES-style string instead of an error. Harmless here (the length mismatch against a real 60-character bcrypt digest already makesbcrypt_verify_helper's own comparison fail safely), but worth knowing if anything ever calls rawcrypt()on macOS expecting a clean failure signal instead of a wrong-algorithm result.The test harness (mostly
tests/run.sh) leans on GNU-coreutils behavior far more than the build does. Thetest-macosjob handles this by installing GNU coreutils, sed and bash through Homebrew and shadowing them ontoPATH; these are the findings that led to it:timeout-- 28 call sites across process/signal/socket tests, several relying specifically on GNU/uutils relay-then-child-status semantics (see the comment above thesignal_pidwait in the Signal.trap test). Confirmed directly: macOS ships notimeoutat all in its base install, and nogtimeouteither unlesscoreutilsis installed via Homebrew (also not preinstalled on the runner image checked).nproc(6 uses) -- confirmed absent from macOS's base install.sysctl -n hw.ncpuworks, but so doesgetconf _NPROCESSORS_ONLN-- POSIX-portable, confirmed working identically on this macOS runner and every Linux/BSD already checked in this document, so it's a better replacement than per-OSsysctl/nprocbranching would be./proc/cpuinfo(physical-core counting, same Thread real-parallelism check already guarded for FreeBSD/BSD above) -- no/procon macOS either, same fix already applies.$EPOCHREALTIME(that same test's own timing) -- bash 5+ only. Confirmed directly: macOS's/bin/bashis the pre-GPLv3 3.2 Apple still ships, and it's whatwhich bashresolves too -- no Homebrew bash is preinstalled or put ahead onPATHby the runner image, unlike FreeBSD/OpenBSD where installing one viapkg/pkg_addwas enough on its own. A real fix here needs the same explicit symlink-into-a-prepended-PATH-directory technique already used for FreeBSD'sgsed, not just abrew install bash.bc-- confirmed present (/usr/bin/bc) and functional; the earlier version of this doc's uncertainty here was unfounded.sed-- confirmed BSD sed (rejects--version), same GNU-extension risk already documented for FreeBSD/musl. Nogsedpreinstalled either. Once shadowed by Homebrew'sgnu-sed(seetest-macos-ci),tests/collection_relay_contracts.sh's:label/n/p/bloop runs fine -- the predicted risk here didn't land, becausesedis one of the tools this job explicitly shadows ontoPATH.grepis not shadowed, though, and that same script'sgrep -oE '\{"..."'(an escaped literal brace under-E) is exactly the GNU-extension trap predicted above, just on the un-shadowed tool instead: GNUgrep -Eaccepts\{as a literal brace outside a valid interval, which is undefined by POSIX and not something Apple's own regex engine grants -- the pipeline silently produced zero matches (masked by the trailing|| true) rather than erroring, so the audit's own empty-table guard is what actually surfaced it. Fixed by using a bracket expression ([{]) instead, unambiguous as a literal brace in both BRE and ERE, on every implementation. The same script'srg-based dispatch-contract check is a second, independent gap: ripgrep isn't part of this runner image's own preinstalled tools (confirmed against the image's own Included Software manifest) and wasn't in this job's Homebrew list either -- added there now, alongsidecoreutils/gnu-sed/bash.- A real runtime (not just test-harness) gap, found the same way: a
fixed-offset Time's
%zprinted "+0000" regardless of its real offset.time_struct_tm(src/vm.c) hand-patchestm_gmtoffinto agmtime_r-producedstruct tmfor a fixed-offset Time;strftime's own%zconversion trusts that field on glibc but not on Darwin's libc, confirmed directly by this same CI run (tests/cases/ time_calendar_constructors.d6,east/west's own%zcases). Fixed at the source rather than excluded: d6 now substitutes%zitself before a fixed-offset Time's format string ever reaches the systemstrftime(3)(substitute_fixed_offset_z), so the directive no longer depends on the platform honoring it, on Darwin or anywhere else. See docs/time.md and the changelog. tests/lsp_test.sh's require-resolution case compared a published diagnostic'suriagainst an expected string built frommktemp -d's own raw return value. Found via abash -xtrace after a plain run failed with no visible output at all (nothing prints between a failing[[ ... ]]and the script's owntrap-driven cleanup, underset -e) -- confirmed directly rather than guessed: the trace showed every earlier assertion in the file passing, then this exact comparison failing on a/private/var/...vs/var/...mismatch.required files (unlike a document's own uri, echoed straight back from what the script itself sent) are resolved by the compiler's loader, which canonicalizes the path -- and on macOS, where/varis itself a symlink to/private/var, that resolves to somethingmktemp -d's own raw output never does. Fixed byrealpath-ing$workonce, immediately aftermktemp -d, the same waydiamond_lspright above it already is -- makes every downstream string comparison agree with whatever canonical form the loader produces, on any platform, rather than assumingmktemp's own return value already is one.tests/fiber_run.c's own stack-unmap probe: after a fiber's stack is freed (free_fiber_stack,munmap), the test callsmincore()on the freed range and expectsENOMEM-- confirming the memory is genuinely gone, not just leaked-but-untouched. Reliable on Linux; confirmed NOT reliable the same way on Darwin (this same CI run: every earlier fiber check passed, then this exact one failed). Most likely explanation: some other allocation already reclaimed the same virtual address range during the malloc/free trafficdiamond_vm_collect/diamond_vm_freethemselves generate, before the probe runs -- notmunmapfailing to unmap it, which isn't platform-conditional. No portable feature test predicts mincore-after-munmap timing, so this is the same class of gap__APPLE__is already the deliberate, narrow exception for elsewhere in this codebase (bcrypt_verify_helper, above) -- the check is now skipped on Darwin (#ifndef __APPLE__) rather than guessed at further without a real macOS box to verify against. (Separately,mincore'svecparameter is declaredunsigned char *on Linux andchar *on Darwin -- an unrelated-Wpointer-signwarning, fixed with a(void *)cast at both call sites.)- Homebrew package names for a future CI job's own dependencies,
confirmed present on the runner image already:
openssl@3,sqlite.postgresql@16andmariadb-connector-cinstall cleanly viabrew install(used to verify thevm.ccompile above) -- their headers land under$(brew --prefix postgresql@16)/includeand both$(brew --prefix mariadb-connector-c)/includeand that same prefix'sinclude/mariadbsubdirectory (the mysql.h split already familiar from FreeBSD's own port).
Unlike the musl job's own narrow carve-out (two BCrypt test cases,
test-musl's own comment), excluding everythingtimeouttouches here would have gutted most of the process/signal/socket coverage -- there was no small subset ofmake testthat dodged this cleanly. Resolved by installing Homebrew'scoreutils/gnu-sed/bashand prepending their real-GNU-tool directories ontoPATH(test-macosjob) rather than rewritingtests/run.sh's own call sites -- the job now runsmake testfor real, not a build-only compile check.
FreeBSD/OpenBSD: confirmed against real VMs
Unlike the macOS section above, this was checked against real, current
kernels -- FreeBSD 15.1-RELEASE and OpenBSD 7.9, both amd64, each booted
for real via vmactions/freebsd-vm/vmactions/openbsd-vm (QEMU, not
emulation of the userland alone). FreeBSD now has a real, passing CI job
(test-freebsd, see "Validated platforms" above); OpenBSD does not, for
the real reason below, not lack of effort.
What it took to get FreeBSD's job from a first real build attempt to
fully green, beyond the prerequisite findings below (each its own commit
on the way there, git log -- .github/workflows/ci.yml src/vm.c tests/run.sh):
gmake, notmake: FreeBSD's basemakeis BSD make (pmake), which doesn't understand this Makefile's GNU-only syntax (ifeq,$(wildcard ...),$(shell ...)) at all.mysql.h/libmariadb.soboth live under FreeBSD's mariadb-connector-c port's ownmariadb/subdirectory (/usr/local/include/mariadb/,/usr/local/lib/mariadb/), on top of the plain/usr/localprefix everything else needs -- the same split Debian/Ubuntu'slibmariadb-devneeds on Linux (Makefile's ownCPPFLAGScomment), just a different extra path.CPPFLAGS_EXTRA/LDFLAGS_EXTRA(added for macOS, see above) needed one realMakefilefix to actually work everywhere: most link recipes (gen_compiled_preludeon) use$(LDLIBS)alone, never$(LDFLAGS)-- only the finald6binary and the API test binaries link with$(LDFLAGS).LDFLAGS_EXTRA's-Lpaths are now prepended intoLDLIBSitself instead, reaching every recipe uniformly.tests/collection_relay_contracts.shuses a GNU sed extension (a:label/n/p/bloop) FreeBSD's base BSD sed rejects outright -- same category as musl/Alpine's own BusyBox sed finding above. FreeBSD's GNU sed port installs asgsed, not asedoverride, and/usr/local/binsits after/usr/binin FreeBSD's defaultPATH, so (unlike Alpine'sapkpackage) installing it doesn't shadow the basesedon its own; the CI job symlinks it into a directory it prepends toPATHexplicitly instead.- Two real runtime bugs, not tooling gaps, found this way (both fixed
in
src/vm.c, both were invisible on Linux/musl):tcp_listen_helper/udp_socket_helper's wildcard bind (getaddrinfo(nullptr, port, {AF_UNSPEC, AI_PASSIVE})) never setIPV6_V6ONLY. Linux's defaultnet.ipv6.bindv6only=0makes an IPv6-wildcard bind already dual-stack, so an IPv4 client connecting to127.0.0.1reaches it with no code needed. FreeBSD (and OpenBSD) defaultnet.inet6.ip6.v6onlyto1-- the identical bind only accepted IPv6 there, so an IPv4 loopback connect got plainECONNREFUSED, nothing about the failure pointing at IPv6 at all. Fixed by disablingIPV6_V6ONLYexplicitly on the v6 candidate, matching Linux's default rather than silently depending on it.UDPSocket#receive(0): FreeBSD'srecvfromleaves the sender address entirely unpopulated (ss_familystaysAF_UNSPEC) on a genuinely zero-length request, later failinggetnameinfowithEAI_FAMILY("Address family not recognized"). Linux populates it regardless of the requested length. Fixed by always requesting at least 1 byte from the kernel (satisfies FreeBSD's requirement to resolve the address) while still reporting zero bytes of data whenever the caller's own request wasreceive(0)--recvfromalways consumes/discards the full datagram regardless of buffer size, so this doesn't changereceive(0)'s documented "consumes without copying" contract.
- Two real, expected (not fixed) platform differences, excluded from
the CI run the same way musl's own two BCrypt cases are (
test-freebsdjob's own comment):bcrypt.d6/active_record_secure_password.d6:BCrypt.hashneeds libxcrypt'scrypt_gensalt_rn/CRYPT_GENSALT_IMPLEMENTS_AUTO_ENTROPYto generate a fresh salt, both declared only in libxcrypt's own<crypt.h>-- which doesn't exist on FreeBSD at all (see the__has_includeguard below), sobcrypt_hash_helperalways takes its "not supported on this platform" branch there. A different missing piece than musl's own gap (musl has<crypt.h>but not bcrypt algorithm support incrypt_ritself), same observable result.BCrypt.verifyis unaffected -- no salt generation, justcrypt_ragainst an existing digest, confirmed genuinely correct on FreeBSD (see below).math_exp_log(_tanh).d6:exp(1.0)/exp(log(5.0))differ from glibc's own libm in the last representable bit (2.7182818284590455vs2.718281828459045,5.0vs4.999999999999999). Not a bug in either library -- IEEE 754 only mandates correctly-rounded results for+/-/*///sqrt, not transcendental functions, so two conforming libm implementations disagreeing in the last ULP is expected.
The rest of this section is the original diagnostic probe's own findings -- confirmed directly, not assumed, before any of the above was attempted, and the prerequisites everything above sits on top of.
ucontext_t/getcontext/makecontext/swapcontext(Fiber support, same dependency as the musl section above): work natively on FreeBSD, no compatibility library needed.<ucontext.h>does not exist at all on OpenBSD --fatal error: 'ucontext.h' file not found, not merely an unimplemented symbol the way musl's case was. This is a real gap in d6's own Fiber implementation (src/vm.cdepends on POSIXucontextunconditionally), not a build/CI-tooling problem: "Known limitations" below already notes there's no fallback fiber implementation (e.g. hand-rolledsetjmp/longjmp+ manual stack switch) planned. OpenBSD Fiber/Thread support is out of reach without that separate engineering effort -- a CI job alone can't close this gap.crypt(3)bcrypt support: native and correct on both -- confirmed by hashing the same password with the same salt on each and getting the identical, valid$2b$hash back. (The probe's first run produced a false NULL on both platforms from a malformed 21-character test salt, not a real platform gap -- bcrypt salts are fixed at 22; re-run with a known-valid one confirmed this cleanly.)-ldl: links fine on FreeBSD. Fails on OpenBSD ("unable to find library -ldl") -- same category as macOS's finding above (dlopenlives in libc directly, no separate.dylib/.soto link against); the sameLDLIBS_DL=override already added for macOS covers this.timeout: present in both base installs (unlike macOS, which has none). FreeBSD's accepts GNU-style long options (--foreground/--kill-after/--preserve-status/--signal, though not--version) -- and its actual relay/exit-status semantics against every real call site intests/run.share now confirmed correct outright: thetest-freebsdjob runs all of them successfully (see the same GNU-vs-uutils distinction docs/portability.md's macOS section flags, for what to check if this ever needs revisiting). OpenBSD's is a minimal native dialect (-fp/-k time/-s signal, no long options at all) -- meaningfully different, not just missing, and untested against real call sites given OpenBSD's own blocker above.nproc: present in FreeBSD's base (/bin/nproc) -- no fix needed there. Absent on OpenBSD, same as macOS;sysctl -n hw.ncpuworks on both as the portable fallback.bc: present and functional on both.bash5+ (for$EPOCHREALTIME): not in either base install, but installs cleanly viapkg/pkg_addand resolves first onPATHahead of any system shell once installed -- confirmed directly bywhich bashafter install on both, unlike macOS's still-unconfirmed PATH ordering.- Toolchain: both ship Clang as the base
cc(FreeBSD 19.1.7, OpenBSD 19.1.7); GCC is available on both via the package manager (FreeBSD ports as plaingcc, OpenBSD asegcc--gccthere is reserved for an ancient bundled version). FreeBSD 15's package-managed base system (pkgbase) already has OpenSSL and SQLite dev packages installed by default in the image used; PostgreSQL/MariaDB Connector package names were not checked (grep only searched already-installed packages, not the full repository).
Net assessment: FreeBSD's test-freebsd job is real and green (see
"Validated platforms" above) -- every prerequisite checked out clean, and
the handful of real gaps it surfaced (two runtime bugs, two expected
platform differences) are now fixed or excluded, same as musl's own job.
OpenBSD is blocked on the missing ucontext.h, a language-runtime gap,
not a portability/tooling one; revisit only alongside (or after) a
non-ucontext Fiber implementation, not as a CI task on its own.
Known limitations (not fixed, by design)
BCrypt.hash/BCrypt.verifydepend on the systemcrypt(3)'s own bcrypt support. Confirmed present via libxcrypt (Fedora, Ubuntu). Confirmed absent on musl (Alpine) --.hashfails with a clear runtime error;.verifyalways reports no match. Unverified on other libcs (a classic BSDcrypt()typically lacks bcrypt too; OpenBSD's owncrypt()has it natively, since that's where bcrypt originated).- Fiber support requires a working POSIX
ucontext.himplementation, native or via a compatibility library (libucontexton musl). No fallback fiber implementation (e.g. a hand-rolledsetjmp/longjmp+ manual stack switch) exists or is currently planned. On WebAssembly there is no stack switching at all, soFiber.newraises.
What hasn't been found (checked, not just assumed)
- No
#ifdef __linux__/__APPLE__/__FreeBSD__/_WIN32-style OS branching exists anywhere in d6's own source (src/,lsp/,tools/,tests/,fuzz/); the only platform conditionals are the JIT's x86-64/glibc gate and__wasm__insrc/vm.h(fiber stubs) andsrc/run_source.c(a portable SHA-256). The vendoredreginold/vendor/onigmo/regex engine has its own, unrelated to this. - No Linux-only APIs (
epoll,eventfd,inotify,sendfile,prctl, anything under<linux/...>) -- socket/pipe multiplexing uses plain POSIXpoll(2)throughout (src/vm.c). - No other glibc-only extensions beyond the crypt.h ones above
(
memmem,asprintf,qsort_r,strverscmp,get_current_dir_name,program_invocation_nameand similar were all checked directly viagrep-- none are used). thread_local/mmap(MAP_ANONYMOUS)/mprotect/sigaction/getaddrinfoare all used, all standard C23/POSIX, and all present and behave identically on both validated libcs.