Skip to content

Instantly share code, notes, and snippets.

@oranke
Forked from luncliff/cmake-tutorial.md
Created February 8, 2019 00:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oranke/7048bf34f7c7e093b6f5f7e37a9bb9b2 to your computer and use it in GitHub Desktop.
Save oranke/7048bf34f7c7e093b6f5f7e37a9bb9b2 to your computer and use it in GitHub Desktop.
CMake 할때 쪼오오금 도움이 되는 문서

CMake 튜토리얼 Lv.1

프로젝트의 구성

CMake는 빌드 시스템에서 필요로 하는 파일을 생성하는데 그 목적이 있습니다.

CMake 배치

CMake를 사용해 프로젝트를 관리하고자 한다면, 필요/의도에 맞게 CMakeLists.txt 파일을 배치해야 합니다.

일반적으로 Root CMake 파일을 두는 것이 편리합니다.
많은 경우 소스코드들은 폴더를 사용해 조직화되어 있으므로, 모든 프로젝트들이 재귀적으로 폴더 내에 CMakeLists.txt를 두고 있으면 관리할 때 폴더 == 프로젝트로 생각할 수 있게 됩니다.

$ tree ./small-project/
./small-project         # Project root folder
├── CMakeLists.txt      # <--- Root CMake
├── include             # header files
│   └── ... 
├── module1             # sub-project
│   ├── CMakeLists.txt
│   └── ... 
├── module2             # sub-project
│   ├── CMakeLists.txt
│   └── ... 
└── test                # sub-project
    ├── CMakeLists.txt
    └── ... 

프로젝트의 이름을 지정할 수 있습니다.
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안에서 여러번 사용되기도 합니다. 빌드 결과 생성되는 프로그램의 이름만 다르다면 크게 문제되지 않습니다.

아래와 같은 구조로 프로젝트가 구성되었다고 해봅시다.

$ tree ./project-example/
./project-example
├── CMakeLists.txt
├── include
│   └── ... 
└── src
    ├── CMakeLists.txt
    ├── main.cpp
    ├── feature1.cpp
    ├── feature2.cpp
    ├── algorithm3.cpp
    └── data_structure4.cpp

우선 Root CMakeList(project-example/CMakeLists.txt)가 아니라 src/ 폴더에 있는 CMakeList(project-example/src/CMakeLists.txt)부터 살펴보겠습니다.

만약 src 폴더에 있는 모든 .cpp 파일들이 실행 파일(exe)을 만든다면, 아래와 같은 내용이 작성되어야 합니다.

# project-example/src/CMakeLists.txt 
# case 1

add_executable(my_exe   # 이후에 나오는 .cpp 파일을 사용해 .exe를 생성한다
    main.cpp
                        # 개행을 여러번 하여도 문제되지 않습니다.
    feature1.cpp
    feature2.cpp
    algorithm3.cpp      # 상대 경로로 소스 코드를 찾아냅니다. 
                        # 현재 사용중인 CMakeList의 위치를 기준으로
                        # 경로를 지시해야 합니다
    data_structure4.cpp
    # ... 
)

만약 라이브러리를 만든다면, 아래와 같은 내용이 작성되어야 합니다.

# project-example/src/CMakeLists.txt 
# case 2

add_library(my_lib      # 이후에 나오는 .cpp 파일을 사용해 라이브러리를 생성한다
    main.cpp
    feature1.cpp
    # ...
)

라이브러리의 링킹의 형태를 명시하지 않는다면 여기서 생성되는 라이브러리는 프로젝트 생성시 BUILD_SHARED_LIBS 변수를 따라서 결정됩니다. 물론 직접 명시할 수도 있습니다.

# project-example/src/CMakeLists.txt 
# case 2.1, 2.2

add_library(my_archive  STATIC  # 정적 링킹 라이브러리(.a, .lib)
    main.cpp
    feature1.cpp
    # ...
)

add_library(my_shared_object  SHARED  # 동적 링킹 라이브러리(.so, .dll)
    main.cpp
    feature1.cpp
    # ...
)

STATIC, SHARED가 대문자인 점에 주의하십시오. CMake파일은 대소문자를 신경써서 사용해야만 합니다. CMake 함수들은 앞서 예시처럼 소문자를 사용해도, PROJECT, ADD_EXECUTABLE처럼 대문자로 작성하여도 문제없이 동작합니다. 하지만 SHARED처럼 일부 예약된 단어(sub-command)들은 대문자만을 사용해야 합니다.

가독성을 고려하여 일관성있게 작성하는 것이 좋습니다. CMake기반 프로젝트에서는 개발자들이 서로의 CMakeList를 주의 깊게 살펴보는 경우가 많습니다.
보고 배울만한 CMake 사례를 모아두십시오. 특정한 저장소나 프로젝트를 따라해도 좋습니다.

이번에는 Root CMakeList(project-example/CMakeLists.txt)를 살펴보겠습니다. 이미 어떻게 프로그램을 생성할 것인지는 앞서 src 폴더에서 작성한 CMakeLists가 잘 처리하고 있기 때문에, Root CMakeList에서는 이를 그대로 사용할 것입니다.

