Post

Cmake Practice

필요성

cmake는 C/C++ 프로젝트의 빌드를 관리하기 위한 툴이다. cmake를 공부하기 전에 cmake의 필요성에 대해 짚고 넘어가고자 한다.

cpp 코드를 하나의 실행파일로 빌드하기 위해서는 전처리, 컴파일, 어셈블리, 링크를 거쳐야 한다.

단계설명확장자
전처리#include, #define 같은 전처리 구문을 처리한다..i
컴파일전처리된 코드를 어셈블리어로 변환한다..s
어셈블리어셈블리어를 기계어로 번역한다..o
링크여러 기계어 파일을 연결하여 하나의 실행파일로 빌드한다..out

cpp 코드를 빌드할 수 있는 컴파일러는 여러가지가 있지만 가장 많이 사용하는 것은 g++이다. 다음 명령어를 입력하여 g++를 통해 빌드할 수 있다.

명령어설명
g++ -E 파일전처리한 결과를 화면에 출력
g++ -S 파일1 [파일2 ...]컴파일까지 수행하여 파일명.s로 저장
g++ -c 파일1 [파일2 ...]어셈블리까지 수행하여 파일명.o로 저장
g++ 파일1 [파일2 ...]모든 과정을 수행하여 a.out으로 저장

빌드과정에서 어셈블리까지의 과정에 많은 시간이 소요된다. 프로젝트의 규모에 따라 수 분에서 수 시간에 달하기도 한다. 하지만 처음에 한 번만 빌드해놓으면 다음부터는 수정한 파일만 다시 빌드하면 되기 때문에 시간을 절약할 수 있다. 이 때 어떤 파일이 수정되었는지 일일이 확인하는 것은 번거롭기 때문에 대부분의 C/C++ 프로젝트는 이 과정을 자동으로 관리하는 cmake를 사용하고 있다.

아래 CMake 관련 내용은 공식 튜토리얼을 따라 Step2까지 진행한다.

https://cmake.org/cmake/help/latest/guide/tutorial/index.html

cmake로 빌드하기

C/C++ 프로젝트를 cmake로 빌드하기 위해서는 다음 과정을 따른다.

  1. CMakeLists.txt라는 이름으로 파일을 만들고 관련 문법을 쓴다.
  2. 명령어 cmake <CMakeLists.txt 위치>를 입력하여 cmake 빌드 파일을 생성한다.
  3. 명령어 cmake --build <cmake 빌드 파일 위치>를 입력하여 프로젝트를 빌드한다.

한 번 위 과정을 수행하면 소스코드나 CMakeLists.txt를 수정하더라도 3번만 수행하면 된다.

관용적으로 소스코드가 있는 폴더와 별개로 build라는 이름의 폴더를 만들어 그 안에 cmake 빌드 파일을 생성하는 방식을 많이 사용한다.

cmake의 기본적인 함수와 변수

다음은 CMakeLists.txt에 들어가는 기본적인 내용이다.

cmake_minimum_required(VERSION (버전))
cmake 버전의 요구사항을 설정한다. 시스템에 설치되어있는 cmake의 버전이 호환되지 않을 경우 빌드가 실패된다. 모든 CMakeLists.txt파일은 항상 이 함수로 시작해야 한다. <버전>3.10, 3.12와 같이 하나의 숫자로 적거나 3.10...3.12와 같이 범위로 지정할 수도 있다.
project((이름) [VERSION (버전)])
해당 프로젝트를 지칭할 이름과 버전을 지정한다. 이름과 버전을 지정하는 이유는 나중에 다른 프로젝트와 정보를 교환할 때 지칭하기 위한 도구로 사용하기 위해서이다. 항상 cmake_minimum_required 다음에 나와야 한다.
add_executable((이름) (파일명))
실행파일의 이름과, 실행파일을 생성할 main 함수가 있는 파일을 지정한다.

