Conan 2.x 패키지 레시피 작성: conanfile.py, CMakeDeps·CMakeToolchain, test_package, 업로드
들어가며: “우리 라이브러리를 Conan 패키지로 배포하고 싶어요”
사내 라이브러리를 여러 프로젝트가 각자 경로에 복사해 쓰기 시작하면, 프로젝트마다 다른 버전이 섞이고 새 팀원은 빌드 방법을 문서로 찾아 헤맵니다. 회사에서 fork한 라이브러리나 레거시 C 라이브러리처럼 ConanCenter에 없는 것은 직접 레시피를 만들어야 하고, 레시피에 settings와 options가 분명하지 않으면 팀원마다 shared/static, Debug/Release가 다른 바이너리가 만들어져 링크 에러가 납니다. conan create가 성공했는데 다른 프로젝트에서 링크하면 undefined reference가 나는 일도 흔한데, 이는 패키지를 실제로 소비해 보는 검증이 없었기 때문입니다.
Conan 레시피(conanfile.py)는 이 문제를 한 파일로 정리합니다. 패키지의 이름과 버전, 바이너리를 구분하는 설정(OS, 컴파일러, 빌드 타입, 아키텍처)과 옵션, 빌드·패키징 방법, 소비자에게 전달할 링크 정보를 담고, test_package로 소비 가능 여부를 검증한 뒤 conan upload로 사내 저장소에 올립니다. 이 글은 Conan 2.x 기준입니다.
flowchart LR
subgraph Before["수동 배포 (Before)"]
B1[소스 복사] --> B2[수동 빌드]
B2 --> B3[경로 설정]
B3 --> B4[프로젝트마다 다름]
end
subgraph After["Conan 레시피 (After)"]
A1[conanfile.py] --> A2[conan create]
A2 --> A3[캐시/원격 저장소]
A3 --> A4[conan install로 통일]
end
Conan 설치와 프로젝트 템플릿
필수 요구사항
| 항목 | 버전 | 비고 |
|---|---|---|
| Conan | 2.x | pip install conan |
| Python | 3.6 이상(최신 버전 권장) | Conan 2.x 실행 |
| CMake | 3.15+ | CMake 기반 레시피, 프리셋은 3.23+에서 편리 |
# Conan 2.x 설치
pip install conan
conan --version
# 기본 프로필 생성 (컴파일러·OS를 자동 감지)
conan profile detect --force
# CMake 라이브러리 레시피 템플릿 생성
conan new cmake_lib -d name=mylib -d version=1.0
conan new로 생성되는 구조:
mylib/
├── CMakeLists.txt
├── conanfile.py
├── include/
│ └── mylib.h
├── src/
│ └── mylib.cpp
└── test_package/
├── CMakeLists.txt
├── conanfile.py
└── src/
└── example.cpp
conanfile.txt vs conanfile.py
| 형식 | 용도 | 빌드·패키징 |
|---|---|---|
| conanfile.txt | 패키지 소비만 할 때 | 없음 (requires, generators만) |
| conanfile.py | 패키지 생성 또는 조건부 의존성 같은 고급 소비 | build(), package(), package_info() |
레시피 작성에는 conanfile.py를 씁니다. conanfile.txt는 [requires]에 패키지 목록만 적을 수 있고 빌드·패키징 로직을 정의할 수 없습니다.
# conanfile.txt (소비 전용)
[requires]
mylib/1.0
[generators]
CMakeDeps
CMakeToolchain
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 덕분에 소스에서 빌드합니다.
자주 만나는 문제
소비 측 링크 에러 (cpp_info.libs 누락)
conan create는 성공했는데 소비 프로젝트에서 undefined reference가 난다면, 대개 package_info()에서 self.cpp_info.libs를 설정하지 않은 것입니다. Conan은 이를 에러로 막지 않으므로, test_package에서 실제 함수를 호출해야 드러납니다.
def package_info(self):
self.cpp_info.libs = ["mylib"]
exports_sources 경로 오류
exports_sources에 지정한 패턴이 레시피 폴더의 실제 경로와 맞지 않으면, 파일이 캐시로 복사되지 않아 build()에서 CMakeLists.txt나 소스를 찾지 못하는 에러가 납니다. 패턴은 레시피 파일 기준 상대 경로입니다.
install 규칙 누락
CMakeLists.txt에 install() 규칙이 없으면 cmake.install()이 아무것도 복사하지 않아 패키지가 빈 채로 만들어집니다. install(TARGETS ...)와 헤더용 install(DIRECTORY ...)를 함께 둡니다.
test_package에서 find_package 실패
CMakeDeps가 만드는 설정 파일 이름과 타깃 이름은 기본적으로 패키지 이름(mylib, mylib::mylib)입니다. 다른 이름을 쓰고 싶다면 package_info()에서 지정합니다.
def package_info(self):
self.cpp_info.libs = ["mylib"]
self.cpp_info.set_property("cmake_file_name", "MyLib")
self.cpp_info.set_property("cmake_target_name", "MyLib::MyLib")
호환 바이너리 없음
ERROR: Missing prebuilt package for 'mylib/1.0.0'
현재 설정(OS, 컴파일러, arch, 옵션)에 맞는 바이너리가 캐시와 remote에 없다는 뜻입니다. --build=missing으로 없는 것만 소스에서 빌드하거나, --build="mylib/*"처럼 특정 패키지를 지정합니다.
의존성 버전 충돌
직접 의존성과 간접 의존성이 같은 패키지의 다른 버전을 요구하면 충돌이 납니다. 레시피에서 한 버전으로 강제할 수 있지만, 실제로 두 버전이 호환되는지는 확인해야 합니다.
def requirements(self):
self.requires("fmt/10.2.1", override=True)
Windows에서 fPIC 옵션 에러
Windows에서 del self.options.fPIC로 옵션을 지운 뒤 프로필이나 명령줄에서 fPIC를 지정하면 존재하지 않는 옵션 에러가 납니다. 옵션 제거는 config_options()에서 하고, 프로필에서는 패턴(mylib/*:fPIC=True)을 쓰면 해당 옵션이 없는 패키지에는 적용되지 않습니다.
includedirs 구조 불일치
헤더를 include/mylib/foo.h에 설치하고 소비자가 #include <mylib/foo.h>로 포함한다면 includedirs는 기본값 include 그대로가 맞습니다. 소비자가 #include <foo.h>로 포함하게 하려면 self.cpp_info.includedirs = ["include/mylib"]로 지정합니다.
레시피 설계 원칙
버전 관리
버전은 한 곳에서만 관리합니다. 레시피에 하드코딩하지 않고 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 최소화
빌드에 필요한 파일만 포함합니다. 불필요한 파일이 들어가면 레시피 리비전이 쓸데없이 바뀝니다.
CI·export-pkg·lockfile·Docker
CI에서 conan create
# .github/workflows/conan-package.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Conan
run: pip install conan
- name: Detect profile
run: conan profile detect --force
- name: Create package
run: conan create . --build=missing
- name: Upload to Artifactory
if: github.ref == 'refs/heads/main'
run: |
conan remote add mycompany ${{ secrets.CONAN_REMOTE_URL }} --index=0
conan remote login mycompany ${{ secrets.CONAN_USER }} -p ${{ secrets.CONAN_PASSWORD }}
conan upload "mylib/*" -r mycompany -c
버전 태그와 연동
VERSION=$(git describe --tags --abbrev=0)
conan create . --version="${VERSION}"
conan upload "mylib/${VERSION}" -r mycompany -c
conan export-pkg (이미 빌드한 결과 패키징)
로컬에서 이미 빌드한 결과가 있을 때 다시 빌드하지 않고 패키지로 만듭니다. 레시피의 package()가 로컬 빌드 폴더를 기준으로 실행됩니다.
conan install . --build=missing
cmake --preset conan-release && cmake --build --preset conan-release
conan export-pkg .
프로필 공유
conan/
└── profiles/
├── linux-gcc12
├── windows-msvc2022
└── macos-clang
conan create . -pr=conan/profiles/linux-gcc12
lockfile로 재현 가능한 빌드
conan lock create . # conan.lock 생성
conan create . --lockfile=conan.lock --build=missing
Docker로 Linux 패키지 생성
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3-pip cmake g++
RUN pip install conan
WORKDIR /app
COPY . .
RUN conan profile detect --force
RUN conan create . --build=missing
다음 글: Conan 2.x 심화 이전 글: [C++ #53-3] vcpkg 고급 활용 | Manifest·Triplet·오버레이·바이너리 캐시