cmake 备忘录

摘要:
记录使用cmake时的常见需求和解决办法。目录1.用于执行CMake的.bat脚本2.判断平台:32位还是64位?e.g.项目根目录/build/vs2017-x64.bat,内容:@echooff::builddirectory::itshouldbesimilarnamewithcmakegeneratornamesetBUILD_DIR=vs2013-x64::platform::x86orx64setBUILD_PLATFORM=x64::cl.execompilerversionsetBUILD_COMPILER=vc12::createdirectoryifnotexistifnotexist%BUILD_DIR%md%BUILD_DIR%::gotobuilddirectorycd%BUILD_DIR%::runcmakebyspecifing:::-generator::-installationdirectory::-CMakeLists.txtlocationcmake-G"VisualStudio122013Win64"^-DCMAKE_INSTALL_PREFIX=D:/lib/glfw/3.3/%BUILD_PLATFORM%/%BUILD_COMPILER%^../..::runbuildbyspecifyingconfigandtarget::note:thismayfail,andpleaseopen.slnanddomanualcompilationandinstallationcmake--build.--configRelease--targetINSTALL::gobacktooldfoldercd..::stucktoshowbuildmessagespause2.判断平台:32位还是64位?能写64位程序的时候轻易别写32位程序方法1:CMAKE_SIZEOF_VOID_P表示void*的大小,可以使用其来判断当前构建为32位还是64位ifmessageelse()messageendif()方法2:判断CMAKE_CL_64是否为true。

记录使用 cmake 时的常见需求和解决办法。

目录

1. 用于执行CMake的.bat脚本

使用.bat脚本调用cmake,可以指定比较复杂的cmake.exe命令的参数。

e.g. 项目根目录/build/vs2017-x64.bat,内容:

@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

2. 判断平台:32位还是64位?

能写64位程序的时候轻易别写32位程序

方法1:CMAKE_SIZEOF_VOID_P 表示 void* 的大小(例如为 4 或者 8),可以使用其来判断当前构建为 32 位还是 64 位 (cmake官方推荐方法)

if(CMAKE_SIZEOF_VOID_P EQUAL 8)
    message(STATUE "64bit")
else()
    message(STATUE "32bit")
endif()

方法2:判断CMAKE_CL_64是否为true。(cmake官方已经明确不推荐使用)

CMAKE_CL_64: Set to a true value when using a Microsoft Visual Studio cl compiler that targets a 64-bit architecture.

适用条件:只适合Windows上的64位判断。

##################################################
# platform
##################################################
if(CMAKE_CL_64)
    message(STATUS "MSVC 64bit")
else()
    message(STATUS "MSVC 32bit")
endif()

3. 判断Visual Studio版本

参考: List of _MSC_VER and _MSC_FULL_VER

##################################################
# 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}")

4. 判断操作系统

注:其中WIN32判断的是windows系统,包括32位和64位两种情况

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)

另一种写法:

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中进行了设定。

5. 判断是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 ()

6. 根据Debug/Release添加不同的库目录

在Visual Studio平台下测试发现,如果指定了A目录到库搜索目录,并且A目录下有名为Debug/Release的目录,则会自动把A/Debug和A/Release添加到库搜索目录。

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

cmake 备忘录第1张

因而,至少在VS平台下,不需要手动根据Debug和Release来分别添加库。

7. Visual Studio属性与对应CMake实现方法

CMAKE修改VS大总结

8. 设定编译选项

也即修改CMAKE_C_FLAGSCMAKE_CXX_FLAGS变量。例如追加两个选项:

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

9. SAFESEH报错

报错信息例如:

