You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
프로젝트의 이름을 지정할 수 있습니다.
CMakeLists.txt에 다음과 같이 작성하는 것으로 프로젝트에 이름을 부여하게 됩니다.
# CMake에서 주석은 #을 사용해 작성합니다project(my_project)
좀 더 상세하게, 언어와 버전을 명시하는 것도 가능합니다.
# 한 줄에 모든 것을 적을 수 있습니다project(my_project LANGUAGES CXX VERSION 1.2.3)
# multi-line으로 작성하는 것 또한 가능합니다project(my_project
LANGUAGES CXX
VERSION 1.2.3
)
이렇게 project를 명시하게 되면 Visual Studio 에서는 같은 이름으로 Solution 파일이 생성됩니다. 즉, project만으로는 프로그램을 생성하지 않습니다.
실제로 프로그램을 생성하기 위해서는 add_executable, add_library 를 사용하여야 합니다.
소스코드 조직화
프로그램(exe, lib, dll, a, so, dylib ...)을 만들기 위해서는 컴파일러에게 제공할 소스코드가 필요합니다.
CMake에게 소스코드 목록으로부터 생성할 프로그램의 타입을 지시하기 위해 사용하는 함수들이 바로 add_executable, add_library입니다. project는 하나만 가능하지만, 이 함수들은 CMakeList안에서 여러번 사용되기도 합니다. 빌드 결과 생성되는 프로그램의 이름만 다르다면 크게 문제되지 않습니다.
우선 Root CMakeList(project-example/CMakeLists.txt)가 아니라 src/ 폴더에 있는 CMakeList(project-example/src/CMakeLists.txt)부터 살펴보겠습니다.
만약 src 폴더에 있는 모든 .cpp 파일들이 실행 파일(exe)을 만든다면, 아래와 같은 내용이 작성되어야 합니다.
# project-example/src/CMakeLists.txt # case 1add_executable(my_exe # 이후에 나오는 .cpp 파일을 사용해 .exe를 생성한다
main.cpp
# 개행을 여러번 하여도 문제되지 않습니다.
feature1.cpp
feature2.cpp
algorithm3.cpp # 상대 경로로 소스 코드를 찾아냅니다. # 현재 사용중인 CMakeList의 위치를 기준으로# 경로를 지시해야 합니다
data_structure4.cpp
# ...
)
만약 라이브러리를 만든다면, 아래와 같은 내용이 작성되어야 합니다.
# project-example/src/CMakeLists.txt # case 2add_library(my_lib # 이후에 나오는 .cpp 파일을 사용해 라이브러리를 생성한다
main.cpp
feature1.cpp
# ...
)
라이브러리의 링킹의 형태를 명시하지 않는다면 여기서 생성되는 라이브러리는 프로젝트 생성시 BUILD_SHARED_LIBS 변수를 따라서 결정됩니다. 물론 직접 명시할 수도 있습니다.
이번에는 Root CMakeList(project-example/CMakeLists.txt)를 살펴보겠습니다. 이미 어떻게 프로그램을 생성할 것인지는 앞서 src 폴더에서 작성한 CMakeLists가 잘 처리하고 있기 때문에, Root CMakeList에서는 이를 그대로 사용할 것입니다.
여기서 current-project/CMakeLists.txt는 add_subdirectory로 하위 디렉터리가 아닌 다른 곳을 참고 하고 있습니다.
# current-project/CMakeLists.txtcmake_minimum_required(VERSION 3.10) # CMake 버전을 명시project(my_project)
add_subdirectory(../far-far-away) # 상대경로를 사용해 접근
이대로 CMake를 실행하면 다음과 같은 오류를 발생시킵니다.
# ...-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - doneCMake Error at CMakeLists.txt:6 (add_subdirectory): add_subdirectory not given a binary directory but the given source directory "X:/Develop/out-of-tree/far-far-away" is not a subdirectory of "X:/Develop/out-of-tree/current-project". When specifying an out-of-tree source a binary directory must be explicitly specified.-- Configuring incomplete, errors occurred!
다행히도, add_subdirectory를 하는 CMakeList에서 경로를 조정하는 방법으로 사용할 수 있습니다. When specifying an 'out-of-tree' source a binary directory must be explicitly specified 문장을 다시 한번 보시기 바랍니다. binary directory를 명시할 것을 요구하고 있습니다.
# current-project/CMakeLists.txtcmake_minimum_required(VERSION 3.10) # CMake 버전을 명시project(my_project)
add_subdirectory(../far-far-away # CMakeList가 위치한 source dir
./build/far-far-away-dir # 빌드 결과물을 배치할 binary dir을 지정
)
CMake 문서의 지시를 따라 위와 같이 수정하면 아래와 같은 결과를 얻습니다.
# ...-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Configuring done-- Generating done-- Build files have been written to: X:/Develop/out-of-tree/current-project
오류가 사라진 것을 확인할 수 있습니다.
폴더를 열어 확인해보면 지정한 build directory 경로에 빌드 파일들이 생성되어 있습니다.
PS X:\Develop\out-of-tree> tree .Folder PATH listing for volume Drive_XVolume serial number is D688-4B7EX:\DEVELOP\OUT-OF-TREE├─current-project│ ├─build│ │ └─far-far-away-dir # <----- ./build/far-far-away-dir│ │ └─CMakeFiles│ └─CMakeFiles│ ├─3.10.1│ ├─a8084ac71a5a995e5212369c2e1624fc│ └─CMakeTmp└─far-far-away
예시: 단일 실행 파일 / 라이브러리
당연하게도, 간단한 프로젝트들이 모여야 큰 프로젝트를 이루게 됩니다. C++로 하나의 프로그램을 만든다면 아마 아래와 같이 CMake 프로젝트를 구성해야 할 것입니다.
OpenCV와 같이 좀 더 복잡하게 구성할 수도 있습니다. 다수의 모듈들을 한번 더 조직화 하기도 합니다. 하위 경로에 있기만 하다면 add_subdirectory를 사용하는데 크게 문제될 일이 없습니다.
출처에 따라서 Internal/External로 두기도 하고, 목적에 따라서 특별한 이름을 붙일 수도 있습니다.
앞서까지는 CMake를 실행하는 방법을 언급하지 않았습니다.
CMake가 빌드 시스템에 맞는 파일들(sln, vcxproj, MakeFiles, ninja ...)을 생성하기 위해선 당연하게도 "생성기"를 명시해야 합니다. 이 생성기는 플랫폼에 맞는 개발 툴을 지정하게 됩니다. 이 과정은 Configure / Generate 라는 2 단계로 구성됩니다.
Configuration : CMakeLists.txt 파일 해석
Generation: 해석 결과를 바탕으로 Project 파일 생성
아래는 커맨드라인으로 각 플랫폼에서 주로 사용되는 IDE에 맞는 파일을 생성하는 것을 보여줍니다
Windows
PowerShell에서는 ""를 사용해 문자열을 명시한 점에 주의하십시오
# Generate for VS
cmake ./Path/to/Root/-G "Visual Studio 15 2017 Win64"
Unix/Linux
# Generate for Ninja
cmake ./Path/to/Root/ -G Ninja
MacOS
# Generate for XCode
cmake ./Path/to/Root/ -G XCode
거듭 강조하자면, CMake의 목적은 파일을 생성하는데 있으며, 프로그램 빌드를 하지는 않습니다.
다만 아래와 같이 커맨드라인에서 -G 옵션으로 지정한 빌드시스템을 호출하도록 명령할 수는 있습니다.
Powershell/Bash 모두 동일합니다.
# ... 지정된 툴을 사용해 빌드를 진행한다 ...
cmake --build .
툴체인 파일은 CMake가 지원하는 다양한 기능들을 사용해 빌드를 수행할 수 있도록 미리 지정된 파일을 의미합니다.
대표적으로 Android NDK에서는 여러 아키텍처로의 크로스 컴파일에 필요한 설정들이 작성된 android.toolchain.cmake 파일이 함께 제공되며, CMake를 사용한 빌드를 수행시에 Gradle에 의해서 자동으로 지정됩니다.
iphone을 대상으로 하는 경우에는 https://github.com/leetal/ios-cmake 를 사용해 XCode 프로젝트를 생성하기도 합니다.
특히 이 함수는 Target의 의존성을 전파시키는 역할도 수행합니다. 위와 같은 경우, PRIVATE와 PUBLIC을 사용해 이를 제어하고 있습니다. C++ class의 멤버 접근 한정자(Access Qualifier)와 유사하게 생각할 수 있습니다.
위와 같이 작성하면 다른 프로젝트에서 my_custom_logger_lib을 link하는 경우, spdlog와 fmt에 있는 헤더파일을 include하기 위한 경로와 빌드 결과 생성되는 라이브러리에 접근할 수 있게 됩니다. 하지만 PRIVATE로 선언된 utf8proc은 이와 같은 정보를 차단합니다.
add_executable(some_test_program)
# ...
)
target_link_libraries(some_test_program
PUBLIC
my_custom_logger_lib # spdlog 와 fmt 이 적혀있지 않지만 자동으로 추가된다
)
의존성이 공유되어야 하는 경우, 혹은 기타 라이브러리와 함께 사용하는 라이브러리라면 PUBLIC, 내부 구현에만 사용되고 공개되지 않는 경우라면 PRIVATE에 배치하는 것이 적합합니다.
플랫폼 대응
하지만 크로스 플랫폼은 아주아주 힘든 일입니다. CMake에서도 이 문제에 대응해보겠습니다
CMake 변수
프로그래머라면 변수에 익숙할 것입니다. CMake에서도 변수가 있으며, 단순한 Boolean, 문자열, 파일 경로, 리스트 등을 표현할 수 있습니다. 이 튜토리얼에서는 이 중 빈번하게 사용되는 변수들을 짚고 넘어가겠습니다
변수의 값을 사용하기 위해 ${}를 사용한 점을 주의깊게 보시길 바랍니다.
message는 문자열을 출력하므로 변수를 그대로 주지 않고 한번 참조하여 문자열로 변환한 것입니다. 그대로 변수 이름만을 제공할 경우 변수 이름을 문자열로 바꿔 출력하는 것을 확인할 수 있습니다
조건부 처리
if/elseif/else/endif
플랫폼이 다르면 System API, 혹은 같은 API더라도 구현 형태나 지원 범위가 상이할 수 있습니다. 조건부 분기문을 사용해 플랫폼에 맞게 미리 작성된 라이브러리를 선택하도록 하면 상대적으로 부담이 줄어들 것입니다.
CMake는 플랫폼 관련 변수들을 제공하고 있으며, Android 혹은 iOS로 크로스 컴파일을 하기 위해 CMake Toolchain을 사용한 경우 그 값을 참고해 처리를 다르게 할 수 있습니다.
# *현재* CMake가 실행되는 시스템을 알려진 변수들로 확인하는 방법# wrapper::system 같은 별명을 붙이면 상대적으로 편해진다if(WIN32)
add_subdirectory(external/winrt)
add_subdirectory(impl/win32)
elseif(APPLE)
add_subdirectory(impl/posix)
# additional implementation for MacOS add_subdirectory(impl/macos)
elseif(UNIX)
add_subdirectory(impl/posix)
# additional implementation with Linux API if(${CMAKE_SYSTEM}MATCHES Linux)
add_subdirectory(impl/linux)
endif()
else()
# 지원하지 않음.# android.toolchain.cmake 혹은 ios.toolchain.cmake 에서 지정하는 변수들 if(ANDROID OR IOS)
message(FATAL_ERROR "No implementation for the platform")
endif()
# ...endif()
가장 윗 줄에 주석으로 적은 것처럼 현재 시스템을 변수로 알려주는 것이라는 점에 주의하시기 바랍니다. 크로스 컴파일을 위한 라이브러리라면 변수를 검사하는 순서, 조건문에 주의를 기울여야 합니다.
접근법
구현이 다르더라고, 공통된 인터페이스(헤더파일)가 있다면 이들을 한곳에 모아놓는 것이 타당할 것입니다. 문서 초반부에 프로젝트 예시로 include 폴더가 꾸준히 나타났다는 것을 기억하십니까?
# some-huge-project/impl/win32/CMakeLists.txtadd_library(my_win32_wrapper
src/i-love-win32.cpp
)
add_library(wrapper::system ALIAS my_win32_wrapper)
target_include_directories(my_win32_wrapper
PUBLIC${CMAKE_SOURCE_DIR}/include# CMAKE_SOURCE_DIR 는 최상위 CMakeLists.txt가 위치한 폴더를 의미한다.# 이 프로젝트에서는 some-huge-project/PRIVATE${CMAKE_CURRENT_SOURCE_DIR}/include# include 폴더를 절대경로를 사용해 접근# CMAKE_CURRENT_SOURCE_DIR 는 현재 해석 중인 CMakeLists.txt가 위치한 폴더를 의미한다.# 즉, 경로는 some-huge-project/impl/win32
)
POSIX API에 맞춘 프로젝트의 CMakeList는 이렇게 작성할 수 있습니다.
# some-huge-project/impl/posix/CMakeLists.txtadd_library(my_posix_wrapper
src/i-love-posix.cpp
)
add_library(wrapper::system ALIAS my_posix_wrapper)
target_include_directories(my_posix_wrapper
PUBLIC${CMAKE_SOURCE_DIR}/include# win32와 header 파일들을 공유한다. # 예컨대 some-huge-project/include에 위치한 system_wrapper.hPRIVATE include# include 폴더를 상대경로 접근# some-huge-project/impl/posix/include를 의미한다
)
예시에서 본것과 같이 이 함수는 target_link_libraries처럼 PUBLIC,PRIVATE을 지정할 수 있으며, PUBLIC에 위치한 폴더들은 경로가 자동으로 전파됩니다.
# some-huge-project/src/CMakeLists.txtadd_executable(my_system_utility
some.cpp
source.cpp
files.cpp
)
target_link_libraries(my_system_utility
PRIVATE
wrapper::system# wrapper 라이브러리들의 ALIAS# target_include_directories에 PUBLIC으로 명시된# ${CMAKE_SOURCE_DIR}/include 폴더를 자동으로 접근할 수 있게 된다
)
add_executable과 add_library의 문제점은 소스 파일 목록을 한번에 결정해서 전달해야 한다는 점입니다. 이는 Target들이 CMakeList 파일의 끝부분에 나타나게 만들며, 2.x 버전 CMake들이 사용했던 방법처럼 List 변수를 사용해 소스파일 목록을 만들어야 하는 불편함이 있습니다
플랫폼이 달라지면 컴파일러도 달라질 수 있습니다. 컴파일러가 달라지면 프로그램 생성에 사용할 수 있는 컴파일 옵션들이 달라지게 됩니다. 이 튜토리얼에서는 간단히 Warning, Optimization를 다르게 적용하는 예시를 보이겠습니다
컴파일러 검사
앞서서 Platform을 검사한 것처럼, Compiler와 관련해 미리 지정된 변수들이 존재합니다.
관련 CMake 변수들
아래의 내용을 CMakeList에 추가한 이후 실행해보시기 바랍니다.
CMAKE_CXX_COMPILER_ID: 컴파일러의 이름
CMAKE_CXX_COMPILER_VERSION: 컴파일러의 버전
CMAKE_CXX_COMPILER: 컴파일러 실행파일의 경로
message(STATUS"Compiler")
message(STATUS" - ID \t: ${CMAKE_CXX_COMPILER_ID}")
message(STATUS" - Version \t: ${CMAKE_CXX_COMPILER_VERSION}")
message(STATUS" - Path \t: ${CMAKE_CXX_COMPILER}")
Windows에서 별다른 조작 없이 Visual Studio 15 2017 Win64를 Generator로 지정하면, 아래와 같이 출력될 것입니다. (Community 버전 기준)
-- Compiler-- - ID : MSVC-- - Version : 19.16.27024.1-- - Path : C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.16.27023/bin/Hostx86/x64/cl.exe
컴파일러를 Clang-cl을 명시하면 아래와 같은 출력을 확인할 수 있습니다.
-- Compiler-- - ID : Clang-- - Version : 7.0.0-- - Path : C:/Program Files/LLVM/bin/clang-cl.exe
Mac OS에서는 아래와 AppleClang에 대한 정보를 보여줍니다.
-- Compiler-- - ID : AppleClang-- - Version : 9.1.0.9020039-- - Path : /Applications/Xcode-9.4.1.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++
아래와 같이 CMake 내에서 컴파일러에 따라서 처리를 다르게 할 수 있습니다.
if(MSVC) # Microsoft Visual C++ Compiler# ...elseif(${CMAKE_CXX_COMPILER_ID}MATCHES Clang) # Clang + AppleClang# ...elseif(${CMAKE_CXX_COMPILER_ID}MATCHES GNU) # GNU C Compiler# ...endif()
선술한 플랫폼 변수들을 함께 고려하면 조합이 많이 발생할 수 있기 때문에, 플랫폼에 따라서 특정 컴파일러만을 지원하는 것이 타당할 것입니다.
컴파일 옵션 사용
include(CheckCXXCompilerFlag)
컴파일러를 식별할 수 있게 되었으니 이제 컴파일러 옵션을 지정해줄 차례입니다. 하지만 그 전에 컴파일러가 해당 옵션을 지원하는지 검사가 필요한 경우도 있습니다. CMake의 기본 모듈들 중에는 이를 지원하는 CheckCXXCompilerFlag라는 모듈이 있습니다.
CMake Module은 간단히 말하자면 미리 작성된 CMake 파일이라고 생각할 수 있습니다. 이런 파일들은 각 sub-project들마다 각각 cmake 폴더를 만들어 배치하는 경우가 많습니다.
tree -L 2 ..├── CMakeLists.txt├── cmake # <---- 이 프로젝트에서 필요한 cmake 파일들을 모아둔다│ ├── display-compiler-info.cmake│ └── test-cxx-flags.cmake├── include│ └── ...├── modules│ ├── ...│ └── ...├── scripts│ └── ...├── src└── test
test-cxx-flag.cmake 파일의 내용은 아래와 같습니다. CMake Module에 대한 내용은 후술할 것이므로, CMake를 처음 접하였다면 같은 내용을 CMakeList에 추가하는 것으로 같은 효과를 가져올 수 있습니다.
# test-cxx-flags.cmake## `include(cmake/check-compiler-flags.cmake)` from Root CMakeList#include(CheckCXXCompilerFlag)
# Test latest C++ Standard and High warning level to prevent mistakesif(MSVC)
check_cxx_compiler_flag(/std:c++latest cxx_latest )
check_cxx_compiler_flag(/W4 high_warning_level )
elseif(${CMAKE_CXX_COMPILER_ID}MATCHES Clang)
check_cxx_compiler_flag(-std=c++2a cxx_latest )
check_cxx_compiler_flag(-Wall high_warning_level )
elseif(${CMAKE_CXX_COMPILER_ID}MATCHES GNU)
check_cxx_compiler_flag(-std=gnu++2a cxx_latest )
check_cxx_compiler_flag(-Wextra high_warning_level )
endif()
이같은 내용을 추가하여 CMake를 실행하면 다음과 같은 내용이 나타날 것입니다. check_cxx_compiler_flag는 해당 컴파일 옵션을 사용할 수 있으면 이후에 지정한 이름으로 변수를 저장합니다.
# ...-- Compiler-- - ID : Clang-- - Version : 6.0.0-- - Path : /usr/bin/clang-6.0
# ...-- Performing Test cxx_latest-- Performing Test cxx_latest - Success-- Performing Test high_warning_level-- Performing Test high_warning_level - Success
# ...
위와 같은 경우, clang-6.0은 -std=c++2a, -Wall를 모두 사용할 수 있으므로 cxx_latest, high_warning_level 변수는 모두 true(1)값을 가질 것입니다. 특정 컴파일 옵션을 사용할 수 없는 경우 경고하거나 우회하는 CMakeList를 상상해보시기 바랍니다.
이제 컴파일 옵션을 사용할 준비과 되었으므로, 간단히 Target에서 사용할 컴파일 옵션을 지정하는 예시를 보이겠습니다.
if(MSVC)
target_compile_options(my_modern_cpp_lib
PUBLIC
/std:c++latest /W4 # MSVC 가 식별 가능한 옵션을 지정
)
else() # Clang + GCC target_compile_options(my_modern_cpp_lib
PUBLIC
-std=c++2a -Wall # GCC/Clang이 식별 가능한 옵션을 지정PRIVATE
-fPIC
-fno-rtti
)
endif()
이번에도 PUBLIC, PRIVATE으로 컴파일 옵션을 전파시킬 수 있습니다. 즉, my_modern_cpp_lib를 target_link_libraries로 사용하는 모든 Target들은 C++ latest, Warn All 옵션으로 빌드가 수행 됩니다.
하지만 옵션의 중복을 걱정할 필요는 없습니다. 중복되는 옵션은 CMake에서 자동으로 하나로 합쳐서 적용하게 됩니다.
최신 C++에서는 Macro를 대체할 방법으로 enum class, constexpr등이 있습니다만, 여전히 Macro에 의존하고 있는 코드가 많은 것 또한 사실입니다. 하지만 수십, 혹은 수백개의 소스 파일에 Macro를 선언하려면 시간이 많이 들뿐만 아니라 이후에 수정하기도 번거로울 것입니다.
if(MSVC)
# 묵시적으로 #define을 추가합니다 (컴파일 시간에 적용) target_compile_definitions(my_modern_cpp_lib
PRIVATE
NOMINMAX # numeric_limits를 사용할때 방해가 되는# max(), min() Macro를 제거합니다
_CRT_SECURE_NO_WARNINGS
# Visual Studio로 C++에 입문했다면 한번쯤 만나본 녀석일 겁니다# 오래된 코드를 위한 프로젝트라면 선택의 여지가 없을 수도 있겠죠...
)
endif()
물론 여기에도 PUBLIC, PRIVATE가 있습니다. 아마 처음부터 읽으셨다면 어떤 기능을 하는지 더는 설명이 필요 없을 것입니다. 다만 이 방법으로 추가하는 Macro는 소스코드에 보이지 않기 때문에 프로젝트를 가져다 쓰는 사람이 찾아내기 어려울 수 있습니다. 평시에 신중하게, 주의하면서 사용하는게 좋을 것입니다.
CMake 파일 작성
변수와 조건문에 대해서 배우고 나면 보통 함수에 대해서 배우게 됩니다. 안타깝게도 이 튜토리얼 CMake Macro와 CMake Function에 대해서는 생략할 것입니다. 대신, 앞서 CheckCXXCompilerFlag와 같은 형태의 CMake Module에 대해서는 짚고 넘어가겠습니다.
CMake는 나름의 문법과 처리방식이 있기 때문에, 전용 확장자가 없는게 더 이상할 것입니다. 최초의 Root CMakeList 호출이나 add_subdirectory는 CMakeLists.txt를 사용하지만, 그렇지 않은 경우라면 보통 .cmake파일을 사용하게 됩니다.
앞서 Toolchain파일들이 이런 확장자를 가지고 있었던 것을 기억해주시기 바랍니다.
아래와 같은 프로젝트를 가정해봅시다.
tree -L 2 ..├── CMakeLists.txt # <---- Root├── cmake│ ├── display-compiler-info.cmake # <---- going to `include`│ └── test-cxx-flags.cmake├── include│ └── ...├── src│ ├── CMakeLists.txt # <---- going to `add_subdirectory`│ └── ...└── test ├── CMakeLists.txt # <---- going to `add_subdirectory` └── ...
add_subdirectory는 기본적으로 함수(서브루틴)의 유효범위라고 할 수 있습니다. 독립적으로 CMake 변수를 가지고, 별도로 지시하지 않는 한 상위 CMakeList의 변수를 변경하지 않습니다. sub-project에서 벗어나면 그 변수들은 사라집니다.
반면 include는 C++ inline과 유사합니다. include를 통해 실행되는 cmake는 현재 CMakeLists의 변수들에 그대로 접근할 수 있습니다. 물론 새로운 변수를 추가할 수도 있죠
# Root/CMakeLists.txtproject(my_new_project)
include(cmake/check-compiler-flags.cmake) # 주석의 코드를 Ctrl+C/V 한 것처럼 동작한다## include(CheckCXXCompilerFlag) # 또다른 CMake 기본 모듈을 가져온다## if(MSVC)# check_cxx_compiler_flag(/std:c++latest cxx_latest )# check_cxx_compiler_flag(/W4 high_warning_level )# elseif(${CMAKE_CXX_COMPILER_ID} MATCHES Clang)# check_cxx_compiler_flag(-std=c++2a cxx_latest )# check_cxx_compiler_flag(-Wall high_warning_level )# elseif(${CMAKE_CXX_COMPILER_ID} MATCHES GNU)# check_cxx_compiler_flag(-std=gnu++2a cxx_latest )# check_cxx_compiler_flag(-Wextra high_warning_level )# endif()#if(cxx_latest) # include 파일 내에서 설정한 변수를 사용 가능하다 target_compile_options()
endif()
message(STATUS${CMAKE_SOURCE_DIR}) # -- Rootmessage(STATUS${CMAKE_CURRENT_SOURCE_DIR}) # -- Rootadd_subdirectory(src) # src/CMakeLists.txt를 실행하기 전에 일부 변수들이 새로 설정된다## message(STATUS ${CMAKE_SOURCE_DIR}) # -- Root# message(STATUS ${CMAKE_CURRENT_SOURCE_DIR}) # -- Root/src#add_subdirectory(test)
# ...
# ...include(cmake/check-compiler-flags.cmake) # 경로를 자세하게 제공한 경우# ...
경로를 참조할 때 특정 경로를 참고하도록 지시할 수도 있습니다.
CMAKE_MODULE_PATH를 사용하면, 파일 이름만으로 include 하는 것이 가능합니다.
# 현재 프로젝트를 기준으로 cmake 폴더를 CMAKE_MODULE_PATH에 추가한다list(APPENDCMAKE_MODULE_PATH${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(check-compiler-flags) # .cmake 라고 명시하지 않아도 된다
실행파일은 exe와 dll (혹은 elf와 so)이 있으면 되지만, 라이브러리는 좀 다릅니다. 엄밀히 말해 빌드된 라이브러리 파일(lib, dll, a, so, dylib ...)과 함께 링킹을 위한 정보가 함께 제공되어야 하기 때문입니다.
원래라면 exp(export) 파일을 사용하는게 맞겠지만, 개발자의 편의를 생각하면 .h, .hpp 파일을 넘어서기 어려울 것입니다.
CMake에서는 install명령으로 헤더파일, CMake Target, 그리고 필요하다면 폴더를 지정된 위치에 '설치(복사)' 하는 방법을 제공합니다.
# 단일 파일을 지정된 폴더에 설치install(FILE LICENSE # ReadMe와 같이 라이브러리와 함께 배포되어야 하는 파일들DESTINATION ./install/
)
# 폴더 전체를 설치install(DIRECTORYinclude# 헤더 파일들을 통째로 옮긴다DESTINATION ./install/
)
# 빌드 결과물을 설치install(TARGETS my_new_library # add_library, add_executable에 사용했던 이름DESTINATION ./install/
)
하지만 하위 프로젝트들도 제각기 설치 경로를 가지고 있다면 정리하기 어려울 것입니다. 이를 위해 CMake에서는 지정 설치 경로를 의미하는 CMAKE_INSTALL_PREFIX 변수가 있습니다.
프로젝트에서 설치 경로를 이 변수를 따르도록 하면 이 프로젝트를 사용하는 상위 프로젝트에서 함께 배포하는데 도움을 줄 수 있습니다.
# 설치를 CMakeList를 기준으로 하지 않고 CMAKE_INSTALL_PREFIX를 기준으로 수행한다install(FILE LICENSE
DESTINATION${CMAKE_INSTALL_PREFIX}/install/
)
install(DIRECTORYincludeDESTINATION${CMAKE_INSTALL_PREFIX}/install/
)
install(TARGETS my_new_library
DESTINATION${CMAKE_INSTALL_PREFIX}/install/
)
이 변수는 특히 커맨드라인에서 자주 지정하는 변수이기도 합니다. 아래와 같이 설정이 다른 경우 설치 폴더를 분리해서 배포, 경로 참조를 쉽게합니다.
CMake를 사용해 빌드 시스템 파일을 생성하는 과정은 2 pass assembler와 유사하다고 할 수 있습니다.
Configuration
CMakeLists.txt 문법 검사
Macro, Function 실행
Cmake Cache 생성
Generation
Target에서 Build System Project 생성
Configuration 단계에서는 CMakeList를 해석하고, 재사용 가능한 값을 토대로 CMakeCache.txt를 생성합니다. CMakeFiles 폴더 역시 이 단계에서 생성됩니다. 이 폴더 안에는 CMake의 log, 변경을 확인하는 stamp 파일들이 보관됩니다.
Generation은 Configuration의 정보를 바탕으로 Build System에서 사용하는 Project 파일을 생성합니다. TargetDirectories.txt와 같은 폴더목록도 이 단계에서 생성됩니다.
Command
파일 생성
앞서 지원범위에서 CMake는 소스 파일을 생성할 수 있다고 설명하였습니다. 여기에 사용되는 것이 바로 command 입니다.
만약 존재하지 않는 파일이 CMakeList에 명시되면, CMake는 이를 오류로 처리합니다.
$ tree ./wierd-project./wierd-project├── CMakeLists.txt├── include│ └── simple.h└── src └── main.cpp
위의 프로젝트는 src폴더에 main.cpp 밖에 없습니다. 이때 CMakeList의 내용이 아래와 같다면
cmake_minimum_required(VERSION 3.8)
# we will generate the file with given commandadd_custom_command(
OUTPUT src/simple.cpp # <-- output pathCOMMAND echo "my first command"# <-- command + args
)
add_executable(wierd_exe
include/simple.h
src/main.cpp
src/simple.cpp
)
target_include_directories(wierd_exe
PRIVATE${CMAKE_CURRENT_SOURCE_DIR}/include
)
다시 CMake를 실행해보면 이번엔 문제없이 Generation 단계까지 마치는 것을 확인할 수 있을 것입니다. 하지만 echo 명령으로 실제로 파일을 생성하지는 않기 때문에, 빌드를 시도하면 No such file or directory 메세지와 함께 실패할 것입니다.
PS C:\wierd-project\build> cmake .. -G "Visual Studio 15 2017 Win64"-- Configuring done-- Generating done-- Build files have been written to: C:/wierd-project/build
기본적으로 이 CMake 함수는 OUTPUT와 COMMAND인자만 제공하면 동작하지만, 보다 정확히 의도를 반영하기 위해서는 다수의 인자를 사용해야 합니다.
이 함수를 처음 접한다면 Target 까지 천천히 튜토리얼을 따른 후, 각각의 인자를 바꿔가며 실행해보기를 권합니다.
먼저, 소스파일을 생성하는 스크립트를 작성해서, COMMAND에서 이를 호출하도록 하여 파일이 생성되는 위치를 확인할 수 있습니다.
Windows라면 cmd 환경을 사용하게 됩니다. (call 을 사용하는 것을 보고 직감하셨나요?) Powershell이 아님에 주의하시기 바랍니다.
특히, 이대로 CMake를 실행하면 Comment가 출력되지 않는 것을 볼 수 있습니다. 이는 Configuration/Generation 단계에서는 command가 실행되지 않는다는 의미입니다.
/path/to/wierd-project/build$ make[ 25%] creating simple.cpp # <-----Scanning dependencies of target wierd_exe[ 50%] Building CXX object CMakeFiles/wierd_exe.dir/src/main.cpp.o[ 75%] Building CXX object CMakeFiles/wierd_exe.dir/src/simple.cpp.o[100%] Linking CXX executable wierd_exe[100%] Built target wierd_exe
이제까지는 상대경로를 사용하면 CMakeList를 기준으로, 즉 SOURCE_DIR를 기준으로 파일을 참조하였습니다.
하지만 add_custom_command의 생성 파일은 빌드 폴더(BINARY_DIR)를 기준으로 합니다.
만약 여기서 빌드가 실패했고, 스크립트 실행 중 no such file or directory 오류가 발생한다면,
이는 build 폴더 내에 src 폴더가 없기 때문일 것입니다.
(create_simple_cpp.sh 스크립트의 내용을 참고하십시오)
그런 경우 src 폴더를 생성한 뒤 다시 시도해보시기 바랍니다.
프로젝트 폴더를 tree로 조회하면 src 폴더에는 여전히 main.cpp만 존재하는 것을 확인할 수 있습니다.
/path/to/wierd-project/build$ make[ 25%] creating simple.cppScanning dependencies of target wierd_exe[ 50%] Building CXX object CMakeFiles/wierd_exe.dir/src/main.cpp.o[ 50%] creating simple.cpp[ 75%] Building CXX object CMakeFiles/wierd_exe.dir/src/simple.cpp.oc++: error: /path/to/wierd-project/build/src/simple.cpp: No such file or directoryc++: fatal error: no input filescompilation terminated.CMakeFiles/wierd_exe.dir/build.make:90: recipe for target 'CMakeFiles/wierd_exe.dir/src/simple.cpp.o' failedmake[2]: *** [CMakeFiles/wierd_exe.dir/src/simple.cpp.o] Error 1CMakeFiles/Makefile2:67: recipe for target 'CMakeFiles/wierd_exe.dir/all' failedmake[1]: *** [CMakeFiles/wierd_exe.dir/all] Error 2Makefile:83: recipe for target 'all' failedmake: *** [all] Error 2
Windows 환경, Generator가 Visual Studio인 경우에도 마찬가지입니다.
Build FAILED."C:\wierd-project\build\ALL_BUILD.vcxproj" (default target) (1) ->"C:\wierd-project\build\wierd_exe.vcxproj" (default target) (3) ->(ClCompile target) -> c1xx : fatal error C1083: Cannot open source file: 'C:\wierd-project\build\src\simple.cpp': No such file or directory [C:\wierd-project\build\wierd_exe.vcxproj] 0 Warning(s) 1 Error(s)Time Elapsed 00:00:02.92
오류메세지의 경로를 확인해보면 두 경우 모두 build 폴더에서 src/simple.cpp를 찾으려 했다는 것을 알 수 있습니다. 이는 add_custom_command의 OUTPUT이 묵시적으로 BINARY_DIR를 기준으로 하기 때문입니다. OUTPUT 인자를 절대경로로 변경하면 빌드가 성공하는 것을 확인할 수 있을 것입니다.
동시에 src 폴더에 main.cpp와 simple.cpp가 함께 위치하는 것도 확인할 수 있습니다
Script with Arguments
다르게 생각해보면, 코드 생성 스크립트가 너무 단순하다는 것도 문제의 원인일 수 있습니다.
스크립트 파일의 위치, 혹은 프로젝트 폴더와 같이 묵시적인 정보를 기반으로 (상대경로를 써서) 내용을 작성했지만,
실제 스크립트는 완전히 다른 경로에서 실행되고 있을 수 있다는 점을 간과한 것이죠.
스크립트가 현재 실행되는 위치와 무관하게 동작해야 할 수도 있습니다.
절대 경로를 인자로 제공받는다면 이 문제를 해결할 수 있을 것입니다.
# script can catch argument with $1, $2 ...
PROJECT_DIR=$1echo"extern const int version = 3;">$PROJECT_DIR/src/simple.cpp;
이제 COMMAND를 통해 스크립트에서 인자를 전달하면 됩니다.
하지만 앞서 working directory에서 확인한 것처럼, CMake가 생성한 프로젝트는 여전히 build/src/simple.cpp를 찾을 것입니다. 따라서 OUTPUT에는 마찬가지로 절대 경로가 필요합니다.
Working Directory 방법이 순수하게 CMakeList의 변경만으로 해결된 반면, Script Argument 방법은 스크립트 파일도 변경해야 했다는 점에 주의하시길 바랍니다.
이는 단점이 될 수 있지만, out-of-tree build가 기본 빌드 시나리오인 경우, 혹은 다수의 스크립트가 함께 사용되는지에 따라 더 타당한 판단이 될 수 있습니다.
Target
보다 정확한 설명
1편에서는 Target을 '빌드 시스템 파일 생성의 단위'라고 설명하였습니다. CMake에서 Target은 'Configuration의 대상'을 말하며, 이는 최종적으로는 Build System에서 사용하는 'Project로 Generation'됩니다. 하지만 이것이 전부는 아닙니다.
초반부에 Project 파일은 '명령서'와 같다고 설명하였습니다. 보통 이 '명령'은 컴파일러/링커 호출을 의미하지만, 좀 더 일반적인 일도 포함될 수 있습니다.
일례로 Unix Makefiles 프로젝트들은 보통 install/uninstall을 지원하며, Visual Studio는 Build event에 사용자 커맨드를 호출할 수 있도록 지원합니다.
Command와 Target은 의존성을 설정할 수 있고, CMake 파일의 작성자가 실행 내용(COMMAND)을 명시한다는 점이 유사합니다. 하지만 Target은 Name, Property를 가진 보다 복잡한 개체를 의미합니다.
CMake 공식문서에서는 Target을 다음과 같이 구분합니다.
Binary Target
executable
library
Pseudo Target
Imported Target
pre-existing dependency
Alias Target
read-only name
이 중 Binary target은 실제로 빌드 시스템 파일을 생성하는 경우를 의미하며, Pseudo target은 이미 존재하는 파일을 사용하는 경우만을 의미합니다. 이 문서에서는 지금까지 Binary Target 만을 중점적으로 서술했는데, 다른 타입의 Target들을 짚어보겠습니다.
Pseudo Target
Imported Target
지금까지 빌드를 위해 사용해왔던 add_executable, add_library 모두 IMPORTED 옵션을 지원합니다.
C++ 에서는 서브 프로그램 뿐만 아니라 소스 코드가 포함된다는 점(include)이 특이함. 이렇게 헤더를 노출하지 않고 extern 선언 혹은 compiler에게 따로 입력으로 주는 exp 파일로 프로그램을 만들 수 있으나, 사람에게는 헤더파일을 제공하는게 가장 편한 방법.
최종 프로그램(executable)을 만들면서 소모하는 방식.
대동소이하게 아래와 같은 폴더트리로 구성하는 편
# cmake might find multiple packages. In the case it will peek the first onefind_package(xyz CONFIG PATHS /path/to/xyz)
# or simplyset(xyz_DIR /path/to/xyz)
find_package(xyz CONFIG)