CMakePresets.json으로 빌드 설정 통일하기: 멀티 플랫폼 프리셋, vcpkg 바이너리 캐시, CI
cmake 명령이 사람마다 다를 때 생기는 일
CMake 프로젝트를 여럿이 다루다 보면 configure 명령이 사람마다 조금씩 달라집니다. 한 명은 -G Ninja -DCMAKE_BUILD_TYPE=Release로, 다른 한 명은 생성기를 지정하지 않고 Makefile로, CI 스크립트는 또 다른 컴파일러로 빌드합니다. vcpkg를 쓰는 사람은 CMAKE_TOOLCHAIN_FILE을 직접 적고, 빌드 디렉터리도 build/, out/, cmake-build-debug/로 제각각입니다. 그러면 “로컬에서는 되는데 CI에서만 실패한다”는 문제를 재현하는 데만 한참이 걸립니다.
CMakePresets.json은 이 옵션 묶음을 이름 붙은 프리셋으로 저장소에 커밋해 두는 방법입니다. 누구든 cmake --preset release만 실행하면 같은 생성기, 같은 빌드 타입, 같은 툴체인 파일로 configure되고, Visual Studio·CLion·VS Code CMake Tools도 같은 파일을 읽어 프리셋 목록을 보여 줍니다. 팀 전체가 같은 configure/build 옵션을 쓰게 만든다는 점에서 Rust의 Cargo 워크플로·npm package.json·lock·Go 모듈·빌드 캐시·Python 가상환경·락이 푸는 “재현 가능한 빌드” 문제와 같은 방향입니다. 큰 그림은 CMake 가이드·빌드 시스템 비교와 함께 보세요.
configure 프리셋은 CMake 3.19부터, build·test 프리셋은 3.20부터 쓸 수 있습니다. 프리셋 파일의 version마다 쓸 수 있는 기능이 다르며, 자주 쓰는 것만 추리면 다음과 같습니다.
| version | 필요한 CMake | 추가된 주요 기능 |
|---|---|---|
| 2 | 3.20 | build·test 프리셋 |
| 3 | 3.21 | condition, toolchainFile, installDir, generator 생략 가능 |
| 4 | 3.23 | include로 다른 프리셋 파일 합치기 |
| 6 | 3.25 | workflowPresets, packagePresets, 테스트 프리셋의 outputJUnitFile |
version을 필요 이상으로 높이면 오래된 CMake를 쓰는 팀원이나 CI 이미지에서 “Unrecognized version” 오류로 파일 전체를 읽지 못합니다. 팀의 최저 CMake 버전을 먼저 정하고 그 버전이 지원하는 값으로 고정하는 것이 안전합니다. 참고로 CMake 4.0(2025년 3월)부터는 cmake_minimum_required(VERSION 3.5) 미만을 선언한 서드파티 프로젝트가 configure 단계에서 실패하는데, 프리셋의 cacheVariables에 "CMAKE_POLICY_VERSION_MINIMUM": "3.5"를 넣어 임시로 넘길 수 있습니다.
Presets가 해결하는 상황들
팀원마다 다른 configure 옵션
개발자 A: cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja
개발자 B: cmake -B build -DCMAKE_BUILD_TYPE=Debug
개발자 C: cmake -S . -B build -DCMAKE_CXX_COMPILER=clang++
세 명이 같은 build/ 디렉터리 이름을 쓰지만 생성기, 빌드 타입, 컴파일러가 모두 다릅니다. 어떤 버그가 한 사람에게서만 나타나면 그 사람의 명령을 그대로 재현하지 않는 한 원인을 찾기 어렵습니다. default, debug, release 프리셋을 정의해 두면 모두 cmake --preset release처럼 이름으로만 configure하게 되어 이런 차이가 사라집니다.
로컬과 CI의 차이
로컬에서는 Ninja와 최신 GCC로 Debug 빌드를 하는데, CI는 러너의 기본 컴파일러와 Makefile로 빌드 타입 없이 빌드한다면 경고 수준, 최적화, 정의되는 매크로가 다릅니다. 그러면 최적화 빌드에서만 드러나는 미정의 동작이나 컴파일러별 차이 때문에 CI에서만 실패하는 일이 생깁니다. ci-debug, ci-release 프리셋을 만들어 CI 스크립트가 cmake --preset ci-release만 호출하게 하면, 같은 프리셋을 로컬에서 실행해 CI 환경을 그대로 재현할 수 있습니다.
vcpkg와 Conan 사용자가 섞인 팀
vcpkg 사용자: -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
Conan 사용자: -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
패키지 매니저마다 툴체인 파일 경로가 다르므로 vcpkg-default, conan-release처럼 프리셋을 따로 두고, 각자 --preset 이름만 고르게 합니다.
멀티 플랫폼과 Sanitizer 빌드
Windows에서는 MSVC, Linux에서는 GCC, macOS에서는 Apple Clang을 쓴다면 플랫폼별 프리셋을 두고 condition으로 해당 OS에서만 보이게 할 수 있습니다. ASan·TSan·커버리지 빌드처럼 매번 -DUSE_ASAN=ON 같은 옵션을 붙여야 하는 빌드도 asan, tsan, coverage 프리셋으로 만들어 두면 이름만으로 전환됩니다.
개인마다 다른 경로와 IDE별 빌드 디렉터리
vcpkg를 /opt/vcpkg에 둔 사람과 C:\vcpkg에 둔 사람이 섞여 있으면 README에 경로를 적어 두어도 “Could not find toolchain file” 에러가 반복됩니다. 공유 파일인 CMakePresets.json은 $env{VCPKG_ROOT}처럼 환경 변수에 위임하고, 개인 경로가 꼭 필요하면 Git에 넣지 않는 CMakeUserPresets.json에 둡니다. CLion은 cmake-build-debug/, Visual Studio는 out/처럼 IDE마다 기본 빌드 디렉터리가 다른 문제도 프리셋의 binaryDir를 ${sourceDir}/build/${presetName}로 통일하면 모든 IDE가 같은 위치를 쓰게 되어, .gitignore에는 build/ 한 줄만 두면 됩니다.
CMake Presets 기본 구조
파일 위치와 역할
| 파일 | 용도 | Git |
|---|---|---|
CMakePresets.json | 프로젝트 공통 설정 | 커밋 |
CMakeUserPresets.json | 개발자 개인 설정 | .gitignore |
두 파일 모두 프로젝트 루트(최상위 CMakeLists.txt 옆)에 둡니다. CMakeUserPresets.json은 CMakePresets.json을 암시적으로 include하므로, 개인 파일의 프리셋이 공통 파일의 프리셋을 그대로 상속할 수 있습니다. 반대로 공통 파일의 프리셋이 개인 파일의 프리셋을 상속할 수는 없습니다.
최소 구성 예제
{
"version": 3,
"configurePresets": [
{
"name": "default",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/default",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_STANDARD": "17"
}
}
],
"buildPresets": [
{
"name": "default",
"configurePreset": "default"
}
]
}
configure는 프리셋 이름으로, 빌드는 빌드 디렉터리 경로나 build 프리셋 이름으로 실행합니다.
cmake --preset default
cmake --build build/default
# 또는 build 프리셋 사용
cmake --build --preset default
상속 (inherits)
공통 설정을 숨긴 base 프리셋에 두고, 다른 프리셋이 이를 상속합니다. 수정할 곳이 한 군데로 모이므로 프리셋이 늘어나도 관리가 쉽습니다.
{
"version": 3,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "17",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "debug",
"inherits": "base",
"displayName": "Debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"inherits": "base",
"displayName": "Release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "debug" },
{ "name": "release", "configurePreset": "release" }
]
}
hidden: true인 프리셋은 상속 전용이라 --preset으로 직접 쓸 수 없고 IDE 목록에도 나오지 않습니다. inherits에는 문자열 하나 또는 ["base", "other"] 같은 배열을 줄 수 있는데, 배열이면 앞쪽 부모의 값이 뒤쪽 부모보다 우선하고, 자식 프리셋에 직접 적은 값은 모든 부모보다 우선합니다. binaryDir의 ${presetName}은 상속받은 자식 프리셋의 이름으로 확장되므로, debug는 build/debug, release는 build/release를 쓰게 됩니다.
매크로 확장
| 매크로 | 설명 |
|---|---|
${sourceDir} | 프로젝트 소스 디렉터리(프리셋 파일이 있는 최상위) |
${presetName} | 현재 프리셋 이름 |
${hostSystemName} | Windows, Linux, Darwin 등 (v3부터) |
$env{VAR} | 환경 변수 VAR (프리셋의 environment로 설정한 값 포함) |
$penv{VAR} | 부모 프로세스 환경의 VAR ("PATH": "/opt/bin:$penv{PATH}"처럼 덧붙일 때 사용) |
빌드 타입·컴파일러·Sanitizer·테스트 Preset 예제
Debug / Release / RelWithDebInfo
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 21,
"patch": 0
},
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "17",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "debug",
"inherits": "base",
"displayName": "Debug (디버그 심볼)",
"description": "디버깅용 빌드",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"inherits": "base",
"displayName": "Release (최적화)",
"description": "배포용 빌드",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "relwithdebinfo",
"inherits": "base",
"displayName": "RelWithDebInfo",
"description": "최적화 + 디버그 심볼",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "debug" },
{ "name": "release", "configurePreset": "release" },
{ "name": "relwithdebinfo", "configurePreset": "relwithdebinfo" }
]
}
cmakeMinimumRequired는 선택 항목이지만, 명시해 두면 더 낮은 CMake로 실행했을 때 원인이 분명한 오류가 납니다. version: 3 자체가 CMake 3.21을 요구하므로 여기서도 3.21로 맞췄습니다. displayName과 description은 IDE의 프리셋 선택 목록에 표시됩니다.
GCC / Clang / MSVC 멀티 컴파일러
{
"version": 3,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "17",
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "gcc",
"inherits": "base",
"displayName": "GCC",
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc",
"CMAKE_CXX_COMPILER": "g++"
}
},
{
"name": "clang",
"inherits": "base",
"displayName": "Clang",
"cacheVariables": {
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++"
}
},
{
"name": "msvc",
"inherits": "base",
"displayName": "MSVC",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Ninja Multi-Config",
"architecture": {
"value": "x64",
"strategy": "external"
},
"cacheVariables": {
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl"
}
}
],
"buildPresets": [
{ "name": "gcc", "configurePreset": "gcc" },
{ "name": "clang", "configurePreset": "clang" },
{ "name": "msvc-release", "configurePreset": "msvc", "configuration": "Release" }
]
}
Ninja Multi-Config는 하나의 빌드 디렉터리에 Debug/Release를 함께 두는 멀티 설정 생성기라 base에서 물려받은 CMAKE_BUILD_TYPE을 쓰지 않고, 빌드할 때 --config(또는 build 프리셋의 configuration)로 고릅니다. 그래서 위 예제의 msvc-release build 프리셋에 configuration을 따로 넣었습니다. 단일 설정 생성기(Ninja, Unix Makefiles)에서는 CMAKE_BUILD_TYPE이 configure 시점에 고정됩니다.
MSVC를 Ninja로 쓸 때 가장 많이 막히는 곳이 architecture입니다. "strategy": "set"은 생성기에 -A x64를 넘기는 옵션이라 Visual Studio 생성기에서만 의미가 있고, Ninja에서는 “Generator Ninja does not support platform specification” 오류가 납니다. Ninja에서는 "strategy": "external"로 두며, 실제 아키텍처는 어떤 개발자 명령 프롬프트(vcvars)에서 실행했는지로 정해집니다. Visual Studio와 VS Code CMake Tools는 external 값을 보고 알맞은 vcvars 환경을 자동으로 잡아 주지만, 일반 PowerShell에서 cmake --preset msvc를 실행하면 cl.exe를 찾지 못합니다. 명령줄에서는 “x64 Native Tools Command Prompt”에서 실행하고, CI에서는 ilammy/msvc-dev-cmd 같은 액션으로 vcvars를 먼저 불러옵니다.
Sanitizer (ASan, TSan, UBSan)
{
"version": 3,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "17",
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "asan",
"inherits": "base",
"displayName": "AddressSanitizer",
"cacheVariables": {
"USE_ASAN": "ON",
"USE_UBSAN": "ON"
}
},
{
"name": "tsan",
"inherits": "base",
"displayName": "ThreadSanitizer",
"cacheVariables": {
"USE_TSAN": "ON"
}
}
],
"buildPresets": [
{ "name": "asan", "configurePreset": "asan" },
{ "name": "tsan", "configurePreset": "tsan" }
]
}
USE_ASAN, USE_TSAN은 CMake 내장 변수가 아니라 프로젝트가 정의하는 옵션입니다. CMakeLists.txt에서 option(USE_ASAN ...)으로 선언하고, 켜져 있으면 -fsanitize=address 같은 컴파일·링크 플래그를 붙이도록 분기해야 프리셋이 효과를 냅니다. ASan과 TSan은 함께 켤 수 없으므로 프리셋을 따로 둡니다.
코드 커버리지 (gcov/lcov)
{
"name": "coverage",
"inherits": "base",
"displayName": "Coverage",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_FLAGS": "--coverage",
"CMAKE_EXE_LINKER_FLAGS": "--coverage"
}
}
--coverage는 GCC와 Clang이 함께 이해하는 옵션으로, 컴파일 시 계측 코드를 넣고 링크 시 gcov 런타임을 붙입니다. 공유 라이브러리도 측정하려면 CMAKE_SHARED_LINKER_FLAGS에도 같은 값을 넣어야 합니다.
condition으로 플랫폼·환경별 분기
condition(v3, CMake 3.21+)을 주면 조건이 거짓인 환경에서는 그 프리셋이 목록에서 빠지고 사용할 수 없습니다.
| type | 의미 |
|---|---|
equals / notEquals | lhs와 rhs 문자열 비교 |
inList / notInList | string이 list에 포함되는지 |
matches / notMatches | string이 정규식 regex와 맞는지 |
allOf / anyOf | 하위 conditions가 모두 참 / 하나라도 참 |
not | 하위 condition을 반전 |
Windows 전용, Linux·macOS 공용, “Linux이면서 CI 환경”인 프리셋은 각각 다음처럼 씁니다.
{
"name": "msvc-release",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
}
{
"name": "unix-release",
"condition": {
"type": "inList",
"string": "${hostSystemName}",
"list": ["Linux", "Darwin"]
}
}
{
"name": "ci-release",
"condition": {
"type": "allOf",
"conditions": [
{ "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" },
{ "type": "notEquals", "lhs": "$env{CI}", "rhs": "" }
]
}
}
정의되지 않은 환경 변수는 $env{CI}에서 빈 문자열로 확장되므로, notEquals "" 비교는 “변수가 설정되어 있는가”를 확인하는 용도로 쓸 수 있습니다. GitHub Actions와 GitLab CI는 CI 변수를 자동으로 설정합니다.
buildPresets 옵션 (targets, jobs, verbose)
{
"buildPresets": [
{
"name": "release",
"configurePreset": "release",
"jobs": 8
},
{
"name": "release-myapp",
"configurePreset": "release",
"targets": ["myapp"]
},
{
"name": "release-verbose",
"configurePreset": "release",
"verbose": true
}
]
}
jobs는 -j, targets는 --target, verbose는 --verbose에 대응하고, configuration은 멀티 설정 생성기에서만 의미가 있는 --config에 대응합니다.
testPresets 예제 (filter, output, execution)
{
"version": 6,
"testPresets": [
{
"name": "default",
"configurePreset": "release",
"output": {
"outputOnFailure": true,
"verbosity": "verbose"
},
"execution": {
"noTestsAction": "error",
"stopOnFailure": true,
"jobs": 4
}
},
{
"name": "unit-only",
"configurePreset": "release",
"filter": { "include": { "label": "unit" } },
"output": { "outputOnFailure": true }
},
{
"name": "ci-test",
"configurePreset": "ci-release",
"output": {
"outputOnFailure": true,
"outputJUnitFile": "${sourceDir}/test-results.xml"
},
"execution": { "noTestsAction": "error", "stopOnFailure": true }
}
]
}
ctest --preset default
ctest --preset unit-only
ctest --preset ci-test
filter.include.label은 set_tests_properties(... PROPERTIES LABELS "unit")로 붙인 레이블과 정규식으로 매칭됩니다. outputJUnitFile은 CI가 테스트 결과를 표로 보여 줄 때 쓰는 JUnit XML을 만들며, 프리셋 version 6(CMake 3.25)부터 지원되므로 이 예제의 version도 6입니다. noTestsAction: "error"는 필터가 잘못되어 테스트가 하나도 실행되지 않았을 때 성공으로 넘어가지 않게 막아 줍니다.
vcpkg·Conan 연동
vcpkg 연동
{
"version": 3,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "17",
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "vcpkg-default",
"inherits": "base",
"displayName": "vcpkg (Release)",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
},
{
"name": "vcpkg-debug",
"inherits": "vcpkg-default",
"displayName": "vcpkg (Debug)",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "vcpkg-windows-static",
"inherits": "vcpkg-default",
"displayName": "vcpkg (Windows, 정적 링크)",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" },
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "x64-windows-static"
}
}
],
"buildPresets": [
{ "name": "vcpkg-default", "configurePreset": "vcpkg-default" },
{ "name": "vcpkg-debug", "configurePreset": "vcpkg-debug" }
]
}
사용하기 전에 각자 셸이나 시스템 환경 변수로 VCPKG_ROOT만 설정해 두면 됩니다.
export VCPKG_ROOT=/path/to/vcpkg
VCPKG_TARGET_TRIPLET을 지정하지 않으면 vcpkg 툴체인이 대상 플랫폼과 아키텍처를 보고 x64-linux, x64-windows 같은 기본 트리플렛을 고릅니다. 정적 링크처럼 기본값과 다른 트리플렛이 필요할 때만 위의 vcpkg-windows-static처럼 별도 프리셋에서 지정하는 편이 다른 플랫폼 사용자에게 영향을 주지 않습니다. 트리플렛은 처음 configure할 때 정해지므로, 바꾼 뒤에는 빌드 디렉터리를 지우고 다시 configure해야 합니다.
Conan 2 연동
Conan 2는 conan install을 실행하면 CMakeToolchain 생성기가 conan_toolchain.cmake를 만듭니다. 프리셋에서는 이 파일 경로를 toolchainFile로 지정합니다.
{
"name": "conan-release",
"inherits": "base",
"displayName": "Conan (Release)",
"toolchainFile": "${sourceDir}/build/conan/conan_toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
빌드 순서는 Conan이 먼저입니다.
# 1. 의존성 설치, 툴체인 파일을 build/conan에 생성
conan install . --output-folder=build/conan --build=missing -s build_type=Release
# 2. CMake configure (binaryDir는 build/conan-release)
cmake --preset conan-release
# 3. Build
cmake --build --preset conan-release
toolchainFile의 경로와 conan install --output-folder 경로가 일치해야 합니다. 프리셋의 binaryDir(여기서는 build/conan-release)는 툴체인 파일 위치와 달라도 괜찮습니다. 또 하나 맞춰야 할 것은 빌드 타입입니다. Conan 기본 프로필은 보통 build_type=Release라서, Debug 프리셋으로 configure하면서 Conan을 Release로 설치하면 패키지 설정을 찾지 못하거나 런타임 라이브러리가 섞입니다. Debug 프리셋을 쓸 때는 -s build_type=Debug로 따로 설치합니다.
참고로 Conan 2의 CMakeToolchain은 생성 폴더에 자체 CMakePresets.json을 만들고, 소스 폴더에 그것을 include하는 CMakeUserPresets.json을 만들기도 합니다. 팀이 직접 관리하는 CMakeUserPresets.json과 섞여 혼란스럽다면 Conan이 만든 프리셋을 그대로 쓰는 방식과 직접 작성한 프리셋을 쓰는 방식 중 하나로 정해 두는 편이 좋습니다.
vcpkg 바이너리 캐시 (2025년 이후 방식)
vcpkg는 포트를 소스에서 빌드하므로, 캐시가 없으면 CI 작업마다 Boost·OpenSSL 같은 의존성을 처음부터 컴파일합니다. 이것을 막는 것이 바이너리 캐시입니다. 한동안은 VCPKG_BINARY_SOURCES=clear;x-gha,readwrite 한 줄로 GitHub Actions 캐시를 쓰는 방법이 널리 퍼졌는데, GitHub가 캐시 서비스 API를 바꾸면서 이 방식이 동작하지 않게 되었고 vcpkg는 2025년에 x-gha 공급자를 제거했습니다. 인터넷 예제 상당수가 아직 이 설정을 쓰고 있어서, 복사해 넣으면 vcpkg 버전에 따라 경고나 오류가 나고 캐시가 동작하지 않습니다. CI 빌드 시간이 갑자기 크게 늘었다면 이것부터 확인하세요.
지금 쓸 수 있는 방법은 두 가지입니다. 첫째는 files 공급자와 actions/cache를 조합하는 방법으로, 설정이 가장 간단합니다.
env:
VCPKG_BINARY_SOURCES: "clear;files,${{ github.workspace }}/.vcpkg-cache,readwrite"
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: .vcpkg-cache
key: vcpkg-${{ runner.os }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }}
restore-keys: vcpkg-${{ runner.os }}-
- run: cmake --preset ci-release
- run: cmake --build --preset ci-release
restore-keys를 두면 vcpkg.json이 바뀌어도 이전 캐시를 받아 바뀐 포트만 다시 빌드합니다. 다만 GitHub Actions 캐시는 저장소당 용량 한도가 있어서, OS·트리플렛이 많은 매트릭스에서는 오래된 캐시가 금방 밀려납니다.
둘째는 GitHub Packages의 NuGet 피드를 캐시로 쓰는 방법으로, vcpkg 문서가 GitHub Actions용으로 안내하는 방식이며 여러 저장소가 캐시를 공유할 수 있습니다.
env:
VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/<OWNER>/index.json,readwrite"
이 방식은 워크플로에서 GITHUB_TOKEN(packages: write 권한)으로 NuGet 소스를 등록하는 단계가 추가로 필요하고, Linux·macOS 러너에서는 mono가 있어야 합니다. 절차는 Microsoft Learn의 vcpkg “GitHub Packages로 바이너리 캐시” 문서를 따르면 됩니다.
VCPKG_BINARY_SOURCES는 CMake 캐시 변수가 아니라 환경 변수라는 점에 주의하세요. 프리셋에 넣을 때는 cacheVariables가 아니라 environment에 넣어야 configure 중에 실행되는 vcpkg가 읽습니다.
{
"name": "ci-release",
"inherits": "vcpkg-default",
"environment": {
"VCPKG_BINARY_SOURCES": "clear;files,$env{GITHUB_WORKSPACE}/.vcpkg-cache,readwrite"
}
}
vcpkg와 Conan 사용자가 함께 있는 프로젝트
한 저장소에서 두 패키지 매니저를 동시에 쓰는 것은 권하지 않지만, 이행 기간처럼 두 방식이 공존해야 한다면 프리셋과 빌드 디렉터리를 완전히 분리해 서로의 캐시가 섞이지 않게 합니다.
{
"version": 3,
"configurePresets": [
{
"name": "vcpkg-release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/vcpkg-release",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_STANDARD": "17"
}
},
{
"name": "conan-release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/conan-release",
"toolchainFile": "${sourceDir}/build/conan/conan_toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_STANDARD": "17"
}
}
]
}
자주 나는 에러와 해결
프리셋 이름을 찾지 못함
cmake --preset default를 실행했는데 “No such preset” 오류가 나면, 해당 이름의 프리셋이 없거나 현재 디렉터리가 프리셋 파일이 있는 프로젝트 루트가 아닌 경우입니다. hidden: true인 프리셋이나 condition이 거짓인 프리셋을 지정하면 숨긴(hidden) 또는 비활성(disabled) 프리셋은 쓸 수 없다는 별도 오류가 납니다. 먼저 사용 가능한 목록을 확인합니다.
cmake --list-presets
상속한 프리셋을 찾지 못함
inherits로 참조한 이름이 같은 파일이나 include된 파일에 없으면 프리셋 파일을 읽는 단계에서 오류가 납니다. CMakeUserPresets.json의 프리셋은 CMakePresets.json의 프리셋을 별도 설정 없이 상속할 수 있지만, 그 반대 방향은 허용되지 않습니다. 공통 파일에서 개인 파일의 프리셋을 상속하도록 작성했다면 이 오류가 나므로, base 프리셋은 항상 공통 파일 쪽에 둡니다. A가 B를 상속하고 B가 다시 A를 상속하는 식의 순환도 “Cyclic preset inheritance” 오류가 되므로, base → debug → debug-asan처럼 한 방향으로만 상속합니다.
toolchainFile 경로 오류
“Could not find toolchain file”이 나는 가장 흔한 원인은 VCPKG_ROOT 환경 변수가 설정되지 않은 것입니다. 이때 $env{VCPKG_ROOT}는 빈 문자열로 확장되어 경로가 /scripts/buildsystems/vcpkg.cmake가 되어 버립니다.
# 확인
echo $VCPKG_ROOT
# 설정 (Linux/macOS)
export VCPKG_ROOT=/opt/vcpkg
# 설정 (Windows PowerShell)
$env:VCPKG_ROOT = "C:\vcpkg"
환경 변수에 의존하는 프리셋은 CI에서 해당 변수를 반드시 설정한 뒤 실행하고, 프로젝트 안에 둔 툴체인이라면 "${sourceDir}/cmake/toolchain.cmake"처럼 ${sourceDir} 기준 경로를 씁니다. 툴체인 파일은 처음 configure할 때만 반영되므로, 경로를 고친 뒤에도 같은 오류가 나면 빌드 디렉터리를 지우고 다시 configure합니다.
Debug와 Release 빌드가 서로 덮어씀
binaryDir를 ${sourceDir}/build로 고정해 두면 debug와 release 프리셋이 같은 디렉터리와 같은 CMakeCache.txt를 공유해 설정이 꼬입니다. "binaryDir": "${sourceDir}/build/${presetName}"으로 바꾸면 프리셋마다 build/debug, build/release처럼 분리됩니다.
Ninja Multi-Config에서 Release로 빌드되지 않음
CMAKE_BUILD_TYPE=Release를 넣었는데 결과물이 Debug 폴더에 생기고 최적화가 안 되어 있다면, Ninja Multi-Config가 CMAKE_BUILD_TYPE을 무시하고 --config가 없을 때 CMAKE_CONFIGURATION_TYPES의 첫 항목(기본값 Debug)으로 빌드했기 때문입니다. 빌드할 때 설정을 지정하거나, 앞의 msvc-release처럼 build 프리셋에 configuration을 넣습니다.
cmake --preset msvc
cmake --build build/msvc --config Release
cacheVariables가 기대와 다르게 적용됨
inherits에 부모를 여러 개 나열하면 앞쪽 부모의 값이 이깁니다. "inherits": ["base", "release-flags"]에서 두 부모가 모두 CMAKE_BUILD_TYPE을 정의했다면 base의 값이 쓰이므로, 덮어쓰고 싶은 부모를 앞에 두거나 자식 프리셋에 직접 적습니다. 멀티 설정 생성기라면 앞 절처럼 CMAKE_BUILD_TYPE 자체가 무시된다는 점도 확인합니다.
JSON 문법 오류와 version 오류
프리셋 파일은 표준 JSON이라 마지막 항목 뒤의 쉼표(trailing comma)를 허용하지 않으며, 이런 실수가 있으면 CMake가 파일 전체를 읽지 못하고 JSON 파싱 오류를 냅니다. 편집기의 JSON 검사나 CMake가 배포하는 프리셋 JSON 스키마를 연결해 두면 저장하기 전에 잡을 수 있습니다. version은 정수이며, 실행 중인 CMake가 지원하는 것보다 높은 값을 쓰면 “Unrecognized version” 오류가 납니다.
개인 설정과 팀 공통 설정 나누기
VCPKG_ROOT, CONAN_HOME처럼 개발자마다 다른 값은 공통 파일에 하드코딩하지 않습니다. 공통 파일은 환경 변수를 참조하게 두고, 그래도 개인별로 다른 경로나 컴파일러를 써야 하는 사람은 Git에 넣지 않는 CMakeUserPresets.json에서 공통 프리셋을 상속해 필요한 값만 바꿉니다. 아래는 프로젝트 루트의 CMakeUserPresets.json 예입니다.
{
"version": 3,
"configurePresets": [
{
"name": "my-vcpkg",
"inherits": "vcpkg-default",
"toolchainFile": "/home/me/vcpkg/scripts/buildsystems/vcpkg.cmake"
}
]
}
configure 프리셋마다 같은 이름의 build·test 프리셋을 짝지어 두면 cmake --preset X, cmake --build --preset X, ctest --preset X처럼 세 명령이 같은 이름으로 이어져 CI 스크립트와 README가 단순해집니다. 캐시 변수의 타입을 IDE가 정확히 알게 하려면 "BUILD_SHARED_LIBS": { "type": "BOOL", "value": "OFF" }처럼 객체 형식으로 적을 수 있습니다.
GitHub Actions·workflowPresets·packagePresets
GitHub Actions에서 프리셋 사용
name: Build
on: [push, pull_request]
jobs:
build-linux:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install CMake
uses: jwlawson/actions-setup-cmake@v2
with:
cmake-version: '3.28'
- name: Configure
run: cmake --preset ci-release
- name: Build
run: cmake --build --preset ci-release
CMakePresets.json에는 base를 상속하고 CMAKE_BUILD_TYPE을 Release로 둔 ci-release configure 프리셋과 같은 이름의 build 프리셋이 있어야 합니다. 러너 이미지에 기본 설치된 CMake 버전은 바뀔 수 있으므로, 프리셋 version이 요구하는 버전 이상을 이렇게 명시적으로 설치해 두면 이미지 업데이트에 흔들리지 않습니다.
CI 캐시로 빌드 시간 줄이기
빌드 디렉터리(build/) 전체를 actions/cache로 보존하는 예제가 많은데, 기대만큼 효과가 없습니다. actions/checkout은 소스 파일을 새로 써서 수정 시각이 모두 체크아웃 시점이 되므로, 복원된 오브젝트 파일보다 소스가 항상 새것으로 보여 Ninja나 Make가 사실상 전부 다시 컴파일합니다. 캐시 키를 CMake 파일 해시로 잡으면 이전 빌드의 생성 파일이 그대로 남아 디버깅을 헷갈리게 만들기도 합니다. 컴파일 결과를 재사용하고 싶다면 파일 수정 시각이 아니라 입력 내용의 해시로 동작하는 ccache를 쓰는 편이 안전합니다.
- uses: hendrikmuhs/ccache-action@v1
with:
key: ${{ runner.os }}-${{ matrix.preset }}
- run: cmake --preset ci-release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- run: cmake --build --preset ci-release
cmake --preset 뒤에 -D를 붙이면 프리셋 값 위에 덮어쓸 수 있어서, 로컬 프리셋은 그대로 두고 CI에서만 ccache를 켤 수 있습니다. 의존성 빌드 시간은 앞에서 다룬 vcpkg 바이너리 캐시로, 프로젝트 코드의 컴파일 시간은 ccache로 나눠서 줄이는 구성이 가장 예측 가능합니다.
멀티 플랫폼 CI (Linux, macOS, Windows)
strategy:
matrix:
include:
- { os: ubuntu-22.04, preset: linux-release }
- { os: macos-14, preset: macos-release }
- { os: windows-2022, preset: msvc-release }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- run: cmake --preset ${{ matrix.preset }}
- run: cmake --build --preset ${{ matrix.preset }}
여기서 linux-release, macos-release, msvc-release는 각 OS용 configure 프리셋과 같은 이름의 build 프리셋이 모두 정의되어 있다고 가정합니다. Windows 잡에서 Ninja로 MSVC를 쓴다면 앞에서 설명한 대로 ilammy/msvc-dev-cmd로 vcvars를 먼저 불러와야 합니다.
workflowPresets (version 6, CMake 3.25+)
configure → build → test → package를 한 번에 실행합니다. 워크플로의 첫 단계는 반드시 configure여야 하고, 이후 단계의 프리셋은 모두 같은 configure 프리셋을 가리켜야 합니다.
{
"version": 6,
"workflowPresets": [
{
"name": "ci",
"steps": [
{ "type": "configure", "name": "ci-release" },
{ "type": "build", "name": "ci-release" },
{ "type": "test", "name": "ci-test" }
]
}
]
}
cmake --workflow --preset ci
CI 스크립트가 cmake --workflow --preset ci 한 줄로 줄어들고, 로컬에서도 같은 명령으로 CI와 똑같은 순서를 재현할 수 있습니다.
include로 설정 분리
대형 프로젝트에서는 프리셋을 여러 파일로 나눌 수 있습니다(version 4+). include되는 각 파일도 자체 version 필드를 가져야 합니다.
{
"version": 4,
"include": ["presets/linux.json", "presets/windows.json", "presets/ci.json"]
}
GitLab CI / Jenkins
프리셋은 CMake 기능이라 CI 종류와 상관없이 같은 명령을 씁니다.
# .gitlab-ci.yml
build:
script:
- cmake --preset ci-release
- cmake --build --preset ci-release
- ctest --preset ci-test
// Jenkinsfile
sh 'cmake --preset ci-release && cmake --build --preset ci-release && ctest --preset ci-test'
packagePresets로 배포 패키지 생성 (version 6, CMake 3.25+)
{
"packagePresets": [
{
"name": "release-tgz",
"configurePreset": "release",
"generators": ["TGZ"]
}
]
}
cpack --preset release-tgz
CPack은 configure된 빌드 디렉터리의 CPackConfig.cmake를 읽으므로, 먼저 release 프리셋으로 configure와 빌드를 마친 뒤 실행합니다.