CalcSum_.obj : error LNK2026: 模块对于 SAFESEH 映像是不安全的。 [E:dbgzzassembleuildvs2013
un.vcxproj]
E:dbgzzassembleuildvs2013Debug
un.exe : fatal error LNK1281: 无法生成 SAFESEH 映像。 [E:dbgzzassembleuildvs2013

简单粗暴的解决办法:

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")

10. 用了link_directory()但是链接不到库

link_directories() 这句话必须在add_executable()之前写 不然找不到库目录

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

11. Debug库带“d”后缀

设置debug模式下编译出的库文件,相比于release模式下,多带一个字母"d"作为后缀。

set_target_properties(TargetName PROPERTIES DEBUG_POSTFIX d)

12. 在cmake中执行目录创建、拷贝文件等脚本

例如创建完调用lmdb库的可执行程序这一target后,需要创建目录。不想每次都手动去创建一个目录,希望在CMakeLists.txt中添加这个操作。

尝试了add_custom_command(),不怎么会用。。没有效果,不被调用。

execute_process()则是可以的,例如:

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

13. 现代 CMake

It's Time To Do CMake Right

https://moevis.github.io/cheatsheet/2018/09/12/Modern-CMake-笔记.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})

此外,还有一些cmake方面的专家的问答、博客等可以关注一下:

13. 创建目录

file(MAKE_DIRECTORY ${SO_OUTPUT_PATH})

ref: Creating a directory in CMake

14. 拷贝文件

包括两种类型:
(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()

15. 转换相对路径为绝对路径

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

16. 循环处理列表

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)

17. 设置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)

注:前一种方法是在单个CMakeLists.txt中设定。对于跨平台编译,则应当避免污染根CMakeLists.txt,应该为每个平台分别使用cmake cache script。而在cache script中需要设定的变量,都应该是缓存变量。
例如,sigmastar.cmake:

set(CMAKE_C_COMPILER gcc CACHE STRING "C compiler")
set(CMAKE_CXX_COMPILER g++ CACHE STRING "C++ compiler")
set(PLATFORM_NAME "SigmaStar" CACHE STRING "")

ref: CMake -DCMAKE_CXX_COMPILER works but SET(CMAKE_CXX_COMPILER …) is ignored?

18. 设定导入库(IMPORTED)及其属性

如果是自己项目中的源码基于cmake构建,其中利用add_library()创建的库目标,可以直接用来作为可执行目标、动态库或静态库的依赖库直接使用。
而如果是别人直接丢过来的库和头文件、没有用cmake封装一次呢?显然我们不应该在Visual Studio的项目属性中手动添加,那样也太刀耕火种、没有匠人精神了。
来吧,手写一个导入库的cmake,就现在:

add_library()命令中指定关键字IMPORTED,再用set_target_properties()命令来设定导入库目标的头文件目录、库目录、库文件名字:

add_library(rock SHARED IMPORTED GLOBAL)
set_target_properties(rock PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES "inc"            #PUBLIC头文件目录
    IMPORTED_IMPLIB "rock.lib" #Windows平台上dll库的.lib库所在位置
    IMPORTED_LOCATION "rock.dll"  #dll库的.dll所在位置,或者.so库的位置,或者静态库的位置
)

其中GLOBAL关键字,是为了让全局可见。例如通过add_subdirectory()添加了mpbase库,里面是上述方式添加的库,但是上级CMakeLists.txt要确保能使用这个库,就需要指定GLOBAL关键字。

以上列出了最常见的三个属性,更多属性看 这里

P.S. 实践发现,如果库文件所在目录很长(超过256个字符),或者添加的导入库对应的库文件有多个,它们的名字会被拼接起来,在CMake+Ninja的NDK开发环境下直接报错说路径太长。因此,导入库并不是一个好的实践。

19. 查看并修改Visual Studio项目属性中的某个设定

问题来自StackOverFlow上某网友的提问:Compile error CMAKE with CUDA on Visual Studio C++

解决步骤:

  1. 用cmake-gui.exe或ccmake加载cmake的cache文件
  2. 查找需要修改的字符串对应的CMake变量
  3. 在CMakeLists.txt中修改、覆盖此变量

20. 添加宏定义

add_definitions(-DUSE_OPENCV)

相当于传递给C/C++编译器:

#define USE_OPENCV
add_definitions(-DLANDMARK_VERSION=2.1.33)

相当于传递给C/C++编译器:

