C++ vcpkg 고급 | overrides·커스텀 Triplet·사내 포트·바이너리 캐시·CI

들어가며: vcpkg 기본을 넘어서

설치, Manifest 모드, triplet 개념, 기본적인 오버레이 포트, 초보 단계에서 자주 만나는 에러(툴체인 미지정, baseline 누락, 패키지 빌드 실패, CMake 버전)는 vcpkg 기초 글에서 다뤘습니다. 이 글은 그 다음, 팀과 CI가 붙으면서 생기는 문제를 다룹니다.

"Windows에서는 되는데 Linux 서버에서만 링크 에러가 나요."
"사내에서 수정한 라이브러리를 vcpkg로 쓰고 싶습니다."
"CI에서 매번 30분씩 vcpkg 빌드하는데, 캐시로 줄일 수 없나요?"
"builtin-baseline 업데이트 후 갑자기 빌드가 깨졌습니다."

이 글에서 다루는 것:

  • baseline 업그레이드 전략과 overrides: 버전 집합을 언제, 어떻게 올릴지
  • 커스텀 Triplet: 팀 전체가 같은 링크 방식·컴파일 옵션을 쓰게 만들기
  • 사내 포트와 패치: 오버레이 포트를 실제 사내 라이브러리에 적용하기, 레지스트리로 넘어가는 시점
  • 바이너리 캐시: CI에서 같은 의존성을 반복 빌드하지 않기
  • 프로덕션 패턴: 멀티 플랫폼 CI 매트릭스, Docker

요구 환경: vcpkg 2024.01+, CMake 3.21+, C++17 이상


고급 활용이 필요해지는 상황

증상대개의 원인이 글의 해결책
플랫폼마다 다른 링크 에러triplet이 플랫폼마다 달라 동적/정적 링크가 섞임커스텀 triplet
사내 fork·패치된 라이브러리 필요공식 레지스트리에 없는 패키지오버레이 포트 → 사내 레지스트리
PR마다 Boost·OpenSSL을 처음부터 빌드빌드 결과를 재사용하지 않음바이너리 캐시
baseline을 올리자 코드가 깨짐여러 패키지가 한꺼번에 메이저 업그레이드됨baseline 전략 + overrides

네 가지는 서로 얽혀 있습니다. 예를 들어 바이너리 캐시의 키에는 triplet 파일 내용과 포트 버전이 들어가므로, triplet을 팀마다 제각각 두면 캐시 적중률도 떨어집니다. 그래서 순서도 “버전 집합 고정 → triplet 통일 → 사내 포트 → 캐시”로 잡는 편이 결과가 안정적입니다.


baseline 업그레이드 전략과 overrides

version>= 같은 버전 제약 문법, platform·features 필드, builtin-baseline의 기본 개념은 기초 글의 Manifest·버전 관리 절을 참고하세요. 여기서는 제약 문법이 실제로 어떻게 해석되는지, 그리고 팀에서 baseline을 어떻게 운영하는지에 집중합니다.

제약은 “최소 버전”만 표현한다

vcpkg의 버전 해석은 npm이나 pip와 다릅니다. vcpkg.json에 쓸 수 있는 제약은 version>=(최소 버전) 하나뿐이고, 상한(version< 같은 것)은 없습니다. 해석 규칙은 “baseline이 정한 버전과 모든 version>= 제약 중 가장 높은 것을 고른다”입니다. 따라서 version>=는 버전을 올릴 수는 있어도 막을 수는 없습니다.

상한이 필요해 보이는 상황, 즉 “baseline을 올렸더니 fmt가 11로 올라가서 깨졌다”는 경우에는 두 가지 선택지가 있습니다.

  1. baseline을 올리지 않거나, 문제 없는 커밋으로 되돌립니다.
  2. overrides로 그 패키지만 특정 버전에 고정합니다.
{
  "name": "my-production-app",
  "version": "1.0.0",
  "dependencies": [
    "fmt",
    { "name": "spdlog", "version>=": "1.11.0" }
  ],
  "overrides": [
    { "name": "fmt", "version": "10.1.1" }
  ],
  "builtin-baseline": "<vcpkg 저장소의 커밋 SHA>"
}

