danbibibi
article thumbnail
Published 2024. 2. 19. 10:42
CMake, CMakeLists.txt 정리 언어/C, C++

CMake

  • 빌드 파일을 생성해주는 프로그램  ( = Makefile, .ninja를 자동으로 생성해주는 프로그램)
  • Makefile은 간단한 프로젝트를 관리하기 좋지만, 프로젝트 크기가 커지거나 여러 플랫폼에서 배포할 때 불편한 점이 많음
  • CMake를 사용 시 프로젝트 최상위 디렉토리에 CMakeLists.txt 파일이 있어야 함
    • 해당 파일에는 반드시 "CMake 프로그램의 최소 버전" 과 "프로젝트 정보"가 포함되어야 함
    • 주석은 '#' 으로 작성

 

#기본 템플릿

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

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

# 라이브러리 파일은 빌드 디렉토리 안에 lib 폴더에 출력.
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

# 실행 파일은 빌드 디렉토리 안에 bin 폴더에 출력.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

add_subdirectory(app)
add_subdirectory(lib)
add_subdirectory(tests)
MinGW를 기반으로 빌드를 한다면 아래와 같이 명령어 입력
$ cmake {CMakeLists.txt가 위치한 경로} -G "MinGW Makefiles"
mac에 cmake 설치
brew install cmake

 

CMakeLists.txt 

실행 파일 만들기

1) /foo.h , /foo.cpp , /main.cpp

// /foo.h
int foo();

// /foo.cpp
#include "foo.h"

int foo() { return 3; }
#include <iostream>
#include<stdlib.h>

#include "foo.h"

using namespace std;

int main() {
  cout << "Foo : " << foo() << "\n";
  system("pause"); // for check console
  return 0;
}

 

2) /CMakeLists.txt

  • CMake에서 실행 파일을 생성하기 위해 add_executable 사용
# CMake 프로그램의 최소 버전
cmake_minimum_required(VERSION 3.5.1)

# 프로젝트 정보
project(Test)

# 생성할 실행 파일을 추가하는 명령
add_executable (program main.cpp foo.cpp)

 

3) /build

$ mkdir build
$ cd build
$ cmake .. -G "MinGW Makefiles"
$ make
$ start program.exe
빌드 디렉토리를 만든후 해당 디렉토리에서 CMak 실행하는 이유
CMake 는 실행시 여러가지 파일들 (캐시 용도로) 을 생성하는데 이 때문에 프로젝트 디렉토리가 난장판이 될 수 있기 때문!!
( + 이미 있는 파일들을 덮어씌우는 문제가 발생할 수 있음)

 

컴파일 옵션 지정하기

  • target_compile_options 을 사용하여 지정
target_compile_options(program PUBLIC -Wall -Werror)

-Wall (모든 경고 표시)
-Werror (경고는 컴파일 오류로 간주)

 

Target 과 Property

  • 타겟 (Target) : 프로그램을 구성하는 요소들 (실행파일, 라이브러리 등)
  • 속성 (Property)
모든 CMake 명령은 타겟을 정의하고(add_executable 와 같이), 해당 타겟들의 속성을 지정하는 명령들(target_compile_options 처럼)로 이루어짐

 

include 경로 지정하기

  • foo.h 가 includes/ 폴더 안에 있는 경우
  • CMakeLists.txt 에 includes 디렉토리를 헤더 파일 경로 탐색 시 확인하라고 알려줘야 함
  • {CMAKE_SOURCE_DIR} : CMake에서 기본으로 제공하는 변수로, 최상위 경로(=프로젝트 경로)를 의미
$ tree
├── CMakeLists.txt
├── foo.cpp
├── includes
│   └── foo.h
└── main.cpp
target_include_directories(program PUBLIC ${CMAKE_SOURCE_DIR}/includes)
# CMake 프로그램의 최소 버전
cmake_minimum_required(VERSION 3.5.1)

# 프로젝트 정보
project(Test)

