본문 바로가기
CMake

[CMake] add_subdirectory() 와 변수 Scope

by 별준 2021. 10. 31.

Refereces

  • Professional CMake : A Practical Guide

Contents

  • add_subdirectory()
  • variable scope
  • include()
  • return() / include_guard()

이번 글에서는 대부분의 프로젝트에서 사용되는 add_subdirectory()와 이로 인해 발생하는 변수의 scope에 대해서 알아보겠습니다. 그리고 add_subdirectory()와 유사하지만 약간은 다른 include()에 대해서도 살짝 알아보도록 하겠습니다.

 


add_subdirectory()

대부분의 프로젝트에서는 소스코드가 하나의 폴더에만 존재하는 것이 아닙니다. 그리고 하나의 Target으로 전체 코드가 빌드되는 것도 아니기 때문에 필연적으로 여러 개의 서브 디렉토리들이 존재하게 됩니다. 하지만, 최상단의 CMakeLists.txt로만 빌드 환경을 구성하는 것은 비효율적이고 파일이 너무나 커지고 관리하기가 힘들어집니다.

 

이런 경우에 add_subdirectory() 명령을 사용하여서 최상단의 프로젝트 경로에서 다른 서브 디렉토리들을 빌드로 가져올 수 있습니다. 단, add_subdirectory()이 호출되는 시점에 해당 서브 디렉토리에 CMakeLists.txt 파일이 존재해야하며, 프로젝트의 빌드 디렉토리에 해당 디렉토리가 생성됩니다.

 

add_subdirectory() 명령은 아래의 형태로 사용할 수 있습니다.

sourceDir는 일반적으로 소스 디렉토리 내에 존재하지만, 꼭 소스 디렉토리의 하위 디렉토리일 필요는 없습니다. 어떠한 디렉토리라도 추가할 수 있는데, 이 sourceDir는 절대경로 또는 상대경로로 지정되고 만약 상대경로라면 현재 소스 디렉토리에 상대적인 경로가 됩니다. 일반적으로 절대경로는 기본 소스 디렉토리 외부에 존재하는 디렉토리를 추가할 때만 사용됩니다.

 

binaryDir는 일반적으로 지정할 필요는 없습니다. 생략한다면, CMake는 sourceDir와 동일한 이름으로 프로젝트 빌드 트리에 디렉토리를 생성합니다. binaryDir는 마찬가지로 절대경로 또는 상대경로로 명시적으로 지정할 수 있는데, 상대경로라면 현재 binary 디렉토리에 대해 상대적이게 됩니다. (자세한 사항은 아래에서 다시 언급하도록 하겠습니다.)

만약, sourceDir가 프로젝트 외부 경로인 경우에는 이에 대한 상대경로를 더 이상 자동으로 구성할 수 없기 때문에 이 경우에는 binaryDir를 명시적으로 지정해주어야 합니다.

 

EXCLUDE_FROM_ALL 키워드는 선택적으로 지정할 수 있고, 이는 추가되는 하위 디렉토리에서 정의된 Target이 기본적으로 프로젝트의 ALL Target에 포함되어야 하는지 여부를 제어하기 위한 것입니다.

(일부 CMake 버전 및 Project Generator 에서는 예상대로 동작하지 않고, 빌드가 제대로 되지 않을 수 있으니 주의해야합니다.)

 

소스 디렉토리는 CMakeLists.txt가 존재하는 디렉토리, 빌드 디렉토리는 빌드 파일이 생성되는 디렉토리입니다.

 

Source and Binary Directory Variables

런타임에 필요한 파일을 복사하거나 빌드 작업을 수행할 때 현재 소스 디렉토리에 해당하는 빌드 디렉토리의 경로가 필요할 수가 있습니다. add_subdirectory()를 사용하게 되면 소스 및 빌드 디렉토리 구조가 복잡해질 수 있고, 동일한 소스 디렉토리에 여러 개의 빌드 디렉토리가 설정될 수도 있습니다. 따라서 개발자가 필요한 디렉토리를 선택하고 사용하기 위해서 CMake의 도움이 필요한데, 이를 위해서 CMake에서는 현재 처리 중인 CMakeLists.txt 파일의 소스 디렉토리와 빌드(binary) 디렉토리를 추적하는 변수들을 제공합니다. 