overrides는 다른 모든 제약보다 우선합니다. 그래서 강력하지만 함정도 있습니다. spdlog처럼 fmt에 의존하는 패키지가 새 버전에서 fmt 11 API를 요구하면, fmt를 10으로 눌러 둔 상태에서는 spdlog 쪽 빌드가 깨집니다. overrides를 쓸 때는 그 패키지에 의존하는 다른 포트도 같이 낮춰야 하는지 확인해야 하고, 고정한 이유를 커밋 메시지나 주석성 문서에 남겨 두지 않으면 몇 달 뒤 아무도 풀지 못하는 고정으로 남습니다.

lock 파일이 없다는 점

vcpkg에는 package-lock.json 같은 lock 파일이 없습니다. 재현성의 기준은 vcpkg.json의 builtin-baseline(과 vcpkg-configuration.json의 레지스트리 baseline)이며, 같은 baseline + 같은 overrides + 같은 vcpkg 도구 버전이면 같은 버전 집합이 나옵니다. baseline이 가리키는 것은 vcpkg 저장소 커밋이므로, 그 커밋이 로컬 vcpkg 클론에 없으면 “baseline을 찾을 수 없다”는 에러가 납니다. vcpkg를 서브모듈로 두거나 CI에서 전체 히스토리를 가져오는(fetch-depth: 0) 이유가 여기 있습니다.

baseline을 올리는 방식

baseline을 한 번 올리면 수십 개 패키지의 버전이 동시에 바뀔 수 있습니다. 제가 권하는 운영 방식은 이렇습니다.

  1. 프로덕션 브랜치에서는 baseline을 고정하고, 올릴 때는 별도 브랜치에서 vcpkg x-update-baseline으로 갱신합니다.
  2. 그 브랜치에서 전체 CI 매트릭스를 돌립니다. 이때 바이너리 캐시가 대부분 무효화되므로 빌드가 오래 걸리는 것이 정상입니다.
  3. 깨지는 패키지가 있으면 그 패키지만 overrides로 임시 고정하고, 고정 해제를 별도 작업으로 남깁니다.
  4. 보안 패치(OpenSSL 등)가 급할 때는 baseline 전체를 올리기보다 해당 패키지만 version>=로 올리는 편이 영향 범위가 작습니다.

baseline을 오래 방치하면 한 번에 올려야 하는 폭이 커져서 결국 아무도 올리지 못하게 됩니다. 분기에 한 번 정도 정기적으로 올리는 편이, 1년 만에 한 번 올리는 것보다 전체 비용이 낮습니다.

플랫폼별 의존성과 CMake를 맞추기

플랫폼 조건("platform": "windows")을 vcpkg.json에 걸었다면 CMake 쪽 find_package에도 같은 조건을 걸어야 합니다. 한쪽만 조건을 걸면, 예를 들어 Linux에서 vcpkg는 OpenSSL을 설치하지 않았는데 CMake는 시스템 OpenSSL을 찾게 되어 “로컬에서는 되는데 CI에서만 다른 버전이 링크되는” 상황이 생깁니다.

# vcpkg.json: { "name": "openssl", "platform": "windows" }
# Windows는 vcpkg의 OpenSSL, Linux는 시스템 OpenSSL을 쓴다는 의도를 CMake에도 똑같이 반영
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)
if(NOT WIN32)
  message(STATUS "Using system OpenSSL ${OPENSSL_VERSION}")
endif()

두 소스를 섞는 구성은 결국 “어떤 OpenSSL이 링크됐는가”를 빌드 로그로 확인하는 습관이 필요합니다. 여유가 있다면 모든 플랫폼에서 vcpkg 버전을 쓰는 쪽이 디버깅 비용이 적습니다.


커스텀 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 패키지 만들기에 더 자세히 있습니다. 여기서는 실제 사내 라이브러리를 포트로 만드는 경우와 공식 포트에 패치를 거는 경우를 봅니다.

