이름이 같은 파일 둘
벤더링한 두 라이브러리는 각자 비공개 config.h를 기대해. 근데 둘은 생판 달라. libmypaint 쪽은 Ariandel이 손으로 쓴 세 줄이야(아래에 파일 전체가 있어). json-c 쪽은 업스트림 CMake가 iOS용으로 뽑아낸 결과물이고. 236줄에 #define 지시문이 64개, HAVE_DLFCN_H부터 버전 문자열까지 다 들어 있어.
충돌은 눈앞에 뻔히 있는데 잘 안 보여. libmypaint 파일엔 #define PACKAGE "libmypaint"가 있고, json-c 파일엔 #define PACKAGE "json-c"가 있어. 매크로는 같고 값은 둘. 어느 파일도 틀리지 않았어. 각자 라이브러리 하나를 빌드하는 데 필요한 사실일 뿐이야. 틀어지는 길은 하나뿐이야. 한 라이브러리가 다른 라이브러리의 사실을 보게 두는 것.
libmypaint는 이 선을 이미 스스로 그어놨어. 공개 조정값(MYPAINT_TILE_SIZE 64, 최대 스레드 수, 밉맵 단계)은 include/에 있는 공개 헤더 mypaint-config.h에 살아. 비공개 빌드 스위치는 config.h에 살고. 파일 이름이 누굴 위한 건지 말해줘.
각자 사는 곳
두 파일 다 자기 타깃 루트, .c 파일 바로 옆에 있어. include/엔 절대 안 들어가. 이 배치가 설계의 전부야. SwiftPM에서 그 두 디렉터리는 뜻이 완전히 다르거든.
- C 타깃의
include/는 공개된 얼굴이야. 그 타깃에 의존하는 모든 타깃의 헤더 검색 경로에 올라가고, Swift 코드가 import하는 Clang 모듈이 돼. - 타깃 루트는 비공개야.
.headerSearchPath(".")는 루트를 그 타깃 자신의 검색 경로에만 넣고, 다른 누구한테도 안 줘.
C 전처리기도 거들어. 따옴표 형식 #include "config.h"는 include하는 파일이 있는 디렉터리부터 먼저 뒤지고, 그다음에야 -I 목록으로 내려가. 자기 config.h 옆에 앉은 .c 파일은 다른 경로가 끼어들기 전에 자기 걸 찾아. (CMyPaint는 autotools 관례인 HAVE_CONFIG_H도 정의해. 근데 d5a88fb의 libmypaint 소스는 config.h를 조건 없이 include하니까, 어느 파일이 걸릴지는 결국 위치가 정해.)
Ariandel 하우스 사인엔 이게 함정 항목으로 적혀 있어. "libmypaint의 config.h와 json-c의 config.h는 서로 다른 타깃의 서로 다른 파일이다. 어느 쪽도 타깃의 공개 include/에 두지 마라. 그러면 Swift가 엉뚱한 매크로를 import한다."
망가지는 걸 직접 보기
이런 규칙은 한 번 깨지는 걸 봐야 지키기 쉬워. 그래서 아래 스케치는 일부러 실수를 만들어. C 타깃 둘, CJson과 CPaint가 있고 CPaint가 CJson에 의존해. 둘 다 PACKAGE를 정의하는 config.h가 있고, 그 값을 돌려주는 함수를 하나씩 내놔. 스크립트는 배치 두 개를 빌드해.
- 누수(각
config.h가 자기 타깃의include/에)는json-c json-c를 찍어.CPaint자기 코드가 json-c 값으로 컴파일된 거야. 에러도 경고도 없이. - 수정(각
config.h가 타깃 루트에)은libmypaint json-c를 찍어.
누수 쪽은 왜 그렇게 될까. 내가 돌린 툴체인(Swift 6.4, Xcode 27)에선 CPaint 컴파일 줄에 의존 타깃의 공개 include/가 제일 먼저 오고, 그다음 자기 include/, 그다음 자기 루트 순서야. 누수 배치에선 paint.c 옆에 config.h가 없으니까 -I 순서가 승부를 내고, 의존 타깃이 이겨.
Swift도 그걸 봐. Swift 파일에 print(PACKAGE)를 넣어봐. 누수 배치에선 "ambiguous use of 'PACKAGE'"로 빌드가 실패해. 두 모듈이 다 그 매크로를 내보내니까. 수정 배치에선 "cannot find 'PACKAGE' in scope"로 실패하는데, 이게 바로 원하는 결과야. 비공개 빌드 사실은 Swift한테 안 보여야 하거든.
무서운 건 데모에서 조용히 넘어간 쪽이야. 아무것도 안 깨졌고 프로그램도 돌았는데, 답이 틀렸어. PACKAGE 자리에 기능을 켜는 매크로를 넣으면, 똑같이 아무 소리 없이 다른 프로그램이 나가.
검색 경로는 설계 대상이야
include/를 API처럼 다뤄. SwiftPM이 그렇게 다루니까. 거기 든 건 앱의 Swift까지 포함해서 모든 의존자한테 한 약속이야. Ariandel 자체 브리지가 그 절제를 보여줘. CAriandelSurface/include/엔 헤더가 딱 넷(ariandel_surface.h, ariandel_kernels.h, ariandel_pixels.h, ariandel_shader_types.h) 있고, 전부 Swift 엔진이 부르거나 같이 쓰라고 둔 거야. 빌드에만 필요한 사실은 자기 집, 타깃 루트에 있어.
라이선스 셋, GPL 코드는 없어
- libmypaint: ISC. 관대한 라이선스야. 고지만 같이 따라가면 쓰고, 복사하고, 고치고, 배포해도 돼.
COPYING이 벤더링 디렉터리에 들어 있어. - json-c: MIT. 같은 정신, 같은 종류의 고지.
- 브러시: CC0. 앱엔
08da4a4시점의 mypaint-brushes 라이브러리가 들어 있어. 일곱 세트에 프리셋 196개, 각자 미리보기, 그리고order.conf. 파일 393개가 업스트림과 바이트까지 같아.
브러시는 한 번 더 들여다볼 만해. 라이선스 파일에 GPL이라는 글자가 있거든. 그건 업스트림의 기계 판독용 저작권 파일이고, 데비안 DEP-5 형식이야. 앱엔 MYPAINT-BRUSHES-LICENSES.dep5라는 이름으로 복사돼 있어. 맨 처음이 Files: *에 GPL-2+, 그다음이 Files: brushes/*에 CC0-1.0, 그 뒤로 작가별 브러시 디렉터리가 역시 CC0로 이어져. DEP-5 규칙은 이거야. 한 파일에 맞는 단락(stanza)이 여럿이면 마지막 걸 적용해. 그래서 Ariandel이 싣는 프리셋과 미리보기는 전부 CC0야. 포괄 조항인 GPL-2+가 덮는 건 업스트림 빌드 스크립트인데, 그건 한 번도 복사된 적이 없어. 벤더링한 libmypaint와 json-c 파일엔 GPL이라는 말 자체가 안 나와. Package.swift의 exclude 목록은 COPYING과 PROVENANCE.md를 컴파일에서 빼(소스가 아니라 문서니까). 그러면서도 자기가 덮는 코드 옆에 그대로 남겨둬.
include/는 "모든 의존자와 Swift에게", 타깃 루트는 "이 타깃에게만". 사실마다 그걸 들어야 할 쪽에 두면, 두 라이브러리가 같은 매크로 이름을 갖고도 한 번도 안 마주쳐.