$ tree ./project-example/
./project-example
├── CMakeLists.txt          # <---- project
├── include
└── src
    ├── CMakeLists.txt      # <---- add_executable/add_library
    └── ... 

간단히 add_subdirectory와 폴더 이름을 주는 것으로 이를 달성할 수 있습니다.
많은 경우 CMakeList는 아래와 같이 재귀적으로, 즉 add_subdirectory만으로 프로젝트를 (File Tree를 따라) 조직화 합니다.

# project-example/CMakeLists.txt
project(my_project  LANGUAGES CXX   VERSION 1.2.3)

add_subdirectory(src)       # 상대경로
외부 경로 가져오기

하지만 경우에 따라선 필요한 CMakeList가 Root CMakeList의 하위에 없을 수도 있습니다. 이런 경우를 Out-of-tree build라고 합니다.

아래와 같은 경우를 생각해봅시다.

$ tree ./out-of-tree
./out-of-tree/
├── current-project
│   └── CMakeLists.txt
└── far-far-away
    ├── CMakeLists.txt
    └── algorithm1.cpp

여기서 current-project/CMakeLists.txtadd_subdirectory로 하위 디렉터리가 아닌 다른 곳을 참고 하고 있습니다.

# current-project/CMakeLists.txt

cmake_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 - done
CMake 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.txt

cmake_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_X
Volume serial number is D688-4B7E
X:\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 프로젝트를 구성해야 할 것입니다.

$ tree ./small-project/
./small-project
├── CMakeLists.txt
├── include
│   └── ... 
└── src
    ├── CMakeLists.txt
    └── ... 

예시: 라이브러리 + 실행 파일 각각 1개씩

만약 라이브러리를 만들었다면, 테스트와 같이 라이브러리 코드를 실행시켜 볼 수 있는 추가 프로젝트가 필요할 것입니다. 이런 경우 또 다른 sub-project를 추가 하면 됩니다.

$ tree ./project-with-test/
./project-with-test
├── CMakeLists.txt
├── include
│   └── ... 
├── src
│   ├── CMakeLists.txt
│   └── ... 
└── test
    ├── CMakeLists.txt
    └── ... 

예시: 2개 라이브러리 + 1개 실행 파일

라이브러리를 하나만 사용하라는 법은 없죠.

$ tree ./multiple-library-project
./multiple-library-project
├── CMakeLists.txt
├── include
│   └── ... 
├── module1
│   ├── CMakeLists.txt
│   └── ... 
├── module2
│   ├── CMakeLists.txt
│   └── ... 
└── test
    ├── CMakeLists.txt
    └── ... 

예시: 계층화된 라이브러리 (depth 2) + 1개 실행 파일

OpenCV와 같이 좀 더 복잡하게 구성할 수도 있습니다. 다수의 모듈들을 한번 더 조직화 하기도 합니다. 하위 경로에 있기만 하다면 add_subdirectory를 사용하는데 크게 문제될 일이 없습니다. 출처에 따라서 Internal/External로 두기도 하고, 목적에 따라서 특별한 이름을 붙일 수도 있습니다.

$ tree ./huge-project
./huge-project
├── CMakeLists.txt
├── cmake
│   └── get-some-package.cmake
├── include
│   └── ... 
├── modules
│   ├── guideline_support_library
│   │   ├── CMakeLists.txt
│   │   └── ...
│   ├── fmt
│   │   ├── CMakeLists.txt
│   │   └── ...
│   ├── grpc
│   │   ├── CMakeLists.txt
│   │   └── ...
│   └── ... 
├── src
│   ├── CMakeLists.txt
│   └── ... 
└── test
    ├── CMakeLists.txt
    └── ... 

git을 사용하는 프로젝트라면 submodule을 추가하면서 함께 조정하면 될 것입니다.

실행 해보기

앞서까지는 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 프로젝트를 생성하기도 합니다.

다르게는 https://github.com/Microsoft/vcpkg 와 같이 라이브러리 탐색에 특화된 툴체인 파일을 사용하는 경우도 있습니다.

# ... Vcpkg에서 제공하는 cmake를 경로로 제공한다 ...
cmake ../ -G "Visual Studio 15 2017 Win64" -DCMAKE_TOOLCHAIN_FILE="X:\Develop\vcpkg\scripts\buildsystems\vcpkg.cmake"

빌드 관계 설정

의존성

프로젝트 내에서 여러 라이브러리를 빌드한다면, 그리고 서로 의존성이 있다면 이를 제어하는 것도 가능합니다. 이 과정은 보통 해당 sub-project들을 모두 확인할 수 있는 CMakeList에서 수행하게 됩니다.

의존성은 add_dependencies 함수를 사용해 명시하게 됩니다. 이름에서 알 수 있듯이 다수를 지정할 수 있습니다.

