vcpkg 기초: 설치, Manifest 모드, Triplet, 버전 고정, 오버레이 커스텀 포트

들어가며: vcpkg가 필요한 순간

C++ 프로젝트에 fmt나 spdlog를 추가하려고 소스를 내려받아 빌드하고 경로를 맞추는 일을 몇 번 반복하다 보면, “로컬에서는 되는데 CI에서만 find_package가 실패한다”, “팀원마다 설치된 라이브러리 버전이 달라 한쪽에서만 빌드가 깨진다” 같은 문제가 따라옵니다. vcpkg는 Microsoft가 관리하는 C++ 패키지 매니저로, 프로젝트가 쓰는 라이브러리를 vcpkg.json에 선언해 두면 CMake 설정 단계에서 그 목록을 소스로부터 빌드해 프로젝트 전용 디렉터리에 설치합니다.

이 글은 vcpkg 설치부터 Manifest 모드, 정적·동적 링크를 정하는 Triplet, builtin-baseline으로 버전을 고정하는 방법, 공식 레지스트리에 없는 사내 라이브러리를 오버레이 포트로 넣는 방법을 순서대로 다루고, 자주 만나는 에러와 CI 구성도 함께 정리합니다. 예제는 vcpkg 2024.01 이후 버전, CMake 3.20 이상, C++17을 기준으로 합니다.

상황vcpkg 기능적용 방법
의존성 선언과 자동 설치Manifest 모드vcpkg.json 생성
CI에서만 빌드 실패Manifest + 툴체인CMAKE_TOOLCHAIN_FILE
팀원 간 버전 불일치builtin-baseline, overridesvcpkg.json 커밋
DLL 없는 배포TripletVCPKG_TARGET_TRIPLET
사내 라이브러리오버레이 포트VCPKG_OVERLAY_PORTS

vcpkg 설치

설치 방법

vcpkg는 Git 저장소를 클론한 뒤 bootstrap 스크립트를 실행하면 미리 빌드된 vcpkg 실행 파일을 내려받습니다.

Linux/macOS:

# 1. vcpkg 클론 (프로젝트 밖 또는 프로젝트 내 서브모듈로)
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
# 2. bootstrap 실행
./bootstrap-vcpkg.sh
# 3. 설치 확인
./vcpkg version

Windows (PowerShell 또는 cmd):

# 1. vcpkg 클론
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
# 2. bootstrap 실행
.\bootstrap-vcpkg.bat
# 3. 설치 확인
.\vcpkg.exe version

완료되면 vcpkg(또는 vcpkg.exe) 실행 파일이 생깁니다. vcpkg 디렉터리에서 실행하거나, PATH에 추가해 전역에서 사용할 수 있습니다.

프로젝트에 vcpkg 서브모듈로 추가 (권장)

팀 전체가 같은 vcpkg 버전을 쓰려면 프로젝트에 서브모듈로 추가합니다.

# 프로젝트 루트에서
git submodule add https://github.com/Microsoft/vcpkg.git vcpkg
# 서브모듈 초기화
git submodule update --init --recursive
# vcpkg bootstrap
cd vcpkg
./bootstrap-vcpkg.sh   # Linux/macOS
# .\bootstrap-vcpkg.bat  # Windows
cd ..

이렇게 하면 프로젝트 커밋마다 vcpkg 커밋도 함께 고정되어, 팀원마다 vcpkg 상태가 달라서 생기는 문제를 줄일 수 있습니다.

사전 요구사항

항목최소 버전
Git2.x
CMake3.20+
C++ 컴파일러MSVC 2019+, GCC 9+, Clang 10+
Python3.8+ (일부 포트에서 필요)

Linux에서는 이 밖에 curl, zip, unzip, tar, pkg-config가 있어야 bootstrap과 여러 포트 빌드가 동작합니다.

# CMake 버전 확인
cmake --version
# C++ 컴파일러 확인
g++ --version   # Linux
clang++ --version  # macOS
cl  # Windows (Visual Studio Developer Command Prompt)

Manifest 모드로 의존성 선언하기

Classic 모드 vs Manifest 모드

구분Classic 모드Manifest 모드
설치 방식vcpkg install fmt 전역 설치vcpkg.json에 선언, CMake 설정 시 자동 설치
의존성 위치vcpkg 설치 디렉터리프로젝트별 격리
재현성낮음 (팀원마다 다름)높음 (vcpkg.json + builtin-baseline)
권장기존 사용처 유지신규 프로젝트

최소 Manifest 모드 예제

프로젝트 구조는 다음과 같습니다.

my-app/
├── CMakeLists.txt
├── vcpkg.json
├── vcpkg/              # git submodule (선택)
└── src/
    └── main.cpp

vcpkg.json:

{
  "name": "my-app",
  "version": "1.0.0",
  "description": "vcpkg Manifest 모드 예제",
  "dependencies": [
    "fmt",
    "spdlog"
  ]
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
# vcpkg 툴체인은 project()보다 먼저 지정해야 함
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake"
    CACHE STRING "Vcpkg toolchain")
project(my-app VERSION 1.0.0 LANGUAGES CXX)
add_executable(my-app src/main.cpp)
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
target_link_libraries(my-app PRIVATE
    fmt::fmt
    spdlog::spdlog
)
target_compile_features(my-app PRIVATE cxx_std_17)

src/main.cpp:

#include <spdlog/spdlog.h>
#include <fmt/core.h>
int main() {
    spdlog::set_level(spdlog::level::debug);
    spdlog::info("Hello, vcpkg! {}", fmt::format("vcpkg works!"));
    return 0;
}

빌드 실행

# vcpkg가 서브모듈인 경우
cmake -B build -S . \
  -DCMAKE_TOOLCHAIN_FILE="$(pwd)/vcpkg/scripts/buildsystems/vcpkg.cmake"
cmake --build build
./build/my-app  # 또는 build\my-app.exe (Windows)

cmake가 툴체인 파일을 읽으면, Manifest 모드에서는 툴체인이 설정 단계 중에 vcpkg install을 실행합니다. vcpkg는 vcpkg.json과 baseline으로 각 포트의 버전을 정하고, 전이 의존성(spdlog가 쓰는 fmt 등)까지 포함한 그래프를 만든 뒤 의존 순서대로 빌드합니다. 각 패키지에는 버전, triplet, 포트 파일 내용, 컴파일러, 의존성 등을 묶은 ABI 해시가 붙고, 같은 해시의 빌드 결과가 바이너리 캐시(기본값은 Linux·macOS의 ~/.cache/vcpkg/archives, Windows의 %LOCALAPPDATA%\vcpkg\archives)에 있으면 다시 빌드하지 않고 압축만 풉니다. 캐시에 없으면 portfile.cmake에 적힌 소스를 내려받아 SHA512를 검증하고 빌드합니다.

설치 결과는 vcpkg 디렉터리가 아니라 빌드 디렉터리 아래의 vcpkg_installed/<triplet>/에 들어가므로, 프로젝트마다 의존성이 격리됩니다. 툴체인은 이 경로를 CMAKE_PREFIX_PATH에 추가하므로 find_package(fmt CONFIG REQUIRED)가 vcpkg_installed/<triplet>/share/fmt/의 설정 파일을 찾고, fmt::fmt 타깃을 링크하면 헤더 경로와 라이브러리가 자동으로 붙습니다. 처음 설정할 때는 패키지를 빌드하느라 시간이 걸리지만, 두 번째부터는 캐시 덕분에 빨라집니다.

Manifest 모드 — 의존성 객체 형태

버전 제약, 플랫폼별 의존성을 지정할 때는 객체 형태입니다.

{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": [
    "fmt",
    {
      "name": "spdlog",
      "version>=": "1.11.0"
    },
    {
      "name": "openssl",
      "platform": "windows"
    }
  ]
}
필드설명
name패키지 이름
version>=최소 버전 (이 버전 이상). 상한 제약 문법은 없음
platform플랫폼 조건 (windows, !windows, linux, osx)
features포트의 선택 기능 (예: Boost의 일부 모듈, curl의 SSL 백엔드)

