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로 빌드하기 위해서는 다음 과정을 따른다.
CMakeLists.txt
라는 이름으로 파일을 만들고 관련 문법을 쓴다.- 명령어
cmake <CMakeLists.txt 위치>
를 입력하여 cmake 빌드 파일을 생성한다. - 명령어
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 빌드 파일이 저장되는 경로가 저장된다.
위 변수 중 <프로젝트이름>_VERSION
나 PROJECT_BINARY_DIR
는 자동으로 지정된다. 하지만 CMAKE_CXX_STANDARD
나 CMAKE_CXX_STANDARD_REQUIRED
와 같은 몇몇 변수는 직접 지정해주어야 작동한다. 이러한 변수를 지정하는 방법은 set
함수를 사용하는 것이다.
- set((이름) (값))
- 변수 이름과 값을 넣어 변수를 선언한다.
set(CMAKE_CXX_STANDARD 11)
과 같이 변수를 지정하여 c++11으로 빌드되도록 지정할 수 있다. 이 변수는 add_execute
를 지정하기 전에 선언되어야 적용된다.
cmake의 변수를 소스코드에서 사용하기
<프로젝트이름>_VERSION
은 cmake에서 지정한 프로젝트의 버전 정보가 저장된다. 이 정보를 프로그램 내에서 가져와 사용하려면 다음 과정을 따른다.
- cmake config file을 만들어 관련 구문을 작성한다.
CMakeLists.txt
에서configure_file
함수를 통해 헤더파일을 생성한다.- 헤더파일을 사용할 수 있도록 include경로를 추가한다.
- 생성될 헤더파일을 소스코드 안에서 사용한다.
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 | 해당 타겟을 사용하는 대상에 대해서 적용 |
PUBLIC | PRIVATE + INTERFACE 둘 다 적용 |