#define LANDMARK_VERSION 2.1.33

21. 设置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")

22. 设置fPIC

问题出现场景:编译动态库libaisf_bodyattr_processor.so的时候,它依赖于静态库libarcsoft_bsd.a,但是libarcsoft_bsd.a库编译时没有指定fPIC编译选项。

设定方法:在编译静态库的时候,

全局设定:

set(CMAKE_POSITION_INDEPENDENT_CODE ON)

或者:

add_library(lib1 lib1.cpp)
set_property(TARGET lib1 PROPERTY POSITION_INDEPENDENT_CODE ON)

ref:What is the idiomatic way in CMAKE to add the -fPIC compiler option?

23. Linux gcc添加链接库"-lm"

target_link_libraries(xxx m)

24. 清空普通变量

unset(<Var>)

25. 清除缓存变量

unset(<Var> CACHE)

26. 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

27. CMake各种编译链接参数的默认值

CMake 默认编译、链接选项

28. 链接器相关问题

检查链接到的重名函数

场景: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})

实际上:

  • gcc的链接器ld:
    通过-Wl, --whole-archive lib_name -Wl, --no-whole-archive能每次分别对一个静态库加载所有的member(函数等);

  • Visual Studio的链接器:
    VS2017(1900)之后才支持-WHOLEARCHIVE来实现同样功能;

  • PC Clang的链接器:
    使用lld作为链接器(比如Xcode现在用肯定是lld),用-Wl,-force_load ${lib}来做到只导入一个静态库中的所有member;

  • NDK的链接器
    怎么说现在用的NDK也是17b起步了,默认编译器是Clang,gcc暂时没考虑;
    虽然编译器很早就(可以)切换到Clang了,但链接器目前还是用的gcc的ld,因此NDK的链接阶段检查重复符号应该用ld的检查方式;
    即使是用当前(2020-01-26 02:16:34)最新的NDK也就是NDK21,手动传入ANDROID_LD=lld后,NDK切换到的链接器lld和MacOS上与AppleClang搭配的lld也还是不一样,链接阶段查重复符号仍然需要传gcc的ld的那一套参数:-Wl, --whole-archive lib_name -Wl, --no-whole-archive,但好处是报错界面更加友好直观了:

cmake 备忘录第2张

ref:
gcc和ld 中的参数 --whole-archive 和 --no-whole-archive
lld中的-force_load参数功能说明是在邮件列表中找到的,官方文档里还更新出来
ndk-20的change log

29. 生成compile_commands.json

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

用于在VSCode等编辑器/IDE中给C/C++代码做函数定义跳转支持。

30. 子目录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)

31. 在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)

效果图:
cmake 备忘录第3张

ref: CMake显式添加文件夹

32. 设置Debug的优化级别参数

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

33. cmake生成VS2019的工程

VS2019开始,需要用-A参数指定是32位程序,还是64位程序。以前的用法不行了。

32位:

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

64位:

cmake -G "Visual Studio 16 2019" -A x64 ....

34. 不要同时使用include_directories()target_include_directories()

如下用法有坑:

include_directories("inc" "src")

add_library(rock src/rock.cpp)

target_include_directories(rock PRIVATE "3rdparty/spdlog")

其中target_include_directories()设置的目录不会生效。经验:只使用其中一种设定include目录的方式。

35. NDK开发中出现error: cannot use 'try' with exceptions disabled

这需要编译器的编译选项中开启exception的支持。现在NDK开发,主流的做法是Android Studio + gradle + cmake做构建,需要在build.gradle中设定-fexceptions:

        externalNativeBuild {
            cmake {
//                cppFlags '-std=c++11 -fexceptions'
//                arguments '-DANDROID_PLATFORM=android-21',
//                    '-DANDROID_TOOLCHAIN=clang',
//                    '-DCMAKE_BUILD_TYPE="Release',
//                    '-DANDROID_ARM_NEON=ON',
//                    '-DANDROID_STL=c++_shared'
//                cppFlags '-std=c++11'
//                arguments '-DANDROID_TOOLCHAIN=clang',
//                        '-DANDROID_STL=c++_static'
                cppFlags "-std=c++11 -fexceptions"
            }
        }

