Recipe CMake for Compile

 

이번에 기업과제에서 코드 환경이 윈도우 Visual studio로 환경이었습니다. 저는 윈도우 환경이 없던 터라 빌드 시스템을 작게나마 만들어 보고자, 이번에는 CMake를 이용한 빌드 시스템에 대해서 정리하는 시간을 가져봤습니다. 더 자세히 알고 싶다면 레퍼런스를 참고하시길 바랍니다.

1. gcc vs make vs CMake

1.1 Compiler

프로그래밍 언어(High-level languange) 를 기계가 이해할 수 있는 언어(Low-level language, assembly, obeject code, machine code)로 번역해주는, Compiler 과정에서 가장 중요하는 역할을 하는 녀석이 바로 Compiler입니다. 필자가 많이 접하는 C/C++ 언어용 컴파일러로는 GCC C(gcc), GCC(g++), Clang, Clang++이 있습니다. 더 많은 컴파일을 알고 싶다면, 여기로. arm64(arm 프로세서), x86_64(intel 프로세서)와 같은 OS 종류에서부터 최적화까지 컴파일러를 통해 할 수 있는데, 오늘은 “어떻게 컴파일러를 이용하여 프로그래밍 언어를 빌드할 수 있는가?” 에 대해서 정리해보고자 합니다.

빌드 과정으로 저는 CMake에 대해서 자세히 다룰 예정인데, 시작하기 전에 컴파일러 중에 gcc, make를 간단하게 짚고 넘어가 보겠습니다.

1.2 gcc

C, C++, Objectvie-C, Foran, Ada, Go, 그리고 D까지. libstdc++과 같은 라이브러리를 함꼐 사용하기 위해 GNU operating system을 위한 컴파일러로 GNU C 컴파일러 gcc를 많이 사용합니다. Mac, Linux, Window 모두 gcc/g++ 컴파일러는 있으니 한 번쯤 확인해보시면 필자의 경우 homebrew에 하나, /usr/bin/ 에 하나 있었습니다. 컴파일러 과정에 대해 을 알고 나면,

Source Code → Preprocessing → Include header, Expand Macro(.i, .ii) 
→ Compiler(gcc, g++) → Assembly Code(.s) → Assembler(as) 
→ Machine Code(.o, .obj) → Linker(ld), Static Library(.lib, .a) 
→ Executable Machine Code(.exe)

그럼 어떻게 컴파일 해야 되는데? 의 질문 할 수 있는데, 그 해답이 바로 이 컴파일러 중 하나인 gcc를 사용하는 방법을 익히는 것 입니다.

1.3 Make

Make는 그럼 무엇일까요? 코드를 하나, 두개만 써놓은 것은 아니고 우리는 수십개의 파일을 자동적으로 컴파일 할 수 있도록 “군집화”해주는 utility 입니다. 여러가지 옵션과 동시에 여러 파일을 컴파일 할 수 있도록 makefile이라는 파일을 통해 커맨드 창에 make만 치면 자동적으로 컴파일이 될 수 있게 해줍니다. 오늘은 CMake를 다룰 예정이라 gcc와 make에 대해 더 자세한 과정은 이 링크를 참고하시면 되겠습니다.

2. CMake

자자, 본격적으로 CMake에 대해서 이야기하도록 하겠습니다. 앞서 gcc와 Make를 먼저 언급한 것은 CMake는 “generator of build systmes”로 내용에 따라 make로 사용할 수 있는 makefile을 만들어주는 용도이기 때문입니다. 그럼 자연스레 make를 통해 빌드를 할 수 있고, make를 이용할 수 있다는 건 컴파일러를 이용할 수 있습니다. 그래서 필자의 경우는 gcc와 make가 아인 CMake를 통해서 Build System을 구축하고자 합니다.

.
├── Source1
│   ├── SubDir1-1
│   │   ├── Config.cmake
│   │   └── func11.cpp
│   ├── SubDir1-2
│   │   ├── Config.cmake
│   │   └── func12.cpp
│  ...  ...
|   └── CMakeLists.txt

├── Source2
│   ├── SubDir2-1
│   │   ├── Config.cmake
│   │   └── func21.cpp
│   ├── SubDir2-2
│   │   ├── Config.cmake
│   │   └── func22.cpp
│  ...  ...
│   ├── CmakeFunc1.cmake
│   ├── CmakeFunc2.cmake
│  ...  ...
│   └── CMakeLists.txt
└── CMakeLists.txt

2.2 CMake Basic Usage

#0 CMakelists.txt 에 프로젝트 명을 적어주기

