Four Targets, One Direction
Ariandel's engine is a Swift package, AriandelKit, and its four library targets stack in a single direction:
CJSONC: json-c, MIT, vendored at commitdcd1572. It's here for one reason: libmypaint parses.mybbrush files with it.CMyPaint: libmypaint, ISC, vendored atd5a88fb(master, 2026-04-14, the 2.0.0-beta API). It depends onCJSONC.CAriandelSurface: Ariandel's own C. It's the tiled surface libmypaint paints into, plus the pixel kernels. It depends onCMyPaint.AriandelEngine: Swift. Brushes, tiles, layers, documents, undo, files, rendering.
Two more targets ride along: ariandel-icon, a small executable that paints the app's icon with the engine itself, and the test target. The package targets iOS 18 and macOS 15, so swift test on a Mac is the fast engine suite. If SwiftPM targets are new to you, Swift Quest covers packages from the ground up.
Why Vendor at All
There were three ways to bring libmypaint in: a git submodule, a package fetched at build time, or the files copied into the repository. Two facts about the upstreams decided it.
- Neither upstream ships a Swift package. libmypaint builds with autotools (
configure.ac) and json-c with CMake. SwiftPM runs neither. Somebody has to run upstream's generators once (libmypaint'sgenerate.pyfor two settings headers, json-c's CMake configured for iOS for three more), and those outputs have to live somewhere. A submodule can't hold extra files without going dirty, and there's no Swift package to fetch. - A shipping app should build from what's in its repository. With libmypaint's and json-c's bytes in the tree, the engine's C builds from the tree alone: no network fetch at build time, no upstream force-push, no moved tag. Every byte that compiles into the app shows up in the history, and an upstream update becomes one reviewable commit.
The cost is real: you own updates. The answer to that cost is a recipe written down beside the files, which comes up in a minute.
Three Kinds of File
"Vendored byte for byte" doesn't mean every file came from upstream untouched. It means every file is honestly one of three kinds, and a PROVENANCE.md in each vendored target says which.
libmypaint (CMyPaint) | json-c (CJSONC) | |
|---|---|---|
| Copied | The sixteen library sources and their private headers, include/, fastapprox/, and glib/mypaint-brush.c (compiled only through mypaint-brush.c's #include) | Twelve library sources, their private headers, the public headers |
| Generated | brushsettings-gen.h and include/mypaint-brush-settings-gen.h, by upstream generate.py at the same commit | json.h, json_config.h, and config.h, by upstream CMake for iOS |
| Local | config.h, three defines | One guard inside the generated config.h: HAVE_ENDIAN_H only under __has_include(<endian.h>), since the iOS SDK has that header and the macOS SDK doesn't |
Note what the third row isn't: a fix to libmypaint's behavior. Local files configure a build. They never change what the engine does.
Proving It
"Unmodified" is only worth something if a machine can check it. The check is almost embarrassingly simple: for each vendored file, ask git for the same path at the recorded commit and cmp the two. One wrinkle: public headers sit in include/ here but at the upstream root, so the path has to be mapped back.
The sketch walks the whole vendored directory: every file at every depth except PROVENANCE.md itself, because a patch hides in a subdirectory as easily as at the top. Run against both targets on 2026-10-12, it found 51 identical libmypaint files (35 at the root and in include/, 15 under fastapprox/ and glib/, and COPYING) and 35 identical json-c files (34 plus COPYING). Each target had exactly three files the upstream commit doesn't contain, and they're exactly the generated and local files its PROVENANCE.md lists. The sketch makes both conditions part of its exit status. You pass it the files PROVENANCE.md declares, and it fails on any byte difference, on any other file the commit doesn't have, wherever in the tree it sits, and on a clone or commit that isn't there at all. That last guard matters more than it looks: without it, a wrong commit makes every file "not upstream", nothing differs, and the check passes. Running generate.py at d5a88fb reproduced both settings headers byte for byte. And git log agrees from a different angle: of 167 commits in the repository, exactly one has ever touched either vendored directory, the one that put them there.
The Rule and the Recipe
The libmypaint PROVENANCE.md ends with the whole policy in four sentences: "Never edit these files. A behavior libmypaint lacks belongs in Sources/CAriandelSurface or Swift. To update, copy the same file set from a newer upstream commit, regenerate the two headers, run swift test, and record the new commit here." json-c's adds one step: re-apply the HAVE_ENDIAN_H guard. That's the price of vendoring, paid in advance and written down where the next updater will trip over it.