Picking the CoreFoundation source for the launchd-842 launchctl port. Apple's launchctl.c is 4,549 LOC of CF-heavy code. Three candidate implementations are on the table: gnustep/libs-base, gnustep/libs-corebase, and swiftlang/swift-corelibs-foundation. This spike audits each repo against the exact symbol set launchctl.c uses and produces a buy/build/skip decision per candidate, with a concrete porting plan for the winner.
Companion to the libxpc Foundation spike (which scoped Foundation at the project level) and the launchd-842 porting plan (which schedules launchctl as task I1e). This spike is narrower: it's specifically about which CF source launchctl.c compiles and links against on FreeBSD.
launchctl.c actually needsThe launchd daemon itself (/sbin/launchd, 235 KiB ELF) was the last big I1c milestone — it builds, links, execs, smoke-passes. Audit of the seven hand-written daemon TUs (launchd.c + core.c + runtime.c + ipc.c + log.c + kill2.c + ktrace.c) finds zero CoreFoundation calls: the daemon reads plists through the lower-level launch_data_t API in liblaunch. So nothing about the daemon depends on this decision.
The downstream consumer that does depend on it is launchctl (the user-facing CLI). support/launchctl.c in the launchd-842 vendor drop is 4,549 LOC, heavily plist-driven, and pulls in <CoreFoundation/CoreFoundation.h>, <CoreFoundation/CFPriv.h>, and <CoreFoundation/CFLogUtilities.h>. The default plan was "port launchctl after the GNUstep stack lands"; this spike re-examines that with actual symbol-level evidence.
Why now and not later: "let's port launchctl with a minimal CF shim and patch the gaps as they surface" is a viable plan only if the gaps are small. A 245-grep on launchctl.c for CF* identifiers fueled an early fear that the gap was large. This spike puts a precise number on it: 49 distinct CF function calls, 1 CF SPI symbol, total surface ~90 identifiers including types and constants. That's small enough that the choice of CF source matters a lot — one candidate with broken plist parsing is fatal, but a different one with 48-of-49 coverage is essentially done.
launchctl.c actually needsFull audit at launchctl_api_audit.md in the working tree. Grep methodology in §12. Summary:
| Bucket | Distinct symbols | Treatment for FreeBSD port |
|---|---|---|
| Public CF API — functions | 48 | Must be present in the CF source we pick. |
| Public CF API — types | 17 | Standard CF*Ref typedefs; in any CF header. |
Public CF API — kCF* constants | 25 | Allocators, booleans, compare, number types, plist mutability, callbacks — in any CF header. |
CF SPI (<CFPriv.h> + <CFLogUtilities.h>) | 1 | Only _kCFSystemVersionBuildVersionKey. CFLog() is included but never called. The CFLogUtilities.h include can be dropped from launchctl.c. |
| IOKit | 7 | Stub no-ops on FreeBSD. Both call paths (IOKitWaitQuiet in fsck/single-user, IORegistryEntryFromPath in do_bootroot_magic) are macOS-specific feature paths. |
| Mach-O | 1 | getsectiondata only inside #if TARGET_OS_EMBEDDED. Zero impact. |
<NSSystemDirectories.h> | 9 | Hand-rolled table replacement (4 paths + a domain bitmask). None of the CF candidates ship this header. |
The 49 distinct CF function calls (CF functions + macros, deduped) are the gold-standard "does this CF source compile launchctl" checklist:
CFArrayAppendValue CFArrayCreateCopy CFArrayCreateMutable
CFArrayGetCount CFArrayGetTypeID CFArrayGetValueAtIndex
CFBooleanGetTypeID CFBooleanGetValue
CFDataCreateWithBytesNoCopy CFDataGetBytePtr CFDataGetLength
CFDataGetTypeID
CFDictionaryAddValue CFDictionaryApplyFunction CFDictionaryCreateCopy
CFDictionaryCreateMutable CFDictionaryGetCount CFDictionaryGetKeysAndValues
CFDictionaryGetTypeID CFDictionaryGetValue CFDictionaryRemoveValue
CFDictionarySetValue
CFErrorCopyDescription
CFGetTypeID CFRelease CFRetain
CFNumberCompare CFNumberCreate CFNumberGetType
CFNumberGetTypeID CFNumberGetValue
CFPropertyListCreateFromStream CFPropertyListCreateFromXMLData
CFPropertyListCreateWithData CFPropertyListCreateXMLData
CFRangeMake
CFReadStreamClose CFReadStreamCopyError CFReadStreamCreateWithFile
CFReadStreamOpen
CFSTR
CFStringCompareWithOptions CFStringCreateWithBytes CFStringCreateWithCString
CFStringCreateWithCStringNoCopy CFStringGetCString CFStringGetLength
CFStringGetTypeID
CFURLCreateDataAndPropertiesFromResource CFURLCreateFromFileSystemRepresentation
CFURLWriteDataAndPropertiesToResource
The 25 kCF* constants are the standard set: kCFAllocatorDefault, kCFAllocatorNull, kCFAllocatorSystemDefault, kCFBooleanFalse, kCFBooleanTrue, kCFCompareEqualTo, the full kCFNumber*Type family (10 of them), kCFPropertyListImmutable, kCFPropertyListMutableContainersAndLeaves, kCFStringEncodingUTF8, kCFTypeArrayCallBacks, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks.
The 17 types are exactly the CF*Ref typedef set you'd expect (CFArrayRef, CFBooleanRef, CFDataRef, CFDictionaryRef, CFErrorRef, CFIndex, CFMutableArrayRef, CFMutableDictionaryRef, CFNumberRef, CFNumberType, CFPropertyListFormat, CFPropertyListRef, CFReadStreamRef, CFStringRef, CFTypeID, CFTypeRef, CFURLRef).
The one SPI symbol — _kCFSystemVersionBuildVersionKey from <CoreFoundation/CFPriv.h> — is referenced once at launchctl.c:4341 inside copySystemBuildVersion() to read /System/Library/CoreServices/SystemVersion.plist. It's literally CFSTR("ProductBuildVersion"). Even a CF source with no SPI surface can have this one constant pasted in.
Headline finding. launchctl.c is essentially a pure-public-CF client. The SPI surface is a single string constant. CFLogUtilities.h is included but unused. The IOKit/Mach-O/NSSystemDirectories surface is small enough to stub locally regardless of which CF candidate we pick.
repo github.com/gnustep/libs-base · LGPL/GPL · ~253k LOC across 221 .m files · cloned at /Users/jmaloney/Documents/launchd/libs-base · full audit at libs-base_audit.md.
libs-base is the GNUstep Foundation implementation — NSString, NSArray, NSDictionary, NSPropertyList, etc. It is the natural pair for libs-corebase and the right answer for any Foundation/NSXxx consumer.
Headers/CoreFoundation/ contains a single 130-line file (CFCGTypes.h) with CGFloat/CGPoint/CGSize/CGRect typedefs only — there so NSGeometry.h can share struct ABI with CG.CF*() C functions defined or exported anywhere in Source/ or Headers/. Grepping for CFTypeRef, CFRetain, CFGetTypeID, CFRuntime, CFPropertyList, CFString, CFDictionary, CFArray, CFData, CFNumber, CFRunLoop, CFBundle returns no hits.CF[A-Z]* identifiers in the tree are: CFCGTypes (above), four CFBundle* string keys looked up in info dicts, a comment in typeEncodingHelper.h explicitly saying NSRange and CFRange are not toll-free bridged, and one dead #ifndef GNUSTEP block in NSURL+GNUstepBase.m referencing CFURLCopyPath only when built against Apple's real CF.No __CFRuntimeBase head in NSObject, no CFTypeID registry, no CFAllocator plumbing. The GNUstep tradition is explicitly that NS and CF are not toll-free on this side. Apple-source code that hands an NSDictionary* across a "looks like a CFDictionaryRef" boundary would crash.
Broad and mature. NSPropertyListSerialization in Source/NSPropertyList.m (4,305 lines) handles XML v1.0, binary v1.0 (bplist00 — both GSBinaryPLParser and GSBinaryPLGenerator), OpenStep ASCII, plus GNUstep ASCII + GNUstep binary extensions. All of which is great for Foundation consumers and irrelevant to launchctl.
gnustep-make (tools-make) + libobjc2 + libffi + libxml2 + libicu + libgnutls + libdispatch (+ optional avahi/libxslt). Effectively the existing gershwin/GNUstep stack.
The side question raised in the audit — "could we build a small Objective-C shim that exposes CFFooBar() signatures wrapping libs-base NSXxx?" — is technically feasible but in practice is many person-weeks of mechanical glue, drags the full libobjc2 + libs-base + ICU + libxml2 + gnutls runtime into every launchctl invocation, and crucially is not toll-free with a real CoreFoundation, so any future handoff to Apple-source CF code would crash. Not pursued.
repo github.com/gnustep/libs-corebase · LGPL 2.1 · ~37k LOC (26k C + 2.4k ObjC + 7.9k headers) · HEAD c629a11 2026-04-26 pkgdemon merge; real activity ended 2021-09-30 · cloned at /Users/jmaloney/Documents/launchd/libs-corebase · full audit at libs-corebase_audit.md.
libs-corebase is the GNUstep CoreFoundation implementation — specifically a C-only CF API clone, the right shape for our problem on paper.
909 distinct CF* function definitions. The core data types (CFString/Array/Dict/Number/Date/Data/UUID/Set/RunLoop/Socket/Stream/Tree/BitVector/CharacterSet/Calendar/Locale/{Date,Number}Formatter/Error) are genuinely implemented with ICU backing. All 49 of launchctl's CF function calls listed in §2 have prototypes here.
This is fatal for launchctl. Source/CFPropertyList.c:
CFXMLPlistCreate at line 1263 is a return NULL; stub. There is no driver behind it.#if 0 block. Not compiled.{ }.launchctl's whole job is loading .plist job descriptions, which Apple ships as either XML or binary plist. Neither format reads through libs-corebase as it stands today.
No CFPriv.h, no CFLog, no CFLogUtilities.h. The _CF* underscore symbols present are GNUstep's own bridging SPI (_CFRuntimeRegisterClass, _CFBridgingRetain, etc.), not Apple's launchd SPI.
CFBundle.m is a 268-LOC pass-through to NSBundle.NSCF*.m bridge classes #import <Foundation/*>.FIXME stubs — not encouraging given that launchctl uses CFURLCreateDataAndPropertiesFromResource + CFURLWriteDataAndPropertiesToResource as its plist read/write path.CFNotificationCenter, CFMachPort, CFMessagePort, CFFileDescriptor, CFPreferences, CFPlugIn. (None of these are used by launchctl, but it indicates the surface gnustep-corebase decided not to cover.)gnustep-make + libobjc + libs-base + ICU + libm. libdispatch optional but recommended for CFRunLoop main-queue integration + CFSocket dispatch sources. Not standalone — pulls libs-base in via the NSCF bridge classes.
return NULL/empty bodies. Without working plist parsing, launchctl cannot load a single job. We would be writing the plist driver ourselves to make libs-corebase functional, which is most of the value of picking a real CF source in the first place.
That said: libs-corebase covers 909 CF functions, which is a much wider surface than launchctl needs. The breadth is real value if a project consumer outside launchctl ends up needing the secondary CF types (CFCalendar, CFDateFormatter, CFBag, CFBitVector, etc.). For launchctl specifically, the plist gap dominates.
repo github.com/swiftlang/swift-corelibs-foundation · Apache-2.0 · HEAD 0e20e4a 2026-05-13 (actively maintained, master up-to-date) · cloned at /Users/jmaloney/Documents/launchd/swift-corelibs-foundation · full audit at swift-corelibs-foundation_audit.md.
swift-corelibs-foundation is the open-source Foundation port originally for Swift on Linux. The relevant part for us is Sources/CoreFoundation/ — a real CoreFoundation C implementation derived from Apple's CF-Lite drops, with Swift refcount hooks added on top.
Sources/CoreFoundation/ ships 78 standalone CF*.c files, ~97,738 LOC total. Highlights for the launchctl-relevant subset:
| File | LOC | Notes |
|---|---|---|
CFString.c | 7,946 | Full mutable/immutable string, encodings, formatting, transforms. |
CFURL.c | 5,494 | Complete CFURL impl. |
CFPropertyList.c | 3,288 | XML + OpenStep + binary plist driver. Real, not stubbed. |
CFBinaryPList.c | 1,815 | Real Apple binary plist reader+writer (bplist00). |
CFOldStylePList.c | 760 | OpenStep / NeXT plist parser. |
CFRuntime.c | 1,867 | The CF class/refcount runtime. |
CFNumber.c / CFArray.c / CFDictionary.c / CFData.c / CFSet.c | 1,322 / 1,039 / 437 / 870 / 339 | Standard containers + Number. |
CFUtilities.c | 1,667 | CFLog, CFShow, hashing. |
CFReadStream/CFStream.c | 1,977 | CFReadStream/CFWriteStream + dispatch. |
CFBundle.c + 11 CFBundle_*.c | 1,925 + ~7k | Full bundle impl. |
These are not stripped-down — file headers say "Copyright (c) 1998-2019, Apple Inc." with Swift runtime hooks added. The plist code in particular is the real Apple driver, exercised by the Swift-side plutil(1) reimplementation included in the same repo.
915 distinct CF* function definitions with real bodies in Sources/CoreFoundation/*.c. Cross-checked against launchctl's 49-function list: 48 of 49 present. The one missing symbol is CFPropertyListCreateFromFile — a deprecated thin wrapper. launchctl can substitute CFReadStreamCreateWithFile + CFPropertyListCreateFromStream (both present); one-line patch.
The full Apple-style "private but exposed" surface ships:
CFPriv.h — 34 KB, 138 visible declarations. File header: "APPLE SPI: NOT TO BE USED OUTSIDE APPLE! *or swift-corelibs-foundation".CFLogUtilities.h — declares CFLog, CFLog1, and the full kCFLogLevel* set. Real impl in CFUtilities.c (__CFLogCString / __CFLogCStringLegacy → asl_set/asl_send on Darwin, syslog on Linux/BSD).CFBundlePriv.h (14 KB), CFURLPriv.h (67 KB), CFStreamPriv.h, CFAttributedStringPriv.h, CFCalendarPriv.h, CFCharacterSetPriv.h, plus per-type *_Private.h for DateFormatter / Error / Locale / Number / PropertyList / String._CF*-prefixed function definitions in the .c files.This is essentially the same SPI launchd-842 was written against. Both CFPriv.h and CFLogUtilities.h are header-and-impl complete.
Both XML and binary, both read and write, all with real implementations:
CFPropertyList.c (3,288 LOC). Validates against the Apple DTD, emits the canonical <?xml ...><!DOCTYPE plist ...> header, supports kCFPropertyListXMLFormat_v1_0.CFBinaryPList.c (1,815 LOC). Real bplist00 format support including the trailer + offset table, integer/real/data/date/UID encoding.CFOldStylePList.c (760 LOC). Read-only; writes return an error and log a message.CFPropertyListCreateWithData, CFPropertyListCreateWithStream, CFPropertyListCreateFromStream, CFPropertyListCreateFromXMLData, CFPropertyListCreateData, CFPropertyListCreateXMLData, CFPropertyListCreateDeepCopy, CFPropertyListIsValid, CFPropertyListWrite, CFPropertyListWriteToStream. Format auto-detection works on read.This is the gotcha. CF source ships as a C-only static library on disk, but the build and the runtime are wired to Swift in three places:
Sources/CoreFoundation/internalInclude/CoreFoundation_Prefix.h:10 hard-defaults DEPLOYMENT_RUNTIME_SWIFT to 1.CMakeLists.txt:181-194 further asserts -DDEPLOYMENT_RUNTIME_SWIFT -DCF_BUILDING_CF -fcf-runtime-abi=swift -fblocks -fconstant-cfstrings.Sources/CoreFoundation/CMakeLists.txt:118-121 links _FoundationICU (fetched via CMake from swift-foundation-icu) and dispatch.With DEPLOYMENT_RUNTIME_SWIFT=1 the CF C runtime takes the Swift-RC path: _CFRetain() becomes swift_retain((void *)cf), _CFRelease() becomes swift_release(void *), init calls __CFInitializeSwift(), and CF class table slots get their ObjC class from __CFSwiftGetBaseClass(). None of those symbols are defined in Sources/CoreFoundation/ — they come from Sources/Foundation/ (Swift) and from libswiftCore.so.
The legacy non-Swift refcount path is fully present in the source — the long bitfield CAS loop in CFRuntime.c:1430-1509 — but is #if'd out by the SwiftRT macro. To build standalone C-only we need to:
DEPLOYMENT_RUNTIME_SWIFT=0 before the prefix header is processed (patch CoreFoundation_Prefix.h:11),-fcf-runtime-abi=swift from compile flags,_FoundationICU and (optionally) dispatch from target_link_libraries, and either provides them from system or drops the .c files that need them. ICU consumers: CFLocale, CFCalendar, CFDateFormatter, CFNumberFormatter, CFStringTransform, CFRegularExpression, CFCharacterSet, CFStringEncodingDatabase, CFICUConverters. None of those are referenced by launchctl, so the relevant .c files can be dropped entirely.The upstream build does not expose a "C-only" target. We have to maintain our own build wrapper. Estimated effort: about a day of CMake/Makefile work plus a small patch series on the prefix header. The legacy non-Swift code path itself is intact upstream and tested — we are flipping a knob, not reviving abandoned code.
CFRunLoop, CFStream, CFSocket. We have it via gershwin (per gershwin_provides memory)..c files can be dropped entirely from the build target._CFXMLInterface module (which we don't need; the launchd plist parser is hand-rolled in CFPropertyList.c).CFPriv.h and CFLogUtilities.h with real impls, and stays under Apache 2.0. Cost: about one engineer-day of build-system surgery (fork prefix header, write a CMake/Makefile target that emits libCoreFoundation.so, drop ICU-only sources). After that, launchctl ports against a real Apple-shape CF.
| Capability | libs-base | libs-corebase | swift-corelibs-foundation |
|---|---|---|---|
| CF C function definitions | 0 | 909 | 915 |
| Coverage of launchctl's 49 CF function calls | 0 of 49 | 49 of 49 (decls) | 48 of 49 (real impl) |
| XML plist read | via NS | return NULL | real (3,288 LOC) |
| XML plist write | via NS | partial | real |
| Binary plist read | via NS | #if 0 | real (1,815 LOC) |
| Binary plist write | via NS | empty {} | real |
CFPriv.h | no | no | 138 decls |
CFLogUtilities.h + CFLog impl | no | no | yes |
| Standalone-buildable today | yes (NS only) | yes | needs prefix-header fork + build wrapper (~1 day) |
| License | LGPL/GPL | LGPL 2.1 | Apache 2.0 |
| Upstream activity | active | 2021-09-30 (one merge 2026-04) | 2026-05-13, active |
| Build deps (minimum) | gnustep-make + libobjc2 + libffi + libxml2 + ICU + libgnutls + libdispatch | gnustep-make + libobjc + libs-base + ICU + libm | libdispatch + libBlocksRuntime (ICU optional — drop for launchctl) |
| Fit for launchctl | no (no CF surface) | no (plist stubs) | yes |
.c files dropped.
The matrix collapses to one viable answer. libs-base contributes nothing on the CF axis. libs-corebase has the right shape but the plist code paths are stubs — and plist parsing is launchctl's primary job. swift-corelibs-foundation's CF source carries the real Apple plist driver, ships the SPI surface launchctl-842 was written against, and stays under Apache 2.0.
The Swift-runtime entanglement is real but bounded: it's a knob (DEPLOYMENT_RUNTIME_SWIFT) the codebase still respects. We flip it, drop ICU-only sources, and write our own build target.
Concrete steps, ordered. Each step is a CI-checkpointable milestone.
Copy Sources/CoreFoundation/ + Sources/CoreFoundation/include/ + Sources/CoreFoundation/internalInclude/ + uuid/ from ../swift-corelibs-foundation into src/libCoreFoundation/ in the freebsd-launchd-mach repo. Match the libdispatch precedent: vendored source, repo-local patches, no submodule.
Originally proposed: strip the nine ICU-consuming .c files (CFLocale*, CFCalendar, CFDateFormatter, CFNumberFormatter, CFStringTransform, CFRegularExpression, CFCharacterSet, CFStringEncodingDatabase, CFICUConverters). Revised after discussion: keep them, install ICU instead. Reasons:
libgnustep-base needs ICU anyway — it's already in our dep graph for the desktop track.Build dep added: libicuuc + libicui18n + libicudata + their -dev headers (via FreeBSD devel/icu port or pkgbase if available). Add to buildpkgs.txt / pkglist.txt. Runtime cost on the ISO: ~30 MiB across the three libs.
The only subdir we still drop is src/libCoreFoundation/BlockRuntime/ — that's the WASI fallback for systems without a real libBlocksRuntime. We already ship /usr/lib/system/libBlocksRuntime.so via the libdispatch build (per the post-2026-05-13 bundling decision), so the upstream fallback is redundant.
Patch internalInclude/CoreFoundation_Prefix.h:11 to set DEPLOYMENT_RUNTIME_SWIFT 0, or #undef + #define it explicitly. Confirm by inspection that CFRuntime.c's legacy bitfield-CAS path is now active and swift_retain/swift_release/__CFInitializeSwift/__CFSwiftGetBaseClass references are #else'd out.
Single src/libCoreFoundation/Makefile following the precedent set by liblaunch, libsystem_kernel, libxpc:
LIB=CoreFoundationSHLIB_MAJOR=6 (or whatever upstream uses; double-check)SRCS= the post-strip .c file listCFLAGS+= -DDEPLOYMENT_RUNTIME_C=1 -DDEPLOYMENT_TARGET_FREEBSD=1 -DCF_BUILDING_CF -fblocks -fconstant-cfstrings -I${.CURDIR}/include -I${.CURDIR}/internalIncludeLDADD+= -ldispatch -lBlocksRuntime -lpthread/usr/lib/system/libCoreFoundation.so.6 per the install-layout spike. Headers to /usr/include/CoreFoundation/.Following the precedent of step 3i (libxpc) and 3m (liblaunch): a new build.sh step (call it 3p or wherever) that make -C src/libCoreFoundation's the lib, ldconfig-hints it (already covered by /etc/ld-elf.so.conf — /usr/lib/system is on it), and builds a tiny test_corefoundation.c at /usr/tests/freebsd-launchd-mach/test_corefoundation that round-trips a small dict through CFPropertyListCreateData + CFPropertyListCreateWithData in both XML and binary modes. Marker: COREFOUNDATION-OK. Expect grammar entry in tests/boot-test.sh.
launchctl.c for the one missing symbolReplace the single CFPropertyListCreateFromFile call (line ~526 vicinity per earlier audit) with a two-liner: CFReadStreamCreateWithFile + CFPropertyListCreateFromStream. Delete the unused #include <CoreFoundation/CFLogUtilities.h>.
Mirrors the launchd daemon's Phase I1c flow: a src/launchd/support/Makefile using bsd.prog.mk, PROG=launchctl, SRCS=launchctl.c, BINDIR=/bin (per the install-layout spike — not /sbin; launchctl is on /bin). Link -lCoreFoundation -llaunch -lxpc -lsystem_kernel -lutil -lpthread. The non-CF shims (IOKit no-ops, NSSystemDirectories hand-roll, os/assumes.h reuse from the daemon build) live alongside.
Boot-time /bin/launchctl help — the no-IPC CLI path, parallel to the LAUNCHD-BUILD-OK approach for the daemon. Expected output: the usage banner. Marker: LAUNCHCTL-BUILD-OK.
One to two engineer-days for steps 8.1–8.5 (the CF library), plus an unknown but bounded amount for 8.6–8.8 (the launchctl shims for the IOKit/NSSystemDirectories surface). The latter is at most a small .c file of stubs — smaller than what we already wrote for the launchd daemon's libsystem_kernel stubs.
Outside the CF question, launchctl.c pulls in three small macOS-only surfaces. None of the three CF candidates ships these — they're handled locally in the launchctl port:
| Symbol | Call path | Treatment |
|---|---|---|
IOKitWaitQuiet | do_potential_fsck, single-user, crash-debug (lines 2153, 2219, 2481) | No-op stub. FreeBSD has no IOKit registry to quiesce. |
IORegistryEntryFromPathIORegistryEntryCreateCFPropertyIOObjectReleaseio_service_tkIOMasterPortDefaultkBootRootActiveKey | do_bootroot_magic (lines 4501-4513) | Stub do_bootroot_magic to a no-op or surround with #ifdef __APPLE__. There's no IODeviceTree:/chosen on FreeBSD. |
getsectiondata, single call site at line 1717 inside GetPropertyListFromCache(), which is wrapped in #if TARGET_OS_EMBEDDED. We don't define TARGET_OS_EMBEDDED on FreeBSD; the whole code path drops out. The #include <mach-o/getsect.h> itself needs the same guard. Zero ELF-equivalent work needed.
<NSSystemDirectories.h> (9 symbols)Sole consumer is load_and_unload_cmd() (lines 2631-2769) iterating /.../Library/LaunchAgents + LaunchDaemons per domain. Replace with a hand-rolled domain table:
static const struct {
unsigned int mask;
const char *path;
} _ns_dirs[] = {
{ NSUserDomainMask, "~/Library" },
{ NSLocalDomainMask, "/Local/Library" }, /* project policy: /Local replaces /Library */
{ NSNetworkDomainMask, "/Network/Library" },
{ NSSystemDomainMask, "/System/Library" },
};
Plus a one-function reimplementation of NSStartSearchPathEnumeration / NSGetNextSearchPathEnumeration against that table. ~30 lines of C.
<os/assumes.h>, <libinfo.h>, <bootfiles.h>, the BSM auditd header — all of these already have shims in src/launchd/freebsd-shims/ from the I1c launchd-daemon build. They get reused as-is.
Natural follow-up question, since gershwin already ships libgnustep-base and the GNUstep stack provides its own libgnustep-corebase with the same CF* API surface as the swift-corelibs CF we're recommending: do these two CoreFoundation implementations conflict if both are installed on the same system?
Answer: they coexist by design, provided no single binary links against both.
Each CF runtime is a separately-named ELF shared library with its own SONAME and its own type registry:
| Property | swift-corelibs CF | gnustep-corebase |
|---|---|---|
| Install path | /usr/lib/system/libCoreFoundation.so.6 | /System/Library/Libraries/libgnustep-corebase.so.0 (gershwin convention) |
| SONAME | libCoreFoundation.so.6 | libgnustep-corebase.so.0 |
| Consumers | Apple-source: launchctl, future configd, asl, notifyd, … | GNUstep apps + frameworks (toll-free with libgnustep-base) |
| Header root | /usr/include/CoreFoundation/ | gershwin's GNUstep tree (/System/Library/Headers/CoreFoundation/ or similar — gershwin's call) |
| Type registry | internal to libCoreFoundation.so.6 | internal to libgnustep-corebase.so.0 |
Each binary's DT_NEEDED records exactly one of the two library names. ldconfig finding both shared objects in different directories is harmless — the dynamic linker resolves DT_NEEDED libCoreFoundation.so.6 by name and goes to /usr/lib/system/; it doesn't see libgnustep-corebase.so.0 at all. The two libraries can sit on the same disk indefinitely without anything bridging them.
CoreFoundation is a typed runtime, not just an API surface. CFArrayGetTypeID() returns a number that comes from the loaded CF's type-registry init — assigned at first call, different between the two implementations. A CFArrayRef created by one CF is just a pointer to an internal struct; the other CF doesn't recognize it as an array (its type ID is different), and CFRelease against the wrong runtime would either crash or corrupt refcounts. Same goes for the per-type vtables: each CF binds CFArrayCreate's function pointer at init time to its own copy of the implementation, and the two copies do not share any state.
So the project-wide rule:
One CF per binary. No single executable or shared library links both libCoreFoundation (swift-corelibs) and libgnustep-corebase. Pick one per consumer based on which lane the consumer is in:
launchctl, configd, asl, notifyd, mDNSResponder, IPConfiguration, …) link -lCoreFoundation.-lgnustep-base -lgnustep-corebase (libgnustep-base toll-free-bridges to libgnustep-corebase for them).If a future need to mix surfaces shows up — e.g. a GNUstep app that wants to call into a CF-typed Apple library — the answer is not "link both CFs in the same process". The answer is one of: (a) the libgnustep-corebase side already provides the API, route through it; (b) the Apple-shape library exposes a non-CF entry point (the way launchd exposes launch_data_t, not CF); or (c) an explicit RPC boundary that does not pass CF objects across the link.
If both libs ever installed headers to the same /usr/include/CoreFoundation/, second-install wins and every translation unit compiled after that point picks up the wrong types. Different header roots prevent it:
/usr/include/CoreFoundation/{CoreFoundation,CFArray,CFDictionary,CFPropertyList,CFPriv,CFLogUtilities,…}.h — the canonical Apple-shape path./System/Library/Headers/ or $GNUSTEP_SYSTEM_HEADERS. As long as it's not /usr/include/CoreFoundation/, no conflict.Both .pc files (if pkg-config is used) get distinct names — libCoreFoundation.pc vs libgnustep-corebase.pc. Consumers ask for one or the other explicitly.
The port is unaffected by the question of whether gershwin's CF stack is installed. launchctl links -lCoreFoundation from /usr/lib/system/ and compiles against headers from /usr/include/CoreFoundation/. It has no knowledge of gershwin's GNUstep stack and doesn't need to. Installing gershwin on the same system puts libgnustep-corebase.so.0 on disk in a different directory; the two CoreFoundations coexist without colliding at link time or at runtime.
This is the same posture gershwin already takes with libdispatch: gershwin ships libdispatch through its own pkg, and we ship our own libdispatch via /usr/lib/system/libdispatch.so. The two installations don't fight because each binary's link line picks one and the other is invisible to it.
The obvious alternative: skip CoreFoundation entirely, rewrite each Apple system service we want to port (launchctl, configd, IPConfiguration, mDNSResponder, asl, …) to use GNUstep NSDictionary/NSArray/NSString in place of the CF equivalents. The project would then have one Foundation stack (GNUstep) for the entire userland.
This sounds clean. In practice it's worse than the swift-corelibs CF path by every meaningful axis. Four reasons:
For launchctl alone, CF is "the plist parser plus a few container helpers" — ~50 distinct functions per the audit. For the bigger Apple-source daemons further out on the roadmap, CF is the data model and the event model:
IPConfiguration (Apple's DHCP/IPv6 client, ~10k LOC in apple-oss-distributions/bootp): CFDictionaryRef as the per-interface configuration state, CFArrayRef for lease history + DNS server lists, CFStringRef for every key passed to configd, CFRunLoopRef as the main event loop, CFRunLoopTimer for DHCP lease renewals + RFC 3315 timers, CFSocketRef for raw DHCP/DHCPv6 packet I/O. The daemon's overall shape is "configure a few CFRunLoopSources, enter CFRunLoopRun, never return." Rewriting that to NS is rewriting the daemon.configd: similar story. The SCDynamicStore is a CFDictionaryRef-backed cross-process store; the notification mechanism is CF-shaped. Plugin loading uses CFBundle/CFPlugIn.mDNSResponder: portable C core, with platform integration that pulls CF on Darwin (CFRunLoop, CFSocket, CFNetService advertisement). The Linux build of the same upstream avoids CF, so this one is the least CF-entangled — but the path of least resistance still uses the platform-Apple variant.asl_object_t) is CF-toll-free-bridged to CFTypeRef on Darwin."Strip CF and rewrite to NS" for IPConfiguration is a multi-month rewrite. For configd it's larger still.
Even setting the daemon internals aside, configd exposes its surface as SystemConfiguration.framework — a CF-typed cross-process store API. Every Apple-source binary that asks the system "what's my hostname?" / "am I on a captive portal?" / "what are my network interfaces?" (and that's a lot of them: mDNSResponder, network setup helpers, security extensions, GUI network prefs equivalents) talks SystemConfiguration in CF terms.
Rewriting configd to use NS internally means rewriting the public API too, at which point every consumer of SystemConfiguration stops compiling against our fork. We'd be maintaining a divergent fork of every consumer for as long as the project exists. That's the spiral the libxpc plan explicitly warned against early.
| Path | One-time cost | Per-daemon cost | Cost on each upstream refresh |
|---|---|---|---|
| swift-corelibs CF | ~1 day to vendor + strip ICU + flip Swift macro + write Makefile | only the daemon-specific platform shims (mach, kqueue, BSD socket dialect, …) | re-vendor; daemon source remains upstream-compatible |
| Rewrite to GNUstep NS | (nothing — GNUstep already exists) | weeks-to-months per daemon (CFDictionary/CFArray/CFString/CFRunLoop/CFSocket replacement throughout) | re-apply the entire NS rewrite to every upstream refresh; binary divergence grows over time |
The first row is engineering done once. The second row is engineering done forever, against a moving upstream target, with no path back to "just compile the official source" if the rewrite gets behind.
The project's internal scope-split memo, written before this audit, says:
swift-corelibs CF, deployed for system services only and never linked by apps (per §10's one-CF-per-binary rule), sits cleanly inside the Apple-source-system-services lane. It's the engine the daemons need; it's not a framework apps would link against. The split holds.
Bottom line. The CF-engine path is the one that uses the upstream's work. The NS-rewrite path is the one that replaces it. For a project sized to "get launchd-adjacent plumbing working on FreeBSD," using the upstream is the only viable plan. GNUstep stays exactly where it was — the right answer for the framework/app layer, not blocking the system-services layer.
The original plan was: port libobjc2 → tools-make → libs-base → libs-corebase, then launchctl. This spike says none of those are launchctl-blocking — and once we follow the reasoning in §11, none of them are configd-blocking, IPConfiguration-blocking, mDNSResponder-blocking, asl-blocking, or notifyd-blocking either. The Apple system-services layer is pure C with CF, by Apple's original design.
| Daemon / tool | Language | Foundation need | What it needs from us |
|---|---|---|---|
launchctl | C | CF | libCoreFoundation |
configd + SystemConfiguration.framework | C | CF + SystemConfiguration (also CF-shaped) | libCoreFoundation + a SystemConfiguration port |
IPConfiguration | C | CF + SystemConfiguration | libCoreFoundation + SystemConfiguration |
mDNSResponder | C | CF on Darwin only (Linux build is CF-free) | libCoreFoundation or the existing portable POSIX build path |
asl family (syslogd, syslog, libasl, aslmanager) | C | CF only for some logging API surface | libCoreFoundation |
notifyd + libnotify + notifyutil | C | none (Mach-port-driven) | nothing CF/NS at all — pure libxpc/libdispatch |
DiskArbitration + userland IOKit helpers | C | CF | libCoreFoundation |
The launchd-adjacent porting roadmap, post-swift-corelibs-CF decision:
libsystem_kernel + libdispatch + libxpc + liblaunch [done — Phase A–I1b]
↓
launchd daemon [done — Phase I1c]
↓
swift-corelibs CoreFoundation [in flight, task #16–20]
↓
launchctl [task #21–23]
↓
SystemConfiguration.framework [later — configd's API surface]
↓
configd [later]
↓ ↓ ↓
IPConfiguration mDNSResponder asl + notifyd DiskArbitration
That whole tree is C-with-CF. GNUstep does not appear in it.
NS*-using — the AppKit-equivalent apps, GUI plumbing, app frameworks. Parallel track to the system services, not blocked by them and not blocking them.Before this audit the plan was "GNUstep block then Apple-services block," conceptually a single critical-path queue. After the audit, the picture is two independent tracks:
Apple-source system services: libCoreFoundation → launchctl → configd → IPConf/mDNS/asl/…
(no GNUstep dep anywhere)
GNUstep / gershwin desktop: libobjc2 → tools-make → libs-base → libs-corebase → apps
(no Apple-source-service dep)
If gershwin-developer already provides libobjc2 + libs-base (worth a single pkg search against the gershwin tree to confirm — the gershwin_provides memory confirms libdispatch is shipped; libobjc2/libs-base coverage TBD), the GNUstep track is largely "consume what's there." Either way, the two tracks run in parallel and don't block each other.
That's the headline take-away from this spike: you can ship a working FreeBSD with launchd + configd + IPConfiguration + mDNSResponder + asl + notifyd + DiskArbitration without ever building libobjc2 or libs-base, and then layer gershwin on top of that whenever the desktop work picks up.
The choice to flip DEPLOYMENT_RUNTIME_SWIFT=0 in our libCoreFoundation raises a natural question: does that close the door on shipping Swift programs (or the Swift toolchain itself) on FreeBSD later? Short answer: no. Swift gets its own lane, just like GNUstep got its own lane, and the three coexist by the same one-CF-per-binary discipline established in §10.
"Swift" splits into three things that are technically independent:
swift / swiftc compiler + REPL + Foundation/XCTest for building Swift code on the system. FreeBSD has a swift-lang pkg.libswiftCore.so + friends, needed by any program written in Swift. Comes from the same toolchain.NS* classes layered over a Swift-RC-mode CF. Distributed as libFoundation.so (Swift) which on Linux/BSD statically embeds the Swift-mode CF C library inside itself. There's no upstream-shipped standalone libCoreFoundation.so in Swift mode — it's an internal static archive of the Swift Foundation build.For Swift programs to work on the ISO we need (1) and (2) installable as a pkg. Item (3) ships with (1) when a Swift program imports Foundation.
Once Swift lands on the system, there are three CF implementations on disk:
| Lane | CF artifact on disk | SONAME | RC mode | Consumers |
|---|---|---|---|---|
| Apple-source system services | /usr/lib/system/libCoreFoundation.so.6 (ours) | libCoreFoundation.so.6 | legacy bitfield-CAS | launchctl, configd, IPConfiguration, mDNSResponder, asl, notifyd, DiskArbitration |
| GNUstep / gershwin desktop | /System/Library/Libraries/libgnustep-corebase.so.0 (gershwin) | libgnustep-corebase.so.0 | GNUstep refcount (toll-free with libgnustep-base NSObject) | GNUstep apps + frameworks |
| Swift runtime | statically embedded inside libFoundation.so (Swift); no standalone shared lib | n/a (private to libFoundation) | Swift ARC (swift_retain/swift_release) | Swift programs that import Foundation |
The Swift Foundation's CF is statically linked into libFoundation.so — it doesn't export a libCoreFoundation.so file at all, so there's nothing for our SONAME to collide with. Apple's own macOS does the same trick: Swift's Foundation bundles a Swift-RC CF internally, separate from the system CoreFoundation.framework.
The mechanism is identical to the GNUstep coexistence story in §10, just with one more lane:
DT_NEEDED picks exactly one of the three: libCoreFoundation.so.6, libgnustep-corebase.so.0, or libFoundation.so.CFTypeID registry, vtable, and refcount allocator. A CFDictionaryRef created in one is unrecognizable to the others — but CF objects never cross process boundaries (only byte-stream representations like plists and XPC messages do), so that's not a real cross-process problem.DT_NEEDED by name, so even with all three libs on the system, no binary loads more than one.Concrete picture for a future Swift-on-ISO sub-project:
swift-lang (or build it from swiftlang/swift source) to the pkgbase / pkglist. Installs /usr/local/bin/swift + /usr/local/lib/swift/freebsd/libswiftCore.so + libFoundation.so etc./usr/local/ as pkgs do — we don't relocate. Swift programs that link Foundation pick up libFoundation.so from there via the toolchain's rpath./usr/lib/system/libCoreFoundation.so.6 is untouched. launchctl still resolves CFArrayCreate to our build; a hypothetical my-tool.swift resolves it to its toolchain's bundled Swift-mode CF (inside libFoundation.so) via Swift's import-resolution.ldconfig on a system with both installed is harmless — the libs have different file paths, different filenames, and the linker is name-driven.A single binary that wants to be both an Apple-source-system-service (linking our libCoreFoundation) and a Swift program (linking Swift's libFoundation) is the one configuration that would break. Two CFs in one address space, two refcount conventions, two type registries — objects from one would crash when handed to the other.
This is a contrived combination — we don't ship anything that mixes those worlds — but the rule is the same as §10: one CF per binary, generalized now to three lanes. If a future use case forces "Swift code that needs to talk to a system service", the right answer is an RPC boundary (xpc / launchd / mach IPC), not double-linking.
Technically yes — the DEPLOYMENT_RUNTIME_SWIFT macro is a build-time choice, and the legacy code path being #else'd not deleted means we could flip it back. But:
libswiftCore.so at startup. That's dozens-of-MB of Swift runtime loaded into every launchctl / configd / IPConfiguration process. Defeats the original reason for picking non-Swift mode.libCoreFoundation.so.7 and re-link every system service. Painful churn.So — flipping back is technically possible, practically pointless. The non-Swift choice is forward-compatible because Swift programs come with their own bundled CF anyway; we never need our build to be Swift-aware.
Bottom line on Swift coexistence. Choosing DEPLOYMENT_RUNTIME_SWIFT=0 does not close the door on Swift on the ISO. Swift lives in its own lane with its own bundled CF (statically linked inside libFoundation.so); our libCoreFoundation lives in the system-services lane; GNUstep lives in the desktop/app lane. Three lanes, no shared SONAMEs to collide, same one-CF-per-binary discipline. Adding Swift later is straightforward and changes nothing about the libCoreFoundation we're building today.
This section lays out, decision by decision, what we're committing to (and what we're not) so the tradeoffs are visible before any of it lands as code. Six decisions feed this spike's plan, plus a closing audit of cross-cutting risks and what stays reversible.
Alternatives rejected: libgnustep-corebase (plist parser stubs — fatal); libgnustep-base (zero CF C surface); rewrite every Apple system service to NS (multi-month, per-daemon, permanent fork — see §11).
| Dimension | Impact |
|---|---|
| License | Apache 2.0. We ship the LICENSE file alongside the vendored source. No commercial-use restrictions; the only obligation is preserving copyright notices. |
| Disk footprint on ISO | /usr/lib/system/libCoreFoundation.so.6 ≈ 2-4 MiB stripped (estimate — will measure during §8). Headers at /usr/include/CoreFoundation/ ≈ 1 MiB. Total install ≈ 3-5 MiB. |
| Source tree on disk | src/libCoreFoundation/ ≈ 10 MiB (78 .c + headers + Unicode tables). One-time cost in the git history. |
| Maintenance burden | We carry a small patch series against upstream (Swift macro flip, BlockRuntime/ drop, Makefile addition). Re-vendoring a future upstream release is a recursive copy + patch re-apply. |
| Upstream drift risk | swift-corelibs CF is actively maintained (HEAD 2026-05-13). Upstream's primary code path is Swift mode; legacy non-Swift mode is #else'd but stays compileable. If upstream ever deletes the legacy branch, our patch-set grows. |
| Forward compatibility | Adding more CF coverage = drop fewer files. Removing CF coverage later = drop more files. Both are build-time toggles, not source rewrites. |
| Dimension | Impact |
|---|---|
| Per-process startup cost | Major win. Every system service that links libCoreFoundation avoids loading libswiftCore.so (>20 MiB) and the Swift runtime init. launchctl startup stays a small-binary exec + libxpc/libdispatch/liblaunch — no Swift in the address space. |
| RAM footprint per process | Saves ~30-40 MiB of Swift-runtime resident pages per CF-using daemon. Critical for the ISO's "boots in 256 MB" posture. |
| Build toolchain | No Swift compiler required. Build wrapper is plain clang + bsd.lib.mk. |
| ABI direction lock-in | One-way SONAME break to flip later. Swift-mode CFTypeRefs are not binary-compatible with non-Swift-mode ones (different refcount layout, different type-table init). If we ever rebuild in Swift mode, we bump to libCoreFoundation.so.7 and re-link every consumer. That's churn we can absorb if the need ever arises. |
| Risk: legacy refcount bugs | The bitfield-CAS refcount path is real code that ran on macOS for years pre-Swift, but upstream's CI today emphasizes the Swift path. We may hit edge cases the upstream doesn't notice. Mitigation: our smoke tests + the relatively narrow CF surface launchctl exercises catch most. |
| Reversibility | One-line revert of the prefix-header patch. Rebuild. Everything still compiles; we just gain a libswiftCore.so dep. So "flip back" is a build-system choice, not a code rewrite. |
| Effect on future Swift on ISO | None — Swift programs live in their own lane with their own bundled CF (see §13). Three lanes, three CFs, zero collision. |
/usr/lib/system/libCoreFoundation.so.6| Dimension | Impact |
|---|---|
| Consistency | Matches the install-layout spike's Apple-libsystem-family path. Sits alongside libsystem_kernel.so, libxpc.so, libdispatch.so, libBlocksRuntime.so, liblaunch.so.1. Anything reaching for "the Apple system stack" finds them together. |
| Header path | /usr/include/CoreFoundation/. Standard Apple-shape <CoreFoundation/CFFoo.h> resolves naturally. |
| ldconfig discovery | Already covered by /etc/ld-elf.so.conf (added in the 2026-05-15 ldconfig migration — /usr/lib/system is on the list). No additional rc.conf surgery. |
| No /usr/local violation | Per project rule. Confirmed. |
| SOVERSION | Set SHLIB_MAJOR=6 to match Apple's macOS CoreFoundation.framework versioning. Open question (see §15) on whether to match exactly or assign our own. |
| Dimension | Impact |
|---|---|
| Disk footprint | Adds ~30 MiB of ICU libs (libicuuc.so + libicui18n.so + libicudata.so). Largest single chunk is libicudata.so — the Unicode tables. |
| Build deps | Adds devel/icu (or pkgbase equivalent) and its -dev headers to buildpkgs.txt. |
| CF feature coverage | Full upstream coverage: CFLocale, CFCalendar, CFDateFormatter, CFNumberFormatter, CFStringTransform, CFRegularExpression, the Unicode-aware CFCharacterSet, encoding converters. Future configd/IPConfiguration ports don't need to wait for us to un-strip. |
| gershwin alignment | libgnustep-base's build already requires ICU. Once gershwin lands, ICU is on the system unconditionally. Installing it for libCoreFoundation costs zero marginal disk in that world. |
| Maintenance | Less local divergence from upstream — we don't maintain a strip-list that needs re-evaluating each release. Smaller patch series. |
| Risk: ICU ABI breaks | ICU SOVERSION bumps about once every two years and is mildly disruptive. We re-link against the new ICU when FreeBSD's pkg updates. Same risk every ICU consumer carries. |
src/libCoreFoundation/BlockRuntime/ subdir, use libdispatch's libBlocksRuntime| Dimension | Impact |
|---|---|
| Why the upstream ships it | The BlockRuntime/ subdir is upstream's fallback for systems without a system libBlocksRuntime.so — primarily WASI. Not for FreeBSD/Linux/Darwin, all of which have a real one. |
| What we use instead | /usr/lib/system/libBlocksRuntime.so built by our vendored swift-corelibs-libdispatch (per the 2026-05-13 "drop FreeBSD-libblocksruntime pkg, bundle via dispatch" decision). Same upstream Apple compiler-rt source as the CF subdir would have used — one canonical copy on the system. |
| Risk | If we ever swap the libdispatch source for a different upstream that doesn't bundle libBlocksRuntime, we lose our system-wide source. Mitigation: dispatch's libBlocksRuntime is small and stable; the source is in two places (our libdispatch vendor + this CF vendor we just dropped) and we could resurrect the CF subdir if needed. |
src/libCoreFoundation/, not as a submodule / external pkg| Dimension | Impact |
|---|---|
| Consistency with prior precedent | Matches what we did with libdispatch, libxpc, launchd-842. Single repo, vendored sources, patches in-tree, no submodules. |
| Build hermeticity | CI doesn't need to fetch external repos. Build is reproducible from a single git clone. |
| Re-vendor cost | Periodic cp -R from a fresh upstream clone + patch re-application. Manual but small; the patch surface is intentionally tiny (one prefix-header line + one Makefile). |
| Provenance tracking | src/libCoreFoundation/VENDOR file pins upstream SHA + date + license. Same shape as the existing libdispatch precedent. |
/sbin/launchd is PID 1 or a user-started daemon.libgnustep-base per the scope_split memory. swift-corelibs Foundation's Swift NS layer is not adopted.| Risk | Likelihood | Mitigation |
|---|---|---|
| Legacy non-Swift refcount path has latent bugs (Apple's CI deprioritizes it) | Low — the path is tested by other downstreams using DEPLOYMENT_RUNTIME_OBJC on Linux | Our COREFOUNDATION-OK smoke marker exercises a real plist round-trip. Bug surfaces show up as test failures or daemon crashes early. |
| Upstream deletes the legacy refcount branch entirely | Low (would break their own Objective-C/Linux build target) | If it ever happens, fork the prior good version. Our patch series is small enough to maintain indefinitely. |
| libdispatch or libBlocksRuntime ABI breaks under our libCoreFoundation | Low — we build both from the same vendored source against the same toolchain | CI catches it the same day; we own all the moving pieces. |
| ICU SOVERSION bump invalidates our libCoreFoundation | Medium (every ~2 years) | Rebuild against new ICU. Standard FreeBSD-ports churn. |
| plist parser hits Apple-format edge cases we don't notice | Medium for binary plists with rare types (UID, custom date encoding) | We inherit Apple's own driver via swift-corelibs; same code Apple's plutil uses. Bug surface is upstream's bug surface. |
SystemVersion.plist SPI symbol (_kCFSystemVersionBuildVersionKey) | Low — one constant, well-defined | Our libCoreFoundation's CFPriv.h already declares it (per the audit, 138 SPI decls including this one). No special handling needed. |
A single binary accidentally links both libCoreFoundation and libgnustep-corebase | Low — different SONAMEs, different install paths | The one-CF-per-binary rule in §10. CI ldd check could enforce it programmatically if we want extra paranoia. |
| Decision | Reversibility |
|---|---|
| swift-corelibs-foundation as the CF source | expensive Switching CF source forces re-linking every consumer (CFType registries differ). Doable but means revisiting launchctl + every downstream port. |
| DEPLOYMENT_RUNTIME_SWIFT=0 | SOVERSION bump Rebuild + libCoreFoundation.so.7 + re-link consumers. Source patches stay tiny. |
| /usr/lib/system install path | trivial Move file + ldconfig refresh + re-link. Path is encoded in Makefile. |
| Install ICU vs strip | trivial Build-time toggle. Strip later = drop files from SRCS. Install later = put them back. |
| Drop BlockRuntime/ subdir | trivial Restore the subdir from upstream, add to SRCS if needed. |
| Vendor in tree vs submodule | trivial History-rewrite or git-subtree split. |
| SOVERSION=6 (matching Apple) | consumer-visible Changing SOVERSION = every consumer re-link. |
Big-picture honesty. The biggest decision in this spike — "use swift-corelibs CF as the CF source" — is expensive to undo, but is well-grounded by the audit evidence (only candidate with working plist support, only one with the SPI surface). The remaining decisions are tactical build-system choices that can flex without major code disruption. The plan optimizes for "small, surgical patches against upstream + ICU/Swift/GNUstep as independent lanes" so the project has room to maneuver as later requirements show up.
pkg search resolves it. (The gershwin_provides memory confirms it ships libdispatch; libobjc2/libs-base coverage is unconfirmed.)libCoreFoundation.so: match Apple's libsystem_corefoundation? Or assign our own? Decide before consumers start linking against it./usr/include/CoreFoundation/CFPriv.h — do we expose the SPI surface to all consumers or hide it? The launchctl source needs it; nothing else does today.This spike is grounded in three pieces of audit work done 2026-05-15 against the cloned repos at /Users/jmaloney/Documents/launchd/{libs-base,libs-corebase,swift-corelibs-foundation}. Methodology was uniform across all three: identify every CF* function definition (function body, not header decl — the gold standard), enumerate CF SPI surface, audit PropertyList implementation specifically, identify build-system entanglements.
Extraction patterns for the launchctl-side inventory (run against launchd-842/src/support/launchctl.c):
grep -oE 'CF[A-Z][A-Za-z0-9_]*' launchctl.c | sort -u # CF identifiers
grep -oE 'k(CF|cf)[A-Za-z0-9_]*' launchctl.c | sort -u # kCF constants
grep -oE '_CF[A-Za-z0-9_]*' launchctl.c | sort -u # _CF SPI
grep -oE '\b_k[A-Za-z0-9_]*' launchctl.c | sort -u # _k SPI constants
grep -oE 'IO[A-Z][A-Za-z0-9_]*' launchctl.c | sort -u # IOKit
grep -oE 'k(IO|io)[A-Za-z0-9_]*' launchctl.c | sort -u # kIO constants
grep -oE '\b(getsect[A-Za-z0-9_]*|getseg[A-Za-z0-9_]*)' launchctl.c | sort -u
grep -oE '\bNS[A-Za-z0-9_]*' launchctl.c | sort -u # NS*
Junk filter: the IO[A-Z] and NS[A-Z] sweeps caught false-positive prefixes (IONARY, IORITY_REVISION from JETSAM_PRIORITY_REVISION, ION_AQUA, etc.). Hand-filtered to keep only real IOKit / NSSystemDirectories identifiers.
The 245-grep-count circulating earlier in the project's planning came from counting raw substring matches (every occurrence, including types and constants repeated across the file). The 49 number used in this spike is distinct function-call symbols, which is the meaningful unit for "does the CF source provide this?"
Raw audit documents (full per-symbol enumeration) are in the working tree:
/Users/jmaloney/Documents/launchd/launchctl_api_audit.md/Users/jmaloney/Documents/launchd/libs-base_audit.md/Users/jmaloney/Documents/launchd/libs-corebase_audit.md/Users/jmaloney/Documents/launchd/swift-corelibs-foundation_audit.mdSpike written 2026-05-15 against launchd-842 vendor drop in freebsd-launchd-mach at commit 0fae6a1 (LAUNCHD-BUILD-OK green). Audits performed against repo clones the same day; HEADs noted in each candidate section. Companion documents: the libxpc Foundation spike (project-level Foundation scoping), the launchd-842 porting plan (which scheduled launchctl as task I1e), and the install-layout spike (which puts launchctl at /bin/launchctl).