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을 다시 만들고 함께 커밋합니다.
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)과 맞춰야 합니다.
패키지를 직접 만들 때
내 라이브러리를 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 포트 직접 만들기