또한 cmake에는 다양한 내부 변수가 존재한다. 다음과 같은 것들이 있다.

CMAKE_CXX_STANDARD
빌드할 때 사용되는 c++ 버전을 설정한다. c++11으로 빌드한다면 11으로 설정한다. 98, 11, 14, 17, 20으로 지정될 수 있다.
CMAKE_CXX_STANDARD_REQUIRED
CMAKE_CXX_STANDARD로 지정한 버전이 현재 시스템에서 적용될 수 없을 때의 행동을 지정한다. False이면 가장 최신 버전으로 빌드되고, True이면 오류를 낸다.
프로젝트이름_VERSION
프로젝트의 버전정보가 저장된다.
PROJECT_BINARY_DIR
cmake 빌드 파일이 저장되는 경로가 저장된다.

위 변수 중 <프로젝트이름>_VERSIONPROJECT_BINARY_DIR는 자동으로 지정된다. 하지만 CMAKE_CXX_STANDARDCMAKE_CXX_STANDARD_REQUIRED와 같은 몇몇 변수는 직접 지정해주어야 작동한다. 이러한 변수를 지정하는 방법은 set 함수를 사용하는 것이다.

set((이름) (값))
변수 이름과 값을 넣어 변수를 선언한다.

set(CMAKE_CXX_STANDARD 11)과 같이 변수를 지정하여 c++11으로 빌드되도록 지정할 수 있다. 이 변수는 add_execute를 지정하기 전에 선언되어야 적용된다.

cmake의 변수를 소스코드에서 사용하기

<프로젝트이름>_VERSION은 cmake에서 지정한 프로젝트의 버전 정보가 저장된다. 이 정보를 프로그램 내에서 가져와 사용하려면 다음 과정을 따른다.

  1. cmake config file을 만들어 관련 구문을 작성한다.
  2. CMakeLists.txt에서 configure_file함수를 통해 헤더파일을 생성한다.
  3. 헤더파일을 사용할 수 있도록 include경로를 추가한다.
  4. 생성될 헤더파일을 소스코드 안에서 사용한다.

cmake config file의 예시는 아래와 같다.

1
2
3
4
// clang-format off
#define 변수명1 @프로젝트이름_VERSION_MAJOR@
#define 변수명2 @프로젝트이름_VERSION_MINOR@
#define 변수명3 "@프로젝트이름_VERSION@"
configure_file((cmake config file 이름) (생성될 헤더파일 이름))
cmake config file을 헤더파일로 생성한다.

위와 같이 작성하고 CMakeLists.txt에서 configure_file(<cmake config file 이름> <생성될 헤더파일 이름>)을 호출하면 아래와 같은 헤더파일이 생성된다.

1
2
3
4
// clang-format off
#define 변수명1 1
#define 변수명2 0
#define 변수명3 "1.0"

보다시피 @로 양옆을 감싸서 cmake의 변수를 가져올 수 있다. 참고로 cmake config file의 확장자는 .h.in이 권장된다. 생성되는 헤더파일의 확장자는 .h가 권장된다.

clang-format off를 주석으로 작성하는 이유는 clang-format으로 인한 자동 포맷팅을 적용하지 않게 하기 위해서이다. 위의 경우 변수 이름을 @로 감싸는 구문이 있는데 여기에 자동 포맷팅이 적용되면 띄어쓰기가 들어가면서 오류가 나기 때문이다. https://stackoverflow.com/a/30487483

이렇게 생성된 헤더파일은 cmake 빌드파일이 생성되는 곳에 함께 생성된다. 따라서 이 헤더파일을 사용하려면 include 경로를 추가해줘야한다.

target_include_directories((실행파일이름) PUBLIC (파일 경로))
해당 실행파일을 링크할 때 include될 파일을 탐색할 경로를 추가한다.

