# 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( )` which adds `/platform//*.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 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 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 peer_of(int fd)` | `SO_PEERCRED` vs `LOCAL_PEERCRED` | | Single instance | `Result 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 | `poll(2)` already used, so a pipe read-end drops straight in | | `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-` | socket file in the runtime dir + `flock(LOCK_EX|LOCK_NB)` | named mutex | macOS has no abstract namespace. Must unlink stale sockets on start — the abstract version got that free | | `$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 diff` shows only moves behind seams. - **Phase 1 done:** `veloxd`, `velox`, `libveloxcore` build 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:** `.dmg` or Homebrew formula installs and runs on a clean machine.