Skip to content
C.W.K.
Stream
Lesson 04 of 04 · published

Two Config Headers

~15 min · c, swiftpm, header-search-path, licensing

Level 0Blank Canvas
0 XP0/37 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Two Files Called config.h

Both vendored libraries expect a private config.h, and the two are nothing alike. libmypaint's is three lines Ariandel wrote by hand (you'll see the whole file below). json-c's is upstream CMake's output for iOS: 236 lines and 64 #define directives, from HAVE_DLFCN_H down to the version string.

Here's the collision hiding in plain sight. libmypaint's file says #define PACKAGE "libmypaint". json-c's says #define PACKAGE "json-c". Same macro, two values, and neither file is wrong. Each one is a fact about building one library. The only way to get it wrong is to let one library see the other's facts.

libmypaint already draws this line itself. Its public tunables (MYPAINT_TILE_SIZE 64, maximum threads, mipmap levels) live in mypaint-config.h, a public header in include/. The private build switches live in config.h. The filename tells you the audience.

Where Each One Lives

Both files sit at their target's root, next to the .c files, and never in include/. That placement is the whole design, because in SwiftPM those two directories mean very different things:

  • A C target's include/ is its public face. It goes on the header search path of every target that depends on it, and it becomes the Clang module that Swift code imports.
  • The target's root is private. .headerSearchPath(".") adds it to that target's own search path and to nobody else's.

The C preprocessor helps here too: the quote form, #include "config.h", looks first in the directory of the file doing the including, and only then down the -I list. A .c file sitting beside its own config.h finds it before any other path gets a vote. (CMyPaint also defines HAVE_CONFIG_H, the autotools convention. At d5a88fb libmypaint's sources include config.h unconditionally, so location is what actually decides which file they get.)

Ariandel's house sign states the rule as a gotcha: "libmypaint's config.h and json-c's config.h are different files in different targets; never put either in a target's public include/, or Swift imports the wrong macros."

Watching It Go Wrong

