본문 바로가기
개발/C++

C++/CMake 복잡한 패키지 구조를 위한 CMakeLists.txt 작성법

by pandatta 2022. 9. 13.

예전 블로그에서 옮겨온 글입니다. 지금은 두 번째 회사를 다닌지 2년이 다 되어가네요... 감개무량


회사를 다닌지 6개월, 제일 많이 성장한 부분은 패키징, 또는 아키텍처 설계다.

Python은 무적의 import로 어떤 폴더에 있는 어떤 코드의 어떤 모듈, 어떤 클래스, 어떤 메소드도 불러올 수 있지만, C++은 그게 아니라서 공부 초반에 고생을 했던 기억이 있다.

사실 지금도 초반이다...ㅠ

최근 들어 가장 고생한건, CMake를 여러 계층으로 패키징된 라이브러리에 적용하는 것이었다. GNU make를 쓸 때는 폴더 이름을 그냥 쓰면 되는 것이었는데, 도대체 튜토리얼들에서 얘기하듯이 그냥 CMakeLists.txt를 폴더에 만들어주기만 하는 것만으로는 빌드가 되지 않는 것이다;;

이 문제점을 해결하기 위해 내가 BioCpp 레포지토리에 작성한 CMakeLists.txt 구조를 소개해볼까 한다.

디렉토리 구조 예시

일단 BioCpp의 디렉토리 구조는 이렇게 생겨먹었다.

.
├─CMakeLists.txt                <- (0) Main CMakeLists.txt
├─data
│      .gitkeep
├─inc
│  └─biocpp
│          algorithms.h
│          biocpp.h
│          io.h
│          molecules.h
├─src
│  │  CMakeLists.txt            <- (1) 1st Subdirectory CMakeLists.txt
│  ├─algorithms
│  │      alignment.cc
│  │      CMakeLists.txt        <- (2) 2nd Subdirectory CMakeLists.txt
│  ├─io
│  │      CMakeLists.txt
│  │      files.cc
│  └─molecules
│          CMakeLists.txt
│          mol_seq.cc
└─tests
    │  CMakeLists.txt
    └─molecules
            CMakeLists.txt
            mol_seq_unittest.cc

여기서 data는 여러 상황에서 필요한 rawdata를 넣기 위해 .gitkeep으로 폴더만 유지하고 있고, inc/biocpp에 헤더들이, src에 한 층의 폴더가 있고, 그 안에 소스코드들이, 그리고 tests 폴더에 테스트가 src와 같은 구조로 있는 형식이다.

디렉토리에 있는 CMakeLists.txt를 보면, 가장 바깥 폴더와, srctests 등 여러 *.cc 소스 파일들이 있는 모든 폴더에 CMakeLists.txt를 만들어준 걸 볼 수 있다.

이 때, (0)과 (1), (2)로 표시해둔 CMakeLists.txt의 예시를 보자. 단, 거꾸로 보자.

(2) 2nd Subdirectory CMakeLists.txt

project(algorithms)

file(GLOB SOURCES *.cc)

add_library(${PROJECT_NAME} OBJECT ${SOURCES})

target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_SOURCE_DIR}/inc)

가장 안쪽의 CMakeLists.txt는 그 폴더에 있는 소스파일들로 라이브러리를 만드는 역할을 한다. 명령어를 설명하자면 다음과 같다.

  1. project(프로젝트명 또는 라이브러리명): CMake에서는 하나의 CMakeLists.txt가 하나의 프로젝트를 만든다고 보면 된다. 여기서는 algorithms라는 폴더의 소스들을 빌드하므로, 폴더와 동일한 이름의 프로젝트 이름을 지정해주었다. 폴더와 다른 이름이더라도, 잘만 정해두고 후술한 곳에서 사용만 잘 하면 된다.
  2. file(GLOB SOURCES *.cc): SOURCES란 변수에 *.cc 확장자를 갖는 모든 폴더 내 소스파일들을 저장해준다.
  3. add_library(라이브러리명 OBJECT 소스파일명): 라이브러리를 소스파일들로 빌드해준다. 이 때 ${PROJECT_NAME}${SOURCES}를 써주면 위에서 지정한 프로젝트명의 라이브러리를 위에서 지정한대로 해당 폴더의 소스파일들로 빌드해주게 한 것이다. OBJECT라 함은 후술할 main CMakeLists.txt에서 이 라이브러리를 object로 삼아 빌드를 한 번 더 할 것임을 예고해준 것이다.
  4. target_include_directories(라이브러리명 PUBLIC 헤더폴더위치): 라이브러리 빌드에 필요한 헤더파일들의 폴더를 지정해준다. 헤더 폴더 위치에 쓰인 ${CMAKE_SOURCE_DIR}는 가장 바깥 폴더의 (0) CMakeLists.txt가 있는 위치이다.