아래와 같은 형태로 프로젝트가 구성되었다고 가정하겠습니다.

$ tree ./multiple-library-project
./multiple-library-project
├── CMakeLists.txt      # <--- Root CMakeList
├── include
│   └── ... 
├── module1
│   ├── CMakeLists.txt
│   └── ... 
├── module2
│   ├── CMakeLists.txt
│   └── ... 
└── test
    ├── CMakeLists.txt
    └── ... 

module2module1의 기능을 사용하고, test는 이 둘의 기능을 모두 사용한다면 이는 Root CMake에서 다음과 같이 명시할 수 있습니다.

# multiple-library-project/CMakeLists.txt

add_subdirectory(module1)
add_subdirectory(module2)
add_subdirectory(test)

# dependency: from -> { to }
add_dependencies(module1 module2)            # `module1` requires `module2`
add_dependencies(test    module1 module2)    # `test` requires `module1` & `module2`

이를 바탕으로 CMake에서 프로젝트가 생성되면, 그 프로젝트는 의존성을 고려하여 module1, module2, test 순서로 빌드를 진행하게 됩니다.

라이브러리들의 수가 많아지거나 경우에 따라 달라지면 별명을 붙여서 상위 CMakeList에서 좀 더 편하게 사용할 수 있도록 할수도 있습니다.

# sub-project CMakeList
add_library(my_custom_logger_lib 
    # ...
)

add_library(module::logger ALIAS my_custom_logger_lib)

디버그 모드와 릴리즈 모드에서 라이브러리 이름이 바뀌는 경우 이는 굉장히 유용한 기능이 됩니다.

# higher-project CMakeList

add_subdirectory(some_exe)
add_subdirectory(custom_logger)

add_dependencies(some_exe   module::logger) # Use with alias

Linking

빌드 순서를 제어하기 위해서 add_dependencies를 사용했다면, 프로그램 생성시에(링킹에) 필요한 (Symbol Table과 같은) 정보들이 공유되도록 하기 위해서는 target_link_libraries를 사용하게 됩니다.

이 문서에서 Target이라는 용어가 처음 사용되었는데, 이는 add_executable 혹은 add_library를 사용해 생성한 대상을 의미합니다. 빌드 시스템 파일 생성의 단위이기도 합니다

add_library(my_custom_logger_lib 
    # ...
)

target_link_libraries(my_custom_logger_lib # 
PUBLIC
    spdlog fmt
PRIVATE
    utf8proc
)

특히 이 함수는 Target의 의존성을 전파시키는 역할도 수행합니다. 위와 같은 경우, PRIVATEPUBLIC을 사용해 이를 제어하고 있습니다. C++ class의 멤버 접근 한정자(Access Qualifier)와 유사하게 생각할 수 있습니다.
위와 같이 작성하면 다른 프로젝트에서 my_custom_logger_lib을 link하는 경우, spdlogfmt에 있는 헤더파일을 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, 문자열, 파일 경로, 리스트 등을 표현할 수 있습니다. 이 튜토리얼에서는 이 중 빈번하게 사용되는 변수들을 짚고 넘어가겠습니다

변수는 set을 사용해서 생성하고, unset을 사용해서 제거할 수 있습니다. 변수는 문자열 기반이며, bash 쉘 프로그래밍처럼 사용할 수 있습니다. 다만 문자열에 대한 참조처럼 사용된다는 특이점이 있습니다.

set(CMAKE_BUILD_TYPE Debug)

message(STATUS CMAKE_BUILD_TYPE)                     # -- CMAKE_BUILD_TYPE
message(STATUS ${CMAKE_BUILD_TYPE})                  # -- Debug
message(STATUS "Configuration: ${CMAKE_BUILD_TYPE}") # -- Configuration: Debug

변수의 값을 사용하기 위해 ${}를 사용한 점을 주의깊게 보시길 바랍니다. 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 폴더가 꾸준히 나타났다는 것을 기억하십니까?

헤더파일들이 위치한 폴더는 제각기 다를 수 있습니다. C 혹은 C++ 프로젝트에서 외부에 공개되는 헤더파일과 공개되지 않는 헤더파일이 다른 위치에 배치되는 것은 흔한 일입니다.

target_include_directories는 Target에서 헤더파일을 찾기 위해 사용하는 폴더를 지정하는 함수이며, 이곳에 위치한 헤더파일들은 #include <my_interface.h>와 같이 <> 형태로 include하는 것이 가능합니다.

아래와 같은 프로젝트 구조를 생각해봅시다.

$ tree ./some-huge-project
./some-huge-project
├── CMakeLists.txt
├── include                 # <--- 공용 헤더 파일
│   ├── system_wrapper.h
│   └── ... 
├── impl
│   ├── win32
│   │   ├── CMakeLists.txt
│   │   ├── include         # <--- 구현 헤더 파일
│   │   │   ├── pch.h
│   │   │   └── my_win32.h
│   │   └── ...
│   ├── posix
│   │   ├── CMakeLists.txt
│   │   ├── include         # <--- 구현 헤더 파일
│   │   │   └── my_posix.h
│   │   └── ...
│   └── ... 
├── src
│   ├── CMakeLists.txt      # <--- system 라이브러리를 사용하는 프로젝트
│   └── ... 
├── ... 