이 변수들은 읽기 전용(read-only)이며, CMake에서 각 CMakeLists.txt를 처리할 때 자동으로 업데이트됩니다. 이 변수들은 항상 절대 경로입니다.

  • CMAKE_SOURCE_DIR : 가장 최상단의 source tree의 경로를 나타냅니다. 즉, 최상단에 위차한 CMakeLists.txt의 경로를 의미합니다. 이 변수는 add_subdirectory()를 통해 다른 서브 디렉토리의 CMakeLists.txt가 수행되더라도 변경되지 않습니다.
  • CMAKE_BINARY_DIR : 가장 최상단의 build tree의 경로입니다. CMAKE_SOURCE_DIR와 마찬가지로 이 변수는 절대 변경되지 않습니다.
  • CMAKE_CURRENT_SOURCE_DIR : CMake에 의해서 현재 처리 중인 CMakeLists.txt가 존재하는 경로입니다. 이는 add_subdirectory()로 새로운 CMakeLists.txt가 수행될 때 업데이트됩니다. 그리고 수행 중인 CMakeLists.txt가 종료되면 다시 이전 경로로 돌아가게 됩니다.
  • CMAKE_CURRENT_BINARY_DIR : 현재 수행 중인 CMakeLists.txt 에 대응되는 빌드 디렉토리의 경로입니다. 이 경로 역시 add_subdirectory()가 호출되어 새로운 CMakeLists.txt가 수행될 때 업데이트되며, 종료될 때 다시 이전 경로로 복구됩니다.

간단한 MyApp 프로젝트를 통해 위 변수들의 값을 살펴보겠습니다.

각 디렉토리의 CMakeLists.txt의 내용은 다음과 같습니다.

- MyApp/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyApp)

message("top:	CMAKE_SOURCE_DIR		= ${CMAKE_SOURCE_DIR}")
message("top:	CMAKE_BINARY_DIR		= ${CMAKE_BINARY_DIR}")
message("top:	CMAKE_CURRENT_SOURCE_DIR	= ${CMAKE_CURRENT_SOURCE_DIR}")
message("top:	CMAKE_CURRENT_BINARY_DIR	= ${CMAKE_CURRENT_BINARY_DIR}")

add_subdirectory(mysub)

message("top:	CMAKE_CURRENT_SOURCE_DIR	= ${CMAKE_CURRENT_SOURCE_DIR}")
message("top:	CMAKE_CURRENT_BINARY_DIR	= ${CMAKE_CURRENT_BINARY_DIR}")

- MyApp/mysub/CMakeLists.txt

message("mysub:	CMAKE_SOURCE_DIR		= ${CMAKE_SOURCE_DIR}")
message("mysub:	CMAKE_BINARY_DIR		= ${CMAKE_BINARY_DIR}")
message("mysub:	CMAKE_CURRENT_SOURCE_DIR	= ${CMAKE_CURRENT_SOURCE_DIR}")
message("mysub:	CMAKE_CURRENT_BINARY_DIR	= ${CMAKE_CURRENT_BINARY_DIR}")

 

기본 빌드 디렉토리는 MyApp/build로 설정하여 CMake를 수행한 결과는 다음과 같습니다.

(build 디렉토리를 생성하여 'cmake ..' 커맨드로 실행해도 되고, 프로젝트 최상단 위치에서 'cmake . -Bbuild' 커맨드를 사용해도 됩니다.)


Variable Scope

2021.10.29 - [CMake] - [CMake] Variable (변수)

위 글에서 변수에 대해 알아볼 때, 변수 범위(scope)에 대해서 짧게 언급하고 넘어갔는데, add_subdirectory() 명령어를 사용하게 되면 CMake는 현재 CMakeLists.txt를 처리하기 위한 새로운 scope를 생성합니다.

이 새로운 scope는 add_subdirectory()를 호출한 scope(부모 scope)자식 scope입니다. 즉, 위의 MyApp 예시에서 MyApp이 부모 scope가 되고, MyApp/mysub가 자식 scope가 되는 것입니다.

