앞 글에서 textstat을 make로 빌드했다. 잘 돌았지만 두 가지를 남겼다. 플랫폼 분기가 전부 추측이었고(-lm이 필요한지 확인하지 않고 OS 이름으로 짐작했다), 윈도우의 nmake는 GNU 문법 첫 줄부터 거부했다.
같은 프로젝트를 CMake로 옮긴다. 무엇이 사라지는지, 그리고 CMake가 대신 무엇을 요구하는지 본다.
CMakeLists.txt 한 장
먼저 결과물이다. 이 파일 하나로 리눅스 Makefile, Ninja, Visual Studio 프로젝트가 전부 나온다.
cmake_minimum_required(VERSION 3.16)
project(textstat VERSION 1.0.0 LANGUAGES C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# 1) 생성 파일 — sed 규칙 대신
set(VERSION ${PROJECT_VERSION})
configure_file(version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h @ONLY)
# 2) 플랫폼별 소스 — ifeq/uname 대신
if(WIN32)
set(PLATFORM_SRC src/platform_win32.c)
else()
set(PLATFORM_SRC src/platform_posix.c)
endif()
# 3) 라이브러리
add_library(textstat_lib STATIC src/count.c src/histogram.c ${PLATFORM_SRC})
target_include_directories(textstat_lib
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # 쓰는 쪽에도 전파된다
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
# 4) libm이 필요한지 '확인'한다 — 짐작하지 않는다
include(CheckLibraryExists)
check_library_exists(m sqrt "" HAVE_LIBM)
if(HAVE_LIBM)
target_link_libraries(textstat_lib PUBLIC m)
endif()
# 5) 실행 파일
add_executable(textstat app/main.c)
target_link_libraries(textstat PRIVATE textstat_lib)
target_include_directories(textstat PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
# 6) 테스트
enable_testing()
add_executable(test_count tests/test_count.c)
target_link_libraries(test_count PRIVATE textstat_lib)
add_test(NAME count COMMAND test_count)make 판보다 짧지도 않다. 줄 수로는 비슷하다. 달라진 건 적는 내용의 성격이다 — 무엇을 실행할지가 아니라 무엇이 무엇인지를 적는다.
단계가 하나 늘어난다 — 구성(configure)
make는 make 한 번이면 끝이었다. CMake는 두 단계다.
$ cmake -S . -B build-make -DCMAKE_BUILD_TYPE=Release # 구성: 빌드 파일을 만든다
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features - done
-- Looking for sqrt in m
-- Looking for sqrt in m - found
-- Configuring done
-- Generating done
-- Build files have been written to: .../build-make
$ cmake --build build-make # 빌드: 만들어진 것을 돌린다
[ 50%] Linking C static library libtextstat_lib.a
[ 75%] Linking C executable textstat
[100%] Linking C executable test_count-B build-make가 소스 밖 빌드다. 산출물이 소스 트리를 어지럽히지 않고, 구성별로 디렉터리를 나눠 병행할 수 있다. make에서 build/$(BUILD)/ 경로를 손으로 조립하던 일이 기본값이 된다.
cmake --build도 눈여겨볼 만하다. 뒤에 뭐가 있든(make든 ninja든 MSBuild든) 호출하는 명령은 하나다. CI 스크립트가 플랫폼별로 갈라지지 않는다.
짐작하지 않고 확인한다
이 글에서 가장 중요한 두 줄이다.
include(CheckLibraryExists)
check_library_exists(m sqrt "" HAVE_LIBM)CMake는 이 지점에서 실제로 시험 링크를 해 본다. 같은 파일을 두 OS에서 구성하면 결과가 갈린다.
# 리눅스
-- Looking for sqrt in m - found
# 윈도우 (MSVC)
-- Looking for sqrt in m - not found그래서 리눅스 링크 라인에는 -lm이 붙고 윈도우에서는 붙지 않는다. make 판에서 ifeq ($(UNAME_S),Linux)로 짐작하던 것을, 1991년에 Autotools가 하던 방식 그대로 확인해서 정한다. 다만 M4와 셸 스크립트가 아니라 CMake 함수 두 줄이다.
macOS는 libm이 libSystem에 있으니 -lm이 없어도 된다 같은 주석도 이제 필요 없다. 확인 결과가 답을 준다.
헤더 의존성은 그냥 된다
make에서는 -MMD -MP와 -include $(DEP)를 손으로 엮어야 했다. CMake에서는 아무것도 하지 않는다.
$ touch include/textstat.h && cmake --build build-make
[ 12%] Building C object CMakeFiles/textstat_lib.dir/src/count.c.o
[ 25%] Building C object CMakeFiles/textstat_lib.dir/src/histogram.c.o
[ 37%] Building C object CMakeFiles/textstat_lib.dir/src/platform_posix.c.o
[ 50%] Linking C static library libtextstat_lib.a
[ 62%] Building C object CMakeFiles/textstat.dir/app/main.c.o
[ 75%] Linking C executable textstat생성 헤더(version.h)의 순서 문제도 사라진다. make에서 순서 전용 의존성(| version.h)을 몰라 첫 빌드가 깨졌던 그 문제는, configure_file이 구성 단계에서 파일을 만들기 때문에 애초에 생기지 않는다.
대신 다른 함정을 만났다.
version.h.in의@VERSION@은 CMake 변수VERSION을 찾는데,project(... VERSION 1.0.0)이 채우는 건PROJECT_VERSION이다. 그대로 두면 조용히 빈 문자열이 들어가textstat --version이 버전 없이 출력된다.set(VERSION ${PROJECT_VERSION})한 줄로 해결했다. 치환은 실패해도 오류를 내지 않으니 결과를 눈으로 확인해야 한다.
타깃과 사용 요구사항
target_include_directories(textstat_lib PUBLIC include)의 PUBLIC이 모던 CMake의 핵심이다.
PRIVATE— 나를 빌드할 때만 쓴다PUBLIC— 나를 빌드할 때도 쓰고, 나를 링크하는 쪽에도 전파된다INTERFACE— 나는 안 쓰지만 나를 쓰는 쪽에 전파된다(헤더 전용 라이브러리)
그래서 textstat 실행 파일 쪽에는 include/ 경로를 적지 않았다. textstat_lib을 링크하는 것만으로 따라온다. -lm도 PUBLIC으로 걸어 뒀으니 라이브러리를 쓰는 쪽이 알 필요가 없다.
이게 make와 갈리는 지점이다. Makefile에서는 라이브러리를 쓰는 쪽이 그 라이브러리에 필요한 플래그를 직접 알아내 다시 적어야 했다. 의존성이 늘수록 그 중복이 곱해진다.
같은 파일, 네 가지 빌드
같은 CMakeLists.txt로 실제로 만들어 본 것들이다.
| 명령 | 결과 |
|---|---|
cmake -B build-make | Makefile — make로 빌드 |
cmake -B build-ninja -G Ninja | build.ninja — 8개 작업으로 병렬 빌드 |
cmake -B build-vs -G "Visual Studio 18 2026" | textstat.slnx + textstat.vcxproj, test_count.vcxproj, ALL_BUILD, RUN_TESTS |
vcvars 안에서 -G Ninja | MSVC 컴파일러로 도는 Ninja 빌드 |
윈도우 쪽 실제 출력이다.
$ cmake --build build-vs --config Release
textstat_lib.vcxproj -> ...\build-vs\Release\textstat_lib.lib
test_count.vcxproj -> ...\build-vs\Release\test_count.exe
textstat.vcxproj -> ...\build-vs\Release\textstat.exe
$ .\build-vs\Release\textstat.exe --version
textstat 1.0.0ALL_BUILD와 RUN_TESTS 프로젝트는 CMake가 넣어 준 것이다. Visual Studio를 열면 솔루션 탐색기에서 평소처럼 F5로 돌릴 수 있다. 윈도우 개발자에게 “이제부터 make를 쓰라”고 요구하지 않는다 — 2편에서 nmake가 막았던 벽이 이렇게 없어진다.
테스트는 CTest가 받는다
make에서는 test: 타겟을 만들고 ./run_tests를 직접 호출했다. CMake에서는 테스트가 빌드 시스템이 아는 대상이 된다.
# 리눅스
$ ctest --output-on-failure
Start 1: count
1/1 Test #1: count ............................ Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
# 윈도우
$ ctest -C Release
1/1 Test #1: count ............................ Passed 0.13 sec
100% tests passed, 0 tests failed out of 1테스트가 여러 개가 되면 -j로 병렬 실행하고, -R로 골라 돌리고, --rerun-failed로 실패한 것만 다시 돌릴 수 있다. 이런 걸 Makefile에 직접 만들어 넣을 이유는 없다.
구성(Debug/Release)이 진짜로 갈린다
플래그를 손으로 관리하지 않는다. 구성 이름만 주면 된다.
$ cmake -B out-dbg -G Ninja -DCMAKE_BUILD_TYPE=Debug
$ cmake -B out-rel -G Ninja -DCMAKE_BUILD_TYPE=Release
Debug 컴파일 플래그: -g
Release 컴파일 플래그: -O3 -DNDEBUG여기서 제너레이터에 따라 방식이 갈리니 알아 둘 필요가 있다.
- 단일 구성(Unix Makefiles, Ninja) — 구성 단계에서
-DCMAKE_BUILD_TYPE=으로 정한다. 디버그와 릴리즈를 함께 두려면 빌드 디렉터리를 둘 만든다 - 다중 구성(Visual Studio, Xcode, Ninja Multi-Config) — 구성 단계에서는 정하지 않고 빌드할 때
--config Release로 고른다
같은 CMakeLists.txt인데 사용법이 다르다. CI 스크립트를 쓸 때 자주 걸리는 자리다.
프리셋 — 명령줄 인자를 파일로
-G Ninja -DCMAKE_BUILD_TYPE=Debug -B build/dev …를 매번 치는 대신, CMakePresets.json에 적어 둔다(CMake 3.19+).
{
"version": 3,
"configurePresets": [
{
"name": "dev",
"generator": "Ninja",
"binaryDir": "build/dev",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "build/release",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
}
],
"buildPresets": [
{ "name": "dev", "configurePreset": "dev" },
{ "name": "release", "configurePreset": "release" }
]
}$ cmake --list-presets
Available configure presets:
"dev"
"release"
$ cmake --preset dev && cmake --build --preset dev팀원과 CI가 같은 명령을 쓰게 만드는 장치다. “내 로컬에서는 되는데”의 상당수가 서로 다른 구성 인자에서 온다.
덤으로 따라오는 것 둘
빌드 파일이 스스로를 갱신한다. CMakeLists.txt를 고치고 빌드만 해도 구성 단계가 다시 돈다.
$ touch CMakeLists.txt && cmake --build build/dev
[0/1] Re-running CMake...
-- Configuring done
-- Generating done에디터가 컴파일 옵션을 알게 된다. CMAKE_EXPORT_COMPILE_COMMANDS ON이 compile_commands.json을 만들고, clangd·VS Code·CLion이 이걸 읽어 정확한 자동완성과 진단을 준다.
{
"directory": ".../build-make",
"command": "/usr/bin/cc -I.../include -I.../build-make -O3 -DNDEBUG -std=gnu11 -o ... -c .../src/count.c",
"file": ".../src/count.c"
}Makefile로는 이걸 얻으려고 bear 같은 도구로 빌드를 가로채야 했다.
CMake가 대신 요구하는 것
공짜는 아니다. 정직하게 적으면 이렇다.
- 구성 단계와 캐시가 생긴다.
CMakeCache.txt에 한 번 정해진 값(컴파일러, 옵션)은 계속 남는다. 컴파일러를 바꾸려면 캐시를 지우거나 빌드 디렉터리를 새로 만들어야 한다. “왜 옛 설정으로 도는가”의 답은 대개 캐시다 - 언어가 하나 더 늘어난다. CMake 스크립트 언어는 변수 확장과 리스트 처리에 독특한 규칙이 있고, 오류 메시지가 친절하지 않다
- 옛 예제가 섞여 든다.
include_directories·add_definitions같은 디렉터리 전역 명령이 아직도 검색 상위에 나온다. 타깃 방식과 섞으면 어디서 온 플래그인지 추적하기 어려워진다 - 제너레이터마다 세부가 다르다. 위의 단일/다중 구성 차이가 대표적이다
그럼에도 남는 계산은 분명하다. 2편에서 손으로 짜던 조건 분기·헤더 의존성·테스트 실행·구성 관리가 전부 없어졌고, 윈도우가 1급 시민이 됐다.
정리
| make 판에서 손으로 하던 것 | CMake 판 |
|---|---|
sed로 version.h 생성 + 순서 전용 의존성 | configure_file (구성 단계에서 생성) |
ifeq ($(UNAME_S),Linux) | if(WIN32) / check_library_exists로 확인 |
-MMD -MP + -include $(DEP) | 자동 |
| 라이브러리 쓰는 쪽이 플래그 재기입 | PUBLIC 사용 요구사항으로 전파 |
test: 타겟에서 직접 실행 | add_test + CTest |
BUILD=debug 분기와 디렉터리 조립 | CMAKE_BUILD_TYPE / --config |
| 윈도우는 별도 Makefile 필요 | 같은 파일로 .slnx + .vcxproj 생성 |
다음 글 Meson과 Bazel은 무엇을 노리는가에서는 최신 도구들이 여기서 무엇을 더 노리는지 본다. CMake가 “어디서든 빌드된다”를 풀었다면, 그다음 질문은 “같은 입력이면 같은 결과가 나오는가, 그리고 남이 빌드한 걸 받아 쓸 수 있는가” 다.
이 글의 모든 명령과 출력은 직접 실행한 것이다. 리눅스는 WSL2 우분투 22.04(CMake 3.22.1, GNU Make 4.3, Ninja 1.10.1), 윈도우는 CMake 4.2.1 + Visual Studio 2026(MSVC 14.51) + Ninja 1.12.0이다.
참고
- CMake 문서 —
configure_file—@ONLY치환 규칙과, 정의되지 않은 변수가 빈 문자열로 치환된다는 동작을 확인했다. - CMake 문서 —
target_link_libraries—PUBLIC/PRIVATE/INTERFACE가 사용 요구사항을 어떻게 전파하는지 확인했다. - CMake 문서 —
CheckLibraryExists— 시험 링크로 라이브러리 존재를 판정하는 모듈의 동작을 확인했다. - CMake 문서 —
cmake-presets(7)— 프리셋 파일의 스키마 버전과--preset사용법을 확인했다. - CTest 문서 — 다중 구성 제너레이터에서
-C <구성>이 필요한 이유를 확인했다.