[w15d1~d2] CMake

GGG·2022년 5월 24일
0
post-thumbnail

(Ubuntu 18.04.6 LTS)
2022.05.23 ~ 2022.05.24
프로그래머스 자율주행 데브코스 3기

모두의 코드 CMake 자료
CMake 할때 쪼오오금 도움이 되는 문서
Effective CMake 영상 - 언젠간 봐야지..
CMake 페이지
거의 아무때나 사용할 수 있는 CMake 템플릿

CMake는 cross-platform, compiler-independent한 방식으로 빌드 파일을 생성해주는 프로그램입니다. 우분투에서 실행하면 기본적으로 Makefile을 생성합니다. CMake를 사용하는 모든 프로젝트에서는 반드시 프로젝트 최상위 디렉토리에 CMakeLists.txt 파일이 있어야 합니다.

CMake 예제 1

먼저 간단한 main.cpp를 만들어줍니다.

#include <iostream>

int main(){
    std::cout << "hello cmake" << std::endl;
    return 0;
}

최상위 CMakeLists.txt 파일에는 반드시 cmake 최소버전과 프로젝트 정보가 들어가야 합니다. 아래는 필수 요소에 실행파일을 만들기 위한 add_executable이 있습니다.

add_executable (<실행 파일 이름> <소스1> <소스2> ... <소스들>)
여러 개의 소스코드가 있다면 이어서 작성하면 됩니다.

# 주석처리는 #을 이용하면 된다.

# cmake 최소 버전
cmake_minimum_required(VERSION 3.22)

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

# C++ 버전과 제한
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 실행 파일 만들기
add_executable(program main.cpp)

프로젝트 정보에서 필수적인 것은 처음의 이름만 필수(hello_cmake)입니다. LANGUAGES는 C: C, C++: CXX를 적어줍니다.

cmake를 실행하면 많은 파일들이 생성되게 됩니다. 프로젝트를 진행하면서 이러한 파일들은 가독성을 떨어뜨리므로 build 디렉토리를 만들어 작업하는 것이 좋습니다.

.
├── build
├── CMakeLists.txt
└── main.cpp

빌드 디렉토리로 이동한 후 cmake를 해줍니다. 이 때 cmake ..에서 ..은 상위 폴더를 나타내며 CMakeLists.txt가 있는 폴더입니다.

build$ cmake ..
-- The CXX compiler identification is GNU 7.5.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jong/cpp_project/cmake_modoo/build

실행 후 파일들의 모습은 아래와 같습니다.

.
├── build
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   │   ├── 3.22.3
│   │   │   ├── CMakeCXXCompiler.cmake
│   │   │   ├── CMakeDetermineCompilerABI_CXX.bin
│   │   │   ├── CMakeSystem.cmake
│   │   │   └── CompilerIdCXX
│   │   │       ├── a.out
│   │   │       ├── CMakeCXXCompilerId.cpp
│   │   │       └── tmp
│   │   ├── cmake.check_cache
│   │   ├── CMakeDirectoryInformation.cmake
│   │   ├── CMakeOutput.log
│   │   ├── CMakeTmp
│   │   ├── Makefile2
│   │   ├── Makefile.cmake
│   │   ├── progress.marks
│   │   └── TargetDirectories.txt
│   ├── cmake_install.cmake
│   └── Makefile
├── CMakeLists.txt
└── main.cpp

Makefile이 만들어진 것을 확인할 수 있으며 build 폴더에서 make를 실행하면 실행파일이 잘 만들어지는 것을 확인할 수 있습니다.

build$ make
[ 50%] Building CXX object CMakeFiles/program.dir/main.cpp.o
[100%] Linking CXX executable program
[100%] Built target program

build$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  Makefile  program

build$ ./program
hello cmake

CMake에서 target은 프로그램을 구성하는 요소들입니다. 실행 파일이 될 수도 있고, 라이브러리 파일이 될 수도 있습니다. CMake의 모든 명령들은 타겟을 기준으로 돌아가며, 타겟에 대한 property를 정의할 수 있습니다.

