No description
  • Zig 65.8%
  • Swift 22.3%
  • C 5%
  • JavaScript 4%
  • Rust 1.9%
  • Other 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-16 15:55:40 -04:00
include update refcas 2026-09-09 10:52:19 -04:00
macos build improvements 2026-09-16 15:55:40 -04:00
native snapshot improvements 2026-09-08 11:35:04 -04:00
src build improvements 2026-09-16 15:55:40 -04:00
vendor upgrade zvec to 0.7.0 2026-08-28 12:28:28 -04:00
.gitignore vendor llama.cpp and zvec sources 2026-08-11 14:32:12 -04:00
build.zig build improvements 2026-09-16 15:55:40 -04:00
build.zig.zon build improvements 2026-09-16 15:55:40 -04:00
README.md build improvements 2026-09-16 15:55:40 -04:00

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:

  1. src/c_api.zig exports a C ABI for the core (yarrow_* functions).
  2. zig build xcframework compiles it as a static library per macOS architecture (with compiler-rt bundled and the refcas/lmdb objects compiled in), merges each arch's cargo-built libmonolith_ffi.a into it with libtool -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 plus include/ into macos/YarrowKit.xcframework via xcodebuild -create-xcframework.
  3. include/module.modulemap names the module, so Swift just import YarrowKit and 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 = true offers each miss to SnapshotFetcher, which fetches over URLSession with 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; reqwest is 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 a 200, 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.