Conan 기초: conanfile로 의존성 선언, 프로필, Remote, CMake 연동
C++ 프로젝트에 fmt나 spdlog 같은 라이브러리를 넣을 때, 패키지 매니저가 없으면 소스를 받아 직접 빌드하고 include 경로와 라이브러리 경로를 CMake에 일일이 알려 줘야 합니다. 이 방식은 개발자마다 설치 위치와 버전이 달라지고, CI 서버에서는 같은 과정을 다시 스크립트로 만들어야 하며, Windows와 Linux에서 빌드 옵션이 달라질 때마다 문서가 늘어납니다.
Conan은 이 일을 선언적으로 바꿉니다. 필요한 패키지와 버전을 conanfile에 적고, 빌드 환경(OS, 컴파일러, 빌드 타입 등)을 프로필로 정해 두면, Conan이 그 조합에 맞는 바이너리를 원격 저장소에서 받거나 없으면 소스에서 빌드하고, CMake가 find_package로 찾을 수 있는 설정 파일을 생성해 줍니다. 이 글은 Conan 2.x를 기준으로 합니다. Conan 1.x와는 명령, 프로필 형식, 레시피 API가 많이 달라서 1.x용 자료의 명령을 그대로 쓰면 동작하지 않는 경우가 많습니다. 패키지 매니저의 개념 자체는 vcpkg와 Conan 기초(#40-1)에서, find_package의 동작은 CMake 입문에서 다룹니다.
설치와 기본 프로필
Conan은 파이썬 패키지로 배포되므로 pip로 설치합니다. 시스템 파이썬을 오염시키지 않도록 가상 환경에 설치하는 것을 권장합니다.
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install conan
conan --version # Conan version 2.x.x
처음 쓸 때는 기본 프로필을 만들어야 합니다. conan profile detect는 시스템의 기본 컴파일러를 찾아 OS, 아키텍처, 컴파일러 종류와 버전을 채운 프로필을 생성합니다.
conan profile detect --force
conan profile show # 감지된 설정 확인
conan profile path default # 파일 위치 (~/.conan2/profiles/default)
감지 결과는 반드시 한 번 확인해야 합니다. 예를 들어 기본 build_type은 Release이고, 컴파일러가 여러 개 설치된 시스템에서는 의도한 것과 다른 컴파일러를 잡을 수 있습니다. Windows에서는 Visual Studio가 설치되어 있으면 일반 명령 프롬프트에서도 MSVC를 감지합니다. Conan 2의 패키지 캐시는 ~/.conan2/(Windows는 %USERPROFILE%\.conan2\)에 있습니다.
conanfile.txt로 의존성 선언
패키지를 가져다 쓰기만 하는 프로젝트라면 conanfile.txt로 충분합니다.
[requires]
fmt/10.1.1
spdlog/1.12.0
nlohmann_json/3.11.2
[generators]
CMakeDeps
CMakeToolchain
[options]
spdlog/*:header_only=False
[requires]는 필요한 패키지와 버전입니다. [generators]의 CMakeDeps는 각 패키지의 fmt-config.cmake 같은 CMake 설정 파일을 만들고, CMakeToolchain은 이 파일들의 위치와 프로필의 컴파일러 설정을 담은 conan_toolchain.cmake를 만듭니다. [options]는 패키지별 옵션으로, Conan 2에서는 패키지이름/*:옵션=값 형식으로 씁니다. 1.x 방식인 spdlog:shared=False도 경고와 함께 받아들이지만 새 형식을 쓰는 것이 맞습니다.
버전은 정확한 버전 대신 범위로도 쓸 수 있습니다.
[requires]
fmt/[>=10.0 <11]
범위를 쓰면 그 안에서 가장 높은 버전을 고르므로, 원격 저장소에 새 버전이 올라오면 같은 conanfile로도 다른 결과가 나옵니다. 범위를 쓴다면 뒤에서 설명할 lockfile로 실제 해석된 버전을 고정해야 재현 가능한 빌드가 됩니다.
설치와 CMake 빌드
my-conan-app/
├── conanfile.txt
├── CMakeLists.txt
└── src/
└── main.cpp
cmake_minimum_required(VERSION 3.20)
project(my-conan-app LANGUAGES CXX)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(nlohmann_json REQUIRED)
add_executable(my-app src/main.cpp)
target_link_libraries(my-app PRIVATE fmt::fmt spdlog::spdlog nlohmann_json::nlohmann_json)
target_compile_features(my-app PRIVATE cxx_std_17)
#include <spdlog/spdlog.h>
#include <nlohmann/json.hpp>
int main() {
nlohmann::json j = {{"app", "my-conan-app"}, {"version", "1.0.0"}};
spdlog::info("Hello, {}! config={}", "Conan", j.dump());
}
conan install . --output-folder=build --build=missing
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/my-app
conan install은 의존성 그래프를 계산하고, 각 패키지에 대해 현재 프로필의 설정과 옵션으로 package ID를 계산한 뒤, 로컬 캐시와 원격 저장소에서 그 ID의 바이너리를 찾습니다. 없으면 --build=missing 덕분에 소스에서 빌드합니다. 끝나면 build/에 conan_toolchain.cmake와 각 패키지의 CMake 설정 파일이 생깁니다.
CMake 단계에서 -DCMAKE_BUILD_TYPE=Release를 빠뜨리면 안 됩니다. 단일 구성 생성기(Makefile, Ninja)에서 conan_toolchain.cmake는 빌드 타입을 정하지 않고, CMakeDeps가 만든 설정 파일은 빌드 타입별로 나뉘어 있어서, 빌드 타입이 비어 있으면 라이브러리 경로를 제대로 연결하지 못합니다. Debug로 빌드하려면 Conan과 CMake 양쪽에 모두 Debug를 지정해야 합니다.
conan install . --output-folder=build-debug --build=missing -s build_type=Debug
cmake -S . -B build-debug -DCMAKE_TOOLCHAIN_FILE=build-debug/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug
Release로 받은 의존성과 Debug 애플리케이션을 섞으면, 특히 MSVC에서는 런타임 라이브러리와 _ITERATOR_DEBUG_LEVEL이 맞지 않아 링크 오류가 납니다. CMake 3.23 이상이라면 Conan이 생성한 CMakePresets.json을 써서 cmake --preset conan-release 식으로 이 설정을 한 번에 맞출 수도 있습니다.
conanfile.py로 의존성 선언
조건에 따라 의존성을 바꿔야 하면 파이썬으로 쓰는 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"
def requirements(self):
self.requires("fmt/10.1.1")
self.requires("spdlog/1.12.0")
if self.settings.os == "Linux":
self.requires("libcurl/8.5.0")
def build_requirements(self):
self.test_requires("gtest/1.14.0")
def validate(self):
check_min_cppstd(self, "17")
def layout(self):
cmake_layout(self)
generators 속성에 CMakeDeps와 CMakeToolchain을 적었다면 generate() 메서드에서 같은 generator를 다시 만들면 안 됩니다. 둘 다 하면 Conan이 중복 선언 오류를 냅니다. generator 설정을 바꿔야 할 때만 속성을 지우고 generate()에서 직접 만들어 설정합니다.
테스트 프레임워크처럼 빌드와 테스트에만 필요한 의존성은 test_requires로 선언합니다. if self.settings.build_type == "Debug": self.requires("gtest/...")처럼 빌드 타입으로 의존성을 바꾸면 Release 빌드에서 테스트를 빌드할 수 없게 되고, 일반 requires로 선언하면 이 패키지를 쓰는 다른 프로젝트까지 gtest를 전이 의존성으로 받게 됩니다. C++ 표준 요구사항 확인은 validate()에 둡니다. 조건이 맞지 않으면 이 패키지 구성이 “Invalid”로 표시되고 설치가 중단됩니다.
cmake_layout을 쓰면 생성 파일 위치가 build/Release/generators/처럼 빌드 타입별로 나뉩니다. 이때는 --output-folder 없이 conan install .만 실행하고, 생성된 프리셋으로 빌드하는 것이 간단합니다.
conan install . --build=missing
cmake --preset conan-release
cmake --build --preset conan-release
옵션은 명령줄에서도 줄 수 있습니다. 셸이 *를 해석하지 않도록 따옴표로 감쌉니다.
conan install . --build=missing -o "spdlog/*:shared=True"
프로필
프로필은 빌드 환경을 정의하는 파일입니다. [settings]는 package ID에 영향을 주는 값(OS, 아키텍처, 컴파일러, 표준 라이브러리, 빌드 타입)이고, [conf]는 CMake 생성기 같은 도구 설정입니다.
# conan/profiles/linux-gcc12
[settings]
os=Linux
arch=x86_64
compiler=gcc
compiler.version=12
compiler.libcxx=libstdc++11
compiler.cppstd=17
build_type=Release
[conf]
tools.cmake.cmaketoolchain:generator=Ninja
# conan/profiles/windows-msvc2022
[settings]
os=Windows
arch=x86_64
compiler=msvc
compiler.version=193
compiler.runtime=dynamic
compiler.cppstd=17
build_type=Release
# conan/profiles/macos-armv8
[settings]
os=Macos
arch=armv8
compiler=apple-clang
compiler.version=15
compiler.libcxx=libc++
compiler.cppstd=17
build_type=Release
conan install . --output-folder=build --build=missing -pr=conan/profiles/linux-gcc12
프로필 파일을 저장소에 넣어 두면 팀원과 CI가 같은 설정으로 같은 바이너리를 고르게 됩니다. conan profile detect로 각자 만든 프로필은 컴파일러 버전이나 cppstd가 조금씩 달라서, 같은 conanfile이라도 서로 다른 package ID가 계산되고 누군가는 계속 소스 빌드를 하게 됩니다.
compiler.version에는 Conan의 settings.yml에 정의된 값만 쓸 수 있습니다. 목록에 없는 값을 쓰면 Invalid setting ... is not a valid 'settings.compiler.version' value 오류가 납니다. 아주 최신 컴파일러라서 목록에 없다면 Conan을 업데이트합니다. 프로필의 버전과 실제 설치된 컴파일러 버전이 다를 때 Conan은 오류를 내지 않고 프로필 값으로 package ID를 계산하므로, 다른 컴파일러용 바이너리를 받아 링크 단계에서 문제가 생길 수 있습니다. 컴파일러를 바꿨다면 프로필도 함께 고쳐야 합니다.
Remote
Remote는 레시피와 바이너리를 내려받는 원격 저장소입니다. 기본으로 ConanCenter가 등록되어 있습니다.
conan remote list
# conancenter: https://center2.conan.io [Verify SSL: True, Enabled: True]
# 사내 저장소(예: Artifactory)를 맨 앞에 추가
conan remote add mycompany https://mycompany.jfrog.io/artifactory/api/conan/conan-local --index 0
conan remote login mycompany myuser
conan remote remove mycompany
Conan은 등록 순서대로 remote를 검색합니다. 사내 패키지나 사내에서 미리 빌드한 바이너리를 먼저 쓰려면 사내 remote를 앞에 둡니다.
# 레시피 버전 검색
conan search "fmt/*" -r conancenter
# 특정 버전의 바이너리 목록과 설정 확인
conan list "fmt/10.1.1:*" -r conancenter
Unable to find 'fmt/x.y.z' in remotes 오류는 그 버전의 레시피가 어느 remote에도 없다는 뜻입니다. conan search로 실제 존재하는 버전을 확인합니다. 레시피는 있는데 내 설정의 바이너리가 없는 경우는 오류가 아니라 Binary missing으로 표시되며, --build=missing으로 해결합니다. 특정 패키지만 소스에서 다시 빌드하고 싶다면 --build="fmt/*"처럼 패턴을 지정합니다.
버전 충돌
의존성 그래프에서 같은 패키지가 서로 다른 버전으로 요구되면 Conan 2는 하나를 임의로 고르지 않고 Version conflict 오류를 냅니다. 예를 들어 앱이 fmt/10.2.1을 직접 요구하는데 spdlog 레시피가 fmt/10.1.1을 요구하면 충돌입니다. (전이 의존성을 버전 범위로 선언한 레시피라면 범위 안의 버전이 선택되어 충돌이 나지 않습니다.)
충돌을 해결하려면 소비자인 conanfile.py에서 어느 버전을 쓸지 명시합니다.
def requirements(self):
self.requires("spdlog/1.12.0")
# 그래프 전체에서 이 버전을 쓰도록 강제
self.requires("fmt/10.2.1", force=True)
force=True는 직접 의존성으로 추가하면서 그래프 안의 다른 요구를 덮어씁니다. 직접 쓰지는 않지만 전이 의존성의 버전만 바꾸고 싶다면 override=True를 씁니다. 어느 쪽이든 spdlog가 실제로 그 fmt 버전과 호환되는지는 직접 확인해야 합니다. 헤더에서 fmt API를 쓰는 라이브러리라면 메이저 버전이 다를 때 컴파일 오류가 날 수 있습니다.
lockfile로 버전 고정
lockfile은 의존성 그래프를 해석한 결과(각 패키지의 정확한 버전과 레시피 리비전)를 파일로 저장합니다. 버전 범위를 쓰거나, ConanCenter의 레시피가 같은 버전에서도 리비전이 갱신될 수 있다는 점을 생각하면 lockfile이 있어야 다음 주에 빌드해도 오늘과 같은 결과가 나옵니다.
# conan.lock 생성 (기본 파일명)
conan lock create .
# lockfile을 지켜 설치
conan install . --output-folder=build --build=missing --lockfile=conan.lock
conan.lock은 저장소에 커밋합니다. conanfile에 새 의존성을 추가했는데 lockfile을 갱신하지 않으면, lockfile에 없는 요구사항이라는 오류로 설치가 실패합니다. 의존성을 바꿨다면 conan lock create .로 lockfile을 다시 만들고 함께 커밋합니다.
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에 올립니다.
CI와 Docker
GitHub Actions
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install conan
- uses: actions/cache@v4
with:
path: ~/.conan2/p
key: conan-${{ runner.os }}-${{ hashFiles('conanfile.*', 'conan.lock', 'conan/profiles/*') }}
- name: Conan install
run: |
conan profile detect --force
conan install . --output-folder=build --build=missing \
-pr=conan/profiles/linux-gcc12 --lockfile=conan.lock
- name: Build
run: |
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build build
캐시 키에 lockfile과 프로필을 넣어야, 의존성이나 빌드 설정이 바뀌었을 때 오래된 캐시를 쓰지 않습니다. 캐시를 쓰더라도 처음 한 번은 --build=missing으로 소스 빌드가 일어날 수 있으므로, 팀 규모가 크다면 CI에서 빌드한 바이너리를 사내 remote에 conan upload해 공유하는 편이 효율적입니다. conan profile detect는 기본 프로필이 없으면 일부 명령이 실패하기 때문에 넣었고, 실제 설정은 -pr로 지정한 저장소의 프로필을 씁니다.
Docker 레이어 캐시
Docker로 빌드한다면 Dockerfile의 순서가 빌드 시간을 크게 좌우합니다. COPY . .로 소스 전체를 복사한 뒤 conan install을 하면, 소스 한 줄만 바뀌어도 그 뒤의 레이어가 모두 무효화되어 의존성을 매번 다시 설치하고 빌드합니다. conanfile과 lockfile, 프로필만 먼저 복사해 conan install 레이어를 만들고 소스는 그다음에 복사하면, 의존성이 바뀌지 않는 한 그 레이어를 캐시에서 재사용합니다.
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y python3-pip cmake g++ \
&& pip3 install --break-system-packages conan
WORKDIR /app
COPY conan/profiles/linux-gcc conan/profiles/linux-gcc
COPY conanfile.txt conan.lock ./
RUN conan install . --output-folder=build --build=missing \
-pr=conan/profiles/linux-gcc --lockfile=conan.lock
COPY . .
RUN cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release \
&& cmake --build build
이미지 안에서 conan profile detect에 맡기기보다 저장소의 프로필을 복사해 쓰면, 로컬과 CI와 Docker 빌드가 같은 바이너리 조합을 고르게 됩니다. 이 프로필의 컴파일러 버전은 베이스 이미지에 설치되는 GCC 버전(Ubuntu 24.04는 GCC 13)과 맞춰야 합니다.
CMake 라이브러리용 conanfile.py 레시피
레시피 메서드와 역할
flowchart TD A[conanfile.py] --> B[layout] A --> C[generate] A --> D[build] A --> E[package] A --> F[package_info] B --> G[소스·빌드 경로 정의] C --> H[CMakeToolchain 등 생성] D --> I[실제 빌드 수행] E --> J[아티팩트를 package 폴더로 복사] F --> K[소비자에게 전달할 정보]
완전한 CMake 라이브러리 레시피
from conan import ConanFile
from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout
class MylibConan(ConanFile):
name = "mylib"
version = "1.0.0"
package_type = "library" # shared 옵션에 따라 정적/공유 라이브러리
# 메타데이터 (선택이지만 권장)
license = "MIT"
url = "https://github.com/yourorg/mylib"
description = "A sample C++ library for Conan packaging"
topics = ("conan", "library", "example")
# 바이너리 구성
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 # Windows에는 fPIC 개념이 없음
def configure(self):
if self.options.shared:
self.options.rm_safe("fPIC") # 공유 라이브러리는 항상 PIC
def layout(self):
cmake_layout(self)
def requirements(self):
# 의존성 예: self.requires("fmt/10.2.1")
pass
def generate(self):
CMakeDeps(self).generate()
CMakeToolchain(self).generate()
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
cmake = CMake(self)
cmake.install()
def package_info(self):
self.cpp_info.libs = ["mylib"] # 실제 라이브러리 파일 이름 (lib 접두사·확장자 제외)
# includedirs 기본값은 ["include"]이므로 install 경로가 include면 생략 가능
CMakeLists.txt (레시피와 연동)
레시피의 cmake.install()이 동작하려면 CMake에 install() 규칙이 있어야 합니다. Conan은 CMAKE_INSTALL_PREFIX를 패키지 폴더로 지정해 cmake --install을 실행합니다.
cmake_minimum_required(VERSION 3.15)
project(mylib VERSION 1.0.0 LANGUAGES CXX)
add_library(mylib src/mylib.cpp)
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(mylib PUBLIC cxx_std_17)
install(TARGETS mylib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
install(DIRECTORY include/ DESTINATION include)
주요 속성·메서드
| 항목 | 설명 |
|---|---|
| exports_sources | 레시피와 함께 Conan 캐시로 복사할 파일 |
| config_options() | 플랫폼별로 옵션 제거 (예: Windows에서 fPIC) |
| configure() | 옵션 간 관계 정리 (shared=True면 fPIC 제거) |
| layout() | 소스·빌드·생성 파일 폴더 구조. cmake_layout()이 표준 |
| generate() | CMakeToolchain, CMakeDeps 등 빌드 설정 파일 생성 |
| build() | 실제 빌드 (cmake configure + build) |
| package() | 빌드 결과를 package 폴더로 복사 |
| package_info() | 소비자가 링크·include할 때 필요한 정보 |
헤더 전용 라이브러리 레시피
헤더 전용 라이브러리는 빌드 단계가 없으므로 build()를 생략하고 package()에서 헤더만 복사합니다. 바이너리가 없으니 OS·컴파일러 설정이 달라도 패키지는 하나면 되도록 package_id()에서 설정 정보를 지웁니다.
from conan import ConanFile
from conan.tools.files import copy
import os
class HeaderOnlyConan(ConanFile):
name = "headeronly"
version = "1.0.0"
description = "Header-only C++ library"
package_type = "header-library"
settings = "os", "compiler", "build_type", "arch" # 의존성 해석용으로만 둠
exports_sources = "include/*"
no_copy_source = True # 빌드 폴더로 소스를 복사하지 않고 소스 폴더에서 바로 사용
def package_id(self):
self.info.clear() # 설정과 무관하게 하나의 패키지 ID
def package(self):
copy(self, "*.h", os.path.join(self.source_folder, "include"),
os.path.join(self.package_folder, "include"))
copy(self, "*.hpp", os.path.join(self.source_folder, "include"),
os.path.join(self.package_folder, "include"))
def package_info(self):
self.cpp_info.bindirs = []
self.cpp_info.libdirs = []
# includedirs 기본값 ["include"] 그대로 사용
패키지 빌드 및 검증
conan create: 빌드 + test_package
# 현재 디렉터리의 conanfile.py로 패키지 생성 (기본 프로필)
conan create .
# 설정·옵션 지정
conan create . -s build_type=Debug
conan create . -o "mylib/*:shared=True"
# 프로필 지정
conan create . -pr=myprofile
conan create는 레시피와 exports_sources를 캐시로 export하고, generate() → build() → package()를 실행한 뒤, test_package/ 폴더가 있으면 그 프로젝트로 패키지를 소비해 봅니다.
test_package 구조
test_package는 방금 만든 패키지를 의존성으로 가져와 빌드·실행해 보는 작은 Conan 프로젝트입니다.
test_package/conanfile.py:
import os
from conan import ConanFile
from conan.tools.build import can_run
from conan.tools.cmake import CMake, cmake_layout
class TestPackageConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeDeps", "CMakeToolchain"
def requirements(self):
self.requires(self.tested_reference_str()) # 테스트 대상 패키지
def layout(self):
cmake_layout(self)
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def test(self):
if can_run(self): # 크로스 컴파일 중이면 실행 생략
cmd = os.path.join(self.cpp.build.bindir, "example")
self.run(cmd, env="conanrun") # 공유 라이브러리 경로가 잡힌 환경에서 실행
test_package/CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(test_package LANGUAGES CXX)
find_package(mylib CONFIG REQUIRED)
add_executable(example src/example.cpp)
target_link_libraries(example PRIVATE mylib::mylib)
test_package/src/example.cpp:
#include <mylib.h>
int main() {
mylib(); // conan new 템플릿이 만드는 함수: 헤더뿐 아니라 실제 심볼을 호출해 링크까지 검증
return 0;
}
conan create 출력 예시
======== Exporting recipe to the cache ========
mylib/1.0.0: Exporting package recipe
...
======== Installing packages ========
-------- Installing package mylib/1.0.0 (1 of 1) --------
mylib/1.0.0: Building from source
mylib/1.0.0: Calling build()
...
======== Testing the package: Building ========
...
======== Testing the package: Executing test ========
mylib/1.0.0 (test package): Running test()
로컬 개발 워크플로
패키지를 매번 conan create하지 않고 로컬에서 고치며 빌드할 때는 conan install로 의존성과 툴체인만 만든 뒤 CMake 프리셋으로 빌드합니다.
conan install . --build=missing
cmake --preset conan-release # Windows 다중 구성 생성기에서는 conan-default
cmake --build --preset conan-release
원격 저장소 배포
remote 추가와 로그인
conan remote list
# 사내 Artifactory 추가 (--index=0: 가장 먼저 검색)
conan remote add mycompany https://mycompany.jfrog.io/artifactory/api/conan/conan-local --index=0
# 인증
conan remote login mycompany myuser -p mypassword
conan upload: 로컬 캐시 → 원격
# 레시피와 바이너리 함께 업로드 (Conan 2.x는 --all 없이 바이너리까지 올림)
conan upload "mylib/1.0.0" -r mycompany -c
# 레시피만 올리려면
conan upload "mylib/1.0.0" -r mycompany --only-recipe -c
# 원격에 올라간 패키지 확인
conan list "mylib/*" -r mycompany
-c는 확인 질문 없이 진행하는 옵션으로, CI에서 필요합니다.
소비 측에서 사내 레포 사용
소비 프로젝트는 conanfile.txt에 mylib/1.0.0을 적고 conan install . --build=missing을 실행하면, 등록된 remote를 순서대로 검색해 맞는 바이너리를 내려받습니다. 맞는 바이너리가 없으면 --build=missing 덕분에 소스에서 빌드합니다.
레시피 설계 원칙
버전 관리
버전은 한 곳에서만 관리합니다. 레시피에 하드코딩하지 않고 Git 태그에서 읽으려면 set_version()을 씁니다.
from conan import ConanFile
from conan.tools.scm import Git
class MylibConan(ConanFile):
name = "mylib"
def set_version(self):
git = Git(self, self.recipe_folder)
self.version = git.run("describe --tags --abbrev=0").strip()
명령줄에서 conan create . --version=1.2.3처럼 지정할 수도 있습니다.
options 설계
shared와 fPIC는 대부분의 C++ 라이브러리 레시피에 두는 표준 옵션이고, 선택 기능은 with_ssl 같은 이름의 불리언 옵션으로 둡니다. 옵션 하나마다 바이너리 조합이 두 배가 되므로, 실제로 필요한 것만 옵션으로 만듭니다.
options = {"shared": [True, False], "fPIC": [True, False], "with_ssl": [True, False]}
default_options = {"shared": False, "fPIC": True, "with_ssl": True}
package_info 명확화
def package_info(self):
self.cpp_info.libs = ["mylib"]
self.cpp_info.defines = ["MYLIB_VERSION=100"] # 소비자에게 전달할 매크로 (필요할 때만)
exports_sources 최소화
빌드에 필요한 파일만 포함합니다. 불필요한 파일이 들어가면 레시피 리비전이 쓸데없이 바뀝니다.
패키지를 직접 만들 때
내 라이브러리를 Conan 패키지로 만들려면 conanfile.py에 이름, 버전, 빌드 방법, 패키징 방법을 정의하고 conan create를 실행합니다.
from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout
class MylibConan(ConanFile):
name = "mylib"
version = "1.0.0"
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/*"
generators = "CMakeToolchain"
def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC
def layout(self):
cmake_layout(self)
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
CMake(self).install()
def package_info(self):
self.cpp_info.libs = ["mylib"]
conan create . --build=missing
conan create . -s build_type=Debug --build=missing
conan create는 소스를 캐시로 복사해 빌드하고 패키징한 뒤 로컬 캐시에 저장합니다. 그 뒤로는 다른 프로젝트의 [requires]에 mylib/1.0.0을 적어 쓸 수 있습니다. cpp_info.libs에는 링크할 라이브러리 이름을 문자열로 적어야 합니다. 이 레시피의 package()는 CMakeLists.txt에 install() 규칙이 있다는 전제입니다. test_package, 헤더 전용 라이브러리, 원격 저장소 업로드 등 레시피 작성은 Conan 레시피 작성에서 자세히 다룹니다.
한 프로젝트에서 vcpkg와 Conan을 함께 쓰는 것은 피하는 것이 좋습니다. 같은 라이브러리가 두 경로로 들어와 서로 다른 빌드 설정의 바이너리가 섞이기 쉽고, CMAKE_TOOLCHAIN_FILE도 하나만 지정할 수 있기 때문입니다.
다음 글: Conan 2.x 패키지 레시피 작성 이전 글: vcpkg 포트 직접 만들기
참고 자료
같이 보면 좋은 글
- Conan 2.x 심화
- Conan 2.x 패키지 레시피 작성
- vcpkg 기초
- CMake 입문 | 수십 개 파일 컴파일할 때 필요한 빌드 자동화 (CMakeLists.txt 기초)