features와 포트 선택은 빌드 시간과 바이너리 크기에 직접 영향을 줍니다. 예를 들어 "boost"를 통째로 적으면 수십 개 라이브러리를 모두 빌드하므로, 실제로 쓰는 것만 boost-filesystem처럼 개별 포트로 적는 편이 CI 시간을 크게 줄입니다. 반대로 어떤 포트는 기본 기능(default features)에 필요한 것이 빠져 있어 링크 단계에서 심볼이 없다는 에러가 나기도 하는데, 이때는 vcpkg search <포트>나 포트의 vcpkg.json에서 기능 목록을 확인해 명시해 줍니다.

{
  "dependencies": [
    { "name": "curl", "features": ["ssl"] },
    { "name": "fmt", "default-features": false }
  ]
}

툴체인 파일을 -DCMAKE_TOOLCHAIN_FILE=... 대신 CMakeLists.txt 안에서 지정하려면 반드시 project() 호출보다 먼저 set(CMAKE_TOOLCHAIN_FILE ... CACHE STRING "")를 두어야 합니다. project()에서 컴파일러와 툴체인이 결정되기 때문에, 그 뒤에 설정하면 조용히 무시되고 find_package가 vcpkg 패키지를 찾지 못합니다. 한 번 구성된 빌드 디렉터리는 캐시에 옛 값이 남아 있으므로, 설정을 바꾼 뒤에는 빌드 디렉터리를 지우고 다시 구성하는 것이 안전합니다. 팀에서는 이 설정을 CMakePresets.json에 넣어 두는 방식(아래 운영 패턴의 CMakePresets.json)이 가장 실수가 적습니다.


Triplet과 정적·동적 링크

Triplet이란?

Triplet은 vcpkg가 패키지를 어떤 대상(아키텍처, OS)과 어떤 링크 방식으로 빌드할지 정하는 설정 묶음입니다. 이름은 x64-windows처럼 <아키텍처>-<OS>[-<변형>] 형식이고, 실제 내용은 vcpkg/triplets/ 아래의 .cmake 파일에 VCPKG_LIBRARY_LINKAGE(static/dynamic), VCPKG_CRT_LINKAGE 같은 변수로 적혀 있습니다.

Triplet설명
x64-windowsWindows 64비트, 라이브러리는 DLL, CRT도 동적(/MD)
x64-windows-staticWindows 64비트, 정적 라이브러리, CRT도 정적(/MT)
x64-windows-static-mdWindows 64비트, 정적 라이브러리, CRT는 동적(/MD)
x64-linuxLinux 64비트, 기본이 정적 라이브러리
x64-osxmacOS Intel, 기본이 정적 라이브러리
arm64-osxmacOS Apple Silicon, 기본이 정적 라이브러리

Triplet 지정 방법

CMake 옵션으로 넘기는 방법:

cmake -B build -S . \
  -DCMAKE_TOOLCHAIN_FILE="$(pwd)/vcpkg/scripts/buildsystems/vcpkg.cmake" \
  -DVCPKG_TARGET_TRIPLET=x64-windows-static

환경 변수로 지정하는 방법(툴체인은 VCPKG_TARGET_TRIPLET이 없으면 이 값을 씁니다):

export VCPKG_DEFAULT_TRIPLET=x64-windows-static
cmake -B build -S .

CMakeLists.txt에서 project() 전에 지정하는 방법:

set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "")
project(my-app LANGUAGES CXX)

vcpkg.json이나 vcpkg-configuration.json에는 triplet을 지정하는 필드가 없습니다. triplet은 CMake 캐시 변수(VCPKG_TARGET_TRIPLET), 환경 변수, 또는 아래 운영 패턴에서 다루는 CMakePresets.json으로 넘기는 것이 정석입니다.

정적 링크 vs 동적 링크

Windows에서는 기본 triplet이 DLL을 만들므로, 실행 파일 하나로 배포하려면 x64-windows-static을 씁니다. 이때 vcpkg 라이브러리는 정적 CRT(/MT)로 빌드되므로 프로젝트도 set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")로 CRT를 맞춰야 하고, 그렇지 않으면 LNK2038: mismatch detected for 'RuntimeLibrary' 링크 에러가 납니다. CRT는 동적으로 두고 라이브러리만 정적으로 넣고 싶다면 x64-windows-static-md가 맞습니다. Linux와 macOS의 기본 triplet은 이미 정적 라이브러리를 만들고, 공유 라이브러리가 필요하면 x64-linux-dynamic 같은 커뮤니티 triplet을 씁니다.

Manifest 모드에서 어떤 triplet으로 설치되었는지는 빌드 디렉터리의 vcpkg_installed/ 아래 디렉터리 이름으로 확인할 수 있고, Classic 모드에서는 vcpkg list가 패키지마다 fmt:x64-windows처럼 triplet을 붙여 보여 줍니다.


builtin-baseline과 overrides로 버전 관리

builtin-baseline

builtin-baseline은 vcpkg 포트 저장소의 특정 커밋 해시입니다. 이 해시가 가리키는 시점의 versions/baseline.json이 각 포트의 기준 버전을 정합니다.

{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": ["fmt", "spdlog"],
  "builtin-baseline": "a1b2c3d4e5f6789012345678901234567890abc"
}

baseline 해시는 vcpkg 저장소의 현재 커밋으로 얻거나, vcpkg x-update-baseline --add-initial-baseline으로 vcpkg.json에 바로 넣을 수 있습니다.

cd vcpkg
git pull
git rev-parse HEAD
# 출력: a1b2c3d4e5f6789012345678901234567890abc

baseline은 고정해 두고, 올릴 때는 별도 브랜치에서 전체 빌드와 테스트를 통과시킨 뒤 반영하는 편이 안전합니다.

lock 파일이 없는 이유 — baseline이 곧 잠금

npm의 package-lock.json이나 Cargo의 Cargo.lock에 익숙하면 vcpkg에서도 lock 파일을 찾게 되지만, vcpkg에는 lock 파일이 없습니다. 대신 버전 결정이 결정적(deterministic)으로 설계되어 있습니다. builtin-baseline이 가리키는 커밋의 versions/baseline.json이 각 포트의 기준 버전을 정하고, 같은 vcpkg.json + 같은 baseline이면 누가 언제 빌드하든 같은 버전이 선택됩니다. 즉 baseline이 들어 있는 vcpkg.json을 커밋하는 것 자체가 버전을 잠그는 행위입니다. 같은 이유로 baseline 해시를 바꾸는 변경은 명시하지 않은 모든 의존성을 한꺼번에 업그레이드하는 것과 같으므로, 코드 리뷰에서 일반 코드 변경보다 신중하게 다루는 것이 좋습니다.

버전 제약 문법 — version>=와 overrides

vcpkg의 제약은 최소 버전(version>=)만 있습니다. version< 같은 상한 문법은 존재하지 않으며, 넣으면 매니페스트 검증 단계에서 에러가 납니다. 해석 규칙은 “baseline 버전과 모든 version>= 제약 중 가장 높은 최소값을 고른다”입니다. 새 버전이 나와도 baseline을 올리지 않는 한 자동으로 따라 올라가지 않으므로, 상한이 필요한 상황 자체가 드뭅니다.

{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": [
    { "name": "spdlog", "version>=": "1.11.0" },
    "fmt"
  ],
  "builtin-baseline": "a1b2c3d4e5f6789012345678901234567890abc",
  "overrides": [
    { "name": "fmt", "version": "10.1.1" }
  ]
}
  • version>=: 이 버전 이상을 요구합니다. baseline 버전이 더 높으면 baseline 버전이 선택됩니다.
  • overrides: 제약 계산을 무시하고 정확히 이 버전을 쓰게 합니다. 전이 의존성까지 포함해 특정 버전을 고정하거나, 새 버전의 회귀 때문에 낮은 버전으로 묶어 둘 때 씁니다. 필요하면 "port-version"으로 포트 리비전까지 지정합니다.
  • version>=나 overrides를 쓰려면 builtin-baseline(또는 vcpkg-configuration.json의 default-registry baseline)이 있어야 합니다. baseline 없이 쓰면 버전 제약에 baseline이 필요하다는 에러가 납니다.