우선 Win32 하위 프로젝트에 있는 CMakeLists 부터 살펴보겠습니다.

# some-huge-project/impl/win32/CMakeLists.txt

add_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.txt

add_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.h
PRIVATE
    include     # include 폴더를 상대경로 접근
                # some-huge-project/impl/posix/include를 의미한다
)

예시에서 본것과 같이 이 함수는 target_link_libraries처럼 PUBLIC,PRIVATE을 지정할 수 있으며, PUBLIC에 위치한 폴더들은 경로가 자동으로 전파됩니다.

# some-huge-project/src/CMakeLists.txt

add_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_executableadd_library의 문제점은 소스 파일 목록을 한번에 결정해서 전달해야 한다는 점입니다. 이는 Target들이 CMakeList 파일의 끝부분에 나타나게 만들며, 2.x 버전 CMake들이 사용했던 방법처럼 List 변수를 사용해 소스파일 목록을 만들어야 하는 불편함이 있습니다

target_sources는 Target에 소스파일을 추가하는 목적으로 사용됩니다.

# preview 가 구현된 소스파일을 추가할지 결정하는 변수
set(USE_PREVIEW true)

# target: my_program
add_executable(my_program
    main.cpp
    feature1.cpp
)

# ...
# add_dependencies
# set_target_properties
# target_include_directories
# target_link_libraries
# ...

if(USE_PREVIEW)
    target_sources(my_program
    PRIVATE
        feature2_preview.cpp
    )
else()
    target_sources(my_program
    PRIVATE
        feature2_mock.cpp
    )
endif()

컴파일러 대응

플랫폼이 달라지면 컴파일러도 달라질 수 있습니다. 컴파일러가 달라지면 프로그램 생성에 사용할 수 있는 컴파일 옵션들이 달라지게 됩니다. 이 튜토리얼에서는 간단히 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 mistakes
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()

이같은 내용을 추가하여 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 Module 폴더 만들기

.cmake/include

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.txt
project(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})         # -- Root
message(STATUS ${CMAKE_CURRENT_SOURCE_DIR}) # -- Root

add_subdirectory(src) # src/CMakeLists.txt를 실행하기 전에 일부 변수들이 새로 설정된다
    #
    #   message(STATUS ${CMAKE_SOURCE_DIR})         # -- Root
    #   message(STATUS ${CMAKE_CURRENT_SOURCE_DIR}) # -- Root/src
    #

add_subdirectory(test)
# ...

앞서서는 include를 위해 아래와 같이 상대적이지만 자세한 경로를 지시했었습니다.

# ...
include(cmake/check-compiler-flags.cmake) # 경로를 자세하게 제공한 경우
# ...

경로를 참조할 때 특정 경로를 참고하도록 지시할 수도 있습니다. CMAKE_MODULE_PATH를 사용하면, 파일 이름만으로 include 하는 것이 가능합니다.

# 현재 프로젝트를 기준으로 cmake 폴더를 CMAKE_MODULE_PATH에 추가한다
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)

include(check-compiler-flags) # .cmake 라고 명시하지 않아도 된다

TBA

설치

설치 경로 지정

실행파일은 exe와 dll (혹은 elf와 so)이 있으면 되지만, 라이브러리는 좀 다릅니다. 엄밀히 말해 빌드된 라이브러리 파일(lib, dll, a, so, dylib ...)과 함께 링킹을 위한 정보가 함께 제공되어야 하기 때문입니다.
원래라면 exp(export) 파일을 사용하는게 맞겠지만, 개발자의 편의를 생각하면 .h, .hpp 파일을 넘어서기 어려울 것입니다.

CMake에서는 install명령으로 헤더파일, CMake Target, 그리고 필요하다면 폴더를 지정된 위치에 '설치(복사)' 하는 방법을 제공합니다.

# 단일 파일을 지정된 폴더에 설치
install(FILE            LICENSE     # ReadMe와 같이 라이브러리와 함께 배포되어야 하는 파일들
        DESTINATION     ./install/
)
# 폴더 전체를 설치
install(DIRECTORY       include     # 헤더 파일들을 통째로 옮긴다
        DESTINATION     ./install/
)
# 빌드 결과물을 설치
install(TARGETS         my_new_library  # add_library, add_executable에 사용했던 이름
        DESTINATION     ./install/
)

경우에 따라 이 과정에서 Binary가 변경되는 경우가 있습니다. MacOS의 .dylib을 생성할 때 이는 꽤 상당히 골치아픈 문제일 수도 있습니다.

하지만 하위 프로젝트들도 제각기 설치 경로를 가지고 있다면 정리하기 어려울 것입니다. 이를 위해 CMake에서는 지정 설치 경로를 의미하는 CMAKE_INSTALL_PREFIX 변수가 있습니다.