# 생성할 실행 파일을 추가하는 명령
add_executable (program main.cpp foo.cpp)

# 컴파일 옵션
target_compile_options(program PUBLIC -Wall -Werror)

# includes 경로를 알려줌
target_include_directories(program PUBLIC ${CMAKE_SOURCE_DIR}/includes)
target_compile_options, target_include_directories 명령어를 사용하기 위해서는 해당 타겟이 먼저 생성되어야 함

따라서, target_compile_options와 target_include_directories 명령어는 해당 타겟이 생성된 후에 작성되어야 올바르게 동작함.
CMake에서 디렉토리의 경로를 지정할 때 왠만하면 절대 경로를 쓰지 않는 것이 좋음
CMak 의 가장 큰 장점이 "여러 플랫폼에서 사용할 수 있다"는 것인데, 절대 경로로 박아 놓으면 다른 시스템에서 사용할 수 없기 때문

 

라이브러리 만들기

  • 라이브러리로 쪼개서 관리하면, 바뀌지 않은 부분은 컴파일 하지 않아도 되서 (링킹만 하면 됨) 개발 속도가 빠름
  • 라이브러리의 각 요소들을 전체를 한꺼번에 묶어 놓았을 때 보다 테스팅 하기 용이함

보통 어느 정도 규모가 큰 C++ 프로젝트

 

make로 작업한 경우

  • 외부 라이브러들이 잘 설치되어 있는지 확인
  • 라이브러리1, 2 가 각각에 맞는 외부 라이브러리들을 참조할 수 있도록 설정
  • 실행 파일을 만들 때 라이브러리 1과 2를 사용하도록 설정

 

CMake로 작업한 경우

add_library 이용하여 위 작업들을 간단하게 수행 가능

add_library (<라이브러리 이름> [STATIC | SHARED | MODULE ] <소스 1> <소스 2> ...)

STATIC : 정적 라이브러리
SHARED : 동적 라이브러리 (동적으로 링크)
MODULE : 동적으로 링크되지 않지만, dlopen과 같은 함수로 런타임시 불러올 수 있는 라이브러리

* 보통 정적으로 링크하면 실행 파일의 크기가 커지는 대신 동적 라이브러리를 사용할 때 보다 빠름

 

1) includes/shapes.h

  • 헤더 파일은 includes 폴더 안에 정의
  • 라이브러리를 사용 시 구현 부분을 참조할 필요는 없지만 헤더는 꼭 참조해야 하기 때문에 라이브러리와 헤더 소스를 분리
class Rectangle {
 public:
  Rectangle(int width, int height);

  int GetSize() const;

 private:
  int width_, height_;
};

 

2) lib/shape.cpp

#include "shape.h"

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

int Rectangle::GetSize() const { // 직사각형의 넓이 반환
  return width_ * height_;
}

 

3) lib/CMakeLists.txt

  • shape 라이브러리는 /lib 안에 구현되어 있으므로 해당 위치에 CMakeLists.txt를 생성
# 정적 라이브러리 shape을 만듦
add_library(shape STATIC shape.cpp)

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

# 해당 라이브러리를 컴파일 할 옵션
target_compile_options(shape PRIVATE -Wall -Werror)
PRIVATE vs PUBLIC
A 라이브러리가 B 라이브러리를 사용한다면 A는 B 의 컴파일 옵션들과 헤더 파일 탐색 디렉토리 목록을 물려받는데,
PUBLIC 으로 설정된 것은 물려 받고, PRIVATE 으로 설정된 것은 물려받지 않음

 

4) /main.cpp

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

using namespace std;

int main(){
    Rectangle r(4, 5);
    cout << r.GetSize();
}

 

5) /CMakeLists.txt

프로젝트 레벨 CMakeLists.txt

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

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

# 확인할 디렉토리 추가
add_subdirectory(lib)