커스텀 포트(오버레이)

오버레이란?

오버레이는 vcpkg 공식 레지스트리보다 우선 적용되는 포트 디렉터리입니다. 사내 라이브러리, 수정된 포트, 아직 upstream되지 않은 패키지를 사용할 때 씁니다.

오버레이 디렉터리 구조

my-ports/
├── internal-lib/
│   ├── vcpkg.json
│   └── portfile.cmake
└── patched-spdlog/
    ├── vcpkg.json
    ├── portfile.cmake
    └── patches/
        └── fix-logging.patch

최소 오버레이 포트 예제 (헤더-온리)

my-ports/header-only-lib/vcpkg.json:

{
  "name": "header-only-lib",
  "version": "1.0.0",
  "description": "헤더 전용 유틸리티 라이브러리",
  "license": "MIT",
  "dependencies": []
}

my-ports/header-only-lib/portfile.cmake:

vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO example/header-only-lib
    REF "v1.0.0"
    SHA512 0
    HEAD_REF main
)
# 헤더만 복사 (빌드 없음)
file(INSTALL "${SOURCE_PATH}/include/"
    DESTINATION "${CURRENT_PACKAGES_DIR}/include"
    FILES_MATCHING PATTERN "*.hpp"
)
# 포트 디렉터리에 둔 usage 파일(사용법 안내)을 함께 설치
file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage"
    DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE")

처음에는 SHA512 0으로 두고 설치를 시도하면, 내려받은 파일의 실제 해시가 에러 메시지에 출력되므로 그 값을 복사해 넣습니다. 이 포트는 헤더만 복사하므로 find_package용 CMake 설정 파일이 없습니다. 사용하는 쪽에서 find_path로 헤더 경로를 찾게 하거나, 라이브러리가 CMake 설치 규칙을 제공한다면 vcpkg_cmake_configure·vcpkg_cmake_install·vcpkg_cmake_config_fixup으로 설정 파일까지 설치하는 편이 편합니다.

오버레이 사용

cmake -B build -S . \
  -DCMAKE_TOOLCHAIN_FILE="$(pwd)/vcpkg/scripts/buildsystems/vcpkg.cmake" \
  -DVCPKG_OVERLAY_PORTS="$(pwd)/my-ports"

vcpkg.json에는 일반 포트처럼 이름으로 추가합니다.

{
  "dependencies": [
    "fmt",
    "header-only-lib"
  ]
}

vcpkg-configuration.json으로 오버레이 등록

프로젝트 루트에 vcpkg-configuration.json을 두면 CMake 옵션 없이 오버레이가 적용됩니다.

{
  "default-registry": {
    "kind": "git",
    "repository": "https://github.com/microsoft/vcpkg",
    "baseline": "a1b2c3d4e5f6..."
  },
  "overlay-ports": ["./my-ports"]
}

Manifest·정적 링크·오버레이 예제 프로젝트

기본 Manifest 모드

vcpkg-basics-demo/
├── CMakeLists.txt
├── vcpkg.json
├── vcpkg/              # git submodule
└── src/
    └── main.cpp

vcpkg.json:

{
  "name": "vcpkg-basics-demo",
  "version": "1.0.0",
  "description": "vcpkg 기초 예제",
  "dependencies": [
    "fmt",
    "spdlog",
    "nlohmann-json"
  ],
  "builtin-baseline": "a1b2c3d4e5f6789012345678901234567890abc"
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
# 툴체인은 반드시 project()보다 먼저 지정
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake"
    CACHE STRING "Vcpkg toolchain")
project(vcpkg-basics-demo VERSION 1.0.0 LANGUAGES CXX)
add_executable(demo src/main.cpp)
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
target_link_libraries(demo PRIVATE
    fmt::fmt
    spdlog::spdlog
    nlohmann_json::nlohmann_json
)
target_compile_features(demo PRIVATE cxx_std_17)

src/main.cpp:

#include <spdlog/spdlog.h>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
int main() {
    spdlog::info("vcpkg 기초 예제");
    nlohmann::json j = {{"name", "vcpkg"}, {"version", "1.0"}};
    spdlog::info("JSON: {}", j.dump());
    return 0;
}

Triplet + 정적 링크

cmake -B build -S . \
  -DCMAKE_TOOLCHAIN_FILE="$(pwd)/vcpkg/scripts/buildsystems/vcpkg.cmake" \
  -DVCPKG_TARGET_TRIPLET=x64-windows-static \
  -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release

오버레이 + Manifest

my-project/
├── CMakeLists.txt
├── vcpkg.json
├── vcpkg-configuration.json
├── vcpkg/
├── my-ports/
│   └── internal-lib/
│       ├── vcpkg.json
│       └── portfile.cmake
└── src/
    └── main.cpp

vcpkg-configuration.json은 앞 절과 같은 형식입니다.


커스텀 Triplet

기본 triplet 목록(x64-windows, x64-windows-static, x64-linux, arm64-osx 등)과 지정 방법은 기초 글의 Triplet 절에 있습니다. 기본 triplet으로 부족해지는 경우는 대개 세 가지입니다. Windows에서 정적 라이브러리 + 동적 CRT를 쓰고 싶을 때, 의존성까지 특정 컴파일 옵션(예: 최적화 수준, -fPIC, 디버그 심볼)을 통일하고 싶을 때, 그리고 Release만 빌드해서 CI 시간을 줄이고 싶을 때입니다.

triplets/x64-windows-static-md.cmake (정적 라이브러리 + 동적 CRT, Release만):

set(VCPKG_TARGET_ARCHITECTURE x64)
set(VCPKG_CRT_LINKAGE dynamic)      # /MD
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_BUILD_TYPE release)       # Debug 빌드 생략

triplets/x64-linux-static-pic.cmake (Linux 정적, 공유 라이브러리에 링크 가능하도록 PIC):

set(VCPKG_TARGET_ARCHITECTURE x64)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Linux)
set(VCPKG_CXX_FLAGS "-fPIC")
set(VCPKG_C_FLAGS "-fPIC")

몇 가지 주의점이 있습니다.

  • CRT 링크 방식은 플래그가 아니라 VCPKG_CRT_LINKAGE로 지정합니다. VCPKG_CXX_FLAGS에 /MT를 직접 넣으면 vcpkg가 넣는 /MD와 충돌해 경고나 링크 에러(LNK2038 RuntimeLibrary 불일치)가 납니다. 또 Windows 데스크톱 triplet에서는 VCPKG_CMAKE_SYSTEM_NAME을 비워 둡니다(값을 넣는 것은 UWP의 WindowsStore, Linux, Darwin, Android 등).
  • Linux에서 CRT를 정적으로 링크(VCPKG_CRT_LINKAGE static)하는 것은 거의 쓰지 않습니다. glibc 정적 링크는 NSS·dlopen 관련 문제가 있어 대부분의 포트가 전제로 하지 않습니다. “정적 링크 배포”라고 할 때 Linux에서는 보통 라이브러리만 정적으로 링크합니다.
  • VCPKG_BUILD_TYPE release는 Debug 구성을 쓰지 않을 때만 쓰세요. Visual Studio 멀티 구성 빌드에서 Debug로 빌드하면 Release 라이브러리와 섞여 CRT 불일치가 납니다.
  • triplet 이름이 곧 캐시 키의 일부입니다. 같은 내용을 팀원마다 다른 이름으로 만들면 캐시를 공유하지 못합니다. triplet 파일은 저장소에 두고 한 벌만 관리하는 편이 좋습니다.

프로젝트 triplet 디렉터리는 명령행 대신 vcpkg-configuration.json에 등록할 수도 있습니다.

