CMake 사용가이드


서론

이 장은 CMake를 다루는 방법에 대해 소개한다.

cmake에 대해 간단히 설명하면 Make라는 명령어로 Makefile을 처리해주는데 이때

이 Makefile을 생성해주는 프로그램 cmake이다

목차

  1. Example
  2. 명령어
  3. Compile option 지정
  4. Target, Property
  5. include path setting
    1. Example
    2. 프로젝트 레벨 CMakeLists.txt
  6. 다른 라이브러리를 사용하는 라이브러리
  7. 파일을 한꺼번에 추가
  8. Cmake 이외의 빌드 시스템 사용하기

Example

빌드하기 위한 예제 코드를 만든다. 파일이름은 main.cc

#include <iostream>

int main() {
  std::cout << "Hello, CMake" << std::endl;
  return 0;
}

아래와 같이 CMakeLists.txt를 작성한다. 꼭 필요한 것은 프로젝트이름(ModooCode)와 LANGUAGES다.

C인 경우 C로 표기하고 C++인 경우는 CXX이다.

# CMake 프로그램의 최소 버전
cmake_minimum_required(VERSION 3.11)

# 프로젝트 정보
project(
  ModooCode
  VERSION 0.1
  DESCRIPTION "예제 프로젝트"
  LANGUAGES CXX)

add_executable (program main.cc)

CMakeLists.txt는 작업하는 최상단 디렉토리에 있어야 한다.

또한 여러 캐시파일들이 생성됨으로 아래와 같이 build폴더를 만들어주어 해당 폴더 내에서 작업한다.

cmake ..

위 명령어를 사용하면 아래와 같이 build폴더 내에 다음과 같이 Makefile등이 생기며 prgram을 실행시킬 수 있다.

명령어

생성할 실행 파일을 추가하는 명령 : add_executable

add_executable (<실행 파일 이름> <소스1> <소스2> ... <소스들>)

Compile option 지정

target_compile_options명령을 사용하면 컴파일 옵션을 지정할 수 있다.

target_compile_options(<실행 파일 이름> PUBLIC <컴파일 옵션1> <컴파일 옵션2> ...)

아래 예시를 봐보자

target_compile_options(program PUBLIC -Wall -Werror)

program 을 빌드할 때 컴파일 옵션으로 -Wall (모든 경고 표시) 과 -Werror (경고는 컴파일 오류로 간주) 을 준다는 의미다.

Target, Property

target: 프로그램을 구성하는 요소들

CMake 명령은 그냥 타겟을 정의하고 (add_executable와 같이), 해당 타겟들의 속성을 지정하는 명령들 (target_compile_options처럼) 로 이루어진 것.

include path setting

CMakeLists.txt에 includes디렉토리를 헤더 파일 경로 탐색 시 확인해라 라고 알려줘야 한다.

${CMAKE_SOURCE_DIR}/includes를 헤더 파일 탐색 경로에 추가한다.

CMake 에서 디렉토리의 경로를 지정할 때 왠만하면 절대 경로를 쓰지 않는 것이 좋다.

target_include_directories(program PUBLIC ${CMAKE_SOURCE_DIR}/includes)

${CMAKE_SOURCE_DIR}은 CMake 에서 기본으로 제공하는 변수로, 최상위 CMakeLists.txt,  즉 cmake ..할 때 읽어들이는 CMakeLists.txt의 경로를 의미한다.(프로젝트 경로)

${CMAKE_SOURCE_DIR}/includes는 현재 프로젝트 경로 안에 includes디렉토리다.

target_include_directories(<실행 파일 이름> PUBLIC <경로 1> <경로 2> ...)

Example

C++ 라이브러리의 경우 헤더와 소스를 따로 분리한다. 그 이유는 라이브러리를 사용할 경우 라이브러리의 구현 부분을 참조할 필요는 없지만 헤더는 꼭 참조해야 하기 때문이다. 따라서 구현 부분을 lib안에, 헤더 파일은 includes에 따로 뺀다.

lib/

shape.cc

#include "shape.h"

Rectangle::Rectangle(int width, int height) : width_(width), height_(height) {}

int Rectangle::GetSize() const {
  // 직사각형의 넓이를 리턴한다.
  return width_ * height_;
}

includes/

shape.h

class Rectangle {
 public:
  Rectangle(int width, int height);

  int GetSize() const;

 private:
  int width_, height_;
};