가장 먼저 하는 것은 cmake 버전 조건과 프로젝트 명을 적는 것입니다(내가 누구인지는 알아야 파일도 실행을 하지… 빼먹지 말기!). 사용한 프로그래밍 언어와 버전을 명시해주는 건 선택사항입니다(써주는 게 좋겠죠?).

cmake_minimum_required(VERSION 3.0.0)
project(SourceAssignment LANGUAGES CXX VERSION 0.1.0)

#1 기본으로 변수명, 디버깅, 캐쉬에 대해 먼저 짚고 넘어가기

  • 변수는? ${변수명}
  • 메세지 출력(c에서 printf, c++에서 cout)은? message(”내용”)
  • Cache처럼 저장/설정돼 있는 변수 확인은? CMakeCache.txt

예를 들어 보면, 설정한 CMAKE_OSX_ARCHITECTURES 를 확인하고 싶다면 아래처럼 사용할 수 있습니다.

# CMakelists.txt
message("CMAKE_OSX_ARCHITECTURES: " ${CMAKE_OSX_ARCHITECTURES})

#2 음, 그럼 변수를 설정할 수도 있어?

스크립트에서 하는 방법, 터미널에서 하는 방법 그리고CMakeCache.txt에 저장하는 방법(이건 vscode 이용시 CMakeCache.txt에 저장해서 빌드할 때 이용가능) 이 있습니다.

  • CMakeLists.txt 에서 설정: set(VAR_NAME VAR)
  • 터미널에서 설정하기:cmake CMakeLists.txt -DVAR_NAME:VAR_DTYPE=VAR
  • CMakeCache.txt에 저장하기: set(VAR_NAME "" CACHE STRING "Description of the argument")

예를 들어 보겠습니다.

  • cmake를 실행할때 변수 ARGU_BUILD_TYPE 변수명으로 “exe”를 받고 싶다면,

      cmake CMakeLists.txt -DARGU_BUILD_TYPE:STRING=exe
    
  • 스크립트에서 변수 ARGU_BUILD_TYPE 를 CMakeCache.txt에 저장하고 싶다면,

      # CMakelists.txt
      set(ARGU_BUILD_TYPE "lib" CACHE STRING "Description of the argument")
      message("Building for " ${ARGU_BUILD_TYPE})
    

#3 그럼 If문도 가능한가?

당연히 가능합니다. If문 조건으로 많이 사용하는 bool같은 녀석인 option과 If문 사용법은 다음과 같습니다.

  • option: option(LIBRARY "Build library" ON)
  • If, else if, else, endif

      # CMakelists.txt
      if(<condition>)
          <commands>
      elseif(<condition>) # optional block, can be repeated
          <commands>
      else()              # optional block
          <commands>
      endif()
    

예시를 들어 보면 아래는 LIBRARY를 ON해서 “Build libarary…” 메세지를 출력하는 스크립트입니다.

# CMakelists.txt
# 만약 ON을 하지 않으면? OFF일 것이다. 

option(LIBRARY "Build library" ON)

if(LIBRARY)
    message(STATUS "Build Library...")
endif()

여기까지 하면 간단한 스크립트를 짤 수 있을 것입니다. 그러면 이제 작게 빌드시스템을 만들어 보겠습니다.

2.3 CMake Build System

필자의 경우는 코드에서 우선 라이브러리로 빌드해, 그 라이브러리를 링크한 실행파일을 만드는 게 목적이었습니다. 독립적으로 빌드를 하는 경우와 전체를 빌드하는 경우, 두 가지 경우를 만들기 위해 아래와 같이 질문이 나왔고, 각각의 답을 통해 전체 빌드 시스템을 짜보도록 하겠습니다.

#1 빌드 시스템을 통해 쉽게 “모드”를 설정할 수 없을까?

Bash 스크립트를 통해 Argument를 받아 library, execute, library + execute 모드로 나눌 수 있습니다.

#2 빌드 시스템을 통해 여러개의 라이브러리와 실행파일을 만들 수 있을까?

CMake의 경우, 여러개의 라이브러리와 실행파일을 빌드가 가능합니다. 이는 아래 두 개를 이용하면 됩니다.

  • add_library(LIBRARY_NAME LIBRARY_TYPE LIBRARY_TARGET_FILES)
  • add_executable(EXEUTE_NAME EXCUTE_TARGET_FILES)

그리고 실행파일의 경우 라이브러리와 링크해야 하므로 아래의 경우를 사용하면 됩니다.

