CMake相關代碼片斷

這列是我平常寫CMake總結的一些代碼片斷,不保證如今寫的全對,可是會不斷更新,包擴增長新內容和修改舊有的錯誤內容。react

last update: 2019-7-20 21:36:01linux

用於執行CMake的.bat腳本

使用.bat腳本調用cmake,能夠指定比較複雜的cmake.exe命令的參數。android

e.g. 項目根目錄/build/vs2017-x64.bat,內容:git

@echo off

:: build directory
:: it should be similar name with cmake generator name
set BUILD_DIR=vs2013-x64

:: platform
:: x86 or x64
set BUILD_PLATFORM=x64

:: cl.exe compiler version
set BUILD_COMPILER=vc12

:: create directory if not exist
if not exist %BUILD_DIR% md %BUILD_DIR%

:: go to build directory
cd %BUILD_DIR%

:: run cmake by specifing:
:: - generator
:: - installation directory
:: - CMakeLists.txt location
cmake -G "Visual Studio 12 2013 Win64" ^
    -DCMAKE_INSTALL_PREFIX=D:/lib/glfw/3.3/%BUILD_PLATFORM%/%BUILD_COMPILER% ^
    ../..

:: run build by specifying config and target
:: note: this may fail, and please open .sln and do manual compilation and installation
cmake --build . --config Release --target INSTALL

:: go back to old folder
cd ..

:: stuck to show build messages
pause

CMakeLists.txt和.cmake中的代碼片斷

判斷平臺:32位仍是64位?

##################################################
# platform
##################################################
if(CMAKE_CL_64)
    set(platform x64)
else(CMAKE_CL_64)
    set(platform x86)
endif(CMAKE_CL_64)

message(STATUS "----- platform is: ${platform}")

判斷Visual Studio版本

參考: List of _MSC_VER and _MSC_FULL_VERgithub

##################################################
# visual studio version
#
if(MSVC_VERSION EQUAL 1600)
    set(vs_version vs2010)
    set(vc_version vc10)
elseif(MSVC_VERSION EQUAL 1700)
    set(vs_version vs2012)
    set(vc_version vc11)
elseif(MSVC_VERSION EQUAL 1800)
    set(vs_version vs2013)
    set(vc_version vc12)
elseif(MSVC_VERSION EQUAL 1900)
    set(vs_version vs2015)
    set(vc_version vc14)
elseif(MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS_EQUAL 1920)
    set(vs_version vs2017)
    set(vc_version vc15)
elseif(MSVC_VERSION GREATER_EQUAL 1920)
    set(vs_version vs2019)
    set(vc_version vc15)
endif()

message(STATUS "----- vs_version is: ${vs_version}")
message(STATUS "----- vc_version is: ${vc_version}")

判斷操做系統

注:其中WIN32判斷的是windows系統,包括32位和64位兩種狀況json

if(WIN32)
    message(STATUS "----- This is Windows.")
elseif(UNIX)
    message(STATUS "----- This is UNIX.") #Linux下輸出這個
elseif(APPLE)
    message(STATUS "----- This is APPLE.")
elseif(ANDROID)
    message(STATUS "----- This is ANDROID.")
endif(WIN32)

另外一種寫法:ubuntu

if (CMAKE_SYSTEM_NAME MATCHES "Windows")
    message(STATUS "----- OS: Windows")
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
    message(STATUS "----- OS: Linux")
elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
    message(STATUS "----- OS: MacOS X")
elseif(CMAKE_SYSTEM_NAME MATCHES "Android")
    message(STATUS "----- OS: Android")
endif()

測試發現,若是在CMAKE_MINIMUM_VERSION()後當即使用CMAKE_SYSTEM_NAME,Linux下獲得結果爲空,Android下獲得爲Android。看起來是Android的toolchain中進行了設定。windows

判斷是Debug仍是Release等版本

(1) CMAKE_BUILD_TYPE取值:默認值由編譯器決定,調用cmake時可經過-DCMAKE_BUILD_TYPE=Release的形式指定其值。緩存

看文檔的話,是用CMAKE_BUILD_TYPE判斷Debug/Release模式。然而CMake文檔的描述其實有問題,不清晰。這個變量的值是由編譯器決定的。對於VS2017,默認狀況下爲空。

