diff --git a/CLAUDE.md b/CLAUDE.md index b9364af..69910ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,7 @@ Read this before touching anything. Then read your lane brief in [docs/agents/]( | GUI | `gui/` | everything else | | EXT | `extension/` | everything else | | PKG/QA | `packaging/`, `.github/`, `tools/testserver/`, `tests/integration/`, `tests/e2e/`, root build files | any lane's feature code | +| PORT | `**/platform//**`, `cmake/platform*.cmake`, `packaging/macos/`, `packaging/windows/` | all logic — see [ADR 0020](docs/adr/0020-cross-platform-strategy.md) | If your task seems to require editing another lane's files, that is a signal the interface is wrong. **File the request; don't reach across.** diff --git a/docs/08-porting.md b/docs/08-porting.md new file mode 100644 index 0000000..683d5fc --- /dev/null +++ b/docs/08-porting.md @@ -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( )` 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. diff --git a/docs/adr/0020-cross-platform-strategy.md b/docs/adr/0020-cross-platform-strategy.md new file mode 100644 index 0000000..69fa086 --- /dev/null +++ b/docs/adr/0020-cross-platform-strategy.md @@ -0,0 +1,69 @@ +# 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.** + +1. **No `#ifdef` in logic.** Platform differences live behind a narrow interface, with one + implementation file per OS. A reader of `sparse_file.cpp` must not need to know which OS + they are on. `#ifdef` is allowed only inside a `platform//` file. +2. **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. +3. **Seams are introduced by the owning lane, on Linux, before any port work.** CORE + introduces `core`'s seams; DAEMON introduces `daemon`'s. This is a pure refactor: move + the existing Linux code behind the interface unchanged, prove the suite is still green. +4. **A new lane, PORT, owns only the per-OS backends** — `**/platform//**` and + `cmake/platform*.cmake`. It never writes logic. That keeps CLAUDE.md §1 intact: CORE + still owns `core/`'s logic, DAEMON still owns `daemon/`'s, and macOS work can proceed in + parallel without cross-lane writes. +5. **`VELOX_OS_*` is set once, centrally**, in `cmake/platform.cmake`, and platform sources + are selected there — not by globbing, and not per-target ad hoc. +6. **Optional dependencies are gated, never removed.** `libsecret` stays `REQUIRED` on + 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`, `velox` + and `libveloxcore` build 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, `launchd` instead of `systemd`, 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 — `#ifdef` drift, or a forked repo that diverges in a month — it is + cheap. diff --git a/docs/agents/AGENT-PORT.md b/docs/agents/AGENT-PORT.md new file mode 100644 index 0000000..fc3da70 --- /dev/null +++ b/docs/agents/AGENT-PORT.md @@ -0,0 +1,50 @@ +# Agent brief — PORT (per-OS backends) + +**Starts after Phase 0: CORE and DAEMON have landed the seams on Ubuntu.** + +## You own +``` +core/src/**/platform//** daemon/src/**/platform//** +cmake/platform*.cmake packaging/macos/** packaging/windows/** +``` +You may read everything. **You never write logic** — not in `core/src/io/*.cpp`, not in +`daemon/src/rpc/*.cpp`, not in `gui/`, never in `contracts/`. + +## Read first +`docs/adr/0020-cross-platform-strategy.md`, then `docs/08-porting.md` — it has the seam +list, the API mapping and the dependency table. Then `CLAUDE.md`. + +## The one rule that matters +**Linux behaviour never changes.** If your port needs a seam that doesn't exist, or a seam +whose shape is wrong, you do not widen it yourself — you file a request with the owning +lane (CORE for `core/`, DAEMON for `daemon/`) exactly as every other lane does. A port that +quietly edits shared logic is how one tree becomes two. + +## Build order (macOS) +1. **Configure at all.** The root `CMakeLists.txt` requires `libsecret-1` unconditionally; + that must be Linux-gated before anything else compiles. File it with PKG/QA — root build + files are theirs. +2. `core/` — `preallocate`, `advise_dontneed`, `flush_durable`. Use `F_FULLFSYNC`, not + `fsync`: `.veloxpart` resume correctness depends on a real flush. +3. `daemon/` — `Wakeup` (self-pipe), `peer_of` (`getpeereid`), `instance_lock` (socket file + + `flock`, with stale-socket cleanup), `runtime_dir` (`$TMPDIR`, `~/Library/Application + Support/Velox`). +4. Unit tests green for both lanes. +5. `tools/testserver` (stdlib Python) and conformance (Node) — expected to run unchanged. + If they don't, that's a bug worth reporting, not patching around. +6. GUI against Qt 6 for macOS. +7. Packaging — `.dmg`/Homebrew, `launchd`, native-messaging manifest locations. Last. + +## Definition of done (Phase 1–2) +- macOS builds `libveloxcore`, `veloxd` and `velox` with `-Wall -Wextra -Werror`. +- Core and daemon unit tests pass on macOS. +- One real download completes on macOS against `tools/testserver` with a SHA-256 that + matches the server's reference — the same gate the Linux vertical slice passed. +- `kill -9` mid-download, restart, resume completes and the checksum still matches. +- The Ubuntu suite is **still** 57/57 from the same commit. + +## Do not +- Do not add `#ifdef` outside a `platform//` file. +- Do not change a fixture, a schema or a hostile-mode expectation to make macOS pass. +- Do not substitute `F_NOCACHE` for `posix_fadvise(DONTNEED)` — see `docs/08` for why. +- Do not start with packaging.