우분투에서 C++ 개발하기(3) - CMake

앞선 시리즈 :
우분투에서 C++ 개발하기(1) - https://ladofa.blogspot.com/2018/07/c-1.html
우분투에서 C++ 개발하기(2) - https://ladofa.blogspot.com/2020/08/c-2.html
여기서 사용하는 예제는 두 번째 시리즈에서 사용한 예제를 그대로 사용한다. 예제를 몰라도 읽는데 문제는 없다.

-------------------------

1편과 2편을 잘 정독했던 분들이라도 여기서부터는 예제가 복잡해서 이해하기 쉽지 않다. 쉬운 예제로 설명하고 싶은데.. 불가능하다. cmake가 원래 복잡한 컴파일 과정을 간단하게 하는 거라서... 여기서부터는 구경만 해도 좋고, 어쩌다 CMakeLists.txt 를 분석할 일이 있으면 그 때 한 번 더 봐주시길...

-------------------------


리눅스에서 C++ 코드를 컴파일하려면 최종적으로는 gcc나 다른 컴파일러를 이용해서 컴파일 명령을 내려줘야 한다. 그러나 파일 개수가 많고 의존성이 방대한 큰 프로젝트에서 일일이 gcc 명령을 내릴 수는 없다. 이를 보완하기 위해 Makefile을 작성해서 컴파일 명령어를 미리 입력해두면 나중에는 별 다른 수고 없이 간단한 명령으로 컴파일이 가능하다. 그런데 Makefile을 작성할 때도 결국은 gcc를 잘 알고 있어야 하고, 각종 옵션과 명령어를 직접 입력하는 것도 힘든 일이다.

CMake 기본


CMake는 이런 작업들조차 단순화하여 간단한 명령어를 통해 입력 가능하도록 돕는다. CMake는 사용자가 작성한 스크립트를 해석해서 Makefile을 만든다. 결국 cmake 를 실행한 뒤 make를 다시 실행해야 한다.

CMake도 프로그램이기 때문에 안 깔려 있으면 직접 깔아야 하는데 우분투에서는 다음과 같이 패키지를 통한 설치를 지원한다.

$sudo apt install cmake


여기서 CMake의  apt 패키지 이름이 cmake이고, 실행 프로그램의 이름도 cmake이다. 터미널에 그냥 cmake라고 입력해보자.

Usage

  cmake [options] <path-to-source>
  cmake [options] <path-to-existing-build>
  cmake [options] -S <path-to-source> -B <path-to-build>

Specify a source directory to (re-)generate a build system for it in the
current working directory.  Specify an existing build directory to
re-generate its build system.

Run 'cmake --help' for more information.

위와 같이 뜨면 성공이다. 컴퓨터에 깔려 있는 cmake 프로그램을 실행한 것이다. 버전을 확인하고 싶으면 다음과 같이 입력한다.

$cmake --version

cmake가 인식하는 스크립트 파일의 이름은 CMakeLists.txt 로 정해져있다. make가 기존에 존재하고 있는 Makefile 을 자동으로 인식하는 것처럼 cmake 역시 CMakeLists.txt 라는 이름의 파일을 찾는다.

이전 시리즈에서 작업했던 것과 같이 my.cpp, my.h, main.cpp 파일이 있다고 가정할 때, 이를 빌드하기 위한 CMakeLists.txt 스크립트는 다음과 같다.


cmake_minimum_required (VERSION 3.10)
project (mytest)
add_executable (mytest my.cpp main.cpp)

맨 첫째 줄은 cmake의 최소 버전을 밝히는 것이다. 해당 버전보다 아래인 cmake로 실행할 수  없게끔 막는다.
project()는 해당 스크립트의 프로젝트 이름을 밝힌다. mytest는 이 프로젝트의 이름이며 실행파일을 생성할 경우 실행파일의 이름이 될 것이다.
add_executable은 이 스크립트를 통해 최종적으로 실행파일을 만들고자 한다는 뜻이다. 실행파일 말고 정적 라이브러리를 만들고 싶을 때는 add_library라는 명령을 이용한다.

이 스크립트를 실행하려면 다음과 같이 입력하면 되는데...

$cmake .

make 는 무조건 실행한 경로에서 Makefile의 존재유무를 찾지만 cmake는 반드시 CMakeLists.txt의 경로를 지정해줘야 한다. cmake 다음에 나오는 점(.)이 바로 경로를 나타내는 것이다. 점 하나 찍으면 현재 경로를 말한다.

cmake는 Makefile을 생성하면서 기타 다양한 파일들도 생성하므로 그냥 현재 폴더에서 실행하면 이런저런 파일이 생겨 지저분해진다. 그래서 보통은 build 디렉토리를 만들어서 거기서 실행한다.