새로운 scope는 다음과 같은 규칙을 가지고 있습니다.

  1. 부모 scope에서 정의된 모든 변수는 자식 scope에서 볼 수 있으며, 다른 변수들과 마찬가지로 해당 값을 읽을 수 있습니다.
  2. 자식 scope에서 생성된 모든 변수들은 부모 scope에서 볼 수 없습니다.
  3. 자식 scope에서 발생한 모든 변수들의 변경사항은 해당 자식 scope에서만 유효합니다. 즉, 변경된 변수가 부모 scope에서 생성되어 자식 scope로 전달된 변수라면 자식 scope에서 변경되었더라도 부모 scope에는 영향을 끼치지 않습니다. 부모 scope에서 정의된 변수가 자식 scope에서 변경되었다면, 수정된 변수는 자식 scope를 벗어날 때 삭제되는 새로운 변수처럼 동작합니다.

정리하자면, 자식 scope에 진입하게 되면 해당 시점에서 부모 scope의 정의된 모든 변수의 복사본을 전달받게 됩니다. 자식 scope의 변수에 대한 모든 변경 사항들은 자식 scope의 복사본에서 수행되고, 부모 scope의 변수는 변경되지 않은 상태로 유지됩니다.

 

MyApp2 프로젝트를 통해 변수의 범위에 대해 살펴보도록 하겠습니다.

MyApp2 의 directory tree

- MyApp2/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyApp2)

set(myVar foo)
message("Parent (before): myVar	= ${myVar}")
message("Parent (before): childVar	= ${childVar}")

add_subdirectory(subdir)

message("Parent (after): myVar	= ${myVar}")
message("Parent (after): childVar	= ${childVar}")

- MyApp2/mysub/CMakeLists.txt

message("Child (before): myVar	= ${myVar}")
message("Child (before): childVar	= ${childVar}")

set(myVar bar)
set(childVar fuzz)

message("Child (after): myVar		= ${myVar}")
message("Child (after): childVar	= ${childVar}")

 

MyApp2/build를 빌드 디렉토리로 설정하고 CMake를 수행한 결과입니다.

  • line 1 : myVar 변수는 부모 scope에서 정의된 변수입니다.
  • line 2 : childVar 변수는 부모 scope에서 정의되지 않았습니다. 따라서 빈 문자열을 출력합니다.
  • line 3 : 자식 scope는 부모 scope의 변수들의 복사본을 전달받으므로 부모 scope에서 정의된 myVar의 값을 출력합니다.
  • line 4 : childVar 변수는 여전히 정의되지 않은 상태이므로 빈 문자열을 출력합니다.
  • line 5 : myVar 변수가 자식 scope에서 'bar'로 변경되었습니다.
  • line 6 : childVar가 자식 scope에서 새롭게 'fuzz'라는 값으로 정의되었습니다.
  • line 7 : 부모 scope에서 정의된 값 그대로 출력합니다. 자식 scope에서 이 값이 변경되었지만, 부모 scope에 영향을 미치지 않습니다.
  • line 8 : 마찬가지로 부모 scope에서 정의되지 않은 childVar(빈 문자열)을 출력합니다. 자식 scope에서 정의된 새로운 변수는 부모 scope에 전달되지 않습니다.

 

위에서 설명한 변수 범위(scope)에 관한 동작은 add_subdirectory()로 추가된 디렉토리가 부모 scope의 변수에 영향을 주지 않고 원하는 모든 변수를 변경할 수 있다는 것을 보여줍니다. 즉, 잠재적으로 의도치 않은 변경으로 부모 scope에 영향을 끼칠 수 있는 모든 가능성을 차단해줍니다.

 

부모 scope로 변수 전달 방법

하지만, 자식 scope에서 변경되거나 추가된 변수가 부모 scope에서 필요한 경우가 있습니다. 예를 들어, 서브 디렉토리의 소스 파일 목록을 생성하고 상위 디렉토리에 다시 전달하는 경우가 있습니다.

이는 set() 명령에서 PARENT_SCOPE 라는 키워드를 추가하여 부모 scope로 변수의 변경 사항을 전달할 수 있습니다. 중요한 것은 부모와 자식 scope 둘 다 변수를 설정한다는 의미는 아닌데, MyApp3 프로젝트 예시를 통해 살펴보도록 하겠습니다.

 