프로젝트에서 설치 경로를 이 변수를 따르도록 하면 이 프로젝트를 사용하는 상위 프로젝트에서 함께 배포하는데 도움을 줄 수 있습니다.

# 설치를 CMakeList를 기준으로 하지 않고 CMAKE_INSTALL_PREFIX를 기준으로 수행한다
install(FILE            LICENSE
        DESTINATION     ${CMAKE_INSTALL_PREFIX}/install/
)
install(DIRECTORY       include
        DESTINATION     ${CMAKE_INSTALL_PREFIX}/install/
)
install(TARGETS         my_new_library
        DESTINATION     ${CMAKE_INSTALL_PREFIX}/install/
)

이 변수는 특히 커맨드라인에서 자주 지정하는 변수이기도 합니다. 아래와 같이 설정이 다른 경우 설치 폴더를 분리해서 배포, 경로 참조를 쉽게합니다.

cmake /path/to/CMakeLists.txt \
    -DCMAKE_INSTALL_PREFIX=~/install/debug/static \    # debug, static
    -DCMAKE_BUILD_TYPE=Debug \
    -DBUILD_SHARED_LIBS=false;
cmake /path/to/CMakeLists.txt \
    -DCMAKE_INSTALL_PREFIX=~/install/debug/dynamic \   # debug, dynamic
    -DCMAKE_BUILD_TYPE=Debug \
    -DBUILD_SHARED_LIBS=true;

cmake /path/to/CMakeLists.txt \
    -DCMAKE_INSTALL_PREFIX=~/install/release/static \  # release, static
    -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=false;
cmake /path/to/CMakeLists.txt \
    -DCMAKE_INSTALL_PREFIX=~/install/debug/dynamic \   # release, dynamic
    -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=true;

위와 같이 실행하고 나면 ~/install 경로에는 아래와 같이 설치될 것입니다.

tree -L 3 ~/install
install
├── LICENSE
├── include
│   └── my_new_lib.h
├── release
│   ├── static
│   │   └── my_new_library.a
│   └── release
│       └── my_new_library.so
└── debug
    ├── static
    │   └── my_new_library.a
    └── release
        └── my_new_library.so

CMake 튜토리얼 Lv.2

CMake의 목적과 기능범위

빌드 시스템(build system)이 하는 일은 연속적인 이벤트(컴파일러/링커 호출)를 통해 최종적으로는 프로그램을 생성하는 것입니다. 빌드 시스템 파일, 혹은 프로젝트 파일은 프로그래머의 의도에 맞게 컴파일러/링커 호출을 조직화한 명령서라고 할 수 있습니다.

1편에서 강조한 대로라면 CMake는 '명령서'를 작성하는 것으로 그 기능이 충분하겠지만, 실제 기능적으로는 좀 더 넓은 지원범위를 가지고 있습니다. CMake가 중심이 되는 프로젝트라면, 이를 활용해 빌드에 필요한 소스 파일까지 생성할 수 있습니다.

하지만 작성자는 이 기능들을 사용하는 것을 추천하지 않습니다.
빌드 시스템 파일을 생성하는것이 CMake의 가장 중요한 부분이며, 오직 그 일에 집중해야 한다고 생각하기 때문입니다

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)

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 Error at CMakeLists.txt:3 (add_executable):
  Cannot find source file:

    src/simple.cpp

  Tried extensions .c .C .c++ .cc .cpp .cxx .m .M .mm .h .hh .h++ .hm .hpp
  .hxx .in .txx

CMake Error: CMake can not determine linker language for target: wierd_exe

위에서 사용한 CMakeList에 새로운 내용을 추가해보겠습니다.

cmake_minimum_required(VERSION 3.8)