$mkdir build
$cd build
build$cmake ..

위와 같이 build 디렉토리를 만든 뒤, 해당 디렉토리 내에서 cmake를 실행한다. 이 때 cmake 뒤에는 점 두개 (..) 가 붙어 있다. 이는 상위 디렉토리를 나타낸다. CMakeLists.txt 파일이 build디렉토리에 있지 않고 그 상위 디렉토리에 있기 때문이다.

빌드에 성공하면 이런 저런 메타파일이 생성되고, 가장 중요한 Makefile이 만들어진다. 이 Makefile의 타겟은 기본적으로 all 과 clean을 가지고 있다. 타깃을 입력하지 않으면 all을 기본으로 한다. 아래와 같이 입력해보자.

build$make

그냥 make라고 하면 자동 생성된 Makefile을 실행시킨다. 여기까지는 거의 공식처럼 사용한다.



빌드에 성공하면 mytest 파일이 만들어져 있다. mytest를 실행해보자.

build$./mytest
3^2 == 9

이제 main.cpp파일을 수정할 것이다. 다음 프린트 구문을 추가한다.

printf("This code is modified.\n" );

다시 mytest를 빌드해서 실행해보자.

build$make
build$./mytest
3^2 == 9
This code is modified.

위와 같이 수정되서 나타난다. 뭔가 신기한 일이 이뤄진 것 같지만 사실 별 것도 아니다. 여기서 일단 CMake는 하는 일이 없다. make를 실행하면 수정된 파일을 감지해서 다시 빌드를 해준다. 그리고 그 결과가 나타난 것 뿐이다.

이제 your.h, your.cpp 파일을 추가할 것이다.

<your.h>
int your_func(int x);

<your.cpp>
#include "your.h"

int your_func(int x)
{
    return x + 10;
}


main.cpp에서도 your_func 을 사용하도록 수정한다.

<main.cpp>
#include <iostream>
#include "my.h"
#include "your.h"

int main(void)
{
    printf("%d^2 == %d\n", 3, my_func(3));
    printf("This code is modified.\n" );
    printf("%d + 10 == %d\n", 3, your_func(3));
}


마지막으로 CMakeLists.txt파일을 다음과 같이 수정한다.

cmake_minimum_required (VERSION 3.10)
project (mytest)
add_executable (mytest my.cpp main.cpp your.cpp)


add_executable에 your.cpp 를 추가한 것이다. 이제 아까와 마찬가지로 새로 빌드를 해본다.

build$make
build$./mytest
3^2 == 9
This code is modified.
3 + 10 == 13

수정된 결과가 반영되어 있다. cmake는 수정된 CMakeLists.txt 파일을 자동으로 반영하여 Makefile을 미리 바꿔놓았다. 그리고 나는 make명령어를 통해  바뀐 Makefile을 실행해서 결과를 확인했다. 

만약 CMakeLists.txt에 오류가 있으면 어떻게 될까?

cmake_minimum_required (VERSION 3.10)
project (mytest)
add_executable (mytest my.cpp main.cpp your.cpp nobody.cpp)

build$make

나는 make를 실행했는데, 실행해보면 CMakeLists.txt파일이 잘못되었다는 에러 메시지가 나온다. cmake는 건들지도 않았는데!

make는 어떻게 cmake를 인식하고 일반적인 빌드 오류가 아닌 cmake 오류를 출력하는 것일까? make가 cmake와 뭔가 연동되어 있는 것일까? Makefile 내부를 보면 cmake 에러가 없는지 확인하고 메시지를 출력하도록 되어 있다. cmake가 Makefile을 생성하면서 그 속에 cmake관련 내용을 스파이처럼 넣어놨다. 그래서 make만 실행해도 CMakeLists.txt의 오류를 검사하게 된다.


라이브러리 구조화

이제 my 라이브러리를 분리해서 컴파일해보자. 보통 프로그램 개발에서 핵심적인 기능을 담당하는 모듈은 기본 프로그램과 분리해서 따로 라이브러리 형식으로 개발하고, 본 프로그램은 해당 라이브러리를 사용하도록 만든다. 지금 예제에서는 mylib 라는 이름의 라이브러리를 개발하는 것으로 가정할 것이다.

일반적으로 라이브러리를 포함한 구조는 다음과 같이 되어 있다. 이 구조를 무조건 지킬 필요는 없지만 보통 이렇다..

  - 라이브러리 이름
    - src
      - files.cpp
    - include
      - files.h
    CMakeLists.txt
  - 또 다른 라이브러리
    ......
