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 ..\..
相关文章
相关标签/搜索