컴파일러는 보통 #include <> 형태로 되는 헤더파일은 시스템 경로에서, #include ""의 형태로 된 헤더파일은 현재 코드의 위치를 기준을 찾는데, 헤더파일을 다른 곳에 위치한 경우 컴파일러가 해당파일들을 찾기 위해서는 컴파일 시에 따로 경로를 지정해주어야합니다.

.
├── build
├── CMakeLists.txt
├── foo.cpp
├── includes
│   └── foo.hpp
└── main.cpp

위와 같은 파일 구조에서 그대로 빌드하는 경우 헤더의 위치를 찾지 못해 에러가 발생합니다.

$ make
Consolidate compiler generated dependencies of target program
[ 33%] Building CXX object CMakeFiles/program.dir/foo.cpp.o

fatal error: foo.hpp: No such file or directory
 #include "foo.hpp"
          ^~~~~~~~~

헤더의 파일을 CMakeLists.txt에 추가할 때는 target_include_directories를 사용합니다.
target_include_directories(<실행 파일 이름> PUBLIC <경로 1> <경로 2> ...)

실행파일 이름(타겟)을 먼저 적은 후 경로를 지정해주면 됩니다. CMake에서 변수를 사용할 때에는 ${ }의 형태로 사용하게 됩니다. ${CMAKE_SOURCE_DIR}은 최상위 CMakeLists.txt의 경로를 의미하게 되며 위의 파일 구조에서 include를 포함하고 싶은 경우 아래와 같이 작성할 수 있습니다.

