앞의 다섯 편은 내 소스를 어떻게 빌드하는가였다. 실제 프로젝트는 거기서 끝나지 않는다. zlib으로 압축하고, OpenSSL로 암호화하고, SQLite에 저장한다. 그러면 새 질문이 생긴다.
그 라이브러리의 헤더는 어디 있고, 어떤 링크 플래그가 필요한가.
리눅스에서는 pkg-config가, 윈도우에서는 vcpkg와 CMake 설정 패키지가 이 질문을 맡는다. 두 세계는 겹치는 부분과 완전히 다른 부분이 섞여 있어서, 한쪽만 알면 다른 쪽에서 헤맨다. 같은 라이브러리(zlib) 하나를 양쪽에서 실제로 찾아 링크해 보면서 정리한다.
문제 — 플래그를 누가 아는가
손으로 적으면 이렇다.
cc main.c -I/usr/include -L/usr/lib/x86_64-linux-gnu -lz -o app한 번은 된다. 문제는 이 줄이 내 컴퓨터에서만 맞다는 것이다.
libdir가 배포판마다 다르다 —/usr/lib64(RHEL),/usr/lib/x86_64-linux-gnu(데비안 계열),/usr/local/lib(직접 빌드)- 정적으로 링크하면 필요한 라이브러리가 늘어난다. zlib은 하나지만 OpenSSL을 정적으로 링크하면
-lpthread -ldl이 따라온다 - 버전 조건이 있다 — “1.2 이상이어야 이 API가 있다”
- 이 정보를 소스 트리 바깥의 누군가가 알고 있어야 한다. 라이브러리를 설치한 쪽이다
pkg-config — 사실은 .pc 파일에 있다
pkg-config의 설계는 단순하다. 라이브러리를 설치할 때 자기 정보를 담은 텍스트 파일(.pc)을 같이 깔아 둔다. 도구는 그 파일을 읽어 문자열을 조립해 준다. 우분투의 zlib1g-dev가 깔아 두는 실물이다.
prefix=/usr
exec_prefix=${prefix}
libdir=${prefix}/lib/x86_64-linux-gnu
sharedlibdir=${libdir}
includedir=${prefix}/include
Name: zlib
Description: zlib compression library
Version: 1.2.11
Requires:
Libs: -L${libdir} -L${sharedlibdir} -lz
Cflags: -I${includedir}위쪽 절반은 변수, 아래쪽 절반은 키워드다. 변수를 두는 이유는 설치 위치가 바뀌면 prefix 한 줄만 고치면 되게 하려는 것이다. 이 의도가 나중에 어떻게 배신하는지는 뒤에서 본다.
쓰는 쪽은 이렇게 묻는다.
$ pkg-config --modversion zlib
1.2.11
$ cc main.c $(pkg-config --cflags --libs zlib) -o app$(...)로 감싸 셸이 명령의 출력을 그대로 컴파일러 인자로 넘긴다. 이게 pkg-config 사용법의 거의 전부다.
| 명령 | 하는 일 |
|---|---|
--cflags | 컴파일 플래그(-I, -D) |
--libs | 링크 플래그(-L, -l) |
--modversion | 버전 문자열 |
--exists | 있는지만 확인 — 출력 없이 종료 코드로 답한다 |
--atleast-version=X | 버전 조건 — 역시 종료 코드로 |
--static | 정적 링크용으로 전이 의존성까지 펼친다 |
--variable=libdir | .pc의 변수 하나만 꺼낸다 |
--list-all | 설치된 모듈 전체 |
종료 코드로 답하는 설계
--exists와 --atleast-version은 아무것도 출력하지 않는다. 스크립트에서 if로 바로 쓰라는 뜻이다. 직접 확인한 종료 코드다.
--exists nosuchlib -> 1
--cflags nosuchlib -> 1
--atleast-version=99 zlib -> 1
--atleast-version=2.0 libfoo(2.1.0) -> 0
--atleast-version=2.1.0 libfoo -> 0
--atleast-version=3.0 libfoo -> 1
버전 비교를 문자열이 아니라 숫자 단위로 한다는 점이 중요하다. 2.10을 2.9보다 크다고 판단한다. configure 스크립트가 이 종료 코드 위에 서 있다.
-I/usr/include가 안 나오는 이유
여기서 자주 걸린다. 위 .pc에는 분명히 Cflags: -I${includedir}가 있는데, 실제로 물어보면 이렇다.
$ pkg-config --cflags --libs zlib
-lz-I도 -L도 없다. 고장이 아니라 의도된 동작이다. pkg-config는 컴파일러가 이미 기본으로 뒤지는 경로(/usr/include, /usr/lib)를 출력에서 일부러 뺀다. 명령줄이 길어지는 것을 막고, 더 중요하게는 기본 경로를 -I로 다시 지정하면 헤더 검색 순서가 흐트러질 수 있기 때문이다.
확인하려면 억제를 풀면 된다.
$ PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --cflags zlib
-I/usr/include--libs 쪽은 PKG_CONFIG_ALLOW_SYSTEM_LIBS=1이다. 평소에는 건드릴 이유가 없다. “플래그가 안 나오는데?”라고 당황하지 않기 위해 알아 둘 사실이다.
전이 의존성과 정적 링크
.pc는 다른 모듈을 참조할 수 있다. 직접 만들어 확인했다.
Name: libfoo
Version: 2.1.0
Requires.private: zlib
Libs: -L${libdir} -lfoo
Libs.private: -lpthread
Cflags: -I${includedir}$ pkg-config --libs libfoo
-L/opt/foo/lib -lfoo
$ pkg-config --static --libs libfoo
-L/opt/foo/lib -lfoo -lpthread -lz동적 링크에서는 libfoo.so가 자기 의존성을 이미 안고 있으니 -lfoo만 있으면 된다. 정적 링크에서는 그 안고 있음이 사라지므로 Requires.private와 Libs.private를 전부 펼쳐야 한다. Requires(private 없는 쪽)는 헤더에까지 노출되는 의존성으로, 항상 펼쳐진다.
CMake의 PUBLIC/PRIVATE와 같은 구분이다. 3편에서 본 사용 요구사항(usage requirements) 개념이 .pc 파일에도 있는 셈이다.
검색 경로
pkg-config는 컴파일된 기본 경로 목록을 갖고 있다.
$ pkg-config --variable pc_path pkg-config
/usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig/usr/local이 /usr보다 앞에 있다. 직접 빌드해 넣은 것이 배포판 패키지를 이긴다는 뜻이다. 이 목록에 없는 곳은 PKG_CONFIG_PATH로 앞에 붙인다.
export PKG_CONFIG_PATH=$HOME/mylibs/lib/pkgconfig:$PKG_CONFIG_PATHPKG_CONFIG_LIBDIR는 다르다 — 기본 목록을 대체한다. 크로스 컴파일에서 호스트 라이브러리가 섞이지 않게 할 때 쓴다.
등록소와 추측 — find_package와 무엇이 다른가
pkg-config의 가치는 라이브러리가 스스로 등록한다는 데 있다. 비교가 될 실험을 하나 했다. 시스템에는 zlib 헤더가 없고 내 홈 디렉터리에만 풀어 둔 상태다.
CMake가 자기 모듈로 찾게 하면 실패한다.
find_package(ZLIB REQUIRED)
-> Could NOT find ZLIB (missing: ZLIB_LIBRARY ZLIB_INCLUDE_DIR)
같은 기계에서 pkg-config에게 물으면 찾는다.
$ PKG_CONFIG_PATH=$HOME/pcabs/lib/pkgconfig pkg-config --cflags --libs zlib
-I/home/ewanshin/pcabs/include -L/home/ewanshin/pcabs/lib -lzFindZLIB.cmake는 표준 위치를 뒤진다(추측). pkg-config는 등록된 사실을 읽는다(등록소). 라이브러리가 표준 위치에 없을 때 갈리고, 그 상황은 크로스 컴파일·컨테이너·직접 빌드에서 늘 생긴다.
절대경로라는 약한 고리
.pc의 prefix=/usr는 설치 시점에 박힌 절대경로다. 파일 위치가 바뀌면 거짓말이 된다. 이게 pkg-config의 가장 흔한 실전 사고다.
같은 .pc를 그대로 두고 실제 파일만 홈 디렉터리로 옮긴 상태를 만들었다.
$ pkg-config --exists zlib; echo $?
0 # 있다고 한다
$ pkg-config --cflags --libs zlib
-lz # 플래그도 깔끔하다
$ cc m.c $(pkg-config --cflags --libs zlib) -o m
m.c:1:10: fatal error: zlib.h: No such file or directorypkg-config는 만족스럽게 대답하는데 컴파일이 깨진다. .pc가 /usr를 가리키고 있고, 거기에는 아무것도 없다. 도구는 파일이 실제로 존재하는지 확인하지 않는다 — 텍스트를 조립할 뿐이다.
고치는 방법이 둘 있는데, 각자 요구하는 디렉터리 레이아웃이 다르다. 실측해 보면 그 전제가 드러난다.
방법 1 — --define-prefix
.pc 파일이 놓인 위치에서 prefix를 거꾸로 계산한다. <prefix>/lib/pkgconfig/foo.pc 관례를 가정하고 두 단계 위로 올라간다.
# .pc가 ~/pcconv/lib/pkgconfig/zlib.pc 에 있을 때
$ pkg-config --define-prefix --cflags --libs zlib
-I/home/ewanshin/pcconv/include -L/home/ewanshin/pcconv/lib -lz # 컴파일·실행 성공그런데 데비안 계열의 multiarch 레이아웃은 한 단계가 더 깊다(lib/x86_64-linux-gnu/pkgconfig). 그러면 이렇게 된다.
# .pc가 ~/sysroot/lib/x86_64-linux-gnu/pkgconfig/zlib.pc 에 있을 때
$ pkg-config --define-prefix --cflags zlib
-I/home/ewanshin/sysroot/lib/include # 틀렸다올바른 답은 ~/sysroot/include인데 ~/sysroot/lib/include가 나왔다. 관례에 기댄 계산이라 관례가 다르면 조용히 틀린다. 경로가 존재하지 않아도 경고 하나 없다.
방법 2 — PKG_CONFIG_SYSROOT_DIR
.pc가 내놓는 모든 경로 앞에 접두사를 붙인다. 크로스 컴파일의 정석이다.
$ PKG_CONFIG_SYSROOT_DIR=$HOME/sr2 pkg-config --cflags --libs zlib
-I/home/ewanshin/sr2/usr/include -L/home/ewanshin/sr2/usr/lib/x86_64-linux-gnu -lz이건 성공했다. 다만 전제가 있다 — sysroot가 원본 트리를 그대로 미러링해야 한다. .pc가 /usr/include라고 말하므로 $SYSROOT/usr/include가 실제로 있어야 한다. 처음에 .deb를 풀 때 usr/를 벗겨 놨더니 $SYSROOT/usr/include가 없어서 똑같이 깨졌다. 트리를 원본대로 다시 만들어서야 통과했다.
정리하면 이렇다.
| 방법 | 요구하는 것 | 깨지는 경우 |
|---|---|---|
--define-prefix | .pc가 <prefix>/lib/pkgconfig에 있을 것 | multiarch 등 다른 레이아웃 — 조용히 틀린 경로 |
PKG_CONFIG_SYSROOT_DIR | sysroot가 원본 트리를 그대로 미러링할 것 | usr/를 벗겨 놓은 추출본 |
빌드 시스템에서 쓰기
셸에서 $(...)로 쓰는 건 예제일 때고, 실전에서는 빌드 시스템이 대신 부른다. 셋 다 결국 같은 pkg-config를 실행한다.
CMake — pkg_check_modules가 .pc를 읽어 임포트 타깃까지 만들어 준다.
find_package(PkgConfig REQUIRED)
pkg_check_modules(ZLIB REQUIRED IMPORTED_TARGET zlib)
target_link_libraries(app PRIVATE PkgConfig::ZLIB)-- Checking for module 'zlib'
-- Found zlib, version 1.2.11
IMPORTED_TARGET을 빼면 ${ZLIB_LIBRARIES} 같은 변수만 채워진다. 타깃으로 받는 편이 낫다 — 포함 경로와 링크 플래그가 타깃에 붙어 전파되므로, 3편에서 본 사용 요구사항 방식과 그대로 맞물린다.
Meson — dependency()가 pkg-config를 1순위로 쓴다. 별도 설정이 없다.
zdep = dependency('zlib', version: '>=1.2')
executable('app', 'm.c', dependencies: zdep)Found pkg-config: YES (/home/ewanshin/bin/pkg-config) 0.29.2
Run-time dependency zlib found: YES 1.2.11
4편에서 Meson이 “관례를 정해 두고 설정을 줄인다”고 했는데, 의존성 탐색이 그 관례의 대표 사례다. CMake는 모듈을 찾고 매크로를 부르는 두 줄이 필요하지만 Meson은 dependency() 하나다.
Autotools — PKG_CHECK_MODULES([ZLIB], [zlib >= 1.2]) 매크로가 configure에서 위의 종료 코드 규약을 그대로 사용한다. pkg-config가 원래 이 세계에서 태어났다.
윈도우 — 대응은 무엇인가
여기서 먼저 정정할 것이 있다. vcpkg-config라는 도구는 없다. 이름이 비슷해 짝처럼 보이지만, 윈도우 쪽은 pkg-config가 혼자 하던 일이 두 층으로 나뉘어 있다.
| 하는 일 | 유닉스 | 윈도우 |
|---|---|---|
| 라이브러리를 가져와 설치 | apt·dnf·직접 빌드 | vcpkg(또는 Conan) |
| 빌드 플래그를 알아냄 | pkg-config (.pc) | CMake 설정 패키지 (XxxConfig.cmake) |
즉 대응 관계는 pkg-config ↔ vcpkg가 아니라 pkg-config ↔ find_package 이고, vcpkg는 apt 자리에 있다.
왜 pkg-config를 그대로 쓸 수 없나
pkg-config 자체는 윈도우에서도 돈다. 문제는 출력이 GCC 문법이라는 것이다. MSVC에 그대로 먹여 봤다.
> cl /nologo main.c -IC:/vi/x64-windows/include -LC:/vi/x64-windows/lib -lz
cl : 명령줄 warning D9002 : 알 수 없는 '-LC:/vi/x64-windows/lib' 옵션을 무시합니다.
cl : 명령줄 warning D9002 : 알 수 없는 '-lz' 옵션을 무시합니다.
main.obj : error LNK2019: main 함수에서 참조되는 확인할 수 없는 외부 기호 __imp_zlibVersion
main.exe : fatal error LNK1120: 1개의 확인할 수 없는 외부 참조입니다.
-I는 우연히 통하지만 -L과 -l은 경고만 내고 무시된다. 그리고 링크에서 죽는다. MSVC 문법으로 고쳐 주면 통과한다.
> cl /nologo main.c /IC:/vi/x64-windows/include /link /LIBPATH:C:/vi/x64-windows/lib z.lib
-lz → z.lib. 즉 라이브러리 파일명 규칙 자체가 다르다. 유닉스는 -lz에서 libz.so/libz.a를 유도하지만, 윈도우는 .lib 파일 이름을 직접 적는다. 이 변환을 .pc 파일 하나로 표현할 방법이 없다. pkg-config가 윈도우의 표준이 되지 못한 근본 이유다.
아무것도 없을 때
CMake의 find_package부터 확인했다. 윈도우에 zlib을 깔지 않은 상태다.
find_package(ZLIB REQUIRED)
-> Could NOT find ZLIB (missing: ZLIB_LIBRARY ZLIB_INCLUDE_DIR)
리눅스와 같은 실패다. 다른 점은 리눅스에서는 apt install zlib1g-dev 한 줄이면 끝나지만 윈도우에는 그 한 줄이 없다는 것이다. 그 빈자리를 채우는 것이 vcpkg다.
vcpkg 매니페스트 모드
요즘 방식은 프로젝트에 의존성 목록을 두는 것이다(매니페스트 모드). 전역에 설치하지 않으므로 프로젝트마다 다른 버전을 가질 수 있다.
{
"name": "pkgdemo",
"version": "1.0.0",
"dependencies": ["zlib"],
"builtin-baseline": "06d00ffa491e4668627728f14b891d22c6fea146"
}CMakeLists.txt는 vcpkg를 전혀 모른다. 그냥 평범한 CMake다.
find_package(ZLIB REQUIRED)
target_link_libraries(app PRIVATE ZLIB::ZLIB)연결은 툴체인 파일로 한다.
> cmake -S . -B b -G Ninja -DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake
-- Running vcpkg install
The following packages will be built and installed:
zlib:[email protected]#2
All requested installations completed successfully in: 16 s
-- Running vcpkg install - done
-- Found ZLIB: optimized;C:/vi/x64-windows/lib/z.lib;debug;C:/vi/x64-windows/debug/lib/zd.lib (found version "1.3.2")
> cmake --build b && b\app.exe
zlib 1.3.2
구성(configure) 단계가 의존성 설치를 겸한다. 툴체인이 vcpkg install을 먼저 돌려 zlib을 소스에서 빌드하고, 그 결과 위치를 CMAKE_PREFIX_PATH에 꽂아 find_package가 찾게 만든다. find_package 자체는 달라지지 않는다 — 그래서 vcpkg를 쓰지 않는 사람도 같은 CMakeLists.txt를 쓸 수 있다.
zlib 하나를 위해 vcpkg가 자기 CMake 4.3.2, 7-Zip, PowerShell 7까지 내려받았다. 호스트에 무엇이 깔렸는지에 기대지 않겠다는 설계다. 4편의 Bazel 밀폐성과 같은 방향이고, 대가도 같다 — 첫 빌드가 느리고 디스크를 먹는다.
실제로 걸린 함정 셋
문서만 읽으면 안 보이는 것들이다. 셋 다 직접 밟았다.
1. VS 번들 vcpkg에는 classic mode가 없다. Visual Studio와 함께 깔리는 vcpkg.exe로 vcpkg install zlib을 하면 이렇게 거부한다.
This vcpkg distribution does not have a classic mode instance
전역 설치를 아예 못 한다. 매니페스트 모드만 쓸 수 있다. 인터넷 예제 대부분이 vcpkg install <패키지>로 시작하니 여기서 막힌다.
2. builtin-baseline이 필수다.
error: this vcpkg instance requires a manifest with a specified baseline in order to
interact with ports. Please add 'builtin-baseline' to the manifest
베이스라인은 레지스트리(microsoft/vcpkg) 커밋 해시다. “이 시점의 포트 목록을 쓴다”는 고정이고, 이게 없으면 팀원마다 다른 버전을 받는다. 값은 레지스트리의 커밋 해시를 그대로 쓴다.
3. MAX_PATH. 깊은 디렉터리에서 빌드하다 이렇게 죽었다.
fatal error C1083: 컴파일러 생성 파일 파일을 열 수 없습니다. '': Invalid argument
파일명 자리가 빈 문자열이다. vcpkg는 <빌드폴더>/vcpkg_installed/vcpkg/blds/<포트>/<트리플렛>/CMakeFiles/CMakeScratch/TryCompile-xxxxxx/...처럼 경로를 깊게 파므로 260자 제한을 넘긴다. 설치 위치만 짧게 옮기면 통과한다.
> cmake ... -DVCPKG_INSTALLED_DIR=C:/vi
에러 메시지가 경로 길이를 언급하지 않아서 컴파일러 설정 문제로 오해하기 쉽다. 윈도우에서 빌드가 이해할 수 없는 이유로 죽으면 경로 길이를 먼저 의심한다.
트리플렛과 DLL
vcpkg는 트리플렛(x64-windows, x64-windows-static, arm64-windows…)으로 아키텍처와 링크 방식을 함께 고른다. 기본값 x64-windows는 동적이라 실행 시 DLL이 필요하다.
CMake 툴체인을 거쳐 빌드하면 이 문제가 안 보인다. 산출물 옆에 DLL을 복사해 주기 때문이다.
b/
app.exe
z.dll <- vcpkg 툴체인이 복사
반면 cl로 직접 빌드한 실행 파일은 그냥 죽었다. PATH에 C:/vi/x64-windows/bin을 넣어 줘야 돌았다. 유닉스의 rpath에 해당하는 것이 윈도우에 없어서, DLL을 실행 파일 옆에 두는 것이 사실상의 규약이다. 배포 때 이걸 잊으면 개발 기계에서만 도는 프로그램이 된다.
반전 — vcpkg는 .pc 파일도 깐다
두 세계가 대립한다고 생각했는데, 설치 결과를 뒤져 보니 그렇지 않았다.
C:\vi\x64-windows\share\zlib\ZLIBConfig.cmake <- CMake 설정 패키지
C:\vi\x64-windows\lib\pkgconfig\zlib.pc <- pkg-config 파일
포트가 남긴 안내문도 둘 다 알려 준다.
zlib is compatible with built-in CMake targets:
find_package(ZLIB REQUIRED)
zlib provides pkg-config modules:
zlib
그리고 그 .pc의 첫 줄이 흥미롭다.
prefix=${pcfiledir}/../..
Libs: "-L${libdir}" "-L${sharedlibdir}" -lz
Cflags: "-I${includedir}"${pcfiledir}는 그 .pc 파일이 놓인 디렉터리다. 절대경로를 박지 않고 자기 위치에서 계산한다. 위에서 본 재배치 사고를 원천적으로 안 겪는 방식이고, 데비안의 prefix=/usr와 정반대의 선택이다. 플래그를 인용부호로 감싼 것도 경로에 공백이 흔한 윈도우를 의식한 것이다.
실제로 윈도우의 pkg-config로 읽어 봤다.
> pkg-config --cflags --libs zlib
-IC:/vi/x64-windows/lib/pkgconfig/../../include -LC:/vi/x64-windows/lib/pkgconfig/../../lib -lz
경로가 정확히 풀렸다. 즉 윈도우에서도 pkg-config 기반 빌드(Meson, autotools 포팅)를 vcpkg 위에서 돌릴 수 있다. MSVC에 직접 먹일 수는 없지만, MinGW나 Meson처럼 GCC 문법을 쓰는 쪽에서는 이 경로가 실제로 유용하다.
macOS — Homebrew는 vcpkg 자리에 있다
질문이 자연스럽게 나온다. 맥에서는 pkg-config를 쓰나, Homebrew를 쓰나? 둘은 층이 달라서 경쟁 관계가 아니다.
- Homebrew = 라이브러리를 가져오는 층 (apt·vcpkg 자리)
- pkg-config = 플래그를 알아내는 층 (유닉스와 동일)
맥은 유닉스라 pkg-config가 그대로 돈다. 다만 기본 제공이 아니다 — Xcode 명령줄 도구에 들어 있지 않아 brew install pkgconf로 따로 깐다. Homebrew 수식들은 .pc 파일을 함께 설치하고, Homebrew의 prefix(/opt/homebrew 또는 /usr/local) 아래 lib/pkgconfig가 검색 경로에 들어간다.
맥에서만 걸리는 것 둘이 있다.
- keg-only 수식 — OpenSSL처럼 시스템 것과 충돌하는 라이브러리는 prefix에 링크되지 않는다.
PKG_CONFIG_PATH를 직접 지정해야 하고,brew info가 그 경로를 알려 준다. 성격이 같은 문제를 이 사이트를 만드는 환경에서도 겪는다 — Homebrewnode@22가 keg-only라PATH를 손으로 넣어야npx가 잡힌다 - 시스템 프레임워크는
.pc세계 밖이다 — CoreFoundation 같은 것은-framework CoreFoundation으로 링크한다.-L/-l모델이 아니라서 pkg-config가 표현할 수 없다. 맥 전용 코드를 링크할 때는 빌드 시스템 쪽에서 직접 다뤄야 한다
이 절은 문서 근거다. 위의 리눅스·윈도우 내용과 달리, 이 글을 쓴 환경에서 맥에 접근할 수 없어 직접 실행해 확인하지 못했다.
pkg-config인가 pkgconf인가
덧붙일 것이 하나 있다. 오래된 pkg-config(0.29.2, freedesktop)는 유지보수가 사실상 멈췄고, 요즘 배포판은 pkgconf 를 pkg-config 명령의 제공자로 쓴다. 명령줄 인터페이스와 .pc 문법이 호환되므로 사용자 입장에서는 대개 구분할 필요가 없다. 이 글의 측정은 우분투 22.04의 pkg-config 0.29.2로 했고, 같은 저장소에 pkgconf 1.8.0이 후보로 올라와 있다.
정리 — 축으로 보면
| 축 | 유닉스 (pkg-config) | 윈도우 (vcpkg + CMake) |
|---|---|---|
| 사실을 담는 곳 | .pc 텍스트 파일 | XxxConfig.cmake 스크립트 |
| 표현력 | 문자열 조립 — 플래그 나열 | CMake 코드 — 조건 분기 가능 |
| 플래그 문법 | GCC 계열 고정 | 컴파일러 중립(타깃으로 추상화) |
| 누가 설치하나 | 배포판 패키지 관리자 | vcpkg가 소스에서 빌드 |
| 버전 고정 | 배포판이 정한 하나 | 매니페스트 + 베이스라인으로 프로젝트별 |
| 재배치 | 절대 prefix — 깨진다 | 상대 경로 계산 — 안전 |
| 실행 시 의존성 | rpath·ldconfig | DLL을 실행 파일 옆에 복사 |
핵심 차이는 표현력이다. .pc는 텍스트 조립이라 “Debug일 때는 zd.lib, Release일 때는 z.lib”를 표현할 수 없다. CMake 설정 패키지는 코드라서 그걸 한다 — 위 출력의 optimized;...z.lib;debug;...zd.lib가 그 결과다. MSVC가 디버그/릴리스 런타임을 섞지 못하게 하는 세계에서는 이 표현력이 선택이 아니라 필수다.
언제 무엇을 쓰나
- 리눅스·맥만 지원하고 배포판 패키지로 충분하다 → pkg-config. 가장 단순하고, 모든 빌드 시스템이 이미 안다
- 윈도우가 지원 대상에 있다 → vcpkg(또는 Conan) +
find_package. pkg-config로 윈도우를 커버하려는 시도는-lz→z.lib변환 벽에 부딪힌다 - 내 라이브러리를 남에게 준다 → 둘 다 깔아 준다. vcpkg가 그렇게 한다.
.pc를 쓸 때는${pcfiledir}상대 경로로 재배치 문제를 피한다 - 크로스 컴파일한다 →
PKG_CONFIG_LIBDIR로 검색 경로를 갈아치우고PKG_CONFIG_SYSROOT_DIR로 sysroot를 붙인다. sysroot는 원본 트리를 그대로 미러링해야 한다
의존성 관리는 빌드 시스템 이야기의 마지막 조각이다. 1편에서 “패키지 계층은 뒤늦게 메워진 층”이라고 했는데, 그 뒤늦음의 대가가 이 글에서 본 갈라짐이다. 플랫폼마다 다른 답을 갖게 됐고, 지금도 하나로 수렴하지 않았다.
이 글의 리눅스 쪽 명령과 출력은 WSL2 우분투 22.04(pkg-config 0.29.2, gcc 11.4.0, CMake 3.22.1, Meson 1.7.0, Ninja 1.10.1)에서, 윈도우 쪽은 Visual Studio 2026(MSVC 19.51, 툴셋 14.51)과 CMake 4.2.1·Ninja 1.12.0, VS 번들 vcpkg(2026-05-27)에서 직접 실행한 것이다. pkg-config와 zlib 개발 파일은 sudo 없이 홈 디렉터리에 풀어 썼고, 그 과정에서 재배치 문제를 실제로 만나 위에 실었다. macOS 절은 직접 실행하지 못해 문서 근거임을 따로 표시했다.
참고
- pkg-config 매뉴얼 —
--define-prefix,PKG_CONFIG_SYSROOT_DIR,PKG_CONFIG_LIBDIR, 기본 경로 억제 환경변수의 정의를 확인했다. - freedesktop — pkg-config 가이드 —
.pc키워드(RequiresvsRequires.private,Libs.private)의 의미와 정적 링크 시 전개 규칙을 확인했다. - vcpkg 문서 — 매니페스트 모드 — 매니페스트 필드와
builtin-baseline의 역할, CMake 툴체인 연동 방식을 확인했다. - vcpkg 문서 — 트리플렛 — 트리플렛이 아키텍처와 링크 방식을 함께 지정한다는 점을 확인했다.
- CMake — FindPkgConfig —
pkg_check_modules의IMPORTED_TARGET동작과 채워지는 변수 목록을 확인했다. - Meson — Dependencies —
dependency()가 pkg-config를 우선 사용하는 방식과 버전 조건 문법을 확인했다. - Homebrew 문서 — keg-only 수식이 prefix에 링크되지 않아
PKG_CONFIG_PATH를 따로 지정해야 하는 사정을 확인했다(맥은 직접 실행하지 못함). - pkgconf — 최신 배포판이
pkg-config명령의 제공자로 pkgconf를 쓰게 된 경위와 호환성 범위를 확인했다. - pkg-config에 대하여 — 이 글의 출발점이 된 국문 자료.
.pc파일과--cflags/--libs의 기본 사용을 다룬다.