Four Targets, One Direction
Everything in this track lives in one SwiftPM package, AriandelKit, and its arrows point one way: CJSONC is used by CMyPaint, which is used by CAriandelSurface, which is used by AriandelEngine, which is used by the ariandel-icon executable and the tests. (AriandelEngine also depends on CMyPaint directly, because it calls mypaint_brush_* itself, and on the family kit's CwkKitCore.) The package exports exactly one product, the AriandelEngine library. The app, generated by XcodeGen, takes only that product from this package (its other links are the family kit's CwkKitCore, CwkKitMedia and CwkKitWeb), and no Swift file in the app imports a C module. C is something the engine has, never something the app sees.
The platforms are iOS 18 and macOS 15, and the macOS floor is not decoration: swift test on a Mac is the fast engine suite, where brushes, tiles, undo, documents and the renderer run with no simulator. Swift 6 language mode for the Swift targets and C11 for the C targets are set once, at the bottom of the manifest, so no target can quietly drift to another mode.
include/ Is the Public Face
A SwiftPM C target is a directory of .c files with an include/ subdirectory. Whatever sits in include/ is the target's public interface. If there is no module.modulemap there (Ariandel has none), SwiftPM writes one for you: an umbrella over the whole directory, which is what the generated map in the second block shows. That is how a single import CAriandelSurface in Swift sees all four public headers, and how this crosses the boundary:
- Functions arrive as Swift functions with C types:
intbecomesInt32,uint16_t *becomesUnsafeMutablePointer<UInt16>. - Nullability comes from the header.
ariandel_surface.hwraps itself in#pragma clang assume_nonnull, so the buffer inArdTileReleasearrives non-optional, whileArdTileAcquire's return, marked_Nullable, arrives optional. The Swift callbacks from lesson 2 match those signatures exactly. - Simple macros arrive as constants:
Tile.sizeisInt(ARD_TILE_SIZE). - A struct declared but never defined arrives as
OpaquePointer, which isBrushSurface.handle. - A function-pointer typedef arrives as a
@convention(c)function type, which is why a capture-freefunccan be passed straight in.
Public headers may include their dependencies' public headers: ariandel_surface.h includes mypaint-surface.h, found because CMyPaint is a dependency. And one header is there purely for layout: ariandel_shader_types.h declares the Metal buffer structs in C so Swift gets C layout, the shader source repeats them field for field, and the package suite pins the C side of all three structs, their strides (40, 112 and 144 bytes) and key field offsets, with MemoryLayout, at the numbers the shader expects. That catches drift on the C side only. The test never reads the shader source, a Swift string compiled at run time, so an edit to the Metal copy alone still gets past it; keeping that copy in step is left to whoever edits it.
cSettings, and the Headers That Must Stay Private
Both vendored libraries include a config.h, unconditionally. libmypaint's is Ariandel's own three lines (no glib, the package names); json-c's is upstream's CMake output with one guard added. Each sits at its target's root, outside include/, reached by .headerSearchPath("."), which applies only while compiling that target's own files. The house gotcha says why the placement is a rule: the two are different files in different targets, and putting either in a public include/ would make it part of the module, so Swift would import the wrong macros. Everything in include/ is a promise to every importer; a build configuration is not.
CMyPaint also sets .define("HAVE_CONFIG_H"), the autotools convention. Read the vendored code before trusting it, though: at this upstream commit no file in either library tests that macro, and config.h is included regardless. The search path is what does the work; the define is a harmless habit. (The sketch below guards its own include with it, so you can see the convention working where a source does test it.)
The exclude lists keep non-sources out of the target: COPYING and PROVENANCE.md stay beside the code they describe, upstream's fastapprox/Makefile.am.local belongs to a build system Ariandel does not use, and glib/mypaint-brush.c is a fragment that mypaint-brush.c pulls in with #include, so it must never be compiled as a file of its own.
Generated, Never Hand-Edited
Some headers in the vendored targets are generated, not copied. libmypaint's brushsettings-gen.h (private) and include/mypaint-brush-settings-gen.h (public) come from upstream's generate.py run over brushsettings.json at the same commit; json-c's json.h and json_config.h come from upstream's CMake configured for iOS. Each target's PROVENANCE.md records the exact command. Treat them like the vendored code they describe: regenerate from the recorded commit, never patch by hand. Track 1 tells the vendoring story in full.
An Executable That Paints Its Own Icon
ariandel-icon is a small command-line target: swift run ariandel-icon <brushes-dir> <out.png>. It loads MyPaint brushes from the directory it is given (the CC0 set the app bundles) and paints what its header comment calls "A night of the Painted World, one copper-red stroke (Pippa's hair) and falling snow", through the same Brush, BrushSurface and CanvasRenderer.flatten the app uses. A fixed-seed generator places the snow, so the icon is identical on every run. It then redraws the picture without an alpha channel, because App Store icons may not carry one (ITMS-90717).
Why make the icon a target rather than an asset someone drew once? It runs the whole engine end to end on a Mac, so a broken brush path shows up as a broken icon. It makes the icon reproducible from source. And it is honest dogfood: each of its strokes starts with PencilMapping.strokeStartInterval as the first deltaTime, the same constant the app feeds libmypaint when a real pen lands (track 4). The manifest lists only the library as a product, and swift run still works, because SwiftPM can run any executable target in the root package.
include/ and keep everything else (configuration, private generated headers, fragments, licenses) out of it. Then let exactly one Swift target stand between the C and everyone else, so the rest of your code imports Swift and never learns there was C underneath.