(1) 1st Subdirectory CMakeLists.txt

add_subdirectory(algorithms)
add_subdirectory(io)
add_subdirectory(molecules)

중간 폴더의 CMakeLists.txt는 여러 subdirectory로 통하는 관문 역할을 한다. 이 폴더에는 소스파일이 따로 없어서 add_library()는 필요하지 않다.

  1. add_subdirectory(폴더명): 빌드에 포함시킬 폴더들을 연결해준다. 라이브러리명이 아닌 폴더명임에 주의하자.

(0) Main CMakeLists.txt

cmake_minimum_required(VERSION 3.12)

project(
  biocpp
  VERSION 0.1.0
  DESCRIPTION "Bioinformatics Library for C++"
  HOMEPAGE_URL https://github.com/panda5176/biocpp
  LANGUAGES CXX
)

add_subdirectory(src)
add_subdirectory(tests)

set(LIBRARIES algorithms io molecules)

foreach(LIBRARY ${LIBRARIES})
  set(LIB_OBJS ${LIB_OBJS} $<TARGET_OBJECTS:${LIBRARY}>)
endforeach()

add_library(${PROJECT_NAME} STATIC ${LIB_OBJS})
# 또는 add_executable(${PROJECT_NAME} STATIC biocpp.cc ${LIB_OBJS})

가장 바깥 폴더에 있는 CMakeLists.txt에서는 필요한 메타데이터를 나열하고, 여태까지 빌드했던 라이브러리들을 모아서 최종 라이브러리 또는 실행파일을 만든다.

  1. cmake_minimum_required(VERSION 버전): CMake를 위한 최소 버전을 명시해준다. 나의 경우 3.0 이상이면 돌아가는 걸로 기억하는데, 다른 프로젝트 등에서 자주 사용하는 target_include_directories() 명령어가 3.12를 요구해서 나도 저렇게 써둔 것 같다.
    아님말고
  2. project(프로젝트명 버전 설명 URL주소 언어): 위에서도 나왔지만, 추가적인 파라미터들을 main CMakeLists.txt에 써주면 좋다. 다들 별로 쓸모없는 것 같지만, CMake 공식문서를 보면 얘네를 변수로 써먹기도 한다(!).
  3. add_subdirectory(폴더명): 역시 폴더명을 써준다.
  4. set(변수명 이름1 이름2 이름3 ...): 이름들을 변수명에 통째로 저장해준다. 여기서는 OBJECT로 만들어준 라이브러리들을 저장했다.
  5. foreach(변수명 반복하고싶은변수명) ... endforeach(): 반복문이다. 반복하고싶은 변수명들을 돌면서 변수명에 저장해서 뱉어준다.
  6. $<TARGET_OBJECTS:${변수명}>: TARGET_OBJECTS는 앞서 OBJECT로 설정해준 라이브러리들을 하나씩 가져와서 LIB_OBJS에 차례차례 저장해주고 있다. 단순히 LIBRARIESadd_library()에 넣어주면 그 변수들은 라이브러리로 인식되는게 아니라서, OBJECT임을 이런 방식으로 밝혀주어야한다.
  7. add_library(프로젝트명 STATIC OBJECT라이브러리들): OBJECT로 지정한 라이브러리들을 섞어서 최종 라이브러리를 만든다. 만들어진 라이브러리는 빌드한 폴더의 lib프로젝트명.a로 저장된다.
  8. add_executable(프로젝트명 STATIC main소스코드 OBJECT라이브러리들): 또는 main() 함수가 있는 소스코드와 라이브러리들을 이용해 실행가능한 파일, 즉 *.exe 등을 만들 수 있다.

완성한 lib프로젝트명.a는 빌드 대상 폴더에서 가져와서 다음과 같이 사용할 수 있다.

$ cmake CMakeLists.txt -B {빌드할 폴더} -G {make generator, 나의 경우 "MinGW Makefiles"}
$ make -C {빌드한 폴더}
$ g++ -o biocpp.test.exe biocpp.test.cc -I inc -L build -l biocpp

build 폴더에 있는 libbiocpp.a 라이브러리 파일과 inc 폴더의 헤더들을 이용해 biocpp.test.cc 소스파일을 컴파일하여 biocpp.test.exe 실행파일을 만든다고 보면 된다.

혹시 GNU make의 MakeFile을 직접 짜보았다면, 결과적으로는 이게 더 단순한 코드임을 알 수 있다. 고생한만큼 편리한 보상이 따를 것이다... 몇 안되는 국내 C++ 사용자 화이팅! 게임업계는 C++ 많이 쓰지만 아무튼 거기도 화이팅!ㅋㅋㅋ

References

댓글