Rules like that are easier to keep once you've watched them break, so the sketch below builds the mistake on purpose. Two C targets, CJson and CPaint, with CPaint depending on CJson. Each has a config.h defining PACKAGE, and each exports one function that returns it. The script builds two layouts.

  • Leak (each config.h in its target's include/) prints json-c json-c. CPaint's own code compiled with json-c's value. No error, no warning.
  • Fixed (each config.h at its target root) prints libmypaint json-c.

Why does the leak go that way? On the toolchain I ran (Swift 6.4, Xcode 27), CPaint's compile line lists its dependency's public include/ first, then its own include/, then its own root. paint.c has no config.h beside it in the leak layout, so the -I order decides, and the dependency wins.

Swift sees it too. Add print(PACKAGE) to the Swift file: in the leak layout the build fails with "ambiguous use of 'PACKAGE'", because both modules export the macro. In the fixed layout it fails with "cannot find 'PACKAGE' in scope", which is exactly what you want. A private build fact should be invisible to Swift.

The demo's quiet version is the scary one. Nothing failed, the program ran, and it was wrong. Swap PACKAGE for a macro that switches a feature on and that same silence ships a different program.

The Search Path Is a Design Surface

Treat include/ like an API, because SwiftPM does. Everything in it is promised to every dependent, the app's Swift included. Ariandel's own bridge shows the discipline: CAriandelSurface/include/ holds exactly four headers (ariandel_surface.h, ariandel_kernels.h, ariandel_pixels.h, ariandel_shader_types.h), each one something the Swift engine is meant to call or share. Build facts stay home.

Three Licenses, No GPL Code

  • libmypaint: ISC. Permissive: use, copy, modify, distribute, as long as the notice comes along. Its COPYING sits in the vendored directory.
  • json-c: MIT. The same spirit, the same kind of notice.
  • Brushes: CC0. The app bundles the mypaint-brushes library at 08da4a4: 196 presets across seven sets, each with its preview, plus order.conf, 393 files byte-identical to upstream.

The brushes deserve a second look, because their license file contains the letters GPL. It's upstream's machine-readable copyright file, in Debian's DEP-5 format, copied into the app as MYPAINT-BRUSHES-LICENSES.dep5. It opens with Files: * under GPL-2+, then Files: brushes/* under CC0-1.0, then the per-author brush directories, also CC0. DEP-5's rule is that the last stanza matching a file applies to it. So every preset and preview Ariandel ships is CC0, and the catch-all GPL-2+ covers upstream's build scripts, which were never copied. The vendored libmypaint and json-c files don't mention the GPL anywhere. The exclude lists in Package.swift keep COPYING and PROVENANCE.md out of the compile (they're documents, not sources) while leaving them beside the code they cover.

A header's directory is a statement about its audience. include/ means "every dependent, and Swift"; the target root means "this target only." Put each fact where its audience is, and two libraries can carry the same macro name without ever meeting.

Code

Ariandel's Sources/CMyPaint/config.h, the whole file·c
#define MYPAINT_CONFIG_USE_GLIB 0
#define PACKAGE "libmypaint"
#define GETTEXT_PACKAGE "libmypaint"
Trimmed from Ariandel's Sources/CJSONC/config.h: the one local guard, and the macro the two files share·c
/* Define to 1 if you have the <endian.h> header file. */
#if __has_include(<endian.h>)
#define HAVE_ENDIAN_H
#endif

/* ... 160-odd lines of CMake output (trimmed) ... */

/* Name of package */
#define PACKAGE "json-c"
A minimal sketch: build the leak and the fix side by side (prints "json-c json-c", then "libmypaint json-c")·bash
#!/bin/bash
# Two C targets, each with a private config.h that defines PACKAGE.
#   leak:  config.h sits in each target's public include/
#   fixed: config.h sits at the target root, beside its .c file
build() {
  local dir=/tmp/config-$1 json=Sources/CJson paint=Sources/CPaint
  rm -rf "$dir"; mkdir -p "$dir"/Sources/{CJson,CPaint}/include "$dir"/Sources/app; cd "$dir"
  cat > Package.swift <<'SWIFT'
// swift-tools-version:6.0
import PackageDescription
let package = Package(name: "ConfigDemo", targets: [
    .target(name: "CJson", path: "Sources/CJson", cSettings: [.headerSearchPath(".")]),
    .target(name: "CPaint", dependencies: ["CJson"], path: "Sources/CPaint",
            cSettings: [.headerSearchPath(".")]),
    .executableTarget(name: "app", dependencies: ["CPaint"], path: "Sources/app"),
])
SWIFT
  [ "$1" = leak ] && { json=$json/include; paint=$paint/include; }
  echo '#define PACKAGE "json-c"'     > $json/config.h
  echo '#define PACKAGE "libmypaint"' > $paint/config.h
  echo 'const char *cjson_name(void);'  > Sources/CJson/include/json_api.h
  echo 'const char *cpaint_name(void);' > Sources/CPaint/include/paint_api.h
  printf '#include "config.h"\nconst char *cjson_name(void) { return PACKAGE; }\n'  > Sources/CJson/json.c
  printf '#include "config.h"\nconst char *cpaint_name(void) { return PACKAGE; }\n' > Sources/CPaint/paint.c
  printf 'import CPaint\nimport CJson\nprint(String(cString: cpaint_name()), String(cString: cjson_name()))\n' > Sources/app/main.swift
  echo "== $1 =="
  swift build 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -E '\.c:[0-9]+:[0-9]+: error'
  [ -x .build/debug/app ] && .build/debug/app
}
build leak
build fixed

External links

Exercise

Take a Swift package (yours, or a scratch one) with at least one C target. List every header in each C target's include/ and, for each one, answer one question: is this promised to every dependent and to Swift? Move anything that isn't (config headers, private helpers) to the target root and add .headerSearchPath("."). Then run the sketch above and change it: give the two config.h files a second macro, say FEATURE_X, defined in only one of them, and predict before you build which layout lets it leak into the other target.
Hint
Each layout's prediction follows from two rules: the quote form looks beside the including file first, and a dependency's include/ is on your compile line, ahead of your own. Write down which file each .c will find, then build and check. If Swift can name the macro, it's public, whatever you meant.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.