# 실행파일을 라이브러리에 링크해 빌드하는 경우
set(VAR_LIBRARY_PATH "LIBRARY_PATH")
add_executable(EXEUTE_NAME EXCUTE_TARGET_FILES)
target_link_libraries(EXEUTE_NAME ${VAR_LIBRARY_PATH})

예를 들어보면 아래처럼 사용할 수 있겠죠?

# CMakelists.txt
add_executable(example main.cpp
                       feature1.cpp
                       ...)

혹은

# CMakelists.txt
set(SRC_FILES main.cpp
              feature1.cpp
              ...)
add_executable(example SRC_FILES)

위 처럼 set을 이용해서 이렇게 쓸 수도 있습니다.

#3 빌드 시스템을 통해 OS아키텍처에 따른 실행파일을 만들 수 있을까?

필자의 경우 x86_64와 arm64가 있었는데 이는 CMAKE_OSX_ARCHITECTURES를 설정하면 됩니다.

# CMakelists.txt
set(CMAKE_OSX_ARCHITECTURES arm64)

x86_64와 arm64, 두 가지 환경을 위한 컴파일의 경우 아래와 같이 설정합니다.

# CMakelists.txt
set(CMAKE_OSX_ARCHITECTURES x86_64;arm64)

#4 빌드 시스템을 통해 Argument(ex. ls -al 에서 -al과 같은 요소)를 받을 수 있을까?

네 받을 수 있습니다. DARGU_BUILD_TYPE 를 통해 가능한데, 이전에 설명한 바와 같이 bash cmake CMakeLists.txt -DVAR_NAME:VAR_DTYPE=VAR 를 이용해 가능합니다.

cmake -DARGU_BUILD_TYPE:STRING=lib

#5 빌드 시스템을 각 폴더 별로 독립적으로 구분지어서 작성할 수 없을까?

이 경우 세 가지가 경우가 있습니다. 첫 번째는 빌드 시스템의 역할을 하는 경우, 두 번째는 Configuration의 역할을 하는 경우, 그리고 마지막은 function을 하는 경우입니다. **빌드 시스템의 경우 CMakeLists.txt, Configuration의 경우 Config.cmake를 이용하면 됩니다. (function의 경우 여기를 참고!)

예를 들어 필자의 상황은 아래와 같다고 가정해보겠습니다.

.
├── Source
│   ├── SubDir
│   │   ├── Config.cmake
│   │   ├── main.cpp
│   │  ...
│   └── CMakeLists.txt
└── CMakeLists.txt

이 경우, 첫 번째는 빌드 시스템의 역할을 하는 경우는 add_subdirectory를 이용해서 아래와 같이 사용하면 됩니다. (${PROJECT_SOURCE_DIR} 의 경우는 현재 프로젝트 경로를 의미하니 Message로 확인해보자. 자세한 내용은 여기에서!)

// ./CMakeLists.txt
add_subdirectory(${PROJECT_SOURCE_DIR}/Source)

두 번째, Configuration의 역할을 하는 경우는 include 를 이용해서 아래와 같이 사용하면 된다.

// ./Source/CMakeLists.txt
include(${PROJECT_SOURCE_DIR}/Source)

이렇게 Configuration을 이용하여 타겟 소스 파일은 서브폴더에서도 지정이 가능하다.

// ./Source/CMakeLists.txt
add_executable(EXECUTE_NAME)
include(EXECUTE_NAME/Config.cmake)
// ./Source/SubDir/Config.cmake
target_sources(EXECUTE_NAME "${PROJECT_SOURCE_DIR}/SubDir/main.cpp")

#6 빌드 시스템이 컴파일러와 버전을 지정할 수 있을까?

# Set Compiler
set(CMAKE_CXX_COMPILER "/usr/bin/clang++")

# Set C++ standard to C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

사실, CMake의 경우 처음에 소개한 두 링크만 천천히 읽어도 따라가는데 크게 지장이 없었습니다. 그리고 세세한 옵션의 경우 그 때 그때 찾던지, Chatgpt를 이용하면 될 듯 싶습니다. CMake를 이용하면 이미 있는 라이브러리나 프레임워크를 찾는 find_package, 크로스 컴파일을 위한 CMake 와 같은 것 또한 가능할 것인데, 위에서 사용한 예시들은 이 링크)에 전체 코드를 첨부해놨습니다. 워낙 레페런스의 글들이 잘 정리해놓으셨다보니 이 글의 목적은 CMake에 대한 전반적인 그림을 그리는 것과 정리를 목적으로 이야기 해봤습니다. 다른 기능들을 해야할 날이 온다면 추가하는 걸로하고 여기서 글을 마치겠습니다.

3. Reference