docs: define the cross-platform porting strategy (ADR 0020, macOS first)
CI / clang-format (push) Canceled after 0s
CI / testserver (push) Canceled after 0s
CI / bootstrap-script (push) Canceled after 0s
CI / bootstrap-script-2604 (push) Canceled after 0s
CI / extension-lint (push) Canceled after 0s
CI / build (clang) (push) Canceled after 0s
CI / build (gcc) (push) Canceled after 0s
CI / sanitizers (dev) (push) Canceled after 0s
CI / sanitizers (tsan) (push) Canceled after 0s
CI / clang-tidy (push) Canceled after 0s
CI / conformance (push) Canceled after 0s
CI / nightly-integration (push) Canceled after 0s
CI / gui-dod (push) Canceled after 0s
CI / gui-dod-nightly (push) Canceled after 0s

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]>
This commit is contained in:
2026-09-15 17:36:27 +04:00
co-authored by Claude Opus 5
parent 3e553f08c0
commit facd851824
4 changed files with 205 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
# 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 | `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-<hash>` | 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.