# we will generate the file with given command
add_custom_command( 
    OUTPUT      src/simple.cpp              # <-- output path
    COMMAND     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 함수는 OUTPUTCOMMAND인자만 제공하면 동작하지만, 보다 정확히 의도를 반영하기 위해서는 다수의 인자를 사용해야 합니다. 이 함수를 처음 접한다면 Target 까지 천천히 튜토리얼을 따른 후, 각각의 인자를 바꿔가며 실행해보기를 권합니다.

먼저, 소스파일을 생성하는 스크립트를 작성해서, COMMAND에서 이를 호출하도록 하여 파일이 생성되는 위치를 확인할 수 있습니다.

$ tree ./wierd-project
./wierd-project
├── CMakeLists.txt
├── include
│   └── simple.h
├── scripts
│   └── create_simple_cpp.sh # <--- src file generation script
└── src
    └── main.cpp

소스 파일을 생성하는 스크립트의 내용은 아주 단순합니다

# create_simple_cpp.sh
echo "extern const int version = 3;" > src/simple.cpp;

위 내용은 Windows의 CLI에서도 실행될 수 있습니다. 다만 스크립트를 호출하는 명령이 다릅니다. UNIX 환경이라면 기본 shell 환경(sh, zsh, bash 등)을 따르면 됩니다. 예시에서는 bash로 가정하겠습니다.

cmake_minimum_required(VERSION 3.8)

# more detailed src file generation command
if(WIN32)
    add_custom_command(
        OUTPUT      src/simple.cpp
        COMMAND     call ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_simple_cpp.sh 
        COMMENT     "creating simple.cpp"
    )
elseif(UNIX)
    add_custom_command(
        OUTPUT      src/simple.cpp
        COMMAND     bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_simple_cpp.sh 
        COMMENT     "creating simple.cpp"
    )
endif()

# ... add_executable ...

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$ tree -L 3 .
.          # <--------------- CMAKE_CURRENT_SOURCE_DIR
├── CMakeLists.txt  
├── build       # <--------------- CMAKE_CURRENT_BINARY_DIR
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   │   ├── 3.10.2
|   |   ...
│   │   └── wierd_exe.dir
│   ├── Makefile
│   ├── cmake_install.cmake
│   ├── src
│   │   └── simple.cpp  # <-------- generated file
│   └── wierd_exe
├── include
│   └── simple.h
├── scripts
│   └── create_simple_cpp.sh
└── src
    └── main.cpp    # <----- where is my friend?

이런 경우, 빌드 과정에서 생성하는 파일을 어떻게 관리할 것인지 먼저 생각해봐야 합니다. 이 튜토리얼에서는 다음의 2가지 방법을 보이겠습니다.

  • Working directory
  • Script with arguments

Working Directory

CLI 환경에서 working directory 명령을 실행한 위치를 의미합니다. (pwd 명령으로 확인할 수도 있습니다) 바로 위에서 실행한 tree의 경우, /path/to/wierd-project가 working directory가 됩니다.

add_custom_command는 선택 인자로 WORKING_DIRECTORY를 명시할 수 있습니다. 이 경로를 SOURCE_DIR로 바꿔서 실행시켜 보겠습니다.

# ...
if(WIN32)
    add_custom_command(
        OUTPUT      src/simple.cpp
        COMMAND     call ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_simple_cpp.sh 
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        COMMENT     "creating simple.cpp"
    )
elseif(UNIX)
    add_custom_command(
        OUTPUT      src/simple.cpp
        COMMAND     bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_simple_cpp.sh 
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        COMMENT     "creating simple.cpp"
    )
endif()
# ...

이렇게 수정하면 아래와 같이 오류가 발생해야 합니다.

/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
[ 50%] creating simple.cpp
[ 75%] Building CXX object CMakeFiles/wierd_exe.dir/src/simple.cpp.o
c++: error: /path/to/wierd-project/build/src/simple.cpp: No such file or directory
c++: fatal error: no input files
compilation terminated.
CMakeFiles/wierd_exe.dir/build.make:90: recipe for target 'CMakeFiles/wierd_exe.dir/src/simple.cpp.o' failed
make[2]: *** [CMakeFiles/wierd_exe.dir/src/simple.cpp.o] Error 1
CMakeFiles/Makefile2:67: recipe for target 'CMakeFiles/wierd_exe.dir/all' failed
make[1]: *** [CMakeFiles/wierd_exe.dir/all] Error 2
Makefile:83: recipe for target 'all' failed
make: *** [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 인자를 절대경로로 변경하면 빌드가 성공하는 것을 확인할 수 있을 것입니다.

cmake_minimum_required(VERSION 3.8)

if(WIN32)
    add_custom_command(
        OUTPUT      ${CMAKE_CURRENT_SOURCE_DIR}/src/simple.cpp
        COMMAND     call ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_simple_cpp.sh 
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        COMMENT     "creating simple.cpp"
    )
elseif(UNIX)
    add_custom_command(
        OUTPUT      ${CMAKE_CURRENT_SOURCE_DIR}/src/simple.cpp
        COMMAND     bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_simple_cpp.sh 
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        COMMENT     "creating simple.cpp"
    )
endif()

add_executable(wierd_exe
    include/simple.h

    src/main.cpp
    src/simple.cpp
)

target_include_directories(wierd_exe
PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

동시에 src 폴더에 main.cpp와 simple.cpp가 함께 위치하는 것도 확인할 수 있습니다

Script with Arguments

다르게 생각해보면, 코드 생성 스크립트가 너무 단순하다는 것도 문제의 원인일 수 있습니다. 스크립트 파일의 위치, 혹은 프로젝트 폴더와 같이 묵시적인 정보를 기반으로 (상대경로를 써서) 내용을 작성했지만, 실제 스크립트는 완전히 다른 경로에서 실행되고 있을 수 있다는 점을 간과한 것이죠. 스크립트가 현재 실행되는 위치와 무관하게 동작해야 할 수도 있습니다. 절대 경로를 인자로 제공받는다면 이 문제를 해결할 수 있을 것입니다.

# script can catch argument with $1, $2 ...
PROJECT_DIR=$1
echo "extern const int version = 3;" > $PROJECT_DIR/src/simple.cpp;

이제 COMMAND를 통해 스크립트에서 인자를 전달하면 됩니다. 하지만 앞서 working directory에서 확인한 것처럼, CMake가 생성한 프로젝트는 여전히 build/src/simple.cpp를 찾을 것입니다. 따라서 OUTPUT에는 마찬가지로 절대 경로가 필요합니다.

# more detailed src file generation command
if(WIN32)
    add_custom_command(
        OUTPUT      ${CMAKE_CURRENT_SOURCE_DIR}/src/simple.cpp
        COMMAND     call ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_simple_cpp.sh ${CMAKE_CURRENT_SOURCE_DIR}
        COMMENT     "creating simple.cpp"
    )
elseif(UNIX)
    add_custom_command(
        OUTPUT      ${CMAKE_CURRENT_SOURCE_DIR}/src/simple.cpp
        COMMAND     bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_simple_cpp.sh ${CMAKE_CURRENT_SOURCE_DIR}
        COMMENT     "creating simple.cpp"
    )
endif()

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 옵션을 지원합니다.

add_executable(protoc_exe IMPORTED /usr/bin/protoc)

add_library(xxx IMPORTED SHARED)
add_library(yyy IMPORTED STATIC)

add_executable(IMPORTED)add_custom_command의 COMMAND로 사용합니다.

이미 빌드 된 라이브러리가 있는 경우 add_library(IMPORTED)를 사용합니다. 해당 라이브러리가 CMake가 아닌 빌드 툴을 사용해서 빌드를 수행했더라도, 별도의 CMake 파일을 작성하여 이를 CMake 프로젝트에 결합시킬 수 있습니다.

다만 이때는 소스 파일에서부터 빌드를 수행하는 것이 아니기 때문에, CMakeLists.txt가 아닌 다른 cmake 설정(config) 파일을 작성하게 됩니다.

CMakeLists.txt가 아닌 다른 파일을 작성한다는 것에 의아할 수 있겠지만, 라이브러리가 미리 빌드되었다는 것은 아래와 같은 정보를 전달받아야 한다는 의미이므로, 빌드와는 의미가 달라지기 때문이라고 해석할 수 있습니다. (같은 것은 같게, 다른 것은 다르게)

  • 헤더 파일을 찾을 폴더(target_include_directory)
  • 라이브러리 의존성(target_link_libraries)
  • 빌드할 당시에 사용한 옵션(target_compile_options)

이는 find_package 부분에서 다룰 것입니다.

Alias Target

이 부분은 OpenMP을 예로 들어 간단하게만 짚고 넘어가겠습니다.

cmake_minimum_required(VERSION 3.8)

# create target without source file list
add_library(xyz IMPORTED SHARED) 

find_package(OpenMP) # https://cmake.org/cmake/help/latest/module/FindOpenMP.html
if(OpenMP_FOUND)
    set_target_properties(xyz
    PROPERTIES
        COMPILE_FLAGS "${OpenMP_CXX_FLAGS}"
    )

    if(ANDROID)
        set_target_properties(xyz
        PROPERTIES
            INTERFACE_LINK_LIBRARIES    omp     # <----- ???
        )
    else()
        set_target_properties(xyz
        PROPERTIES
            INTERFACE_LINK_LIBRARIES     OpenMP::OpenMP_CXX # <----- ???
        )
    endif() 
endif()

만약 이것이 특정한 빌드 시스템에서 사용하는 파일이었다면 omp보다는 좀 더 구체적인 이름을 사용하고 있었을 것입니다. 플랫폼에 따라서 omp.dll, omp.lib, libomp.a, libomp.so, libomp.dylib 등이 될 것입니다.

이 점을 생각하면 omp가 CMake Target의 이름이라는 것을 알 수 있습니다. CMake 파일들은 특정 플랫폼에서 사용하는 이름, 확장자에 대해 거의 신경을 쓰지 않으며, 가능한 그렇게 되도록 작성합니다. 달리 말해, CMake의 방식을 우선적으로 고려합니다.

target_link_libraries(some_exe
PRIVATE
    xyz
)

별다른 주석이 없다면, CMake와 그 파일의 사용자는 위 내용에 대해서 다음과 같은 생각을 할 것입니다.

  1. xyz라는 이름의 Target이 존재한다
  2. 만약 xyz가 Target이 아니라면, xyz는 라이브러리를 의미한다
    1. lib[xyz].a 혹은 lib[xyz].so와 같은 이름의 파일이 link_directories로 지정한 폴더들 안에 존재한다
    2. [xyz].lib 혹은 [xyz].dll와 같은 이름의 파일이 시스템 라이브러리 폴더에 존재한다

이것이 의미하는 바는, xyz라는 이름만으로는 혼동의 여지가 있다는 것입니다. 앞선 OpenMP 예시에서 ::을 사용한 부분이 있었습니다. 이를 (C++ 개발자라면 모두가 친숙한) Namespace라고 합니다. 아래와 같이 사용할 수 있습니다.

# ...
elseif(NOT WIN32)
    set_target_properties(custom_mp
    PROPERTIES
        INTERFACE_LINK_LIBRARIES    OpenMP::OpenMP_CXX
    )
endif()

이런 ALIAS를 발견한다면, CMake와 읽는이 모두 이것이 Target의 이름이라고 확신할 수 있습니다. Alias Target은 아래와 같이 매우 간단한 방법으로 추가할 수 있습니다.

# https://github.com/catchorg/Catch2/blob/master/CMakeLists.txt

# ...
add_library(Catch2 INTERFACE)
# ...
add_library(Catch2::Catch2 ALIAS Catch2)
# ...

Custom Target

TBA

CMake 튜토리얼 Lv.3

Package

Package 구성

  • 일반적인 패키지:
    • 실행 프로그램(executable)
    • 문서 파일(license, readme 등)
  • 프로그래밍 패키지: 일반 패키지 + 개발에 필요한 요소들
    • 서브 프로그램(library)
    • 실행 프로그램(example, test 등): 경우에 따라 소스 코드 형태로 제공하기도

C++ 에서는 서브 프로그램 뿐만 아니라 소스 코드가 포함된다는 점(include)이 특이함. 이렇게 헤더를 노출하지 않고 extern 선언 혹은 compiler에게 따로 입력으로 주는 exp 파일로 프로그램을 만들 수 있으나, 사람에게는 헤더파일을 제공하는게 가장 편한 방법.

최종 프로그램(executable)을 만들면서 소모하는 방식. 대동소이하게 아래와 같은 폴더트리로 구성하는 편

  • C++ package
    • bin : 실행 프로그램(executable)
    • lib : 미리 빌드된 라이브러리(so, lib 등)
    • include : 소스 코드(헤더)
    • docs : 문서가 (많이) 있는 경우
    • share : 기타 필요한 파일들. 주로 빌드 지원 파일

굳이 이 방식을 준수하려고 할 필요는 없으며, 중요한 것은 프로젝트에서의 일관성.

Package 찾기

find_package에 경로 주기

# cmake might find multiple packages. In the case it will peek the first one
find_package(xyz    CONFIG PATHS /path/to/xyz)

# or simply
set(xyz_DIR     /path/to/xyz)
find_package(xyz    CONFIG)

config 파일 작성

# xyz-config.cmake

set(INTERFACE_DIR   /path/to/include)
set(LIBS_DIR        /path/to/libs)

message(STATUS  "  include  \t: ${INTERFACE_DIR}")
message(STATUS  "  libs     \t: ${LIBS_DIR}")

if(NOT IOS)
    add_library(xyz SHARED IMPORTED)
else()
    add_library(xyz STATIC IMPORTED)
endif()

# this is released by someone
set_property(TARGET xyz APPEND PROPERTY 
    IMPORTED_CONFIGURATIONS RELEASE 
)

# include path
set_target_properties(xyz PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES ${INTERFACE_DIR}
)
# link
if(ANDROID)
    set_target_properties(xyz PROPERTIES
        IMPORTED_LOCATION ${LIBS_DIR}/${ANDROID_ABI}/libxyz.so
    )
elseif(IOS)
    set_target_properties(xyz PROPERTIES
        IMPORTED_LOCATION ${LIBS_DIR}/iphone/libxyz.a
    )
elseif(WIN32)
    set_target_properties(xyz PROPERTIES
        IMPORTED_IMPLIB     ${LIBS_DIR}/windows/xyz.lib
        IMPORTED_LOCATION   ${LIBS_DIR}/windows/xyz.dll
    )
    message(STATUS  "  IMPORTED_IMPLIB  \t: ${LIBS_DIR}/windows/xyz.lib")
    message(STATUS  "  IMPORTED_LOCATION\t: ${LIBS_DIR}/windows/xyz.dll")
elseif(APPLE)
    set_target_properties(xyz PROPERTIES
        IMPORTED_LOCATION ${LIBS_DIR}/osx/libxyz.dylib
    )
elseif(UNIX)
    set_target_properties(xyz PROPERTIES
        IMPORTED_LOCATION ${LIBS_DIR}/linux/libxyz.so
    )
endif()

# compile flag
if(NOT MSVC) # possibly Clang or GCC
    set_target_properties(xyz PROPERTIES
    COMPILE_FLAGS
        "-fPIC"
    )
endif()

find_package(OpenMP)

if(OpenMP_FOUND)
    if(ANDROID)
        set_target_properties(xyz PROPERTIES
        INTERFACE_LINK_LIBRARIES
            "omp"
        )
    elseif(NOT WIN32)
        set_target_properties(xyz PROPERTIES
        INTERFACE_LINK_LIBRARIES
            OpenMP::OpenMP_CXX
        )
    endif()
    
    set_target_properties(xyz PROPERTIES
    COMPILE_FLAGS
        "${OpenMP_CXX_FLAGS}"
    )
endif()

target_link_directories

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment