The tree is Ubuntu-first, but the Linux-specific surface is about ten files and already sits at the bottom of the I/O and RPC stacks. Rather than fork the repo or scatter ifdefs, platform differences go behind narrow seams with one backend directory per OS, selected centrally in cmake/platform.cmake. Linux stays the reference implementation and its behaviour does not change: the owning lanes (CORE, DAEMON) move today's Linux code behind the seams on Ubuntu first, with the suite still green, before any macOS code exists. A new PORT lane owns only the per-OS backends, so macOS work never writes shared logic and CLAUDE.md's lane rule stays intact. Adds ADR 0020 (the decision and phases), docs/08-porting.md (seam list, API mapping for macOS and Windows, dependency table, verification gates) and docs/agents/AGENT-PORT.md (the lane brief), plus the PORT row in the ownership table. Co-Authored-By: Claude Opus 5 <[email protected]>
5.2 KiB
08 — Porting guide (macOS first, Windows later)
Read docs/adr/0020-cross-platform-strategy.md first; this file is the concrete inventory
it refers to. Rule: Linux behaviour never changes. #ifdef never appears in logic.
Directory layout
Platform backends live beside the code they serve, one directory per OS:
core/src/io/platform/linux/file_ops.cpp core/src/io/platform/macos/file_ops.cpp
core/src/io/platform/file_ops.hpp # the seam — no #ifdef, no OS types
daemon/src/rpc/platform/linux/{wakeup,peercred,instance_lock,runtime_dir}.cpp
daemon/src/rpc/platform/macos/{...}.cpp
daemon/src/rpc/platform/*.hpp # the seams
cmake/platform.cmake # VELOX_OS_* + source selection
cmake/platform.cmake sets exactly one of VELOX_OS_LINUX, VELOX_OS_MACOS,
VELOX_OS_WINDOWS, and exposes velox_platform_sources(<target> <dir>) which adds
<dir>/platform/<os>/*.cpp. Selection happens there and nowhere else.
The seams
Five interfaces cover the whole port. Signatures are indicative, not binding — the owning lane settles them in Phase 0.
| Seam | Interface | Why |
|---|---|---|
| File preallocation | Result<void> preallocate(int fd, uint64_t bytes) |
posix_fallocate is Linux/glibc |
| Cache advice | void advise_dontneed(int fd, uint64_t off, uint64_t len) |
posix_fadvise has no macOS equivalent |
| Durable flush | Result<void> flush_durable(int fd) |
fdatasync vs F_FULLFSYNC |
| Loop wakeup | class Wakeup { int pollfd(); void signal(); void drain(); } |
eventfd is Linux-only |
| Peer identity | Result<PeerId> peer_of(int fd) |
SO_PEERCRED vs LOCAL_PEERCRED |
| Single instance | Result<Lock> acquire(string_view runtime_dir) |
abstract sockets are Linux-only |
| Runtime dir | string runtime_dir(), string data_dir() |
XDG vs ~/Library |
API mapping
| Linux (today) | macOS | Windows (later) | Note |
|---|---|---|---|
posix_fallocate(fd,0,n) |
fcntl(F_PREALLOCATE) then ftruncate(n) |
SetFileValidData / SetEndOfFile |
macOS needs the ftruncate; F_PREALLOCATE alone does not set size. Fall back to ftruncate on failure, exactly as the Linux path already does for EOPNOTSUPP |
posix_fadvise(DONTNEED) |
no equivalent — no-op | FILE_FLAG_NO_BUFFERING |
Do not substitute F_NOCACHE: it changes caching for the whole descriptor, not a written range. A no-op is honest; record it |
fdatasync(fd) |
fcntl(fd, F_FULLFSYNC), fall back to fsync |
FlushFileBuffers |
fsync on macOS does not guarantee the drive flushed. .veloxpart resume integrity depends on this — use F_FULLFSYNC |
pwrite |
same | WriteFile + OVERLAPPED |
POSIX, no work |
O_NOFOLLOW |
same | FILE_FLAG_OPEN_REPARSE_POINT |
POSIX, no work |
eventfd |
self-pipe (pipe2/`O_NONBLOCK |
O_CLOEXEC`) | event object |
timerfd |
poll() timeout computed from the next deadline |
waitable timer | Simplest port: the loop already has a deadline set |
SO_PEERCRED + struct ucred |
getpeereid(fd,&uid,&gid) |
named-pipe token | Same-UID check is the security property; keep it |
abstract socket \0velox-daemon-<hash> |
socket file in the runtime dir + `flock(LOCK_EX | LOCK_NB)` | named mutex |
$XDG_RUNTIME_DIR |
$TMPDIR (per-user, already private) |
%LOCALAPPDATA% |
macOS has no XDG runtime dir |
$XDG_DATA_HOME |
~/Library/Application Support/Velox |
%APPDATA% |
|
libsecret / Secret Service |
Keychain (Security.framework) |
DPAPI / Credential Manager | Behind the credential-store seam. CLAUDE.md §4 still applies: never SQLite, never logs |
systemd user units |
launchd plist (~/Library/LaunchAgents) |
Service/Task Scheduler | Phase 3 |
Build dependencies
| Ubuntu | macOS (Homebrew) |
|---|---|
qt6-base-dev, qt6-svg-dev, qt6-tools-dev |
qt@6 |
libcurl4-openssl-dev |
system libcurl, or curl |
libsqlite3-dev |
system sqlite, or sqlite |
libssl-dev |
openssl@3 (set OPENSSL_ROOT_DIR) |
libsecret-1-dev |
none — Keychain is in the SDK |
nlohmann-json3-dev |
nlohmann-json |
nodejs, npm |
node |
The root CMakeLists.txt currently does pkg_check_modules(LIBSECRET REQUIRED ...)
unconditionally. That must become Linux-only, or macOS cannot configure at all. It is the
single hard blocker for a first macOS build.
What does not change
contracts/ and both generated clients, tools/mockd, tests/conformance,
tools/testserver, the whole extension, and every hostile-mode expectation. If a port
tempts you to change a fixture or a schema, stop — that is a contract change and it goes
through PROTO.
Verification gates
- Phase 0 done: Ubuntu suite still 57/57, and
git diffshows only moves behind seams. - Phase 1 done:
veloxd,velox,libveloxcorebuild on macOS; core unit tests pass. - Phase 2 done: conformance (mockd and live veloxd) and the engine hostile-mode matrix pass on macOS; one real download completes with a matching SHA-256.
- Phase 3 done:
.dmgor Homebrew formula installs and runs on a clean machine.