但有时候,发现上述设定后并不生效;尝试删除.externalBuild目录重新构建,仍然报exception无法处理。后来发现,问题出在CMakeLists.txt加载的.cmake脚本中(用的代码框架是其它部门同事写的,所以不是很熟悉),他给手动设定了这么一句:

	SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions -fno-short-enums -Werror=non-virtual-dtor")

去掉-fno-exception即可。

36. cmake-gui中可见(可检索)的变量

目前遇到的有两种:

  1. option( )命令定义的变量,e.g.
option(USE_OPENCV “use opencv?” CACHE PAH)
  1. 缓存变量。e.g.
set(varName "value" CACHE STRING "")

当然,还可以使用mark_as_adances来设定,则默认在cmake-gui中不可见。

37. Ninja error: Filename longer than 260 characters

NDK开发中,CMake+Ninja构建,如果文件名超过260个字符会失败。这个限制略蛋疼。

ninja: error: Stat(../../deps/arcsoft_landmark/2.1.12021.33/libs/Android/armeabi-v7a/libarc_net_sgl.a;E:/dbg/myy/landmark_demo/deps/arcsoft_landmark/2.1.12021.33/libs/Android/armeabi-v7a/libarcsoft_face_detection.a;E:/dbg/myy/landmark_demo/deps/arcsoft_landmark/2.1.12021.33/libs/Android/armeabi-v7a/libarcsoft_landmark_tracking.a;E:/dbg/myy/landmark_demo/deps/arcsoft_landmark/2.1.12021.33/libs/Android/armeabi-v7a/libarcsoft_videooutline.a): Filename longer than 260 characters

38. cmake判断C/C++编译器

比如我想要定制一些安全的编译选项,发现需要区分msvc,gcc和clang。容易直接想到CMAKE_C_COMPILERCMAKE_CXX_COMPILER。但考虑到-G Xcode和命令行下分别输出,得到的结果并不都是clang,这就很蛋疼。

使用CMAKE_CXX_COMPILER_ID比较方便,像上面提到的case会做合并输出AppleClang。而如果是NDK-r17c则输出Clang。

常见的平台下对应输出:

  • MSVC
  • Clang
  • GNU
  • AppleClang

更多结果看官方文档 CMAKE_<LANG>_COMPILER_ID

39. cmake字符串追加元素,并且用空格分隔

简单有效的两种方式:

方法1:用set:

set(MY_FLAGS "${MY_FLAGS} -Werror=shadow")

方法2:封装一个函数:

function(afq_list_append __string __element)
    set(${__string} "${${__string}} ${__element}" PARENT_SCOPE)
endfunction()

40. cmake --build的使用:cmake执行编译链接、安装

本质上,当使用cmake ..,或cmake -G "Visual Studio 15 2017 Win64" ../..类似命令时,是执行“pre-make”,相当于是makefile的生成器,可以说对于你的项目代码来说并没执行编译链接,更没有安装。