main.cpp
CMakeLists.txt

이렇게 글씨로 쓰면 구조가 안 보일까봐 VSCode 캡쳐 화면도 준비했다.



라이브러리마다 CMakeLists.txt 파일이 따로 있고 여기에는 src 디렉토리와 include 디렉토리가 있다. 빌드를 마치고 나서 src 폴더에 있는 cpp파일은 당연히 소스코드니까 공개하지 않지만 include 폴더에 있는 h, hpp 파일은 라이브러리를 사용하는 측에게 공개해야 한다. C의 라이브러리들은 헤더파일이 있어야 쓸 수 있다. 그런 이유를 포함해서 관리상의 편리함 등 여러 가지 이유로 src와 include는 분리해놓는 것이다.

혹시나 다른 사람이 만든 C언어 소스를 보게 되면 항상 include 디렉토리와 src 디렉토리가 따로 나뉜 걸 보게 된다. 또한 src를 컴파일한 결과는 bin 혹은 build 디렉토리에 모셔놓게 된다. 만약 소스코드가 아닌 빌드된 라이브러리를 다운받게 되면 include 속에 있는 헤더파일과 bin디렉토리 내부의 컴파일된 바이너라 파일만 보게 된다. 왜 그런지를 이해하려면 1편을 참고한다...


하여튼 mylilb 속에 있는 CMakeLists.txt의 내용은 다음고 같다.


cmake_minimum_required (VERSION 2.8)
project (mylib)
add_library (mylib src/my.cpp)
target_include_directories(mylib PUBLIC include)


아까의 CMakeLists.txt와 비교해서 다른 점은 add_executable 대신 add_library로 바뀌어 있다는 것이다. 실행파일을 만들지 않고 라이브러리를 생성한다는 뜻이다. 여기에는 사용되는 모든 cpp파일을 적어주어야 한다. 그리고 target_include_directories가 추가되었는데, 여기에 include 디렉토리를 알려줘야 한다.

다음과 같이 빌드해볼 수 있다.

mylib$mkdir build
mylib$cd build
mylib/build$cmake ..
mylib/build$make

빌드하고 나면 libmylib.a 파일이 생성되었다. 이것을 메인에서 이용하면 된다. 바깥쪽에 있는 CMakeLists.txt의 내용은 다음과 같다.

cmake_minimum_required (VERSION 2.8)
project (mytest)
add_executable (mytest main.cpp)
target_link_libraries(mytest PUBLIC mylib)
target_link_directories(mytest PUBLIC mylib/build)
target_include_directories(mytest PUBLIC mylib/include)

target_link_libraries에 라이브러리 이름을 추가한다. 라이브러리 이름이란 *.a에서 맨 앞에 lib를 제외한 것이다. gcc빌드할 때와 마찬기자로 라이브러리 파일은 맨 앞에 lib가 prefix로 붙는다. 파일 확장자가 앞쪽에 붙었다고 생각하면 된다.

target_link_directories 에는 라이브러리 파일의 경로를 추가한다. target_include_directorie 에는 헤더 파일의 경로를 추가하면 된다.

사실 이런 식으로 하는 것은 아예 동떨어진 다른 라이브러리를 가져오는 것과 동일하다. 그런데 현재 디렉토리/프로젝트 구조는 mylib 를 포함하고 있는데, 이 특징을 전혀 살리지 않은 것이다. 하위 디렉토리에 라이브러리가 들어있는 상황에서는 조금 더 간결하게 cmake명령을 만들 수 있다.

이제 add_subdirectory 를 이용해서 디렉토리를 구조화한 보람을 찾아보자.  mylib 디렉토리 내에 있는 CMakeLists.txt는 그대로 두고 메인 프로그램의 CMakeLists.txt를 다음과 같이 수정한다.

cmake_minimum_required (VERSION 2.8)
project (mytest)
add_subdirectory(mylib)
add_executable (mytest main.cpp)
target_link_libraries(mytest PUBLIC mylib)

add_subdirectory에 mylib를 추가했다. 이렇게 하면 mylib 디렉토리 속에 있는 CMakeLists.txt를 인식하고 가져온다. 라이브파일의 경로나 헤더 파일의 경로는 따로 추가할 필요가 없다. 다만 target_link libraries에 mylib를 추가하면 끝이다.

이렇게 하고 build 디렉토리를 만들어서 cmake를 해보면 mylib와 mytest가 같이 만들어지게 된다. 즉 여러 개의 CMakeLists.txt에 각각 명령어를 실행할 필요 없이 한 번에 연쇄적으로 실행된다. 실제로 build 내부에 mylib 디렉토리가 따로 생성되며 여기에 빌드된 라이브러리가 들어 있다.