cmake에서 빌드파일이 생성되는 경로는 PROJECT_BINARY_DIR으로 자동으로 저장된다. 따라서 target_include_directories(<실행파일이름> PUBLIC ${PROJECT_BINARY_DIR})을 적어주면 된다.

라이브러리 추가하기

모든 소스파일을 하나의 디렉토리에 넣고 프로젝트를 빌드하기 보다는 계층화/조직화하여 프로젝트를 구성하는 것이 더 선호된다. 하위 디렉토리로 조직화된 하나의 폴더를 라이브러리라고 하고, 이 라이브러리 안에는 별개의 CMakeLists.txt가 들어있어서 독립적인 관리를 할 수 있다.

라이브러리를 추가하기 위해 필요한 함수들은 다음과 같다.

add_library((라이브러리 이름) (소스파일1) [(소스파일2) …])
라이브러리를 생성한다. 이 라이브러리를 지칭할 이름을 입력하고 라이브러리를 구성할 소스파일을 선언한다. 라이브러리로 사용할 폴더의 CMakeLists.txt안에 일반적으로 적어준다.
add_subdirectory((라이브러리 경로))
cmake에서 접근할 하위 디렉토리를 추가한다. 최상단 CMakeLists.txt에서 라이브러리 폴더를 사용하려면 먼저 이 함수를 통해 알려줘야 한다.
target_link_libraries((프로젝트 이름) PUBLIC (라이브러리 이름))
라이브러리를 특정 프로젝트에 추가한다.

위와 같이 라이브러리를 연결하면 이제 앞서 헤더파일을 사용하기 위해 target_include_directories를 사용했던 것과 마찬가지로 라이브러리의 소스파일의 위치를 추가해줘야 한다. 이전과 다른 점은 configure_file 함수를 사용하지 않았기 때문에 cmake 빌드 파일 경로에 라이브러리 소스파일이 생성되는 것이 아니므로 현재 소스파일 경로의 라이브러리 위치를 추가해야 한다.

PROJECT_SOURCE_DIR
프로젝트 소스파일의 경로, 즉 CMakeLists.txt가 있는 위치가 저장된다.

위 변수를 이용하여 라이브러리 경로를 연결해주면 된다.

빌드 옵션 추가하기

cmake는 빌드할 때마다 옵션을 사용자 선호대로 줄 수 있는 기능을 제공한다. cmake를 빌드할 때 옵션을 cmake . -D<변수명>=<값>처럼 줄 수 있다. 이 옵션을 cmake에서 인식하기 위해 다음과 같은 함수를 사용한다.

option((변수명) (기본값))
변수명을 입력하여 어떤 옵션이 들어올 수 있는지 cmake에게 알려준다. 기본값을 지정하여 만약 옵션이 지정되지 않았을 경우의 동작을 제어할 수 있다.
if((변수명))
여타 다른 언어의 if와 같은 기능을 한다. <변수명>ON일 때만 이후의 명령어가 작동한다. endif()로 끝맺어주어야 한다.
target_compile_definitions((라이브러리이름) PRIVATE (변수명))
해당 라이브러리를 빌드할 때 변수명을 선언한 옵션(-D<변수명>)을 준다. 해당 소스코드에서 #ifdef <변수명> ~ #else ~ #endif와 같은 구문으로 제어할 수 있다.

위 함수를 추가하여 특정 옵션이 주어졌을 때만 add_library로 라이브러리를 생성하여, target_link_libraries로 기존의 라이브러리에 연결하는 등의 동작을 하도록 구성할 수 있다.

스코프 지정하기

위 설명 중간중간에 나온 PUBLIC, PRIVATE는 스코프를 지정해주는 것이다. 정리하자면 다음과 같다.

스코프설명
PRIVATE해당 타겟에 대해서만 적용
INTERFACE해당 타겟을 사용하는 대상에 대해서 적용
PUBLICPRIVATE + INTERFACE 둘 다 적용
This post is licensed under CC BY 4.0 by the author.