- Zig 65.8%
- Swift 22.3%
- C 5%
- JavaScript 4%
- Rust 1.9%
- Other 1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| include | ||
| macos | ||
| native | ||
| src | ||
| vendor | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| README.md | ||
Yarrow
A macOS app whose core logic is written in Zig, with storage provided by
refcas (branch zig) and a Swift/AppKit
UI. The same Zig core will later back an iOS (UIKit) target.
Layout
build.zig, build.zig.zon Zig package: core logic + refcas dependency
src/yarrow.zig Core logic (Zig API; unit tests live here)
src/c_api.zig C ABI exported to Swift — keep in sync with include/yarrow.h
include/yarrow.h The header Swift sees (as module YarrowKit)
include/module.modulemap Declares the YarrowKit Clang module
macos/project.yml XcodeGen spec — regenerate with `xcodegen generate`
macos/Sources/Core/ Platform-agnostic Swift wrapper (no AppKit/UIKit imports;
will be shared with the iOS target)
macos/Sources/App/ AppKit app (programmatic, no storyboards)
macos/Sources/App/Snapshot/ Hidden-webview renderer and its preparation script
macos/Sources/App/Library/ The page grid: store-backed list, cards, image cache
macos/TestPages/ Local page exercising the preparation script
macos/TESTING.md Manual snapshot test matrix (run by hand, offline checks)
spikes/ De-risking spikes and their findings (Phase 0, shadow DOM)
macos/YarrowKit.xcframework Built artifact (gitignored)
Building
Requirements: Zig 0.16.0 (the version refcas pins), Xcode, and Rust via
rustup: zig build test-snapshot cargo-builds native/monolith-ffi for the
host, and zig build xcframework for both macOS architectures — so
rustup target add aarch64-apple-darwin x86_64-apple-darwin once (and
aarch64-apple-ios aarch64-apple-ios-sim for zig build ios-check).
XcodeGen only if you change project.yml.
Open macos/Yarrow.xcodeproj in Xcode and run (⌘R), or zig build run from
the command line — both launch the same bundle. A pre-build phase runs
zig build xcframework, so the Zig core is rebuilt automatically on every
build. Note the phase extends PATH with ~/.local/zig and Homebrew — adjust
in project.yml if your zig lives elsewhere.
The app links the macOS slice inside that xcframework directly and reads
include/ from the source tree, rather than declaring the xcframework as
a framework dependency. Xcode processes a framework dependency — copying
its library and headers into the build products — before any script phase
runs, and nothing reorders that, so every core change would reach the app
one build late and a fresh clone could not plan the build at all. With the
direct link, a change to the Zig core or to include/yarrow.h lands in the
same build, and the first build on a fresh clone needs no manual step.
From the command line:
zig build test # core unit tests (no Rust toolchain needed)
zig build test-snapshot # snapshot FFI + pipeline tests (cargo-builds native/monolith-ffi)
zig build xcframework # rebuild macos/YarrowKit.xcframework (universal arm64+x86_64)
zig build ios-check # cross-build the Rust crate for iOS (forward-compat, build only)
zig build run # build and launch the app — the ⌘R equivalent (needs Xcode)
zig build # native static lib into zig-out/ (dev convenience)
zig build run shells out to xcodebuild and opens the bundle it reports,
so it launches the same binary Xcode does rather than a parallel build. It
does not depend on the xcframework step — the project's pre-build phase
already runs that, and depending on it here would build the core twice per
launch. Add -Dconfiguration=Release for a release run. It is the only step
that requires Xcode; the rest need Zig, and cargo for the snapshot half.
How the binding works
Following the approach from Zig and SwiftUI:
src/c_api.zigexports a C ABI for the core (yarrow_*functions).zig build xcframeworkcompiles it as a static library per macOS architecture (with compiler-rt bundled and the refcas/lmdb objects compiled in), merges each arch's cargo-builtlibmonolith_ffi.ainto it withlibtool -static— the snapshot C API calls into the Rust side, and consumers link exactly one library —lipos the arm64 + x86_64 pair into one fat archive, and wraps the result plusinclude/intomacos/YarrowKit.xcframeworkviaxcodebuild -create-xcframework.include/module.modulemapnames the module, so Swift justimport YarrowKitand calls the C functions directly.
The macOS slice is universal (Intel + Apple Silicon). iOS is
forward-compatible but not built yet: zig build ios-check cross-builds
the Rust crate for aarch64-apple-ios / aarch64-apple-ios-sim; when the
iOS target is real, its Zig slices and extra -library/-headers pairs
join xcodebuild -create-xcframework (an xcframework carries one library
per platform), and macos/Sources/Core/ is written to be reused as-is.
Snapshots
A capture renders in a hidden WKWebView, freezes with
createWebArchiveData, and the vendored monolith (vendor/monolith,
behind native/monolith-ffi) turns the archive into one self-contained
HTML file — every asset resolved from the archive itself, never the
network. ROADMAP_SNAPSHOT.md records the design and its decisions;
vendor/monolith/VENDOR.md lists every patch carried on the vendored
copy.
Two knobs worth knowing about:
-
Budgets (
yarrow.SnapshotBudgets): 256 MiB archive in, 512 MiB HTML out, 30s conversion. Blowing one is a permanent verdict, not a retry — a re-render will not shrink the page. -
Host-fetch fallback, off by default. Assets the archive lacks are normally left as absolute URLs. Setting
CoreScheduler.hostFetchFallback = trueoffers each miss toSnapshotFetcher, which fetches overURLSessionwith the render's cookies. Turn it on only if the manual matrix (macos/TESTING.md) shows meaningful misses — it makes a capture depend on network conditions at conversion time, which the default deliberately avoids.Note the split: monolith never fetches anything. It asks, through a callback, and the Swift layer answers;
reqwestis compiled out, so the Rust side has no HTTP client to use even by mistake. The main reason is cookies — an asset fetched without the render's session usually comes back as a login page with a200, which would then be inlined as if it were the real resource.ROADMAP_SNAPSHOT.md's decisions list has the full reasoning, including what this arrangement does not buy (it is not a sandbox boundary; the entitlement is per-process and WebKit needs it regardless).
Storage
The core depends on the refcas module (content-addressed records over LMDB,
with proteus for serialization), pinned by commit in build.zig.zon. Update
with:
zig fetch --save=refcas https://forgejo.gllghr.net/d/refcas/archive/<commit>.tar.gz
The x86_64 macOS slice opts into refcas's software SHA-256 path
(-Dsoftware-sha256) since Intel Macs predate SHA-NI; Apple Silicon uses the
hardware instructions.