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]>
3.9 KiB
3.9 KiB
ADR 0020 — Porting to macOS (and later Windows) without forking the tree
Status: accepted · Supersedes nothing · Applies to every lane
Context
The project was built Ubuntu-first and says so in its own project description. A survey of
main (2026-09-15, 57/57 green) found the Linux-specific surface is far smaller than the
Ubuntu-first framing suggests — roughly ten files, all of them already isolated at the
bottom of the stack:
| Area | Linux-only thing | Files |
|---|---|---|
core/io |
posix_fallocate, posix_fadvise, fdatasync |
sparse_file.cpp |
core/meta |
fdatasync |
veloxpart.cpp |
daemon/rpc |
eventfd |
event_loop.{hpp,cpp}, main.cpp |
daemon/rpc |
timerfd |
main.cpp, sched/scheduler.hpp |
daemon/rpc |
SO_PEERCRED / struct ucred |
uds_server.{hpp,cpp} |
daemon/rpc |
abstract-namespace socket lock | single_instance.{hpp,cpp} |
daemon/rpc |
$XDG_RUNTIME_DIR layout |
runtime_dir.{hpp,cpp} |
| build | libsecret-1 is REQUIRED at the root |
CMakeLists.txt |
Everything else is already portable: the event loop is poll(2) (POSIX, not epoll), the
transfer engine is libcurl, the GUI is Qt 6, tools/mockd and tests/conformance are
Node, tools/testserver is stdlib Python, and the extension is a WebExtension.
Decision
One tree, one logic, per-OS backends behind seams.
- No
#ifdefin logic. Platform differences live behind a narrow interface, with one implementation file per OS. A reader ofsparse_file.cppmust not need to know which OS they are on.#ifdefis allowed only inside aplatform/<os>/file. - Linux is the reference implementation. A port may never change Linux behaviour. The gate is mechanical: the full suite stays green on Ubuntu, and the Linux backend keeps the same syscalls it uses today. If a port needs a semantic change, that is an ADR of its own, not a port commit.
- Seams are introduced by the owning lane, on Linux, before any port work. CORE
introduces
core's seams; DAEMON introducesdaemon's. This is a pure refactor: move the existing Linux code behind the interface unchanged, prove the suite is still green. - A new lane, PORT, owns only the per-OS backends —
**/platform/<os>/**andcmake/platform*.cmake. It never writes logic. That keeps CLAUDE.md §1 intact: CORE still ownscore/'s logic, DAEMON still ownsdaemon/'s, and macOS work can proceed in parallel without cross-lane writes. VELOX_OS_*is set once, centrally, incmake/platform.cmake, and platform sources are selected there — not by globbing, and not per-target ad hoc.- Optional dependencies are gated, never removed.
libsecretstaysREQUIREDon Linux and is replaced by Keychain on macOS behind the same credential-store seam.
Phases
- Phase 0 — on Ubuntu, by CORE + DAEMON. Introduce the seams and move today's Linux code behind them. Zero behaviour change; 57/57 stays green. No macOS code exists yet.
- Phase 1 — on macOS, by PORT. Implement the macOS backends until
veloxd,veloxandlibveloxcorebuild and their unit tests pass. - Phase 2 — parity.
tools/testserver, conformance and the engine's hostile-mode matrix pass on macOS. The GUI builds against Qt 6 for macOS. - Phase 3 — packaging.
.dmg/Homebrew,launchdinstead ofsystemd, and the macOS native-messaging manifest locations. Deliberately last.
Consequences
- Windows later is the same shape: a third backend directory, no new strategy. The seams chosen here are POSIX-flavoured but interface-level, so a Win32 backend is additive.
- CI must build both, or macOS rots silently. Until a macOS runner exists, Phase 1 is verified by hand on the porting machine and the Linux gate stays authoritative.
- The cost is one indirection at the bottom of the I/O and RPC stacks. Measured against
the alternative —
#ifdefdrift, or a forked repo that diverges in a month — it is cheap.