Conan 2.x 심화: lockfile로 의존성 고정, 크로스 컴파일, 사내 Artifactory 연동
들어가며: 기초 다음에 생기는 문제들
설치, conanfile.txt, 기본 프로필, remote 추가, CMake 연동, 그리고 처음 쓸 때 자주 나는 에러(find_package 실패, Version conflict, conan_toolchain.cmake를 찾지 못함)는 Conan 기초 글에서 다뤘습니다. 이 글은 팀과 CI, 여러 타깃 플랫폼이 붙으면서 생기는 문제를 다룹니다.
“어제까지 되던 빌드가 오늘 안 됨”: 직접 의존성은 fmt/10.1.1처럼 고정했어도, 의존성의 의존성이 zlib/[>=1.2.11 <2] 같은 버전 범위로 선언되어 있으면 Conan Center에 새 버전이 올라오는 순간 그래프가 바뀝니다. 아무도 버전을 바꾸지 않았는데 결과가 달라집니다.
사내 라이브러리 배포: 팀에서 만든 lib-core를 여러 프로젝트에서 씁니다. 수동으로 복사·빌드하면 버전 관리가 안 되므로 사내 Artifactory에 lib-core/1.2.3으로 올리고 싶습니다.
크로스 컴파일: x86_64 호스트에서 ARM 타깃 빌드를 해야 합니다. Conan Center에 ARM 바이너리가 없는 패키지는 소스에서 빌드해야 하고, 그때 빌드 도구(CMake, protoc 등)는 호스트용이어야 합니다.
CI가 느림: 매 빌드마다 같은 의존성을 다시 다운로드·빌드합니다.
| 항목 | 내용 |
|---|---|
| lockfile | 의존성 그래프 고정, 의도적인 업데이트만 허용 |
| conanfile.py 고급 | 옵션, override/force, 배포용 레시피 |
| 크로스 빌드 | host/build 두 프로필, tool_requires |
| 사내 레포 | Artifactory 업로드, 바이너리 재사용 |
| 프로덕션 패턴 | CI 캐시, 빌드 스크립트 |
lockfile로 의존성 고정
왜 버전을 적어도 바뀌는가
Conan 2.x에서 레시피의 requires는 정확한 버전일 수도, 범위([>=1.2 <2])일 수도 있습니다. Conan Center의 레시피는 전이 의존성에 범위를 쓰는 경우가 많습니다. 게다가 같은 버전이라도 레시피 자체가 수정되면 recipe revision이 새로 생기고, Conan은 기본적으로 최신 revision을 씁니다. 즉 “버전은 같은데 레시피가 달라져 빌드 옵션이 바뀐” 경우도 있습니다. lockfile은 이 두 가지, 버전과 revision을 모두 기록합니다.
lockfile 만들고 쓰기
# 현재 conanfile로 그래프를 해석해 conan.lock 생성
conan lock create . --lockfile-out=conan.lock
# 이후 설치는 lockfile 기준
conan install . --lockfile=conan.lock --build=missing
conan.lock은 Git에 커밋합니다. lockfile은 기본적으로 엄격해서, conanfile에 lockfile에 없는 의존성을 추가하면 설치가 실패합니다. 이 실패는 버그가 아니라 lockfile이 제 역할을 하는 것입니다. 새 의존성을 추가하는 것도 의도적인 변경이므로 lockfile을 다시 만들어야 합니다.
lockfile에 여러 구성을 담기
Debug/Release, Linux/Windows처럼 여러 구성에서 쓰는 lockfile은 각 구성으로 해석한 결과를 합쳐야 합니다. 구성에 따라 조건부 의존성이 달라지기 때문입니다.
conan lock create . -s build_type=Release --lockfile-out=conan.lock
conan lock create . -s build_type=Debug --lockfile=conan.lock --lockfile-out=conan.lock
conan lock create . -pr:h=profiles/windows-msvc --lockfile=conan.lock --lockfile-out=conan.lock
두 번째 명령부터는 기존 lockfile을 입력으로 받아 확장합니다. 한 구성으로만 만든 lockfile을 커밋하면, 다른 구성(예: Windows에서만 쓰는 의존성)이 CI에서 “lockfile에 없음”으로 실패합니다.
의존성 하나만 올리기
# fmt 항목만 lockfile에서 지우고 다시 해석 → fmt만 새 버전으로
conan lock remove --requires="fmt/*" --lockfile=conan.lock --lockfile-out=conan.lock
conan lock create . --lockfile=conan.lock --lockfile-out=conan.lock
lockfile 전체를 지우고 다시 만들면 fmt뿐 아니라 범위로 걸린 모든 전이 의존성이 한꺼번에 최신으로 바뀝니다. 한 번에 하나씩 올리면 문제가 생겼을 때 원인이 분명하고, 코드 리뷰에서 lockfile diff도 읽을 만한 크기로 유지됩니다.
conanfile.py 고급
소비용 conanfile.py의 기본 형태, 플랫폼별 조건부 의존성, 의존성 옵션을 -o로 넘기는 방법은 기초 글의 conanfile.py 절에 있습니다. 여기서는 옵션을 스스로 정의하는 레시피, 충돌 해결, 배포용 레시피를 봅니다.
옵션 정의와 검증
from conan import ConanFile
from conan.tools.build import check_min_cppstd
from conan.tools.cmake import cmake_layout
class MyAppConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeDeps", "CMakeToolchain"
options = {"with_ssl": [True, False], "with_tests": [True, False]}
default_options = {"with_ssl": True, "with_tests": False}
def layout(self):
cmake_layout(self)
def requirements(self):
self.requires("fmt/10.1.1")
self.requires("spdlog/1.12.0")
if self.options.with_ssl:
self.requires("openssl/3.2.0")
def build_requirements(self):
if self.options.with_tests:
self.test_requires("gtest/1.14.0")
def validate(self):
check_min_cppstd(self, "17")
몇 가지를 짚고 넘어가겠습니다.
- 검증은
validate()에 둡니다.configure()에서 예외를 던지면 그래프 해석 자체가 멈추지만,validate()에서 실패하면 Conan이 “이 구성은 지원하지 않음(Invalid configuration)“으로 표시하고conan graph info등에서 이유를 보여 줍니다. - 테스트 전용 의존성은
test_requires로 선언합니다. 일반requires로 넣으면 이 패키지를 쓰는 소비자에게까지 gtest가 전파됩니다. cmake_layout을 쓰면 생성 파일 위치가 바뀝니다.conan install .만 실행하면build/Release/generators/conan_toolchain.cmake(멀티 구성 생성기에서는build/generators/)에 생깁니다. 기초 글처럼--output-folder=build와-DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake를 쓰던 방식과 섞으면 툴체인 경로가 맞지 않습니다.cmake_layout을 쓸 때는 Conan이 만들어 주는CMakeUserPresets.json으로cmake --preset conan-release를 쓰는 편이 경로 실수가 없습니다.
conan install . -o "&:with_ssl=False" --build=missing
cmake --preset conan-release
cmake --build --preset conan-release
&:는 “소비자 자신(현재 conanfile)“의 옵션이라는 뜻입니다. 의존성의 옵션은 -o "spdlog/*:shared=True"처럼 패턴으로 지정합니다(1.x의 spdlog:shared 문법은 2.x에서 쓰지 않습니다).
버전 충돌: override와 force
두 의존성이 같은 패키지를 다른 버전으로 요구하면 Conan 2는 그래프 단계에서 멈춥니다.
ERROR: Version conflict: Conflict between fmt/9.1.0 and fmt/10.1.1 in the graph.
해결 방법은 둘입니다.
def requirements(self):
self.requires("lib-a/1.0") # fmt/9.1.0 요구
self.requires("lib-b/2.0") # fmt/10.1.1 요구
# 1) override: 그래프 안에 fmt가 있다면 이 버전으로 맞춤 (직접 의존성은 추가하지 않음)
self.requires("fmt/10.1.1", override=True)
# 2) force: 직접 의존성으로 추가하면서 아래쪽 요구를 모두 덮어씀
# self.requires("fmt/10.1.1", force=True)
override는 “fmt를 직접 쓰지는 않지만 그래프의 fmt 버전은 내가 정한다”는 뜻이고, force는 “fmt를 직접 쓰고, 아래에서 뭐라 하든 이 버전”이라는 뜻입니다. 어느 쪽이든 Conan은 API 호환성을 확인해 주지 않습니다. lib-a가 fmt 9의 API로 작성되어 있다면 fmt 10으로 맞춘 순간 lib-a를 소스에서 빌드할 때 컴파일 에러가 날 수 있습니다. 버전 충돌을 override로 “해결”했다면, 그 조합으로 전체가 실제로 빌드되고 테스트가 통과하는지까지 확인해야 해결입니다.
배포용 레시피
배포용 레시피 작성은 Conan 레시피 작성 글에서 자세히 다룹니다. 여기서는 사내 배포에 필요한 최소 형태만 보입니다.
from conan import ConanFile
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
class LibCoreConan(ConanFile):
name = "lib-core"
version = "1.2.3"
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False], "fPIC": [True, False]}
default_options = {"shared": False, "fPIC": True}
exports_sources = "CMakeLists.txt", "src/*", "include/*"
def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC
def configure(self):
if self.options.shared:
self.options.rm_safe("fPIC")
def layout(self):
cmake_layout(self)
def requirements(self):
# 공개 헤더에 fmt 타입이 노출되므로 transitive_headers 필요
self.requires("fmt/10.1.1", transitive_headers=True)
def generate(self):
CMakeDeps(self).generate()
CMakeToolchain(self).generate()
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
CMake(self).install()
def package_info(self):
self.cpp_info.libs = ["lib-core"]
파일을 copy()로 하나하나 복사하기보다 CMake의 install() 규칙을 쓰는 편이 Windows(.lib/.dll)와 Linux(.a/.so)를 따로 신경 쓸 필요가 없습니다. 그리고 transitive_headers를 빼먹으면, lib-core를 쓰는 쪽에서 lib-core 헤더가 #include <fmt/core.h>를 할 때 fmt 헤더를 찾지 못합니다. Conan 2는 기본적으로 전이 의존성의 헤더를 소비자에게 노출하지 않기 때문입니다.
크로스 컴파일
host 프로필과 build 프로필
Conan 2의 크로스 빌드는 두 프로필로 표현합니다. host 프로필은 결과물이 실행될 타깃, build 프로필은 빌드가 실행되는 머신입니다. requires는 host용으로, tool_requires(CMake, protoc, flatc 같은 빌드 도구)는 build용으로 해석됩니다. 이 구분 덕분에 “ARM용 protobuf 라이브러리 + x86_64에서 실행되는 protoc”가 한 번의 install로 해결됩니다.
# profiles/linux-armv8
[settings]
os=Linux
arch=armv8
compiler=gcc
compiler.version=11
compiler.libcxx=libstdc++11
compiler.cppstd=17
build_type=Release
[conf]
tools.build:compiler_executables={"c": "aarch64-linux-gnu-gcc", "cpp": "aarch64-linux-gnu-g++"}
[buildenv]
AR=aarch64-linux-gnu-ar
STRIP=aarch64-linux-gnu-strip
Conan 1.x 예제에서 흔히 보이는 [env] 섹션은 2.x에서 사라졌습니다. 컴파일러 경로는 tools.build:compiler_executables로, 빌드 중 환경 변수는 [buildenv]로 지정합니다. 1.x 프로필을 그대로 복사해 쓰면 에러 없이 무시되는 항목이 생겨서, 크로스 컴파일러가 아니라 호스트 gcc로 빌드된 x86_64 바이너리가 ARM 패키지로 캐시에 들어가는 사고가 납니다. 크로스 빌드 결과물은 file 명령으로 아키텍처를 한 번 확인하는 습관이 필요합니다.
conan install . -pr:h=profiles/linux-armv8 -pr:b=default --build=missing
cmake --preset conan-release
cmake --build --preset conan-release
file build/Release/myapp # ELF 64-bit LSB ... ARM aarch64 인지 확인
conan_toolchain.cmake가 CMAKE_SYSTEM_NAME, CMAKE_SYSTEM_PROCESSOR, 컴파일러를 설정하므로 CMake 명령에 따로 넘길 필요는 없습니다.
ARM 바이너리가 없을 때
Conan Center는 주요 x86_64·armv8(macOS) 구성의 바이너리만 제공합니다. 임베디드 Linux ARM 구성은 대부분 --build=missing으로 소스에서 빌드됩니다.
# 없는 것만 빌드
conan install . -pr:h=profiles/linux-armv8 --build=missing
# 특정 패키지만 강제로 소스 빌드
conan install . -pr:h=profiles/linux-armv8 --build="fmt/*" --build=missing
한 번 빌드한 ARM 바이너리를 사내 remote에 올려 두면 다음부터는 다운로드로 끝납니다. 크로스 빌드에서 사내 remote의 가치가 가장 크게 드러나는 지점이 여기입니다.
사내 레포지토리 연동
remote 추가·목록·우선순위 조정(--index)은 기초 글의 Remote 절을 참고하세요. 여기서는 패키지를 만들어 올리고, CI에서 재사용하는 흐름을 봅니다.
# Artifactory Conan 레포 등록 (우선 검색되도록 index 0)
conan remote add mycompany https://mycompany.jfrog.io/artifactory/api/conan/conan-local --index 0
conan remote login mycompany ci-user -p "$ARTIFACTORY_TOKEN"
# 패키지 생성 (name/version은 레시피에 있음)
conan create . -pr:h=profiles/linux-armv8 --build=missing
# 레시피와 바이너리 업로드 (-c: 확인 프롬프트 없이)
conan upload "lib-core/1.2.3" -r mycompany -c
Conan 2의 conan upload는 레시피와 캐시에 있는 모든 바이너리를 올립니다(1.x의 --all이 기본 동작이 됨). 그래서 CI 러너 캐시에 로컬 실험용 바이너리가 섞여 있으면 그것까지 올라갑니다. 업로드용 CI 잡은 깨끗한 캐시에서 conan create한 직후 올리는 식으로 분리하는 편이 안전합니다.
remote 순서도 보안과 관련이 있습니다. 사내 패키지 이름이 Conan Center에 같은 이름으로 존재하면, Conan Center가 먼저 검색되는 설정에서는 공개 패키지가 받아질 수 있습니다. 사내 remote를 앞에 두는 것에 더해, 사내 패키지 이름에 조직 접두사를 붙이거나 Artifactory의 가상 레포(virtual repository)로 Conan Center를 프록시해 remote를 하나만 노출하는 방식이 이런 혼동을 막습니다.
고급 설정에서 나는 에러
find_package 실패, 기본적인 Version conflict, 툴체인 파일을 찾지 못하는 문제는 기초 글의 에러 절에 있습니다.
Build type mismatch
증상: Debug로 빌드했는데 Release 라이브러리와 링크됩니다(MSVC에서는 LNK2038 _ITERATOR_DEBUG_LEVEL 불일치).
해결: conan install . -s build_type=Debug로 구성별로 따로 설치합니다. cmake_layout은 구성별 디렉터리를 나눠 주므로 Release와 Debug를 같은 빌드 트리에서 오갈 수 있습니다.
lockfile에 없는 의존성
ERROR: Requirement 'openssl/3.2.0' not in lockfile 'requires'
원인: conanfile에 의존성을 추가했거나, lockfile을 다른 구성(프로필·옵션)으로만 만들었음.
해결: conan lock create로 해당 구성까지 포함해 다시 만듭니다(1절의 여러 구성 합치기 참고).
compiler.version is not valid
원인: 프로필의 컴파일러 버전이 Conan의 settings.yml에 없는 값이거나(너무 새로운 컴파일러), 시스템 컴파일러와 다름.
해결: conan profile detect --force로 다시 감지하거나, 새 컴파일러라면 Conan을 업데이트합니다.
크로스 빌드에서 Binary missing 후 소스 빌드 실패
증상: fmt/10.1.1: WARN: Missing binary 후 소스 빌드를 시도하다 CMake를 찾지 못하거나, 호스트용 도구가 실행되지 않음(Exec format error).
원인: 빌드 도구를 requires로 넣어 타깃(ARM)용으로 받았거나, build 프로필을 지정하지 않음.
해결: 빌드 도구는 tool_requires로 선언하고, -pr:b를 명시합니다.
def build_requirements(self):
self.tool_requires("cmake/[>=3.25 <4]")
self.tool_requires("protobuf/3.21.12") # protoc는 build 머신용
CI와 바이너리 재사용
CI 캐시
- name: Cache Conan
uses: actions/cache@v4
with:
path: ~/.conan2/p
key: conan-${{ runner.os }}-${{ hashFiles('conanfile.*', 'conan.lock', 'profiles/**') }}
- name: Conan install
run: |
conan profile detect --force
conan install . --lockfile=conan.lock --build=missing
~/.conan2 전체가 아니라 패키지 저장소인 ~/.conan2/p를 캐시하면, 프로필·remote 설정이 캐시에서 복원되어 CI 설정과 어긋나는 일을 피할 수 있습니다. 캐시 키에 프로필 파일을 넣지 않으면 컴파일러 버전을 바꿔도 옛 캐시가 복원되고, Conan은 package ID가 달라 새로 빌드하므로 캐시가 쓸모없이 커지기만 합니다.
사내 remote를 바이너리 캐시로
러너별 캐시보다 효과가 큰 것은 main 브랜치 빌드가 --build=missing으로 새로 만든 바이너리를 사내 remote에 올리는 구성입니다. 그러면 PR 빌드와 다른 개발자는 다운로드만 합니다. 이때 PR 빌드에는 업로드 권한을 주지 않아야 검증되지 않은 바이너리가 공유 remote에 섞이지 않습니다.
빌드 스크립트
#!/bin/bash
set -euo pipefail
PROFILE=${1:-default}
conan install . --lockfile=conan.lock --build=missing -pr:h="$PROFILE" -pr:b=default
cmake --preset conan-release
cmake --build --preset conan-release
로컬과 CI가 이 스크립트 하나를 쓰게 하면 “CI에서만 다른 옵션”이 줄어듭니다. 프로필은 profiles/ 디렉터리에 두고 저장소에 커밋하거나, conan config install <git 저장소>로 팀 공통 설정(프로필, remote, 전역 conf)을 배포할 수 있습니다.
의존성 업데이트 흐름
- 브랜치에서
conan lock remove+conan lock create로 의존성 하나를 올립니다. - 전체 CI 매트릭스(모든 프로필)로 빌드·테스트합니다.
- 통과하면
conan.lockdiff와 함께 머지합니다. 새 바이너리는 main 빌드가 사내 remote에 올립니다.
참고 자료
같이 보면 좋은 글
- C++ Conan 기초 | 설치·conanfile·프로필·CMake 연동 [#53-4]
- C++ Conan 레시피 작성 | 패키지·빌드·원격 저장소 [#53-4]
- C++ vcpkg 고급 | overrides·커스텀 Triplet·사내 포트·바이너리 캐시
- C++ 빌드 시스템 비교 | CMake·Meson·Bazel·Makefile·패키지 매니저 선택 가이드