본문 바로가기
CMake

[CMake] Flow Control - if

by 별준 2021. 10. 30.

References

  • Professional CMake : A Practical Guide

Contents

  • if
  • foreach
  • while

CMake를 사용할 때, 특정 컴파일러를 사용하거나 특정 플랫폼용으로 빌드할 때만 특정 컴파일러 플래그를 사용하기를 원할 수 있습니다. 또는, 특정 조건이 충족될 때까지 어떠한 단계를 반복해서 진행해야 할 수도 있습니다.

이러한 흐름 제어(flow control)은 다른 프로그래밍 언어처럼 CMake에서도 if / foreach / while 등으로 지원하고 있습니다. 

CMake에서 이 3가지 명령은 일반적으로 대부분의 프로그래밍 언어에서의 동작과 동일한 기능을 제공합니다.

 


if() command

if() 명령의 사용법은 다음과 같습니다. 다른 언어와 마찬가지로 여러 절의 elseif()가 추가될 수 있습니다.

(CMake 초기 버전에는 else() 및 elseif() 의 argument로 expression1을 반복해서 추가해주어야 했지만, CMake 2.8.0 이후로는 argument를 비워두어야 합니다.)

 

그리고 if()와 elseif() 명령의 표현식(ex, expression1, expression2)은 다양한 형식을 취할 수 있습니다. CMake에서는 일반적인 boolean 로직뿐만 아니라 file system tests, version comparison, 그리고 어떤 것이 존재하는 지 테스트하는 등의 다양한 조건들을 지원합니다.


Basic Expressions

가장 기본적인 표현식은 하나의 상수를 사용하는 것입니다.

true / false를 판단하는 CMake의 로직은 다른 대부분의 프로그래밍 언어보다 조금 더 복잡한 편입니다.