{
  "overlay-triplets": ["./triplets"],
  "overlay-ports": ["./my-ports"]
}

이렇게 두면 CMake 명령에는 -DVCPKG_TARGET_TRIPLET=x64-windows-static-md만 남아서 CI 스크립트와 로컬 명령의 차이가 줄어듭니다.

바이너리 캐시

vcpkg는 포트를 빌드한 결과를 zip으로 묶어 캐시에 저장하고, 다음 빌드에서 같은 입력(포트 파일, 버전, triplet 파일, 컴파일러 버전, 의존성의 해시 등을 합친 ABI 해시)이 나오면 빌드 대신 복원합니다. 로컬에서는 기본적으로 켜져 있어(%LOCALAPPDATA%\vcpkg\archives, ~/.cache/vcpkg/archives) 두 번째 빌드부터 빨라지지만, CI 러너는 매번 새로 뜨므로 캐시를 따로 보존해야 합니다.

디렉터리 캐시

export VCPKG_BINARY_SOURCES="clear;files,$PWD/vcpkg-cache,readwrite"
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE="$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake"

clear는 기본 소스를 지우고 지정한 소스만 쓰겠다는 뜻입니다. files 소스의 경로는 절대 경로여야 합니다.

GitHub Actions에서 디렉터리 캐시를 보존

env:
  VCPKG_BINARY_SOURCES: "clear;files,${{ github.workspace }}/vcpkg-cache,readwrite"
steps:
  - uses: actions/checkout@v4
    with:
      submodules: recursive
  - name: Restore vcpkg binary cache
    uses: actions/cache@v4
    with:
      path: ${{ github.workspace }}/vcpkg-cache
      key: vcpkg-${{ runner.os }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'triplets/**') }}
      restore-keys: vcpkg-${{ runner.os }}-
  - name: Configure and Build
    run: |
      cmake -B build -S . \
        -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake
      cmake --build build

인터넷의 예제 중에는 vcpkg/buildtrees, vcpkg/packages를 캐시하는 것이 많은데, 이 디렉터리는 빌드 중간 산출물이라 용량이 크고 재사용 효과가 불확실합니다. 캐시할 대상은 바이너리 캐시 디렉터리 하나면 충분하고, 소스 다운로드를 아끼고 싶다면 vcpkg/downloads 정도를 추가합니다.

restore-keys로 부분 적중을 허용하면 vcpkg.json이 바뀌어도 바뀌지 않은 패키지는 캐시에서 복원됩니다. 다만 actions/cache는 키가 이미 있으면 덮어쓰지 않으므로, 부분 적중 후 새로 빌드한 패키지는 새 키로 저장될 때만 보존됩니다. 저장소별 캐시 용량 제한(오래된 캐시부터 삭제)도 있어서 Boost·Qt처럼 큰 의존성이 여러 triplet으로 쌓이면 캐시가 계속 밀려날 수 있습니다.

공유 원격 캐시

여러 워크플로·여러 저장소가 캐시를 공유해야 하면 원격 소스가 낫습니다. GitHub에서는 GitHub Packages의 NuGet 피드를 쓰는 방식이 문서화되어 있습니다.

env:
  VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/<OWNER>/index.json,readwrite"

NuGet 소스에는 인증 설정(nuget sources add ... -Password ${{ secrets.GITHUB_TOKEN }})이 별도로 필요합니다. Azure Artifacts, S3(x-aws), Google Cloud Storage(x-gcs), Azure Blob(x-azblob)도 지원합니다.

원격 캐시에서 쓰기 권한은 신중하게 나눠야 합니다. 포크에서 올라온 PR 빌드에 readwrite를 주면 누구나 캐시에 바이너리를 넣을 수 있게 됩니다. main 브랜치 빌드만 readwrite, PR 빌드는 read로 두는 구성이 일반적입니다.

캐시가 적중하지 않을 때

“캐시를 넣었는데 매번 새로 빌드한다”의 원인은 대부분 ABI 해시가 매번 달라지는 것입니다. 흔한 원인은 이렇습니다.

  • 러너 이미지 업데이트로 컴파일러 버전이 바뀜(GitHub 호스티드 러너는 주기적으로 바뀝니다)
  • triplet 파일이나 오버레이 포트가 빌드마다 생성되어 내용이 조금씩 다름
  • VCPKG_KEEP_ENV_VARS나 환경 변수에 따라 달라지는 값이 해시에 섞임

vcpkg install --debug 출력에는 패키지별 ABI 해시 입력 목록이 찍히므로, 두 빌드의 출력을 비교하면 어떤 입력이 달라졌는지 바로 보입니다. 캐시 문제는 추측으로 고치기보다 이 비교부터 하는 편이 빠릅니다.

프로덕션 구성 예제

프로젝트 구조

my-vcpkg-app/
├── .github/workflows/build.yml
├── CMakeLists.txt
├── CMakePresets.json
├── vcpkg.json
├── vcpkg-configuration.json   # overlay-ports, overlay-triplets, registries
├── triplets/
│   └── x64-windows-static-md.cmake
├── my-ports/
│   └── internal-lib/
├── vcpkg/                     # git submodule (baseline 커밋을 포함)
└── src/main.cpp

CMakePresets.json (팀 설정 통일)

{
  "version": 3,
  "configurePresets": [
    {
      "name": "vcpkg-default",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake"
      }
    },
    {
      "name": "windows-static",
      "inherits": "vcpkg-default",
      "cacheVariables": {
        "VCPKG_TARGET_TRIPLET": "x64-windows-static-md"
      }
    }
  ]
}

프리셋을 쓰면 로컬과 CI가 같은 명령(cmake --preset windows-static)을 쓰게 되어, “CI에서만 triplet이 다르다”는 종류의 차이가 사라집니다.

멀티 플랫폼 CI 매트릭스

name: Build (vcpkg)
on:
  push:
    branches: [main]
  pull_request:
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            triplet: x64-linux
          - os: windows-latest
            triplet: x64-windows-static-md
          - os: macos-latest
            triplet: arm64-osx
    runs-on: ${{ matrix.os }}
    env:
      VCPKG_BINARY_SOURCES: "clear;files,${{ github.workspace }}/vcpkg-cache,readwrite"
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: actions/cache@v4
        with:
          path: ${{ github.workspace }}/vcpkg-cache
          key: vcpkg-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'triplets/**', 'my-ports/**') }}
          restore-keys: vcpkg-${{ matrix.triplet }}-
      - name: Configure
        run: >
          cmake -B build -S .
          -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake
          -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }}
          -DCMAKE_BUILD_TYPE=Release
      - name: Build
        run: cmake --build build --config Release

캐시 키를 OS가 아니라 triplet 기준으로 잡은 것에 주의하세요. 같은 OS에서 triplet을 둘 이상 빌드할 때 서로의 캐시를 밀어내지 않게 하려는 것입니다.

Docker + vcpkg

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    git cmake g++ build-essential curl zip unzip tar pkg-config ninja-build
RUN git clone https://github.com/microsoft/vcpkg.git /vcpkg && \
    /vcpkg/bootstrap-vcpkg.sh -disableMetrics
ENV VCPKG_ROOT=/vcpkg
WORKDIR /app
# 의존성 선언만 먼저 복사해 레이어 캐시를 살림
COPY vcpkg.json vcpkg-configuration.json ./
COPY triplets/ triplets/
RUN /vcpkg/vcpkg install --x-install-root=/app/vcpkg_installed
COPY . .
RUN cmake -B build -S . \
      -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake \
      -DVCPKG_INSTALLED_DIR=/app/vcpkg_installed \
    && cmake --build build

bootstrap에는 curl zip unzip tar가 필요하고, 많은 포트가 pkg-config를 요구합니다. 그리고 의존성 파일만 먼저 복사해 vcpkg install을 별도 레이어로 두면, 소스 코드만 바뀐 빌드에서는 의존성 레이어가 Docker 캐시에서 재사용됩니다. 이 두 줄을 합쳐 COPY . . 뒤에 두면 소스 한 줄 수정에도 의존성 전체를 다시 빌드하게 됩니다. 또 이 예제는 vcpkg를 git clone으로 받았으므로 baseline 커밋이 포함되지만, --depth 1로 얕게 클론하면 baseline을 찾지 못합니다.