MyApp3 프로젝트 디렉토리 트리

- MyApp3/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyApp3)

set(myVar foo)
message("Parent (before):  myVar	= ${myVar}")

add_subdirectory(mysub)
message("Parent (after):   myVar	= ${myVar}")

- MyApp3/mysub/CMakeLists.txt

message("child (before):   myVar	= ${myVar}")
set(myVar bar PARENT_SCOPE)
message("child (after):    myVar	= ${myVar}")

CMake를 실행한 결과는 다음과 같습니다.

주목해야 할 점은 mysub 디렉토리, 즉 자식 scope에서 myVar 변수를 PARENT_SCOPE 키워드를 통해 수정을 했는데, 자식 scope의 myVar는 변경되지 않았다는 점입니다.

이는 PARENT_SCOPE 키워드를 사용하면, 현재 지역 변수(자식 scope에서의 변수)가 변경되는 것을 막아주고, 부모 scope의 변수가 수정됩니다.

 

따라서, 자식 scope에서 부모 scope의 이름을 재사용하는 것은 실수나 오해의 소지가 많기 때문에 사소하지만 아래처럼 사용하는 경우가 많습니다.

 

변수뿐만 아니라, policy나 property들도 범위가 존재합니다.

예를 들어, policy의 경우 add_subdirectory() 로 서브 디렉토리가 생성되면, 부모 scope에 영향을 주지 않고 policy를 적용할 수 있는 새로운 scope를 생성합니다. 마찬가지로 상위 디렉토리 property에 영향을 미치지 않는 하위 디렉토리의 CMakeLists.txt에 설정할 수 있는 디렉토리 property가 있습니다. policy와 property는 다음에 더 자세히 다루어보겠습니다.

 


include()

CMake에서 다른 디렉토리의 내용을 가져오기 위해 또 다른 명령어가 있는데, 바로 include() 명령입니다.

위의 두 가지 방법으로 사용이 가능합니다.

첫 번째 형식은 add_subdirectory()와 유사하지만, 다음의 몇 가지 차이점이 존재합니다.

  1. include() 명령은 파일이 argument로 주어지지만, add_subdirectory()는 디렉토리가 argument로 주어지고, 해당 디렉토리 내에서 CMakeLists.txt 파일을 찾습니다. include()에 전달된 파일 이름은 일반적으로 .cmake 확장자를 갖지만 아무거나 사용해도 무관합니다.
  2. include()는 새로운 변수 scope를 생성하지 않습니다.
  3. include()와 add_subdirectory()는 기본적으로 새로운 policy scope를 정의하지만, include() 명령에서 NO_POLICY_SCOPE를 사용하면 새로운 policy scope를 정의하지 않도록 할 수 있습니다.
  4. CMAKE_CURRENT_SOURCE_DIR 및 CMAKE_CURRENT_BINARY_DIR 변수의 값은 include()로 명명된 파일을 처리할 때에는 변경되지 않습니다.

 

두 번째 형식은 완전히 다른 목적으로 사용되는데, 명명된 모듈을 로드하는데 사용됩니다.

첫 번째 형식에서 4가지 차이점 중에 1번을 제외한 나머지 모든 사항이 두 번째 형식에도 적용됩니다.

모듈에 관해서는 추후에 다루도록 하겠습니다 !

 

 

include() 명령을 사용하게 되면 CMAKE_CURRENT_SOURCE_DIR 값이 변경되지 않기 때문에 include()가 호출될 때 포함된 파일이 포함된 디렉토리를 알아내기가 어려울 수 있습니다. 또한 fileName이 항상 CMakeLists.txt인 sub_directory()와 달리 include()는 파일 이름이 어떤 것이든 될 수 있으므로 포함된 파일이 자체 이름을 결정하기 어려울 수 있습니다.

이러한 문제를 해결하기 위해서 CMake에서 다음의 변수들을 제공하고 있습니다.

  • CMAKE_CURRENT_LIST_DIR : include()이 호출될 때 업데이트된다는 점을 제외하고는 CMAKE_CURRENT_SOURCE_DIR와 유사합니다. 이 변수는 처리 중인 현재 파일(include()로 호출된)의 디렉토리가 필요할 때 사용될 수 있습니다. 항상 절대 경로입니다.
  • CMAKE_CURRENT_LIST_FILE : 현재 진행중인 파일의 이름입니다. 이 값 또한 절대 경로입니다.
  • CMAKE_CURRENT_LIST_LINE : 현재 처리 중인 파일의 line number 값 입니다. 거의 사용되지는 않지만, 일부 디버깅에서 사용될 수 있습니다.

위 세 변수는 include() 명령으로 실행하는 파일뿐만 아니라 CMake에서 처리 중인 모든 파일에 대해 동작합니다. add_subdirectory() 명령으로 실행하는 CMakeLists.txt 파일에 대해서도 위 세 변수는 설명한 것과 동일한 값을 가지며, 이 경우에는 CMAKE_CURRENT_LIST_DIR는 CMAKE_CURRENT_SOURCE_DIR와 동일한 값을 갖습니다.

아래 MyApp4 예제를 살펴봅시다.

- MyApp4/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyApp4)

add_subdirectory(mysub)
message("====")
include(mysub/CMakeLists.txt)

- MyApp4/mysub/CMakeLists.txt

message("CMAKE_CURRENT_SOURCE_DIR	= ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR	= ${CMAKE_CURRENT_BINARY_DIR}")
message("CMAKE_CURRENT_LIST_DIR	= ${CMAKE_CURRENT_LIST_DIR}")
message("CMAKE_CURRENT_LIST_FILE	= ${CMAKE_CURRENT_LIST_FILE}")
message("CMAKE_CURRENT_LIST_LINE	= ${CMAKE_CURRENT_LIST_LINE}")

CMake를 실행하면 다음의 결과를 얻을 수 있습니다.


return() / include_guard()

현재 진행 중인 파일의 나머지 부분을 중지하고 상위 디렉토리로 다시 반환하는 상황이 있을 수 있습니다.

이 경우에 return() 명령을 사용할 수 있는데, 다른 프로그래밍 언어처럼 어떠한 값을 반환할 수는 없습니다. 이 명령의 유일한 효과는 현재 범위의 프로세스를 종료하는 것이며, 함수 내부가 아닌 include(), find_package() 또는 add_subdirectory()를 통해 호출된 파일의 프로세스를 종료합니다. (함수 내에서 return()은 함수 부분에서 다시 언급하겠습니다.)

 

또한, 매우 큰 프로젝트에서 동일한 파일을 여러 위치에서 include() 명령으로 호출하는 경우가 발생할 수 있습니다. 따라서, 이것을 확인하고 파일을 CMake 처리 동안 한 번만 호출되도록 방지하는 로직이 필요합니다. 이는 C/C++ 헤더 파일을 여러번 include하지 않도록 방지하는 것과 유사하며, 아래의 방법을 사용하거나 include_guard()를 사용할 수 있습니다.

CMake 3.10 이상 버전에서는 C/C++에서 '#pragma once'와 같은 기능을 하는 include_guard()를 사용하여 보다 간결하게 사용할 수 있습니다. (include_guard()는 위의 if-endif가 들어갈 자리에 추가하면 됩니다.)

if-endif 코드를 매뉴얼로 작성하는 것과 비교했을 때 매우 간결하게 사용될 수 있습니다. 또한 이 명령은 이전에 처리된 파일을 검사할 때 그 범위를 지정하도록 DIRECTORY 와 GLOBAL 키워드를 제공합니다. DIRECTORY는 현재 디렉토리 범위 이내에서만 include된 파일이 이전에 처리되었는지 확인하며, GLOBAL은 프로젝트의 다른 곳에서 먼저 처리된 경우에 현재 파일의 처리를 바로 종료하도록 합니다.

 

'CMake' 카테고리의 다른 글

[CMake] Properties  (0) 2021.11.02
[CMake] Functions and Macros  (0) 2021.11.01
[CMake] Looping - foreach, while  (0) 2021.10.31
[CMake] Flow Control - if  (0) 2021.10.30
[CMake] Lists  (0) 2021.10.29

댓글