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
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:
@@ -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.
|
||||
Reference in New Issue
Block a user