포트 구조와 핵심 개념

포트란?

포트(port)는 vcpkg가 한 패키지를 내려받아 빌드하고 설치하는 데 필요한 레시피입니다. 하나의 포트 디렉터리에는 다음 파일들이 들어갑니다.

ports/my-library/
├── vcpkg.json        # 메타데이터, 버전, 의존성, 기능(feature)
├── portfile.cmake    # 다운로드·빌드·설치 스크립트
├── usage             # (권장) 설치 후 출력할 사용법 안내
└── patches/          # (선택) 빌드 수정용 패치
    └── 001-fix-cmake.patch

라이선스 파일은 포트 디렉터리에 두지 않고, portfile.cmake에서 vcpkg_install_copyright로 설치 결과의 share/<port>/copyright에 복사합니다.

포트 생명 주기

flowchart LR
  subgraph download[1. 다운로드]
    D1[vcpkg_from_github 등] --> D2[소스 추출]
  end
  subgraph build[2. 빌드]
    B1[vcpkg_cmake_configure] --> B2[vcpkg_cmake_install]
  end
  subgraph install[3. 설치]
    I1[include/lib/share 복사] --> I2[usage·copyright]
  end
  download --> build --> install

패키지 생성 시퀀스

sequenceDiagram
    participant Dev as 개발자
    participant Vcpkg as vcpkg
    participant GH as GitHub/소스
    participant CMake as CMake
    Dev->>Vcpkg: vcpkg install my-lib --overlay-ports=...
    Vcpkg->>Vcpkg: vcpkg.json 파싱
    Vcpkg->>GH: 소스 다운로드 (SHA512 검증)
    GH-->>Vcpkg: 아카이브
    Vcpkg->>Vcpkg: 압축 해제
    Vcpkg->>CMake: vcpkg_cmake_configure
    CMake-->>Vcpkg: 설정 완료
    Vcpkg->>CMake: vcpkg_cmake_install
    CMake-->>Vcpkg: 빌드·설치 완료
    Vcpkg->>Vcpkg: config_fixup, usage, copyright
    Vcpkg-->>Dev: 패키지 설치 완료

portfile.cmake에서 자주 쓰는 변수:

변수설명
PORT현재 포트 이름 (예: my-library)
VERSIONvcpkg.json의 버전 문자열
SOURCE_PATH다운로드 함수가 OUT_SOURCE_PATH로 채워 주는 소스 경로 (관례상 이 이름을 씀)
CURRENT_PACKAGES_DIR설치 대상 디렉터리
CURRENT_BUILDTREES_DIR빌드 중간 결과
TARGET_TRIPLET대상 triplet (예: x64-windows, x64-linux)
VCPKG_TARGET_IS_WINDOWS대상 플랫폼이 Windows인지 여부

portfile.cmake 작성

기본 흐름

# 1. 링크 방식 제한 (선택)
vcpkg_check_linkage(ONLY_STATIC_LIBRARY)
# 2. 소스 다운로드 (+ 패치)
vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO example/my-library
    REF "v${VERSION}"
    SHA512 <해시값>
    HEAD_REF main
    PATCHES
        001-fix-cmake.patch
)
# 3. CMake 설정·빌드·설치
vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}")
vcpkg_cmake_install()
# 4. CMake config 경로 수정 (find_package용)
vcpkg_cmake_config_fixup(PACKAGE_NAME "my_lib" CONFIG_PATH "lib/cmake/my_lib")
# 5. 정리·기타
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE")

vcpkg는 설치가 끝나면 결과 디렉터리 구조를 검사하는 post-build 검사를 실행합니다. debug/include가 남아 있거나 라이선스가 없거나, 정적 라이브러리 triplet인데 DLL이 들어 있으면 경고나 에러가 나므로 4~5단계의 정리 작업이 필요합니다.

소스 다운로드 함수

GitHub 저장소라면 vcpkg_from_github가 가장 흔합니다. REF는 태그나 커밋이고, HEAD_REF는 --head 옵션으로 최신 브랜치를 빌드할 때 쓰는 브랜치 이름입니다.

vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO example/my-lib
    REF "v1.0.0"
    SHA512 <해시값>
    HEAD_REF main
)

일반 URL에서 아카이브를 받을 때는 vcpkg_download_distfile로 내려받고 vcpkg_extract_source_archive로 풉니다. 예전 예제의 vcpkg_extract_source_archive_ex는 폐기(deprecated)되었습니다.

vcpkg_download_distfile(ARCHIVE
    URLS "https://example.com/releases/my-lib-1.0.0.tar.gz"
    FILENAME "my-lib-1.0.0.tar.gz"
    SHA512 <해시값>
)
vcpkg_extract_source_archive(
    SOURCE_PATH
    ARCHIVE "${ARCHIVE}"
)

아카이브를 제공하지 않는 Git 서버라면 vcpkg_from_git을 씁니다. 이 함수의 REF는 태그 이름이 아니라 40자리 커밋 SHA여야 하며, 태그나 브랜치에만 있는 커밋을 가져오려면 FETCH_REF로 그 이름을 함께 줍니다.

vcpkg_from_git(
    OUT_SOURCE_PATH SOURCE_PATH
    URL "https://git.example.com/my-lib.git"
    REF 0123456789abcdef0123456789abcdef01234567   # 커밋 SHA
    FETCH_REF "v1.0.0"
)

SHA512 얻는 방법

처음에는 SHA512 0으로 두고 설치를 시도하면, vcpkg가 파일을 내려받은 뒤 실제 해시를 에러 메시지에 출력합니다.

error: failing download because the expected SHA512 was all zeros, please change the expected SHA512 to: <128자리 16진수 해시>

이 값을 복사해 portfile.cmake의 SHA512에 넣습니다. 해시는 내려받은 아카이브의 무결성을 검증하는 값이므로 REF를 바꿀 때마다 같은 절차로 갱신해야 합니다. 같은 태그라도 GitHub가 아카이브를 다시 만들면 해시가 달라질 수 있다는 점도 알아 두면, 갑자기 해시 불일치가 났을 때 원인을 빨리 찾을 수 있습니다.

패치 적용

업스트림 빌드 스크립트가 vcpkg 환경과 맞지 않거나 버그가 있을 때는 패치를 포트에 넣고, 다운로드 함수의 PATCHES 인자로 지정합니다. 경로는 포트 디렉터리 기준입니다. 예전 예제의 vcpkg_apply_patches는 폐기된 함수입니다.

vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO example/my-lib
    REF "v${VERSION}"
    SHA512 <해시값>
    PATCHES
        001-fix-cmake.patch
)

패치는 vcpkg가 풀어 둔 소스 트리에서 만듭니다. 이 디렉터리는 Git 저장소가 아니므로, 먼저 커밋을 하나 만들어 두고 수정한 뒤 git diff로 차이를 뽑습니다.

cd buildtrees/my-lib/src/<버전-해시>.clean
git init && git add . && git commit -m base
# 소스 수정 후
git diff > /path/to/ports/my-lib/patches/001-fix-cmake.patch

vcpkg install my-lib --editable로 설치하면 다음 실행 때 소스 트리를 다시 풀지 않으므로, 수정과 빌드를 반복하며 패치를 다듬기 편합니다.

vcpkg_copy_pdbs (Windows 디버그 심볼)

Windows에서 DLL을 만드는 포트는 설치 뒤 vcpkg_copy_pdbs()를 호출해 각 DLL의 .pdb를 bin 디렉터리에 함께 복사합니다. 그래야 사용하는 쪽에서 포트 내부까지 디버깅할 수 있습니다.

vcpkg_cmake_install()
vcpkg_copy_pdbs()