The default will be "empty" or "Debug" depending on the compiler. The value of the variable will be only of interest in places where SOME_VAR_${CONFIG} is used. So to answer your question. From my understanding the debug flags could be added. The documentation (http://www.cmake.org/cmake/help/v3.3/variable/CMAKE_BUILD_TYPE.html) is unfortunately not so clear

ref: What happens for C/C++ builds if CMAKE_BUILD_TYPE is empty?

(2) 值的判斷
Debug和Release,用MATCHES判斷;
空值""用NOT判斷.

if (CMAKE_BUILD_TYPE MATCHES "Debug" OR CMAKE_BUILD_TYPE EQUAL "None" OR NOT CMAKE_BUILD_TYPE)
    message(STATUS "----- CMAKE_BUILD_TYPE is Debug")
elseif (CMAKE_BUILD_TYPE MATCHES "Release")
    message(STATUS "----- CMAKE_BUILD_TYPE is Release")
elseif (CMAKE_BUILD_TYPE MATCHES "RelWitchDebInfo")
    message(STATUS "----- CMAKE_BUILD_TYPE is RelWitchDebInfo")
elseif (CMAKE_BUILD_TYPE MATCHES "MinSizeRel")
    message(STATUS "----- CMAKE_BUILD_TYPE is MinSizeRel")
else ()
    message(STATUS "----- unknown CMAKE_BUILD_TYPE = " ${CMAKE_BUILD_TYPE})
endif ()

根據Debug/Release添加不一樣的庫目錄

在Visual Studio平臺下測試發現,若是指定了A目錄到庫搜索目錄,而且A目錄下有名爲Debug/Release的目錄,則會自動把A/Debug和A/Release添加到庫搜索目錄。

set(MY_LIB_DIR
"testbed/lib/"
"testbed/lib/ceva/"
)

於是,至少在VS平臺下,不須要手動根據Debug和Release來分別添加庫。

Visual Studio屬性與對應CMake實現方法

CMAKE修改VS大總結

設定編譯選項

也即修改CMAKE_C_FLAGSCMAKE_CXX_FLAGS變量。例如追加兩個選項:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /we4013 /we4431")

SAFESEH報錯

簡單粗暴的解決辦法:

if (CMAKE_SYSTEM_NAME MATCHES "Windows")
    #message("inside windows")
    # add SAFESEH to Visual Studio. copied from http://www.reactos.org/pipermail/ros-diffs/2010-November/039192.html
    #if(${_MACHINE_ARCH_FLAG} MATCHES X86) # fails
    #message("inside that branch")
    
    # in VS2013, there is: fatal error LNK1104: cannot open file "LIBC.lib"
    # so, we have to add /NODEFAULTLIB:LIBC.LIB
    # reference: https://stackoverflow.com/questions/6016649/cannot-open-file-libc-lib
    set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB") 
    set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB")
    set (CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB")
    #endif()
endif (CMAKE_SYSTEM_NAME MATCHES "Windows")

link_directories() 這句話必須在add_executable()以前寫 否則找不到庫目錄

或者先add_executable() 再 target_link_directories(XXX PRIVATE some direcotry)

Debug庫帶「d」後綴

設置debug模式下編譯出的庫文件,相比於release模式下,多帶一個字母"d"做爲後綴。

set_target_properties(TargetName PROPERTIES DEBUG_POSTFIX d)

在cmake中執行目錄建立、拷貝文件等腳本

例如建立完調用lmdb庫的可執行程序這一target後,須要建立目錄。不想每次都手動去建立一個目錄,但願在CMakeLists.txt中添加這個操做。

嘗試了add_custom_command(),不怎麼會用。。沒有效果,不被調用。

execute_process()則是能夠的,例如:

execute_process(COMMAND echo "=====")

現代的CMake

It's Time To Do CMake Right

https://moevis.github.io/cheatsheet/2018/09/12/Modern-CMake-%E7%AC%94%E8%AE%B0.html

https://cliutils.gitlab.io/modern-cmake/modern-cmake.pdf

https://cliutils.gitlab.io/modern-cmake/

目前我主要用這幾個(而不是會影響到全部target的全局設定):

target_compile_definitions(): 目標添加編譯器編譯選項,例如target_compile_definitions(shadow_jni PRIVATE -DUSE_STB -DUSE_ARM)

target_include_directories():目標添加包含文件,例如target_include_directories(shadow_jni PRIVATE "shadow_jni/body_detection" "shadow_jni/util" ${SNPE_INC_DIR})

target_link_directories():目標添加連接庫查找目錄,例如target_link_directories(shadow_jni PRIVATE ${SNPE_LIB_DIR})

target_link_libraries():目標添加連接庫,例如target_link_libraries(shadow_jni ${log-lib} ${graph-lib} ${SNPE_LIB})

看起來不是很好學的樣子。

建立目錄

file(MAKE_DIRECTORY ${SO_OUTPUT_PATH})

ref: Creating a directory in CMake

拷貝文件

包括兩種類型:
(1)和某個target綁定的文件拷貝,使用add_custom_command()

add_custom_command(TARGET your_target
        PRE_BUILD

        COMMAND ${CMAKE_COMMAND} -E copy
        ${MY_SO_NAME}
        ${SO_OUTPUT_PATH}/
)

(2)和target無關的,或者說對於全部target而言都須要作文件拷貝,用execute_process()

foreach(lib_name_pth ${LIBS_TO_COPY})
    message(STATUS "--- ${lib_name_pth}")
    execute_process(COMMAND ${CMAKE_COMMAND} -E copy ${lib_name_pth} ${SO_OUTPUT_PATH})
endforeach()

轉換相對路徑爲絕對路徑

get_filename_component(SO_OUTPUT_PATH_ABS
        ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${ANDROID_ABI}
        ABSOLUTE)

ref: CMake: Convert relative path to absolute path, with build directory as current directory

循環處理列表

cmake中的列表也是字符串,不過,經過list(APPEND)獲得的列表字符串,能夠用foreach來遍歷其中每一個字符串。舉例:

foreach(loop_var arg1 arg2 arg3)
    message(STATUS "--- ${loop_var}")
endforeach(loop_var)

foreach(loop_var ${SNPE_LIB_ALL})
    message(STATUS "--- ${loop_var}")
endforeach(loop_var)

設置C/C++編譯器

例如在Linux須要切換gcc/g++版本,ubuntu16.04默認是gcc/g++ 5.4,SNPE須要gcc/g++ 4.9。
經過設定CMAKE_C_COMPILERCMAKE_CXX_COMPILER來作到。
注意:project(<ProjName>)命令必須在設定編譯器以後出現,不然編譯器的設定不起做用,將使用系統默認編譯器。

if (UNIX)
    message(STATUS "----- This is Linux.")
    set(CMAKE_C_COMPILER "gcc-4.9")
    set(CMAKE_CXX_COMPILER "g++-4.9")
endif()

project(gamma)

查看並修改Visual Studio項目屬性中的某個設定

問題來自StackOverFlow上某網友的提問:Compile error CMAKE with CUDA on Visual Studio C++

解決步驟:

  1. 用cmake-gui.exe或ccmake加載cmake的cache文件
  2. 查找須要修改的字符串對應的CMake變量
  3. 在CMakeLists.txt中修改、覆蓋此變量

設置fPIE

error: Android 5.0 and later only support position-independent executables (-fPIE).

問題出如今:鏈接一個靜態庫到一個可執行程序,並在android6.0上運行。

解決辦法:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE -pie")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fPIE -pie")

Linux gcc添加連接庫"-lm"

target_link_libraries(xxx m)

清空普通變量

unset(<Var>)

清除緩存變量

unset(<Var> CACHE)

FindXXX.cmake簡單例子

我遇到的使用場景知足的條件:

  • 使用了不少依賴項;
  • 每一個依賴項僅僅提供了.a/.lib/.so庫文件和.h/.hpp頭文件,沒有提供XXX-config.cmake腳本;
  • 每一個依賴項的庫文件包括debug和release兩種

此時若是繼續在CMakeLists.txt中「一把梭」,各類設定都寫在單個文件中,能夠執行就不夠高了。每一個依賴寫成FindXXX.cmake,則後續直接使用find_package(XXX)很方便,定位排查依賴項問題、可移植性都獲得了加強。

我理解的FindXXX.cmake基本步驟

步驟1:FindXXX.cmake第一行:include(FindPackageHandleStandardArgs)

步驟2:千方百計設定<PackageName>_INCLUDE_DIRS<PackageName>_LIBRARIES的值,而且避免硬編碼。具體又能夠包括:

  • 使用set()list(APPEND <Var>)來設定變量的值
  • 使用find_library()來找庫文件和頭文件(比直接寫爲固定值要靈活)

步驟3:find_package_handle_standard_args(<PackageName> DEFAULT_MSG <PackageName>_INCLUDE_DIRS <PackageName>_LIBRARIES)

步驟4:根據是否查找成功進行打印

例子1:單個頭文件和單個庫文件

這裏假設個人package叫作milk,對應的目錄結構爲:

--- milk
    --- include
         --- milk.h
    --- lib
         --- milk.lib

Findmilk.cmake

# provides `milk_LIBRARIES` and `milk_INCLUDE_DIRS` variable
# usage: `find_package(milk)`

include(FindPackageHandleStandardArgs)

set(milk_ROOT_DIR ${PROJECT_SOURCE_DIR}/third_party/milk CACHE PATH "Folder contains milk")

set(milk_DIR ${milk_ROOT_DIR})

find_path(milk_INCLUDE_DIRS
          NAMES milk.h
          PATHS ${milk_DIR}
          PATH_SUFFIXES include include/x86_64 include/x64
          DOC "milk include"
          NO_DEFAULT_PATH)

# find milk.lib
find_library(milk_LIBRARIES
             NAMES milk
             PATHS ${milk_DIR}
             PATH_SUFFIXES lib lib64 lib/x86_64 lib/x86_64-linux-gnu lib/x64 lib/x86
             DOC "milk library"
             NO_DEFAULT_PATH)

find_package_handle_standard_args(milk DEFAULT_MSG milk_INCLUDE_DIRS milk_LIBRARIES)

if (milk_FOUND)
  if (NOT milk_FIND_QUIETLY)
    message(STATUS "Found milk: ${milk_INCLUDE_DIRS}, ${milk_LIBRARIES}")
  endif ()
  mark_as_advanced(milk_ROOT_DIR milk_INCLUDE_DIRS milk_LIBRARIES)
else ()
  if (milk_FIND_REQUIRED)
    message(FATAL_ERROR "Could not find milk")
  endif ()
endif ()

能夠看到,這種case的寫法很簡單直觀。

例子2:同時存在Debug和Release版本的庫

這個例子中假設依賴項叫作LEMON,目錄結構:

lemon
   --- lib
       --- debug
            --- lemon_core.lib
            --- lemon_extra.lib
       --- release
            --- lemon_core.lib
            --- lemon_extra.lib

debug和release庫的處理
但願在調用find_package(xxx)以後,Visual Studio或XCode等IDE能自動切換debug和release的庫。則須要爲debug庫的路徑添加debug字段,爲release庫添加optimized字段。
e.g.

set(LEMON_LIBRARIES
    debug "${LEMON_DIR}/lib/debug/lemon.lib"
    optimized "${LEMON_DIR}/lib/release/lemon.lib"
  )

考慮到硬編碼不是一個好的方案,庫文件可能放在liblib64lib/Release等目錄中,應當先用find_library()進行查找,而後再set庫文件變量LEMON_LIBRARIES

多個庫的find_library寫法
對於依賴庫中的多個庫,天然的想法是使用foreach()來處理每一個庫文件。

考慮到find_library(lemon_lib_name)會產生緩存變量lemon_lib_name,這會致使再次調用find_library(lemon_lib_name)時再也不查找。須要unset(${lemon_lib_name} CACHE)該緩存變量來確保查找成功。直接給出完整例子:

include(FindPackageHandleStandardArgs)

set(LEMON_ROOT_DIR ${PROJECT_SOURCE_DIR}/third_party/lemon CACHE PATH "Folder contains lemon")

set(LEMON_DIR ${CEVA_ROOT_DIR})

set(LEMON_DIR ${LEMON_ROOT_DIR})

set(LEMON_LIBRARY_COMPONENTS lemon_core lemon_extra)

foreach(lemon_component ${LEMON_LIBRARY_COMPONENTS})
  unset(LEMON_LIBRARIES_DEBUG CACHE)
  find_library(LEMON_LIBRARIES_DEBUG
            NAMES ${lemon_component}
            PATHS ${LEMON_DIR}
            PATH_SUFFIXES lib lib/debug lib/debug
            DOC "lemon library component ${lemon_component} debug"
            NO_DEFAULT_PATH)

  unset(LEMON_LIBRARIES_RELEASE CACHE)
  find_library(LEMON_LIBRARIES_RELEASE
              NAMES ${lemon_component}
              PATHS ${LEMON_DIR}
              PATH_SUFFIXES lib lib/release lib/Release
              DOC "lemon library component ${lemon_component} release"
              NO_DEFAULT_PATH)

  list(APPEND LEMON_LIBRARIES
    debug ${LEMON_LIBRARIES_DEBUG}
    optimized ${LEMON_LIBRARIES_RELEASE}
  )
endforeach()

find_package_handle_standard_args(LEMON DEFAULT_MSG LEMON_LIBRARIES)

if (LEMON_FOUND)
  if (NOT LEMON_FIND_QUIETLY)
    message(STATUS "Found LEMON: ${LEMON_LIBRARIES}")
  endif ()
  mark_as_advanced(LEMON_ROOT_DIR LEMON_LIBRARIES)
else ()
  if (LEMON_FIND_REQUIRED)
    message(FATAL_ERROR "Could not find lemon")
  endif ()
endif ()

例子3:找dll

注意:CMAKE_FIND_LIBRARY_SUFFIXES的使用

set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll")

ref: https://stackoverflow.com/questions/14243524/cmake-find-library-matching-behavior

CMake各類編譯連接參數的默認值

CMake 默認編譯、連接選項

連接器相關問題

檢查連接到的重名函數

場景:A庫的代碼中定義了函數play(),B庫的代碼中也定義了函數play(),可是這兩個play()函數的實現不一樣,而且被可執行目標C同時連接。
連接器默認是找到一個符號就再也不查找,所以默認能連接而且能夠運行,只不過運行結果不是所期待的。

容易查到,Linux下gcc對應的連接器中可使用--whole-archive--no-whole-archive參數來包含靜態庫中的全部符號。

若是是gcc,則使用gcc -Wl --whole-archive someLib --no-whole-archive

若是是Visual Studio,則須要>=2015 update2的版本中才支持/WHOLEARCHIVE選項,VS2013要哭泣了。

於是,在CMakeLists.txt中,能夠設定連接器的全局設定:

if(CMAKE_SYSTEM_NAME MATCHES "Windows")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /WHOLEARCHIVE")
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
     set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--whole-archive") 
endif()

缺點:

  • 全部庫的符號都進行導入,不能靈活處理單個不須要導入全部符號的庫
  • 系統默認導入的庫,例如Windows下的USER32.dll和KERNEL32.dll會產生衝突

TODO: 對於單個target,如何設定?

if (CMAKE_SYSTEM_NAME MATCHES "Windows")
    set_target_properties(inter
        PROPERTIES LINK_FLAGS
        "/WHOLEARCHIVE:gender /WHOLEARCHIVE:smile"
    )
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
    # 不起做用
    set_target_properties(inter
        PROPERTIES LINK_FLAGS
        "-Wl,--whole-archive gender   -Wl,--whole-archive smile"
    )
endif()

或者:

set(MYLIB -Wl,--whole-archive mytest -Wl,--no-whole-archive)
target_link_libraries(main ${MYLIB})

ref: gcc和ld 中的參數 --whole-archive 和 --no-whole-archive

生成compile_commands.json

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

子目錄CMakeLists.txt中產生變量給父目錄中的CMakeLists.txt使用

set設定變量而且設定PARENT_SCOPE參數。

目錄結構舉例:

helloworld
    - CMakeLists.txt
    - src
        hello.cpp
        hello_internal.h
        CMakeLists.txt
    - inc
        hello.h
        CMakeLists.txt
    - testbed
        main.cpp
        CMakeLists.txt

當項目中代碼變多,就可能須要分紅多個目錄存放。每一個目錄下放一個CMakeLists.txt,寫出它要處理的文件列表,而後暴露給外層CMakeLists.txt,使外層CMakeLists.txt保持清爽結構。

set(hello_srcs
    ${CMAKE_CURRENT_SOURCE_DIR}/hello.cpp
)
set(hello_private_incs
    ${CMAKE_CURRENT_SOURCE_DIR}/hello.h
)

set(hello_srcs ${hello_srcs} PARENT_SCOPE)
set(hello_private_incs ${hello_private_incs} PARENT_SCOPE)

在IDE中將targets分組顯示:使用folder

包括兩步:

  1. 在最頂部的CMakeLists.txt中添加一句
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
  1. 在但願出如今文件夾的項目的add_library或add_executable後添加
set_property(TARGET ${prj_target} PROPERTY FOLDER Samples)

效果圖:

ref: CMake顯式添加文件夾

設置Debug的優化級別參數

set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0")

cmake生成VS2019的工程

VS2019開始,須要用-A參數指定是32位程序,仍是64位程序。之前的用法不行了。

32位:

cmake -G "Visual Studio 16 2019" -A Win32 ..\..

64位:

cmake -G "Visual Studio 16 2019" -A x64 ..\..
相關文章
相關標籤/搜索