lib 폴더 내의 CMakeLists.txt

# 정적 라이브러리 shape을 만든다
add_library(shape STATIC shape.cc)

# 해당 라이브러리 컴파일 시 사용할 헤더파일 경로
target_include_directories(shape PUBLIC ${CMAKE_SOURCE_DIR}/includes)

# 해당 라이브러리를 컴파일 할 옵션
target_compile_options(shape PRIVATE -Wall -Werror)

먼저 add_library명령을 통해서 만들어낼 라이브러리 파일을 추가한다. add_library의 사용법은 간단하다.

중간에 어떠한 형태의 라이브러리를 만들지 설정할 수 있다.

  • STATIC: 정적 라이브러리
  • SHARED: 동적 라이브러리
  • MODULE: 동적으로 링크되지는 않지만, dlopen과 같은 함수로 런타임 시에 불러올 수 있는 라이브러리를 생성
add_library (<라이브러리 이름> [STATIC | SHARED | MODULE ] <소스 1> <소스 2> ...)

아래 명령어는 다음을 의미한다.

  1. shape 를 컴파일 할 때 헤더 파일 검색 경로에 ${CMAKE_SOURCE_DIR}/includes 를 추가하라.
  2. shape 를 참조 하는 타겟의 헤더 파일 검색 경로에 ${CMAKE_SOURCE_DIR}/includes 를 추가하라.
target_include_directories(shape PUBLIC ${CMAKE_SOURCE_DIR}/includes)

라이브러리를 컴파일 하는 옵션은 아래와 같다. 옵션에 PRIVATE으로 설정되어 있는걸 볼 수 있는데, 그 이유는 shape빌드할 때에는 -Wall과 -Werror옵션을 사용하고 싶지만, shape를 사용하는 애들에게까지 이 옵션을 강제하고는 싶지 않기 때문이다.

target_compile_options(shape PRIVATE -Wall -Werror)

프로젝트 레벨 CMakeLists.txt

add_subdirectory명령을 통해서 CMake 가 추가로 확인해야 할 디렉토리의 경로를 지정한다.

그러면 CMake 실행 시에, 해당 디렉토리로 들어가서 그 안에 있는 CMakeLists.txt실행한다.

# CMake 프로그램의 최소 버전
cmake_minimum_required(VERSION 3.11)

# 프로젝트 정보
project(
  ModooCode
  VERSION 0.1
  DESCRIPTION "예제 프로젝트"
  LANGUAGES CXX)
 
# 확인할 디렉토리 추가
add_subdirectroy(lib)

add_executable (program main.cc)

# program에 shape를 링크
target_link_libraries(program shape)

그리고 아래 명령어는 program을 빌드 할 때 shape라이브러리를 링크 시킨다.

실행 파일은 PUBLIC이냐 PRIVATE이냐의 여부가 크게 중요하지는 않다.

왜냐하면 실행 파일을 다른 타겟이 참조할 수 는 없기 때문이다.

target_link_libraries(program shape)

그래서 다음과 같이 사용가능하다.

target_link_libraries(program shape)

그리고 main.cc는 아래와 같이 만든다

#include <iostream>
#include "shape.h"

int main(){
    Rectangle r(2, 15);
    std::cout << "Get Size : " << r.GetSize() << std::endl;
    return 0;
}

따라서 최종 디렉토리는 아래와 같다.

$ tree
├── CMakeLists.txt
├── includes
│   └── shape.h
├── lib
│   ├── CMakeLists.txt
│   └── shape.cc
└── main.cc

실행하면 build디렉토리를 확인해보면 libshape.a파일이 생긴 것을 볼 수 있다. 라이브러리를 만들게 되면 CMake는 앞에 lib을 붙인 라이브러리 파일을 생성한다.

다른 라이브러리를 사용하는 라이브러리

예를 들어서 우리의 Shape라이브러리에서 thread라이브러리를 사용한다고 하자.

#include <iostream>
#include <thread>

#include "shape.h"

Rectangle::Rectangle(int width, int height) : width_(width), height_(height) {}

int Rectangle::GetSize() const {
  std::thread t([this]() { std::cout << "Calulate .." << std::endl; });
  t.join();

  // 직사각형의 넓이를 리턴한다.
  return width_ * height_;
}

리눅스의 경우 보통 thread라이브러리를 사용하려면 pthread라이브러리를 링크시켜줘야 한다.

따라서 아래와 같이 shape의 CMakeLists.txt를 수정해줘야 한다.

add_library(shape STATIC shape.cc)
target_include_directories(shape PUBLIC ${CMAKE_SOURCE_DIR}/includes)
target_compile_options(shape PRIVATE -Wall -Werror)

# 추가 명령어
# pthread 라이브러리를 링크
target_link_libraries(shape PRIVATE pthread)

target_link_libraries 를 통해서 shape 에 pthread 라이브러리를 추가해준다.

target_link_libraries 로 의존 라이브러리(Dependency) 를 추가할 때 추가하는 방식이 세 가지가 있다. 다음과 같은 가이드라인을 따르면 좋다.

만일 어떤 라이브러리 A 를 참조한다고 할 때

  • A 를 헤더 파일과 내부 구현에서 모두 사용한다면 : PUBLIC
  • A 를 내부 구현에서만 사용하고 헤더 파일에서는 사용하지 않는다면 : PRIVATE
  • A 를 헤더 파일에서만 사용하고 내부 구현에서는 사용하지 않는다면 : INTERFACE

위 경우 <thread> 를 내부 구현 (shape.cc) 에서만 사용하고 헤더 파일 (shape.h) 에서는 사용하고 있지 않는다. 따라서 이 경우 pthread 를 PRIVATE 으로 링크해주는 것이 맞습니다. 이를 통해서 shape 를 사용하는 다른 라이브러리가 불필요하게 pthread 를 링크해주는 일을 막을 수 가 있다.

파일을 한꺼번에 추가

cmake에서 타겟을 빌드하는데 필요한 소스파일을 명시하려면 아래와 같았다.

아래 명령어는 파일들이 새로 추가할 때 마다 위 add_library를 수정해줘야 한다.

add_library(shape STATIC shape.cc color.cc circle.cc)

이 디렉토리에 있는 파일들을 모두 이 라이브러리를 빌드하는데 사용해줘!라고 명령할 수 있는 방법을 제공한다. file명령의 GLOB_RECURSE옵션은, 인자로 주어진 디렉토리와 해당 디렉토리 안에 있는 모든 하위 디렉토리 까지 재귀적으로 살펴본다는 의미다.

${CMAKE_CURRENT_SOURCE_DIR}: CMake에서 기본으로 제공하는 변수로 현재 CMakeLists.txt가 위치한 디렉토리, 즉 현재 디렉토리를 의미한다.

file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS
  ${CMAKE_CURRENT_SOURCE_DIR}/*.cc
)

add_library(shape STATIC ${SRC_FILES})

위 명령은 현재 디렉토리 안에 있는 모든 .cc 로 끝나는 파일들 (하위 디렉토리 포함)을 나타낸다.

그리고 해당 파일들을 모두 모아서 SRC_FILES라는 변수를 구성하라는 의미다.

(하위 디렉토리를 포함하고 싶지 않다면 GLOB_RECURSE대신에 GLOB을 주면 된다.)

CONFIGURE_DEPENDS: 만약에 GLOB으로 불러오는 파일 목록이 이전과 다를 경우 (예를 들어서 파일을 추가하거나 지웠을 때) CMake 를 다시 실행해서 빌드 파일을 재생성 하라는 의미가 된다.

따라서 만약에 디렉토리 안에 파일이 추가 되더라도, cmake ..을 다시 실행할 필요 없이 그냥 make만 실행해도 CMake 가 다시 실행되면서 빌드 파일을 재작성 한다.

그러면 SRC_FILES 변수 안에 파일들의 목록이 쭈르륵 들어가 있으므로 아래 명령어를 입력하면

shape 를 빌드하는데 필요한 파일들을 모두 지정할 수 있다.

add_library(shape STATIC ${SRC_FILES})

Cmake 이외의 빌드 시스템 사용하기

요새 많이 사용되는 Ninja를 사용하고 싶을 수 있고, 아니면 비주얼 스튜디오를 사용할 경우 비주얼 스튜디오용 빌드 파일을 생성해야 한다.

아래와 같은 명령어를 사용하면 된다.

$ cmake .. -DCMAKE_GENERATOR=Ninja

가지 중요한 점은 이미 빌드 시스템을 설정하였다면 바꿀 수 없다는 것 새 디렉토리를 만들어서 CMake 명령을 다시 실행하거나, 기존 디렉토리 안의 파일들을 모두 지워야 한다.