vcpkg_fixup_pkgconfig (pkg-config)

라이브러리가 .pc 파일을 설치한다면 vcpkg_fixup_pkgconfig()가 그 안의 경로를 vcpkg 설치 구조에 맞게 고칩니다. Autotools 포트에서 흔하지만 CMake 프로젝트가 .pc를 함께 설치하는 경우에도 필요합니다.

헤더 전용·CMake·Autotools 라이브러리 포트 예제

헤더 전용 라이브러리

single_include/ 아래 헤더만 있는 라이브러리는 빌드 없이 헤더를 복사하면 됩니다.

custom-overlay/header-only-lib/vcpkg.json:

{
  "name": "header-only-lib",
  "version": "2.1.0",
  "description": "Header-only utility library",
  "homepage": "https://github.com/example/header-only-lib",
  "license": "MIT"
}

custom-overlay/header-only-lib/portfile.cmake:

vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO example/header-only-lib
    REF "v${VERSION}"
    SHA512 0
    HEAD_REF main
)
# 헤더만 복사 (빌드 없음)
file(INSTALL "${SOURCE_PATH}/single_include/"
    DESTINATION "${CURRENT_PACKAGES_DIR}/include"
    FILES_MATCHING PATTERN "*.hpp"
)
file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage"
    DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE")

custom-overlay/header-only-lib/usage:

header-only-lib is header-only and can be used from CMake via:

  find_path(HEADER_ONLY_LIB_INCLUDE_DIRS "header_only_lib/foo.hpp")
  target_include_directories(main PRIVATE ${HEADER_ONLY_LIB_INCLUDE_DIRS})

vcpkg CMake 툴체인은 설치 경로를 검색 경로에 추가할 뿐 모든 타깃에 include 디렉터리를 자동으로 넣어 주지는 않으므로, 헤더 전용이라도 find_path로 경로를 찾게 안내하는 것이 정확합니다. 업스트림이 header_only_libConfig.cmake를 제공한다면 CMake 빌드를 돌려 설치하고 find_package로 쓰게 하는 편이 더 좋습니다.

CMake 기반 라이브러리

CMake로 빌드되고 fmt에 의존하며, 정적 링크만 지원하는 라이브러리의 예입니다.

custom-overlay/my-sample-lib/vcpkg.json:

{
  "name": "my-sample-lib",
  "version": "1.0.2",
  "description": "A sample C++ library for vcpkg packaging tutorial.",
  "homepage": "https://github.com/example/my-sample-lib",
  "license": "MIT",
  "dependencies": [
    "fmt",
    {
      "name": "vcpkg-cmake",
      "host": true
    },
    {
      "name": "vcpkg-cmake-config",
      "host": true
    }
  ]
}

custom-overlay/my-sample-lib/portfile.cmake:

vcpkg_check_linkage(ONLY_STATIC_LIBRARY)
vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO example/my-sample-lib
    REF "v${VERSION}"
    SHA512 <실제 해시>
    HEAD_REF main
)
vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}")
vcpkg_cmake_install()
vcpkg_cmake_config_fixup(PACKAGE_NAME "my_sample_lib" CONFIG_PATH "lib/cmake/my_sample_lib")
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage"
    DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE")

custom-overlay/my-sample-lib/usage:

my-sample-lib provides CMake targets:
find_package(my_sample_lib CONFIG REQUIRED)
target_link_libraries(main PRIVATE my_sample_lib::my_sample_lib)

사용하는 프로젝트의 vcpkg.json과 CMakeLists.txt는 다음과 같습니다.

{
  "dependencies": ["fmt", "my-sample-lib"]
}
find_package(my_sample_lib CONFIG REQUIRED)
target_link_libraries(my-app PRIVATE my_sample_lib::my_sample_lib)

vcpkg_cmake_config_fixup은 업스트림이 lib/cmake/my_sample_lib에 설치한 my_sample_libConfig.cmake를 vcpkg 관례 위치인 share/my_sample_lib로 옮기고, debug와 release 설정 파일을 하나로 합치며 안의 경로를 고칩니다. CONFIG_PATH를 생략하면 share/<PACKAGE_NAME>에 이미 있다고 가정하므로, 업스트림의 설치 위치를 확인해 맞춰 줘야 합니다.

Autotools 기반 라이브러리

configure 스크립트로 빌드하는 라이브러리는 make 계열 헬퍼를 씁니다. 오래 쓰인 vcpkg_configure_make/vcpkg_install_make 조합은 최근 vcpkg에서 vcpkg-make 포트의 vcpkg_make_configure/vcpkg_make_install로 대체되는 중이므로, 쓰는 버전의 문서를 확인하세요.

vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO example/legacy-lib
    REF "v1.0.0"
    SHA512 <해시값>
)
vcpkg_configure_make(
    SOURCE_PATH "${SOURCE_PATH}"
    AUTOCONFIG          # configure 스크립트가 없으면 autoreconf로 생성
)
vcpkg_install_make()
vcpkg_fixup_pkgconfig()
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/COPYING")

configure 없이 손으로 쓴 Makefile만 있는 라이브러리는 이 헬퍼로 바로 다루기 어렵습니다. 업스트림에 CMakeLists.txt를 추가하는 패치를 넣거나, 포트 안에 작은 CMakeLists.txt를 두고 소스 디렉터리에 복사해 vcpkg_cmake_configure로 빌드하는 방식이 흔합니다.

vcpkg create로 생성한 초안 다듬기

vcpkg create zlib2 https://github.com/madler/zlib/archive/v1.2.11.tar.gz zlib-1.2.11.tar.gz

생성되는 portfile.cmake 초안은 대략 다음과 같은 모양입니다(버전마다 주석과 세부 내용은 다릅니다).

vcpkg_download_distfile(ARCHIVE
    URLS "https://github.com/madler/zlib/archive/v1.2.11.tar.gz"
    FILENAME "zlib-1.2.11.tar.gz"
    SHA512 <vcpkg create가 계산한 해시>
)
vcpkg_extract_source_archive(
    SOURCE_PATH
    ARCHIVE "${ARCHIVE}"
)
vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}")
vcpkg_cmake_install()

여기에 vcpkg.json의 설명·라이선스·host 의존성을 채우고, 업스트림이 CMake config 파일을 설치한다면 vcpkg_cmake_config_fixup()을, 그 밖에 debug/include 정리와 vcpkg_install_copyright를 추가합니다.

패키지 설정 파일 없음, baseline 누락, 빌드 실패: 에러 해결

“Could not find a package configuration file provided by ‘fmt’”

CMake Error: Could not find a package configuration file provided by "fmt"

툴체인 파일이 지정되지 않았거나, 이미 툴체인 없이 구성된 빌드 디렉터리를 재사용하고 있을 때 납니다. 툴체인은 첫 구성 때만 적용되므로, 나중에 옵션을 추가해도 기존 빌드 디렉터리에는 반영되지 않습니다.

# 1. 툴체인 파일 반드시 지정
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE="$(pwd)/vcpkg/scripts/buildsystems/vcpkg.cmake"
# 2. build 폴더 삭제 후 재시도 (캐시된 잘못된 설정 제거)
rm -rf build
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=...

포트가 baseline에 없다는 에러

builtin-baseline이 가리키는 시점에 그 포트가 아직 없었거나, 포트 이름을 잘못 적었을 때 납니다(예: nlohmann_json이 아니라 nlohmann-json). vcpkg를 갱신한 뒤 baseline을 올립니다.

cd vcpkg && git pull && cd ..
./vcpkg/vcpkg x-update-baseline   # vcpkg.json의 builtin-baseline을 현재 커밋으로 갱신

”Building package xxx failed”

Building package spdlog:x64-linux failed

포트 빌드 중 컴파일 에러, 시스템 도구 누락(예: Linux의 pkg-config), 다운로드 실패 등이 원인입니다. 에러 메시지에 buildtrees/<포트>/ 아래의 로그 파일 경로가 출력되므로, 그 로그의 마지막 부분을 보는 것이 가장 빠릅니다. vcpkg 자체의 동작을 자세히 보고 싶다면 설정 시 옵션을 넘깁니다.

cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=... -DVCPKG_INSTALL_OPTIONS="--debug"

”undefined reference” / 링크 에러

undefined reference to `spdlog::...'

헤더는 찾는데 링크가 실패한다면 target_link_libraries에 타깃이 빠졌거나, Windows에서 정적·동적 triplet과 CRT 설정이 섞인 경우가 대부분입니다.

# find_package 후 반드시 target_link_libraries에 추가
find_package(spdlog CONFIG REQUIRED)
target_link_libraries(my-app PRIVATE spdlog::spdlog)

C++ 표준 불일치

의존성의 헤더가 요구하는 C++ 표준보다 낮은 표준으로 프로젝트를 빌드하면, 헤더 안의 C++17 문법(std::optional, if constexpr 등)에서 컴파일 에러가 납니다. 라이브러리 문서에서 요구 표준을 확인하고 프로젝트 표준을 맞춥니다.

target_compile_features(my-app PRIVATE cxx_std_17)
# 또는
set(CMAKE_CXX_STANDARD 17)

오버레이 포트를 찾을 수 없음

Error: Could not find port internal-lib

VCPKG_OVERLAY_PORTS 경로가 잘못되었거나, 포트 디렉터리 이름과 그 안 vcpkg.json의 name이 다를 때 납니다. 상대 경로는 기준 디렉터리에 따라 다르게 해석되므로 절대 경로를 쓰는 편이 안전합니다.

# 경로 확인 (절대 경로 권장)
ls -la my-ports/internal-lib/vcpkg.json
cmake -B build -S . \
  -DVCPKG_OVERLAY_PORTS="$(pwd)/my-ports"

요청한 버전이 버전 데이터베이스에 없음

version>=나 overrides에 적은 버전이 현재 vcpkg 클론의 versions/ 데이터베이스에 없을 때 납니다. 오타이거나, vcpkg 서브모듈이 오래되어 그 버전의 포트가 아직 등록되지 않은 경우입니다.

# 해당 포트에 등록된 버전 목록 확인
cat vcpkg/versions/f-/fmt.json | head -40
# vcpkg 서브모듈을 갱신한 뒤 baseline도 함께 올리기
cd vcpkg && git pull && cd ..
./vcpkg/vcpkg x-update-baseline

CI에서만 “Could NOT find” 발생

로컬에서는 예전에 Classic 모드로 설치한 패키지가 우연히 잡히고, CI에는 그 패키지가 없는 경우입니다. 의존성을 모두 vcpkg.json에 선언하고 CI에서도 같은 툴체인 파일을 지정하면 로컬과 CI가 같은 방식으로 설치합니다.

”vcpkg integrate install” 했는데 안 됨

vcpkg integrate install은 Visual Studio의 MSBuild 프로젝트(.vcxproj)에 vcpkg를 사용자 전역으로 연결하는 명령입니다. CMake 프로젝트에는 아무 설정도 바꾸지 않고, 써야 할 툴체인 파일 경로만 출력해 줍니다. CMake에서는 그 경로를 -DCMAKE_TOOLCHAIN_FILE=...이나 CMakePresets.json으로 전달해야 합니다.


usage·copyright·플랫폼 처리

usage 파일

포트의 share/<port>/usage 파일 내용은 vcpkg install이 끝날 때 출력됩니다. 사용자가 find_package 이름과 타깃 이름을 추측하지 않아도 되도록 정확한 CMake 코드를 적어 둡니다.

my-lib provides CMake targets:
find_package(my_lib CONFIG REQUIRED)
target_link_libraries(main PRIVATE my_lib::my_lib)
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE")
# 라이선스 파일이 여러 개라면 모두 나열하면 하나로 합쳐 설치됨
vcpkg_install_copyright(FILE_LIST
    "${SOURCE_PATH}/LICENSE"
    "${SOURCE_PATH}/LICENSE.md"
)

debug/include 제거

debug와 release 빌드는 같은 헤더를 설치하므로 debug 쪽 include는 중복이고, 남겨 두면 post-build 검사가 경고합니다.

file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")

플랫폼별 처리

if(VCPKG_TARGET_IS_WINDOWS)
    set(PLATFORM_OPTIONS -DMYLIB_USE_WINSOCK=ON)
endif()
vcpkg_cmake_configure(
    SOURCE_PATH "${SOURCE_PATH}"
    OPTIONS ${PLATFORM_OPTIONS}
)

WIN32 같은 CMake 내장 변수를 -D로 덮어쓰면 업스트림의 플랫폼 감지가 엉뚱하게 바뀌므로, 플랫폼에 따라 다르게 줄 옵션은 업스트림이 정의한 옵션 이름으로 넘깁니다. 특정 플랫폼을 아예 지원하지 않는 포트라면 vcpkg.json에 "supports": "!uwp"처럼 지원 범위를 적어 둡니다.

버전 변수 사용

portfile.cmake에서는 vcpkg가 vcpkg.json의 버전을 VERSION 변수로 넘겨 주므로, 버전을 두 곳에 따로 적지 않아도 됩니다.

vcpkg_from_github(
    REF "v${VERSION}"
    ...
)

오버레이·공식 레지스트리·사내 레지스트리 배포 전략

오버레이 포트 (사내/로컬)

공식 레지스트리에 없는 라이브러리, 사내 fork, 버그 수정 패치를 적용한 버전을 쓸 때 가장 간단한 방법입니다. 포트 디렉터리를 프로젝트 저장소에 함께 두고 경로만 알려 주면 됩니다.

company-cpp/
├── vcpkg/                    # git submodule
├── ports/                    # 사내 포트 모음
│   ├── company-utils/
│   │   ├── vcpkg.json
│   │   ├── portfile.cmake
│   │   └── usage
│   ├── company-protocol/
│   │   ├── vcpkg.json
│   │   └── portfile.cmake
│   └── patched-openssl/      # 사내 수정 OpenSSL
│       ├── vcpkg.json
│       ├── portfile.cmake
│       └── patches/
│           └── 001-fix.patch
├── vcpkg-configuration.json  # 오버레이 경로 등록
├── app-a/
│   ├── vcpkg.json
│   └── CMakeLists.txt
└── app-b/
    ├── vcpkg.json
    └── CMakeLists.txt

vcpkg-configuration.json:

{
  "default-registry": {
    "kind": "git",
    "repository": "https://github.com/microsoft/vcpkg",
    "baseline": "<vcpkg 저장소 커밋 SHA>"
  },
  "overlay-ports": ["./ports"]
}

vcpkg는 매니페스트(vcpkg.json)가 있는 디렉터리에서 vcpkg-configuration.json을 찾습니다. 위 구조처럼 앱이 하위 디렉터리에 있다면 각 앱 디렉터리에 이 파일을 두거나(overlay-ports는 "../ports"), 앱의 vcpkg.json 안에 "vcpkg-configuration" 필드로 같은 내용을 넣어야 합니다. 오버레이는 버전 이력을 관리하지 않으므로, 포트를 고치면 그 커밋 이후의 모든 빌드가 새 내용을 씁니다. 여러 버전을 동시에 유지해야 한다면 아래의 Git 레지스트리가 맞습니다.

공식 레지스트리 PR

오픈소스 라이브러리를 vcpkg 공식 레지스트리(microsoft/vcpkg)에 기여하는 절차:

  1. vcpkg 저장소를 fork하고 ports/<port-name>/에 vcpkg.json, portfile.cmake, usage를 추가합니다.
  2. 여러 triplet에서 vcpkg install로 빌드를 확인하고, vcpkg format-manifest로 매니페스트 형식을 맞춥니다.
  3. 포트를 커밋한 뒤 vcpkg x-add-version <port-name>을 실행합니다. 이 명령은 커밋된 포트 디렉터리의 Git 트리 해시를 읽어 versions/baseline.json과 versions/<첫 글자>-/<port-name>.json을 갱신하므로, 포트를 커밋하기 전에 실행하면 원하는 결과가 나오지 않습니다.
  4. 버전 파일 변경을 커밋하고 PR을 올린 뒤 CI 결과와 메인테이너 리뷰에 대응합니다.
cd vcpkg
git add ports/my-new-port
git commit -m "[my-new-port] new port"
./vcpkg x-add-version my-new-port
git add versions/
git commit -m "[my-new-port] add version"

Git 기반 사내 레지스트리

팀 전체가 버전을 지정해 쓰는 사내 패키지 저장소가 필요하면 Git 레지스트리를 만듭니다. 구조는 공식 레지스트리와 같습니다.

my-vcpkg-registry/
├── ports/
│   └── internal-lib/
│       ├── vcpkg.json
│       └── portfile.cmake
└── versions/
    ├── baseline.json
    └── i-/
        └── internal-lib.json

프로젝트의 vcpkg-configuration.json:

{
  "default-registry": {
    "kind": "git",
    "repository": "https://github.com/microsoft/vcpkg",
    "baseline": "<vcpkg 저장소 커밋 SHA>"
  },
  "registries": [
    {
      "kind": "git",
      "repository": "https://github.com/my-org/my-vcpkg-registry",
      "baseline": "<사내 레지스트리 커밋 SHA>",
      "packages": ["internal-lib"]
    }
  ]
}

Git 레지스트리의 baseline은 브랜치 이름이 아니라 그 레지스트리 저장소의 커밋 SHA입니다. 이 커밋의 versions/baseline.json에 적힌 버전이 각 포트의 기본 버전이 되며, 커밋을 고정해 두기 때문에 레지스트리에 새 버전이 올라와도 프로젝트가 baseline을 갱신하기 전까지는 빌드가 바뀌지 않습니다. 사내 레지스트리에서도 포트를 고칠 때마다 공식 레지스트리와 같은 방식으로 x-add-version을 실행해 버전 파일을 갱신해야 합니다.

바이너리 캐시로 CI 가속

포트를 매번 소스에서 빌드하면 CI가 느려지므로, 빌드 결과를 바이너리 캐시에 저장해 재사용합니다. 캐시 키에는 포트 내용, 의존성, triplet, 컴파일러 정보가 들어가므로 오버레이 포트도 똑같이 캐시됩니다. GitHub Actions에서 GitHub Packages(NuGet 피드)를 캐시로 쓰는 설정의 예는 다음과 같습니다.

env:
  VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/<OWNER>/index.json,readwrite"

NuGet 피드 인증 설정이 추가로 필요하며, 공유 디렉터리를 쓰는 files,<경로>,readwrite나 클라우드 스토리지 제공자도 고를 수 있습니다. 예전에 쓰이던 x-gha 제공자는 GitHub Actions 캐시 API 변경으로 제거되었습니다.

소스 유형별 portfile 함수

소스 유형다운로드 함수빌드 함수
GitHubvcpkg_from_githubvcpkg_cmake_configure
GitLabvcpkg_from_gitlabvcpkg_cmake_configure
일반 URLvcpkg_download_distfile + vcpkg_extract_source_archivevcpkg_cmake_configure
Git 서버vcpkg_from_gitvcpkg_cmake_configure
Autotoolsvcpkg_from_*vcpkg_configure_make (또는 vcpkg_make_configure)
Mesonvcpkg_from_*vcpkg_configure_meson + vcpkg_install_meson
헤더 전용vcpkg_from_*file(INSTALL …)

멀티 플랫폼 CI·캐시·사내 오버레이 공유

멀티 플랫폼 CI 매트릭스

# .github/workflows/build.yml
strategy:
  matrix:
    include:
      - os: ubuntu-latest
        triplet: x64-linux
      - os: windows-latest
        triplet: x64-windows-static
      - os: macos-latest
        triplet: arm64-osx
steps:
  - uses: actions/checkout@v4
    with:
      submodules: recursive
  - name: Configure
    run: |
      cmake -B build -S . \
        -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake \
        -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }}

CI vcpkg 캐시

CI에서 캐시할 대상은 buildtrees나 packages(빌드 중간 산물과 임시 디렉터리)가 아니라 vcpkg의 바이너리 캐시입니다. 바이너리 캐시는 ABI 해시로 찾으므로, 의존성이 일부만 바뀌어도 바뀌지 않은 패키지는 캐시에서 그대로 복원됩니다.

- name: Cache vcpkg binaries
  uses: actions/cache@v4
  with:
    path: ${{ github.workspace }}/vcpkg-binary-cache
    key: vcpkg-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }}
    restore-keys: vcpkg-${{ matrix.triplet }}-
- name: Configure
  env:
    VCPKG_BINARY_SOURCES: "clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite"
  shell: bash
  run: |
    mkdir -p vcpkg-binary-cache
    cmake --preset vcpkg-default

restore-keys로 이전 캐시를 복원해도 해시가 맞는 패키지만 재사용되므로 잘못된 바이너리가 섞일 걱정은 없습니다. 팀 규모가 커지면 파일 디렉터리 대신 사내 NuGet 피드나 클라우드 스토리지를 VCPKG_BINARY_SOURCES로 지정해 여러 파이프라인이 캐시를 공유하기도 합니다.

CMakePresets.json

{
  "version": 3,
  "configurePresets": [
    {
      "name": "vcpkg-default",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake"
      }
    },
    {
      "name": "vcpkg-static",
      "inherits": "vcpkg-default",
      "cacheVariables": {
        "VCPKG_TARGET_TRIPLET": "x64-windows-static"
      }
    }
  ]
}

의존성 업데이트 전략

1. 개발: 주기적으로 baseline 업데이트, 로컬 테스트
2. 스테이징: 업데이트 후 CI 전체 통과 확인
3. 프로덕션: builtin-baseline이 고정된 vcpkg.json 커밋으로 버전 고정

사내 오버레이 공유

회사 레포지토리: company/vcpkg-ports
각 프로젝트에서:
  -DVCPKG_OVERLAY_PORTS="$(pwd)/../vcpkg-ports"
또는 서브모듈로 vcpkg-ports 포함

참고 자료


자주 묻는 질문 (FAQ)

vcpkg Classic 모드와 Manifest 모드 차이는?

Classic 모드는 vcpkg install fmt처럼 명령으로 vcpkg 디렉터리에 전역 설치하므로, 무엇이 설치되어 있는지가 그 머신의 상태에 따라 다릅니다. Manifest 모드는 프로젝트의 vcpkg.json에 의존성을 선언하고 CMake 설정 시 빌드 디렉터리에 설치하므로, 저장소만 있으면 어디서든 같은 의존성을 재현할 수 있습니다.

CMAKE_TOOLCHAIN_FILE을 매번 지정해야 하나요?

CMakePresets.json에 cacheVariables로 넣어 두면 cmake --preset vcpkg-default로 한 번에 설정됩니다. IDE(CLion, VS)에서도 preset을 선택하면 됩니다.

사내 라이브러리를 vcpkg로 쓰려면?

오버레이 포트를 만듭니다. my-ports/내라이브러리/에 vcpkg.json과 portfile.cmake를 두며, -DVCPKG_OVERLAY_PORTS로 경로를 지정합니다. 자세한 내용은 vcpkg 패키지 만들기를 참고하세요.

builtin-baseline을 언제 업데이트하나요?

의존성의 보안 패치나 버그 수정이 필요할 때, 또는 새 라이브러리를 추가하려는데 현재 baseline에 없을 때 올립니다. baseline을 올리면 명시하지 않은 모든 포트가 함께 바뀌므로, 별도 브랜치에서 전체 빌드와 테스트를 통과시킨 뒤 반영합니다.

다음 글: vcpkg 고급 활용 | Manifest·Triplet·오버레이·바이너리 캐시

이전 글: Visual Studio로 C++ 프로파일링·메모리 진단하기


같이 보면 좋은 글