add_executable (program main.cpp)

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

 

 

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

  • ex) shape 라이브러리에서 thread 라이브러리르 사용하는 경우
  • thread 라이브러리를 사용하려면 pthread 라이브러리를 링크시켜줘야 함
  • 따라서 아래와 같이 shape의 CMakeLists.txt를 수정해줘야 함
#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_;
}
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)
라이브러리 A를 참조할 때,
A를 헤더 파일과 구현 내부에서 모두 사용한다면 : PUBLIC
A를 내부 구현에서만 사용하고 헤더 파일에서는 사용하지 않는다면 : PRIVATE
A를 헤더 파일에서만 사용하고 내부 구현에서는 사용하지 않는다면 : INTERFACE

위 경우 <thread> 를 내부 구현(shape.cpp)에서만 사용하고 헤더 파일(shape.h) 에서는 사용하지 않으므로 PRIVATE으로 링크해주는 것이 맞음 !!

 

파일들 한번에 추가하기

  • add_library를 이용하는 경우, 파일들을 새로 추가할 때 마다 add_library를 수정해줘야 함
  • 이러한 귀찮음을 해결하기 위해 해당 디렉토리에 있는 파일들을 모두 해당 라이브러리를 빌드하는데 사용하도록 할 수 있음
  • file : CMake 에서 파일들을 관련해서 다룰 때 사용하는 명령
  • GLOB_RECURSE 옵션 : 인자로 주어진 디렉토리와 해당 디렉토리 안에 있는 모든 하위 디렉토리 까지 재귀적으로 살펴봄
file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS
  ${CMAKE_CURRENT_SOURCE_DIR}/*.cc
)

add_library(shape STATIC ${SRC_FILES})
CMake에서 변수 사용, ${변수이름}
* make는 소괄호 ()로 감싸주는 반면, CMake는 중괄호 {}로 감싸줌

 

원하는 라이브러리를 설치하는 FetchContent

  • CMake 에서 제공하는 FetchContent 를 사용하면 왠만한 외부 라이브러리들을 쉽게 불러오고 설치할 수 있음 (Python의 PIP와 같은 역할)
  • FetchContent 를 사용하기 위해서는 적어도 3.11 이상 버전의 CMake 를 사용해야함 
  • 이전 버전의 경우 ExternalProject 모듈 사용 
  • ex) fmt 라이브러리를 사용하고 싶은 경우
# FetchContent 모듈을 불러옴
include(FetchContent)

# 어디에서 데이터를 불러올지 명시
FetchContent_Declare(
  Fmt
  GIT_REPOSITORY "https://github.com/fmtlib/fmt"
  GIT_TAG "7.1.3"
  )

# fmt 를 사용할 수 있도록 설정
FetchContent_MakeAvailable(Fmt)
FetchContent vs ExternalProject
FetchContent 는 CMake 를 실행하는 시점에서 외부 파일들을 불러오는 반면 ExternalProject 는 빌드 타임에 불러옴

 

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

  • "Unix Makefiles" (디폴트)
  • "Ninja"
  • "Visual Studio 16 2019" 등등
$ cmake .. -DCMAKE_GENERATOR=Ninja
이미 빌드 시스템을 설정하였다면 바꿀 수 없음
디렉토리를 만들어서 CMake 명령을 다시 실행하거나, 기존 디렉토리 안의 파일들을 모두 지워야 함

 

 

 

CMake 할때 쪼오오금 도움이 되는 문서

CMake 할때 쪼오오금 도움이 되는 문서. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

'언어 > C, C++' 카테고리의 다른 글

[C/C++] 공백을 포함한 파일을 문자열 변수에 쓰기  (0) 2024.05.27
make, Makefile 정리  (0) 2024.02.13
[C++] stringstream  (0) 2024.01.02
RapidJSON 정리  (0) 2023.12.27
json-c 정리  (1) 2023.12.26
profile

danbibibi

@danbibibi

꿈을 꾸는 시간은 멈춰 있는 것이 아냐 두려워하지 마 멈추지 마 푸른 꿈속으로