target_include_directories(project PUBLIC ${CMAKE_SOURCE_DIR}/includes

해당 내용을 포함하여 코드를 수정하고, 실행한 결과는 아래와 같습니다.

# CMakeLists.txt
cmake_minimum_required(VERSION 3.22)

project(cmakecmake
        VERSION 0.1
        DESCRIPTION "예제 프로젝트"
        LANGUAGES CXX
        )

add_executable(program main.cpp foo.cpp)
target_include_directories(program PUBLIC ${CMAKE_SOURCE_DIR}/includes)
// foo.cpp
#include "foo.hpp"

int foo() { return 3; }

// foo.hpp
int foo();

// main.cpp
#include <iostream>
#include "foo.hpp"

int main(){
    std::cout << "Foo: " << foo() << std::endl;
    return 0;
}
build$ ./program
Foo: 3

CMake 예제 2 - 여러 라이브러리

규모가 큰 C++ 프로젝트의 경우 여러 라이브러리로 나누어져서 개발하게 됩니다. 하나의 거대한 라이브러리로 만드는 것보다 각각의 요소들로 쪼개는 것이 좋습니다. 여러 개로 나눈 경우 바뀐 파일을 대상으로만 컴파일하면 되고, 개별 요소들을 확인하기에 용이합니다.

.
├── build
├── CMakeLists.txt
├── examples
│   ├── exec_module1.cpp
│   ├── exec_module1_module2.cpp
│   └── exec_module3.cpp
├── main.cpp
├── modules
│   ├── CMakeLists.txt
│   ├── module1
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   └── src
│   ├── module2
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   └── src
│   └── module3
│       ├── CMakeLists.txt
│       ├── include
│       └── src
└── thirdparty
    ├── Eigen3
    │   ├── build
    │   ├── eigen
    │   └── install
    └── OpenCV
        ├── build
        ├── install
        └── opencv

최상위 폴더에 build, examples, modules(라이브러리), thirdparty(외부 라이브러리)의 디렉토리와 CMakeLists, main이 위치한 형태로 CMake를 만들어보았습니다.

.
├── build
├── CMakeLists.txt
├── examples
├── modules
└── thirdparty

초기에 파일 시스템은 위와 같이 만들었습니다. 먼저 thirdparty 디렉토리로 이동해, Eigen3와 OpenCV 폴더를 만들어 줍니다. 해당 폴더 안에서 git clone을 이용해 소스 코드를 받아줍니다. 추가적으로 install과 build 디렉토리를 만들어줍니다.

2022.05.24 기준
OpenCV
github 페이지: https://github.com/opencv/opencv
Clone with HTTPS: https://github.com/opencv/opencv.git

Eigen3
gitlab 페이지: https://gitlab.com/libeigen/eigen
Clone with HTTPS: https://gitlab.com/libeigen/eigen.git
C++ project

thirdparty/Eigen3$ git clone https://gitlab.com/libeigen/eigen.git
thirdparty/Eigen3$ mkdir build
thirdparty/Eigen3$ mkdir install

이후 CMake를 이용해 빌드를 해주고, make를 실행합니다. 그 후 make install을 이용해 빌드파일을 제외한 파일들을 install에 만들어줍니다. make에 j옵션을 통해 코어 수를 지정할 수 있습니다.

thirdparty/Eigen3/build$ cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=../install ../eigen
thirdparty/Eigen3/build$ sudo make -j2
thirdparty/Eigen3/build$ sudo make install

-D옵션은 CMAKE 관련된 인자를 줄 때 사용합니다. -G 옵션을 통해 Generator를 직접 지정할 수 있습니다. CMake 명령어에서 사용할 수 있는 옵션과 예시는 아래와 같습니다. Build type은 debug와 release가 있고, install prefix를 지정하면 동일한 라이브러리를 다양한 버전으로 사용하고 싶을 때 용이합니다. 지정하지 않으면 기본 설치 경로 /usr/local/lib에 설치됩니다. install은 유저가 필요한 파일들만 옮겨주는 것으로 CMakecache 등은 두고 필요한 것만 옮겨 편리합니다.

cmake [설치할 파일 가장 상단의 CMakeLists.txt 경로]
cmake -DCMAKE_BUILD_TYPE=Debug [설치할 대상 가장 상단의 CMakeLists.txt 디렉토리 경로]
cmake -DCMAKE_BUILD_TYPE=Release [설치할 파일 가장 상단의 CMakeLists.txt 경로]
cmake -DCMAKE_BUILD_TYPE=Debug -GNinja [설치할 파일 가장 상단의 CMakeLists.txt 경로]
cmake -DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_INSTALL_PREFIX=../install [설치할 파일 가장 상단의 CMakeLists.txt 경로]

이와 같은 방식으로 외부 라이브러리를 설치할 수 있습니다. 로컬 라이브러리는 이번 실습에서는 modules에 위치하며 대략적인 파일 구조는 아래와 같습니다.

.
├── CMakeLists.txt
├── module1
│   ├── CMakeLists.txt
│   ├── include
│   │   └── module1
│   │       └── ClassMat.hpp
│   └── src
│       └── ClassMat.cpp
├── module2
│   ├── CMakeLists.txt
│   ├── include
│   │   └── module2
│   │       └── ClassEigenMat.hpp
│   └── src
│       └── ClassEigenMat.cpp
└── module3
    ├── CMakeLists.txt
    ├── include
    │   └── module3
    │       └── ClassBothMat.hpp
    └── src
        └── ClassBothMat.cpp

먼저 모듈 가장 바깥에 위치한 CMakeLists.txt의 내용은 내부에 각각의 모듈에 위치한 CMakeLists를 읽어올 수 있도록 add_subdirectory를 사용합니다.

add_subdirectory(module1)
add_subdirectory(module2)
add_subdirectory(module3)
# 해당 디렉토리의 CMakeLists.txt를 읽도록 subdirectory 지정

module3의 파일들을 하나씩 살펴보면 아래와 같습니다.

//ClassBothMat.hpp
#ifndef HELLO_CMAKE_CLASSEIGENMAT_HPP
#define HELLO_CMAKE_CLASSEIGENMAT_HPP

#include "Eigen/Dense"
#include "opencv2/opencv.hpp"

class ClassEigenMat2
{
public:
    ClassEigenMat2() = default;
private:
    Eigen::Matrix3d eigen_mat_;
};

class ClassMat2
{
public:
    ClassMat2() = default;
private:
    cv::Mat cv_mat_;
};

#endif
//ClassBothMat.cpp
#include "module3/ClassBothMat.hpp"

CMakeLists.txt와 사용한 함수들의 용도는 아래와 같습니다.

cmake_minimum_required(VERSION 3.22)
project(module3 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 변수를 만드는 작업
set(MODULE3_SOURCE_FILES
    src/ClassBothMat.cpp
    # 이후 다른 소스들 생기면 여기에 이어서 적기
    )

# 라이브러리 정의하는 작업. module3
add_library(module3
    ${MODULE3_SOURCE_FILES}
    )

# OPENCV 찾기. REQUIRED: 없으면 빌드하지 않겠다. 시스템 파일 먼저 찾는데 HINTS 주면 해당 경로 먼저.
# ${CMAKE_SOURCE_DIR}은 최상단 cmake가 위치한 디렉토리를 의미한다.
# ~config.cmake가 있는 디렉토리 위치를 적어주어야한다.
find_package(Eigen3 REQUIRED HINTS ${CMAKE_SOURCE_DIR}/thirdparty/Eigen3/install/share/eigen3/cmake)
find_package(OpenCV REQUIRED HINTS ${CMAKE_SOURCE_DIR}/thirdparty/OpenCV/install/lib/cmake/opencv4)

# OpenCV를 찾았는 지 체크. message(STATUS "string")
if (OpenCV_FOUND)
    message(STATUS "OpenCV Found! - ${OpenCV_DIR}")
endif()
if (Eigen3_FOUND)
    message(STATUS "Eigen3 Found! - ${Eigen3_DIR}")

    set(Eigen3_LIBS Eigen3::Eigen)
endif()

# 소스코드 외 헤더파일 연결
target_include_directories(module3 PUBLIC
    # PRIVATE: 외부 노출 x
    # PUBLIC: 상위 라이브러리들도 접근 가능
    # 경로 지정. 여기서는 include 디렉토리.
    include
    ${Eigen3_INCLUDE_DIRS}
    ${OpenCV_INCLUDE_DIRS}
    )

# 라이브러리 연결
# Eigen은 Header only library이기 때문에 상위단에서 접근 위해서 target_link_libraries PUBLIC 필요
target_link_libraries(module3 PUBLIC
    ${Eigen3_LIBS}
    ${OpenCV_LIBS}
    )

마지막으로 실행파일을 만들기 위한 코드가 있는 example 디렉토리의 exec_module1_module2.cpp 함수는 아래와 같은 형태로, OpenCV와 Eigen을 사용하는 module3 라이브러리를 사용합니다.

#include "module1/ClassMat.hpp"
#include "module2/ClassEigenMat.hpp"

#include <iostream>

int main()
{
    const auto mat_module1 = ClassMat();
    const auto mat_module2 = ClassEigenMat();
    std::cout << "module1, module2 success!" << std::endl;
    return 0;
}

해당 소스 코드를 빌드하기 위한 가장 상단의 CMakeLists.txt는 아래와 같이 작성할 수 있습니다.

cmake_minimum_required(VERSION 3.22)
project(hello_cmake
        LANGUAGES CXX
        )

set(CMAKE_CXX_STANDARD 14)
# 변수에 값을 넣어주기

set(CMAKE_CXX_STANDARD_REQUIRED ON)
# C++ 17, 20 등의 기능이 사용되면 기능 제한

add_subdirectory(modules)
# subdirectory의 CMake를 실행하기위한 목적. modules 디렉토리 내.

add_executable(exec_module3 examples/exec_module3.cpp)
target_link_libraries(exec_module3 PRIVATE
        module3)

모두 마친후 가장 상단의 build 디렉토리에서 cmake ..을 이용해 빌드 파일을 만들고, make를 이용해 실행파일을 생성할 수 있습니다.

profile
GGG

0개의 댓글