따옴표(")로 닫히지 않은 하나의 상수(a sing unquoted constant)의 경우 규칙은 다음과 같습니다.

  • value의 값이 1, ON, YES, TRUE, Y, 또는 0이 아닌 숫자인 경우에 이는 true로 간주됩니다.
  • value의 값이 0, OFF, NO, FALSE, N, IGNORE, NOTFOUND, 빈 문자열("") 또는 문자열의 끝이 -NOTFOUND로 끝나는 문자열인 경우에 이는 false로 간주됩니다.
  • 만약 위 두 경우 모두 포함되지 않는 경우에 이는 변수 이름(또는 문자열)로 처리되고 아래처럼 처리됩니다.
# Examples of unquoted constants
if(YES)
if(0)
if(TRUE)

# These are also treated as unquoted constants because the
# variable evaluation occurs before if() sees the values
set(A YES)
set(B 0)
if(${A}) # Evaluates to true
if(${B}) # Evaluates to false

# Does not match any of the true or false constants, so proceed
# to testing as a variable name in the fall through case below
if(someLetters)

# Quoted value, so bypass the true/false constant matching
# and fall through to testing as a variable name or string
if("someLetters")

(위 예시는 endif()가 생략되어 있습니다.)

 

 

상수가 아닌 경우에는 다음의 형식으로 사용될 수 있습니다.

따옴표로 묶이지 않은 변수나 따옴표로 묶인 문자열이 될 수 있는데, 변수 이름이 사용된 경우에는 그 변수가 정의되어 있고, 변수의 값이 false constants가 아니라면 그 결과는 true로 간주됩니다. 만약 변수가 정의되어 있지 않은 상태라면 false constant 중의 하나인 빈 문자열("")에 해당되므로 그 결과는 false로 간주됩니다.

# Common pattern, often used with variables defined
# by commands such as option(enableSomething "...")
if(enableSomething)
# ...
endif()

 

만약 if-expression이 따옴표로 묶인 문자열이라면 동작은 조금 더 복잡합니다.

따옴표로 묶인 문자열이라면 CMake 3.1 이상에서는 항상 결과는 false로 간주됩니다. (하지만 이는 policy setting으로 재정의 할 수 있습니다.)

CMake 3.1 이전 버전에서는 문자열의 값이 기존 변수의 이름과 일치하면 따옴표로 묶인 문자열의 값이 해당 변수 이름으로 대체됩니다.

 

cmake_minimum_required(VERSION 3.10)
project(flow_control)

set(exp test)

if("exp")
  message("true")
else()
  message("false")
endif()

cmake_policy(VERSION 2.8.0)
if("exp")
  message("true")
else()
  message("false")
endif()

첫 번째 if문은 false, CMake 버전 3.1 이하로 설정했을 때에는 동일한 if문이 true로 처리되는 것을 확인할 수 있습니다.

 

자세한 내용은 CMP0054를 참조하시길 바랍니다 !


Logic Operators

CMake는 AND, OR, and NOT 논리 연산자(logical operators)와 괄호(우선순위를 위해) 지원합니다.

규칙에 따라서 가장 안쪽 괄호부터 시작하여 괄호 안의 표현식이 먼저 평가됩니다.

 


Comparison Tests

CMake는 3가지 카테고리(숫자, 문자열, 버전)로 비교 테스트(comparison test)가 나누어지는데, 모두 아래의 동일한 패턴으로 사용됩니다.

두 개의 피연산자 value1과 value2는 변수이름이거나 (따옴표로 묶인)값이 될 수 있습니다. 값이 정의된 변수의 이름이라면 변수로 취급되고, 그렇지 않다면 문자열이나 값으로 직접 처리됩니다.

따옴표로 묶인 값은 위에서 본 것처럼(CMP0054), CMake 3.1 이전에는 문자열이 존재하는 변수의 이름과 동일하면 해당 문자열이 변수의 값으로 대체되고, CMake 3.1 이상에서는 대체없이 값이 그대로 사용됩니다.

 

3가지 카테고리에 따라서 OPERATOR 키워드가 따로 제공되는데 이름만 다를 뿐 동작은 동일합니다.

 

숫자 비교의 경우에는 비교되는 값들이 숫자인 경우에는 예상대로 동작합니다.

cmake_minimum_required(VERSION 3.10)
project(flow_control)

# Valid Numeric expressions, all evaluating as true
if(2 GREATER 1)
  message("2 GREATER 1 = true")
endif()

if("23" EQUAL 23)
  message("\"23\" EQUAL 23 = true")
endif()

set(val 42)
if(${val} EQUAL 42)
  message("\${val} EQUAL 42 = true")
endif()

if("${val}" EQUAL 42)
  message("\"\${val}\" EQUAL 42 = true")
endif()

# Invalid expression that evaluates as true with at
# least some CMake versions. Do not rely on this behavior.
if("23a" EQUAL 23)
  message("\"23a\" EQUAL 23 = true")
else()
  message("\"23a\" EQUAL 23 = false")
endif()

마지막 예시는 CMake Version마다 다를 수 있는데, 저의 경우에는 3.16.3 버전인데 true로 간주하고 있습니다.

 

버전 비교는 숫자 비교와 유사합니다. 버전의 형태는 major[.minor[.patch[.tweak]]] 로 간주되는데, 여기서 각 구성 요소(major, minor, ...)들은 음이 아닌 정수이어야 합니다. 두 버전 번호를 비교할 때는 major를 먼저 비교하고, major가 동일한 경우에만 minor가 존재할 때 minor를 비교합니다. 누락된 요소는 0으로 처리됩니다.

다음의 예시는 모두 true로 간주됩니다.

 

문자열인 경우에는 사전순으로 두 값이 비교됩니다. 문자열의 내용에 대한 가정은 없지만 앞에서 언급한 변수/문자열이 대체되는 상황의 가능성은 생각하고 있어야 합니다. 문자열 비교는 예기치 못한 상황이 발생하는 가장 일반적인 문법 중의 하나입니다.

 

정규표현식으로 문자열을 비교할 수도 있습니다.

value는 위에서 언급한 CMake의 변수와 문자열 규칙을 따르고, 정규표현식과 비교됩니다. 만약 값이 일치하면 표현식은 true로 간주됩니다. 기본적으로 CMake는 기본적인 정규표현식만 지원합니다.

괄호를 사용하여 일치하는 값의 일부를 취할 수 있는데, 이 명령은 CMAKE_MATCH_<n> 형식의 이름으로 변수를 설정합니다. 여기서 <n>은 일치시킬 그룹을 의미합니다.

cmake_minimum_required(VERSION 3.10)
project(flow_control)

set(who Barney)
if("Hi from ${who}" MATCHES "Hi from (Fred|Barney).*")
	message("${CMAKE_MATCH_1} says hello")
endif()


File System Tests

CMake에는 파일 시스템을 쿼리하는데 사용할 수 있는 명령도 존재합니다. (저는 꽤 자주 사용하는 것 같습니다.)

위 명령어를 보면 바로 어떤 동작을 의미하는지 잘 아시리라 생각됩니다.

EXISTS는 해당 파일 또는 디렉토리가 존재하는지 체크하는 것이고,

IS_DIRECTORY는 해당 path가 디렉토리(즉, 파일이 아닌지)인지 체크하고,

IS_SYMLINK는 해당 path가 심볼릭 링크인지 체크하고,

IS_ABSOLUTE는 해당 path가 절대 경로인지 체크하고,

IS_NEWER_THAN은 file1이 file2보다 더 최신인지 확인합니다.

 

몇 가지 주의해야할 사항이 있는데, IS_NEWER_THAN 연산자는 파일 중 하나가 누락되거나 두 파일의 timestamp(즉, 같은 파일)인 경우에 true로 판단합니다. 따라서 실제 IS_NEWER_THAN을 수행하기 전에 file1과 file2의 존재 여부를 먼저 체크하는 것이 좋습니다. 또한 IS_NEWER_THAN을 사용할 때에 전체 경로로 지정해야 합니다.


Existence Tests

마지막으로 살펴볼 if() 명령의 유형은 다양한 CMake entities의 존재 여부 체크입니다.

위의 각 명령들은 만약 특정 name의 entity가 현재 명령어가 수행된 시점에 존재한다면 true를 반환하게 됩니다.

 

  • DEFINED

지정된 이름의 변수가 있다면 true를 반환합니다. 변수의 값은 관련이 없고 존재 여부만 테스트됩니다. 특정 환경 변수가 정의되어 있는지 확인한데도 사용할 수 있으며, CMake 3.14 버전부터는 캐시 변수도 체크할 수 있습니다.

  • COMMAND

지정된 이름을 가진 CMake 명령어, 함수 또는 매크로가 존재하는지 테스트합니다. 이는 커맨드를 사용하기 전에 정의되어 있는지 여부를 확인하는데 유용하게 사용될 수 있습니다. CMake에서 제공되는 명령어의 경우에는 CMake 버전을 체크하는 것이 더 좋지만, 프로젝트에서 제공되는 함수 및 매크로의 경우에는 이 명령어를 통해 존재 여부를 확인할 수 있습니다.

 

  • POLICY

특정 policy(정책)이 CMake에 알려져 있는지(적용되어 있는지) 체크합니다. policy는 일반적으로 'CMPXXXX'의 형식이며 여기서 XXXX는 항상 4자리 숫자입니다. policy에 대해서는 다른 글에서 더 살펴보도록 하겠습니다.

 

  • TARGET

add_excutable(), add_library(), add_custom_target() 명령어를 통해서 정의된 name의 Target(타겟)이 존재하는 경우 true를 반환합니다. 이 명령어는 다른 외부 프로젝트를 가져오고 해당 프로젝트가 종속성을 갖는 다른 하위 프로젝트를 공유하는 복잡한 계층 구조에서 유용하게 사용될 수 있습니다.(해당 Target을 생성하기 전에 Target이 이미 정의되어 있는지 확인할 수 있습니다.)

 

  • TEST

지정된 name의 CMake test가 이전에 add_test() 명령으로 정의된 경우에 true를 반환합니다.

 

CMake 3.5 에서는 리스트에서 어느 값이 존재하는지 확인하는 명령어가 추가되었습니다.

이 표현식은 listVar 변수에 value가 포함되어 있으면 true를 반환합니다. 여기서 value는 일반적인 변수나 문자열이지만, listVat는 리스트 변수이어야 합니다.


자주 사용되는 if 문법

마지막으로 정말 자주 사용되는 몇 가지 if 사용법에 대해서 알아보고 마무리하겠습니다.

대부분의 경우, 미리 정의된 CMake 변수(컴파일러 및 타겟 플랫폼과 관련된) 의존하는 경우입니다.

예를 들어, 두 개의 C++ 소스 파일이 있는 프로젝트가 있다고 가정해보겠습니다. 하나는 visual studio(window) 컴파일러를 위한 소스 코드이고 다른 하나는 다른 모든 컴파일러로 빌드하기 위한 소스코드입니다.

이 경우에는 다음과 같이 구현될 수 있습니다.

사실 위 경우, 대부분의 프로젝트에서 동작할 가능성이 높지만 실제로 올바른 방법은 아닙니다.

만약 Window에서 빌드되었지만, visual studio가 아닌 MinGW 컴파일러를 사용하는 경우가 고려되지 않았습니다.

이 경우에는 window 시스템이지만 source_generic.cpp가 더 적절한 소스코드가 될 수 있습니다.

따라서 다음과 같이 더 정확하게 구현해야 합니다.

WIN32나 MSVC는 CMake가 실행되면서 설정되는 변수들입니다. 

cmake 커맨드를 입력할 때 --trace 옵션을 주면 모든 로그들을 확인할 수 있는데, 이때 해당 변수가 세팅되는 걸 확인할 수 있습니다.

 

또 다른 예로는 CMake generator를 사용한 조건부 동작이 있습니다.

위 경우는 macOS용에서 Xcode generator가 사용될 때를 체크하고 있습니다. 물론 Ninja나 Unix Makefiles를 사용하려는 경우에는 else()로 빠져서 수행될 것입니다.

 

조금 더 복잡한 프로젝트에서는 아래의 패턴을 사용하여 조건부로 하위 디렉토리를 포함시키거나 CMake option이나 캐시 변수를 사용하여 다양하게 다른 작업들을 수행할 수 있도록 합니다.

이런 경우에는 개발자가 CMakeLists.txt 파일을 직접 수정하지 않고도 해당 옵션을 켜거나 끌 수 있습니다. 이는 빌드의 특정 부분을 활성화하거나 비활성화하는데 유용하게 사용될 수 있습니다.

'CMake' 카테고리의 다른 글

[CMake] add_subdirectory() 와 변수 Scope  (0) 2021.10.31
[CMake] Looping - foreach, while  (0) 2021.10.31
[CMake] Lists  (0) 2021.10.29
[CMake] String Handling (string 명령어)  (0) 2021.10.29
[CMake] Variable (변수)  (0) 2021.10.29

댓글