cmake --build的用法说明:(copy自CMake 手册详解(二)

--build <dir>: 构建由CMake生成的工程的二进制树。(这个选项的含义我不是很清楚—译注)
该选项用以下的选项概括了内置构建工具的命令行界面

  <dir>          = 待创建的工程二进制路径。


  --target <tgt> = 构建<tgt>,而不是默认目标。
  --config <cfg> = 对于多重配置工具,选择配置<cfg>。
  --clean-first  = 首先构建目标的clean伪目标,然后再构建。
                   (如果仅仅要clean掉,使用--target 'clean'选项。)

  --             = 向内置工具(native tools)传递剩余的选项。
运行不带选项的cmake --build来获取快速帮助信息。

实际使用经验:cmake生成了这些cache文件后,可以打开Visual Studio编译,或执行make编译(Linux下)。但这些都是native tool。通用的方式则是用cmake包装好的接口:

# 执行编译(如果是可执target,则包括链接过程)
cmake --build . --config Release

# 执行某个target的编译(如果是可执target,则包括链接过程)
cmake --build . --config Release --target xx

# 执行安装
cmake --install . --prefix d:/lib/openblas/clang-cl/x64 -v

41. C/C++和汇编混编

在cmake里混合编译C/C++与汇编代码,通过enable_language(ASM_<DIALECT>)可以做到。

例如x86(32位)的MASM(Visual Studio支持的一种汇编语法):

enable_language(ASM_MASM)

此外往往还需要给Visual Studio设置SEH:

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")

汇编文件则和C/C++文件一起,正常添加为target的依赖即可:

add_executable(run src/main.cpp src/CalcSum_.asm)

例如src/CalcSum_.cpp:

    .model flat, c
    .code

; extern "C" int CalcSum_(int a, int b, int c)
;
; Description:  This function demonstrates passing arguments between
;               a C++ function and an assembly language function.
;
; Returns:      a + b + c

CalcSum_ proc

; Initialize a stack frame pointer
        push ebp
        mov ebp,esp

; Load the argument values
        mov eax,[ebp+8]                     ; eax = 'a'
        mov ecx,[ebp+12]                    ; ecx = 'b'
        mov edx,[ebp+16]                    ; edx = 'c'

; Calculate the sum
        add eax,ecx                         ; eax = 'a' + 'b'
        add eax,edx                         ; eax = 'a' + 'b' + 'c'

; Restore the caller's stack frame pointer
        pop ebp
        ret

CalcSum_ endp
        end

src/main.cpp:

#include <stdio.h>

#ifdef USE_ASSEMBLY
extern "C" int CalcSum_(int a, int b, int c);
#else
int CalcSumTest(int a, int b, int c)
{
    return a + b + c;
}
#endif

int main() {
    int a = 17, b = 11, c = 14;
#ifdef USE_ASSEMBLY
    int sum = CalcSum_(a, b, c);
#else
    int sum = CalcSumTest(a, b, c);
#endif

    printf(" a: %d
", a);
    printf(" b: %d
", b);
    printf(" c: %d
", c);
    printf(" sum: %d
", sum);

    return 0;
}

42. cmake设置pthread

第一种:

target_link_libraries(xxx pthread)

有时候不管用(例如ARM Android平台),则用下面的方式:

find_package( Threads )
target_link_libraries( testbed ${CMAKE_THREAD_LIBS_INIT} log)

注意ARM Android(也就是通常说的NDK开发中),需要给cmake传一个选项:

-DANDROID_PLATFORM=android-24 ^

(我是armv8所以用24)

43. cmake使用pkg-config

有些依赖库只提供.pc文件,甚至已经配置了CMake脚本但是安装后还是只有.pc而没有XXXConfig.cmake或xxx-config.cmake。并且不仅是Linux,Windows上也这样。
这就不得不在CMake中尝试去加载.pc文件。原理是,cmake里面封装了对pkg-config工具的兼容,可以认为是一个插件,用这个插件去加载.pc文件。实际测试发现Linux和Windows都可以用。

Linux安装pkg-config:

sudo apt install pkg-config

Windows安装pkg-config-lite

使用:先找到xx.pc文件,然后分成目录和文件前缀两部分,在cmake中配置

举例1:Ubuntu 16.04下用apt安装openblas并在CMake中用pkg-config方式配置:

cmake_minimum_required(VERSION 3.15)

project(cmake_pkg_config_example)

set(ENV{PKG_CONFIG_PATH} /usr/lib/pkgconfig)
find_package(PkgConfig)
pkg_search_module(OBS REQUIRED blas-openblas)

message(STATUS "=== OBS_LIBRARIES: ${OBS_LIBRARIES}")
message(STATUS "=== OBS_INCLUDE_DIRS: ${OBS_INCLUDE_DIRS}")

举例2:Windows 10下用CMake配置Pangolin安装中配置的zlib

cmake_minimum_required(VERSION 3.15)

project(cmake_pkg_config_example)

#指定pkg-config.exe绝对路径
set(PKG_CONFIG_EXECUTABLE "D:/soft/pkg-config/bin/pkg-config.exe")

#指定zlib.pc所在目录
set(ENV{PKG_CONFIG_PATH} "D:/lib/pangolin/share/pkgconfig")

find_package(PkgConfig)
message(STATUS "--- PKG_CONFIG_FOUND: ${PKG_CONFIG_FOUND}")
message(STATUS "--- PKG_CONFIG_VERSION_STRING: ${PKG_CONFIG_VERSION_STRING}")

pkg_search_module(ZLIB REQUIRED zlib)

message(STATUS "=== ZLIB_LIBRARIES: ${ZLIB_LIBRARIES}")
message(STATUS "=== ZLIB_INCLUDE_DIRS: ${ZLIB_INCLUDE_DIRS}")

再详细点的话,看这篇:在cmake中使用pkg-config

44. cmake多行注释

从cmake 3.0版本开始,可以使用多行注释

#[[
第一种注释方式
#]]
#[===============[
第二种注释方式
#]===============]

ref: CMake Multiple line comments - Block Comment

45. 命令行-D指定参数

在cmake脚本中指定其中任意一种:

  1. cmake缓存类型的变量,例如
set(CAFFE_TARGET_VERSION "1.0.0" CACHE STRING "Caffe logical version")
  1. option
option(USE_OPENCV "Do we use OpenCV?" ON)

46. include()指令

两种作用:

  1. 包含文件,例如include(utils.cmake)

  2. 包含模块,在CMAKE_MODULE_PATH->CMAKE MODULE DIRECTORY依次查找。例如include(ExternalModule),会在这两个路径列表中查找ExternalModule.cmake文件并加载。

CMAKE_MODULE_PATH是在CMake脚本中用户可以自行修改的变量。
CMAKE MODULE DIRECTORY是什么,官方文档没明确说。其实说的应该是cmake安装后的Modules目录,例如/usr/local/share/cmake/Modules

47. list追加元素,或list首部插入元素

追加(链表尾部插入):

list(APPEND CMAKE_MODULE_PATH "cmake/Modules")

首部插入(链表首元素前插入):

list(INSERT CMAKE_PREFIX_PATH 0 "$ENV{HOME}/soft/opencv/build")

48. cmake中使用IWYU

IWYU 是 google 的开源项目,用来移除不必要的头文件。在cmake中使用IWYU

49. target_link_libraries()

cmake 3.13 开始提供的命令。低版本cmake无法使用。

50. CMake构建NDK项目提示asm/types.h找不到

用CMake构建NDK项目时,会传入toolchain的cmake脚本文件android.toolchain.cmake给CMake。这个文件中会做若干设定,其中就包括include路径。

我遇到的情况是,自己手动修改CMAKE_C_FLAGSCMAKE_CXX_FLAGS时,覆盖了它们原有的(android.toolchain.cmake修改后的)值,导致asm/types.h找不到。

我的错误设定:

set(CMAKE_C_FLAGS "${MY_CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${MY_CMAKE_CXX_FLAGS}")

正确做法应该是追加内容而非修改:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MY_CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MY_CMAKE_CXX_FLAGS}")

P.S. 排查方法:由于我是基于ninja构建的(cmake+ndk的组合下,现在通常用ninja),通过对比”能正常构建的工程“和”提示asm/types.h找不到的工程“之间${CMAKE_BINARY_DIR}目录下的rules.ninjabuild.ninja来发现问题所在。

51. windows下创建的共享库,没生成.lib文件

.lib是导入库,里面存访对外可见(暴露)的符号(函数、变量)。.dll应该搭配一个.lib导入库才能使用。

如果是自己的源码生成的dll共享库,则在CMakeLists.txt一开始,添加:

set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

则可以导出所有的符号。

ref: CMake linking against shared library on windows: error about not finding .lib file

而如果只想导出一部分符号,则可以为每个函数分别指定导出规则。

52. 拷贝dll

在Windows下,Visual Studio中,如果用了动态库(例如opencv、zlib等),需要把dll放到PATH环境变量中,使得运行时能找到dll。
而其实Windows下的PATH查找,是会在CMAKE_BINARY_DIR目录下查找的。如果不想改PATH环境变量,也不希望每次都要手动拷贝dll,包括清掉build目录后重新构建时也不想手动拷贝,那么可以用cmake命令来搞。

举个例子,调用zlib库执行文本压缩解压,用到了zlib1.dll,其中executable target名字是demo。

zlib的二进制包下载:https://nsis.sourceforge.io/mediawiki/images/b/bb/Zlib-1.2.8-win64-AMD64.zip

zlib的调用示例代码:https://blog.csdn.net/yuhuqiao/article/details/82188963

cmake中拷贝zlib1.dll的写法:

# each time the `demo` target is built, we copy zlib1.dll if it is changed.
add_custom_command(TARGET demo
    POST_BUILD

    COMMAND ${CMAKE_COMMAND} -E copy_if_different
    ${ZLIB_DLL}
    ${CMAKE_BINARY_DIR}/
)

53. 压缩或解压.zip/tar.gz

以解压doctest.zip到项目根目录为例:

execute_process(
    COMMAND ${CMAKE_COMMAND} -E tar xzf ${CMAKE_SOURCE_DIR}/doctest.zip
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)

tar命令是从cmake 3.2开始支持的内置命令。

https://stackoverflow.com/questions/48891928/cmake-unzip-automaticallywhile-building-when-the-zip-file-changes

免责声明:文章转载自《cmake 备忘录》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇ABAP锁、数据库锁如何从网页安装群晖NAS并设置静态IP下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

【转】linux kvm虚拟机配置及常见问题处理

egrep '(vmx|svm)' --color=always /proc/cpuinfo 注意:如果查询的结果中包含有vmx,那么就可以证明服务器是支持虚拟化的 安装包 yum -y groupinstall "Virtualization" "Virtualization Client" "Virtualization Platform" modpr...

Ubantu 16.04升级内核版本和还原到升级之前的内核版本的方法

一、查看系统信息 1、查看发布版本: 命令: lsb_release -a 运行结果: / 2、查看内核版本: 命令: uname -sr 运行结果: 二、升级内核的方法 1、内核下载地址:http://kernel.ubuntu.com/~kernel-ppa/mainline/ 。打开地址后,拖动鼠标到网页最底端,找到最新版本的内核v4.15...

Linux /proc/$pid部分内容详解

auxv /proc/[pid]/auxv包含传递给进程的ELF解释器信息,格式是每一项都是一个unsigned long长度的ID加上一个unsigned long长度的值。最后一项以连续的两个0x00开头。举例如下: # hexdump -x /proc/2948/auxv 0000000 0021 0000 0000 0000...

安卓开发_浅谈自定义组件

  在Android中,所有的UI界面都是由View类和ViewGroup类及其子类组合而成。其中,View类是所有UI组件的基类,而ViewGroup类是容纳这些UI组件的容器。 其本身也是View类的子类。      在实际开发中,View类还不足以满足程序所有的需求。这时,便可以通过继承View类来开发自己的组件。   开发自定义组件的步骤:   1...

nginx使用:正向代理、反向代理、负载均衡。常用命令和配置文件

文章目录前言 原文地址→→ 一、nginx简介 1. 什么是 nginx 和可以做什么事情 2.Nginx 作为 web 服务器 3. 正向代理 4. 反向代理 5. 负载均衡 6.动静分离 二、Nginx 的安装(Linux:centos为例)1. 准备工作2. 开始安装3. 运行nginx4. 防火墙问题 三、 Nginx 的常用命令和配置文件 1....

linux中class_create和class_register说明

http://blog.csdn.net/angle_birds/article/details/16802099 本文介绍linux中class_create和class_register的相关使用方法 1 class结构体介绍     内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同...