사내 git 저장소의 라이브러리

my-ports/internal-lib/vcpkg.json:

{
  "name": "internal-lib",
  "version": "1.0.0",
  "description": "사내 공통 라이브러리",
  "license": null,
  "dependencies": [
    "fmt",
    { "name": "vcpkg-cmake", "host": true },
    { "name": "vcpkg-cmake-config", "host": true }
  ]
}

my-ports/internal-lib/portfile.cmake:

vcpkg_from_git(
    OUT_SOURCE_PATH SOURCE_PATH
    URL "https://git.company.com/libs/internal-lib.git"
    REF "3f2c1a9e0b7d..."   # 태그 이름이 아니라 커밋 SHA
    HEAD_REF main
)
vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}")
vcpkg_cmake_install()
vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/internal-lib)
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE")

사내 포트에서 가장 자주 밟는 함정은 REF입니다. vcpkg_from_git의 REF에는 커밋 SHA를 넣어야 합니다. 태그는 다시 찍힐 수 있고, 그러면 바이너리 캐시 키는 같은데 내용이 다른 패키지가 나오는 가장 추적하기 어려운 종류의 문제가 생깁니다. 또 포트의 version을 올리지 않고 REF만 바꾸면, 이미 캐시된 옛 바이너리가 그대로 복원될 수 있습니다(캐시 키는 포트 파일 내용도 포함하므로 대부분은 바뀌지만, 사람이 보기에 “버전은 같은데 내용이 다른” 상태가 됩니다). 소스를 바꿀 때는 version이나 port-version도 같이 올리는 규칙을 두는 편이 안전합니다.

CI 러너가 사내 git 서버에 접근할 수 있어야 한다는 점도 잊기 쉽습니다. 로컬에서는 SSH 키로 잘 받아지다가 CI에서만 포트 다운로드가 실패하는 경우, 대부분 URL이 SSH 형식인데 러너에는 HTTPS 토큰만 있는 경우입니다.

공식 포트에 패치 걸기

공식 포트를 오버레이로 복사한 뒤 패치만 추가하는 방식입니다. 패치는 vcpkg_from_github의 PATCHES 인자로 넘깁니다(예전 vcpkg_apply_patches는 더 이상 권장되지 않습니다).

my-ports/spdlog/portfile.cmake (오버레이 디렉터리 이름이 공식 포트 이름과 같아야 대체됩니다):

vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO gabime/spdlog
    REF "v${VERSION}"
    SHA512 <공식 포트의 값을 그대로 복사>
    HEAD_REF v1.x
    PATCHES
        fix-logging.patch
)
vcpkg_cmake_configure(
    SOURCE_PATH "${SOURCE_PATH}"
    OPTIONS -DSPDLOG_ENABLE_PCH=OFF
)
vcpkg_cmake_install()
vcpkg_copy_pdbs()

오버레이로 공식 포트를 대체하는 순간, 그 포트는 baseline을 올려도 따라 올라가지 않습니다. 오버레이는 버전 해석보다 우선하기 때문입니다. 그래서 공식 포트 패치는 upstream에 PR을 올려서 반영되면 오버레이를 지우는 것을 목표로 해야 하고, 오버레이 디렉터리마다 “왜, 언제까지”를 README에 적어 두지 않으면 몇 년 된 spdlog가 조용히 남아 있게 됩니다.

오버레이에서 레지스트리로 넘어갈 때

오버레이는 “현재 디렉터리에 있는 그 버전 하나”만 제공합니다. 프로젝트 여러 개가 같은 사내 포트를 서로 다른 버전으로 써야 하는 시점이 오면 git 레지스트리가 필요합니다. 레지스트리는 ports/와 versions/ 디렉터리를 가진 git 저장소이고, 프로젝트는 vcpkg-configuration.json에서 이를 참조합니다.

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

packages 목록에 적은 이름은 공식 레지스트리가 아니라 사내 레지스트리에서 해석됩니다. 레지스트리 쪽은 포트를 추가할 때마다 vcpkg x-add-version으로 versions/ 데이터베이스를 갱신해야 하므로, 사람 손보다는 레지스트리 저장소의 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을 찾지 못합니다.