외부 패키지 추가


마지막으로 외부 패키지를 찾아보도록 하자. 다른 사람이 빌드한 라이브러리를 추가할 때 위에서 소개한 바와 같이 include 디렉토리와 라이브러리를 일일이 추가하는 것은 상당히 번거롭다. 때문에 누군가 고수님께서 우리를 위해 라이브러리를 만드실 때는 CMake를 활용해서 자기 라이브러리를 잘 추가할 수 있도록 메타 정보를 같이 만들어둔다. 우리는 해당 메타 정보만 읽어들이면 라이브러리 추가에 필요한 각종 파라미터(include디렉토리, lib 디렉토리 등)를 자동으로 얻을 수 있다.

대표적인 라이브러리로 OpenCV가 있다. OpenCV를 본 프로젝트에 추가하려면 CMakeLists.txt에 다음과 같이 입력한다.


cmake_minimum_required (VERSION 2.8)
project (mytest)
find_package(OpenCV REQUIRED)
add_subdirectory(mylib)
add_executable (mytest main.cpp)
target_link_libraries(mytest PUBLIC mylib ${OpenCV_LIBS})
target_include_directories(mylib PUBLIC ${OpenCV_INCLUDE_DIRS})


find_package는 주어진 이름의 패키지(메타정보)를 찾아서 라이브러리의 목록과 헤더 파일의 디렉토리 목록을 변수에 저장한다. OpenCV 패키지는 어떻게 찾는가? 그 방법에 대한 링크가 있다.


대략 9가지 정도의 방법으로 패키지를 찾는다(...) 보통은 라이브러리 설치할 때 sudo make install 과 같은 명령어를 입력하면 그 속에 CMake 패키지 등록 과정이 포함되어 있다.

라이브러리를 찾는데 성공하면 OpenCV_LIBS와 OpenCV_INCLUDE_DIRS 가 변수로 제공된다. 이것을 타겟 빌드하는데 추가해주면 된다. 다른 라이브러리인 경우 OpenCV 자리에 다른 이름을 넣으면 된다.

만약 cmake가 없었다면? 우리는 OpenCV를 활용하기 위해 OpenCV 헤더 정보가 있는  include디렉토리를 직접 찾아서 입력해야 하고, 라이브러리 파일도 직접 찾아서 (아마 Makefile에) 추가해줘야 했을 것이다. 라이브러리 버전이 바뀌거나 업데이트되면? 또 달라진 경로와 파일을 일일이 수정해줘야 한다. 이 과정을 cmake가 알아서 해준다.

여기까지 와서 왜 라이브러리와 인클루드 디렉토리를 추가해야 하는가? 이런 의문이 들면 C컴파일 방법을 처음부터 다시 배워야 한다. 이글 맨 위의 링크에서 1탄으로 가시기 바란다.

참고로 CMake 스크립트에서는 순서가 중요하다. target_xxx 인 명령어는 add_executable이나 add_library 뒤에 와야 한다. 그 밖의 명령어는 add_executable/add_library 앞에 오면 된다. 제일 중요한 부분인데.... 마지막에 밝힌다.



  이상으로 우분투에서 C+ 개발하기 시리즈를 마친다.

--------

VSCode로 빌드하기... 는 그냥 연재 안 하는 것으로...  검색해보면 다른 글이 많으니 굳이 내가 수고할 필요가 없다. Remote SSH로 연결해서 컴파일하고, 디버그하고.. 그런 내용일 것이다.
여러분들이 라즈베이파이, 어디 서버 등 리눅스에서 돌릴 프로그램을 개발한다면 파이썬이든 C++ 이든 무조건 VSCode의 Remote SSH 기능을 활용하는 것을 추천한다.

--------

제 블로그에서 가장 인기있는 글이 바로 여기 시리즈라서 따로 인사를 남깁니다. 방문하신 모든 분들, 댓글 달아주신 분들 감사합니다.


댓글 8개:

  1. 수고하셨습니다. 00\
    -벨라대디

    답글삭제
  2. 결국 gcc를 편하게 쓰기 위해 Make가 나왔고 그 Makefile 을 자동으로 작성해주는 Cmake 라는 파일이 나온거네요. c++입문에 큰 도움이 되었습니다 감사합니다.

    답글삭제
  3. 와... 정말 좋은 설명이었습니다. 상세한 예시와 함께 설명해주셔서 감사합니다.

    답글삭제
  4. 형편없는 글에 좋은 격려해주셔서 모두 감사합니다~

    답글삭제

Powered by Blogger.