고급 설정에서 나는 에러

툴체인 미지정, baseline에 포트가 없음, 패키지 빌드 실패, CMake 버전 같은 기본 에러는 기초 글의 에러 절을 보세요. 아래는 triplet·오버레이·캐시·버전 고정을 쓰면서 새로 나타나는 것들입니다.

”multiple definition” / LNK2038 RuntimeLibrary 불일치

error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MT_StaticRelease' doesn't match value 'MD_DynamicRelease'

원인: 의존성은 한 CRT로, 앱은 다른 CRT로 빌드됨. 또는 vcpkg 라이브러리와 시스템에 설치된 같은 라이브러리가 섞임. 해결: 앱의 CRT 설정을 triplet의 VCPKG_CRT_LINKAGE와 맞춥니다. CMake 3.15+에서는 CMAKE_MSVC_RUNTIME_LIBRARY로 지정합니다(MultiThreaded$<$<CONFIG:Debug>:Debug>는 정적 CRT, 뒤에 DLL이 붙으면 동적 CRT).

“spdlog requires C++17”

원인: 의존성이 요구하는 표준보다 프로젝트 표준이 낮음. 오버레이 포트로 새 버전을 넣었을 때 자주 생깁니다. 해결: target_compile_features(my-app PRIVATE cxx_std_17)처럼 타깃에 요구 사항을 걸어 둡니다.

오버레이 포트를 찾지 못함

error: while loading internal-lib: the port directory does not exist

원인: VCPKG_OVERLAY_PORTS에 상대 경로를 써서 빌드 디렉터리 기준으로 해석됐거나, 오버레이 디렉터리 한 단계를 잘못 지정함(포트 하나의 디렉터리가 아니라 포트들을 담은 부모 디렉터리를 지정해야 함). 해결: 절대 경로를 쓰거나 vcpkg-configuration.json의 overlay-ports에 등록합니다(이 파일 기준 상대 경로로 해석됨).

overrides 버전이 버전 DB에 없음

error: no version database entry for fmt at 10.1.3

원인: overrides에 적은 버전이 레지스트리의 versions/ 데이터베이스에 없음(그 버전의 포트가 존재한 적이 없거나, 로컬 vcpkg 클론이 오래됨). 해결: vcpkg x-history fmt로 사용 가능한 버전과 port-version을 확인합니다.

CI에서만 “Could NOT find”

원인: 로컬에는 Classic 모드로 전역 설치한 패키지가 남아 있어서 vcpkg.json에 빠진 의존성이 우연히 찾아지고 있었음. 해결: vcpkg.json에 의존성을 선언합니다. 로컬에서도 새 빌드 디렉터리와 새 vcpkg 클론으로 한 번씩 빌드해 보면 이런 숨은 의존성이 드러납니다.

바이너리 캐시 복원 실패

warning: failed to restore package from binary cache

원인: 캐시 파일이 중간에 잘렸거나(용량 제한, 중단된 업로드) 권한 문제. 해결: vcpkg는 복원에 실패하면 소스에서 다시 빌드하므로 빌드 자체는 계속됩니다. 반복되면 해당 캐시를 비우거나 CI 캐시 키를 바꿉니다.


정리

항목핵심
버전제약은 version>=뿐, 고정은 overrides, 재현성은 baseline + 커밋된 설정 파일
TripletCRT는 VCPKG_CRT_LINKAGE로, 파일은 저장소에 한 벌만
사내 포트REF는 커밋 SHA, 공식 포트 오버레이는 baseline을 따라가지 않음, 여러 프로젝트면 레지스트리
캐시바이너리 캐시 디렉터리를 보존, triplet별 키, PR 빌드는 읽기 전용
프로덕션CMakePresets로 로컬·CI 명령 통일, Docker는 의존성 레이어 분리

다음 글: [C++ #53-4] Conan 레시피 작성 | 패키지 배포·의존성 관리 이전 글: [C++ #53-2] Visual Studio C++


같이 보면 좋은 글