跨平台构建:cmake实践

跨平台构建系统的要点有下列几项:

  1. 项目组织:开源项目内一般会存在多个不同的构建目标,类型有:可执行文件、动态库、静态库,这些项目各有自己的独立源文件目录
    1. 根目录
      根目录下有一个 CMakeList.txt,里面定义了cmake版本需求、项目名、c++标准需求等,包含的构建目标公共部分都可以放在此处。
      脚本示例:

      CMAKE_MINIMUM_REQUIRED(VERSION 3.15) #需要使用3.15引入的CMAKE_MSVC_RUNTIME_LIBRARY
      PROJECT(mylib)

      SET(CMAKE_CXX_STANDARD 14)
      SET(CMAKE_CXX_STANDARD_REQUIRED ON)
      SET(CMAKE_CXX_EXTENSIONS OFF)

    2. 包含子目录
      脚本示例:
      ADD_SUBDIRECTORY(src)
      • 子目录下必需要有 CMakeList.txt,否则会报错
      • 子目录下的脚本可以定义构建目标或者安装目标,或者只定义下级目录的公共部分并包含下级目录,算是简单的模块化
    3. 其它
      1. 获取环境变量
        SET(EXTERNAL_PATH $ENV{FRAME_EXTERNAL_PATH})
        上句获取环境变量 FRAME_EXTERNAL_PATH 的值,并存入变量 EXTERNAL_PATH,以后可以用 ${EXTERNAL_PATH} 访问此值
  2. 构建目标内源文件组织:源文件是基础编译单元,不同后缀会对应不同的编译器
    构建目标的源文件的组织方式:headers_public + sources_cross_platform + sources_platform
    1. 上面每一项源文件都可以利用通配符获取目录下所有文件,利用正则表达式和排除列表移除不需要的文件
      这样做的好处:为构建目标增加源文件时,不再需要修改 CMakeList.txt 脚本
      1. 脚本示例(mylib):
        FILE(GLOB_RECURSE headers_public "../include/*.h")
        FILE(GLOB_RECURSE sources_cross_platform LIST_DIRECTORIES false "*.*")
        FILE(GLOB_RECURSE sources_platform_all LIST_DIRECTORIES false "platform/*.*")

        FOREACH(TMP_SOURCE_PATH ${sources_platform_all})
            LIST(REMOVE_ITEM sources_common ${TMP_SOURCE_PATH})
        ENDFOREACH()

        FILE(GLOB sources_blacklist 
            "core/KeyEvent.cpp"
            "core/MouseEvent.cpp"
            )
        FOREACH(TMP_SOURCE_PATH ${sources_blacklist})
            LIST(REMOVE_ITEM sources_common ${TMP_SOURCE_PATH})
        ENDFOREACH()

        IF(WIN32)
            FILE(GLOB_RECURSE sources_platform LIST_DIRECTORIES false "platform/win/*.*")
        ENDIF()

      2. 注意事项:
        • FILE 语句是一个文件搜索器,使用 GLOB_RECURSE 来做递归搜索,GLOB 不做递归,后面可以跟多个支持通配符的路径,以当前脚本目录为根目录,可以使用相对目录来搜索上层目录
        • FILE 定义的是一个文件列表,最后生成的列表里面的文件名都是全路径,且以 / 作为分隔符
        • 文件列表可以使用 LIST(APPEND sources "${CMAKE_CURRENT_SOURCE_DIR}/platform/ControlHelper.cpp") 文件时,必须是全路径,前面带上当前路径 ${CMAKE_CURRENT_SOURCE_DIR}/
        • 文件列表的其它操作同样需求全路径,否则无效果,cmake 会自动忽略不存在的文件
        • LIST_DIRECTORIES false 是必须的,否则生成的 VS 内会有一些空目录Filter
    2. 最后再将这些源码合并成一个构建目标
      1. 脚本示例(mylib):

        ADD_LIBRARY(gbase SHARED ${headers_public} ${sources_cross_platform} ${sources_platform})

      2. 注意事项
        因为在执行到 ADD_LIBRARY 语句时,headers_public 之类列表的值必须已经全部初始化完毕,后续的添加是没有用的,我也没有找到再添加源文件的途径,所以:
        1. 各项收集、过滤、排除列表之类操作必须要在前面完成
        2. OS相关源文件收集也要在前面完成,后面还有为构建目标设置OS相关属性,所以多次判断OS在所难免
    3. 在VS中将文件按目录树层次显示
      1. 脚本示例(mylib):

        SOURCE_GROUP(TREE "${CMAKE_CURRENT_SOURCE_DIR}/../include" PREFIX "Public\\" FILES ${headers_public})

      2. 注意事项
        • FILES 对应列表不能是使用 RELATIVE 修饰的 FILE列表,否则生成的列表内是相对路径,不能对应 TREE 需要的 root,无法正确生成
        • SOURCE_GROUP 的 PREFIX 格式:"Public\\" 后面的 \\ 是必须
  3. 构建目标属性设置
    每个构建目标的属性都有几个来源,通用的来源有这些:上级脚本的公共设置,本脚本的公共设置,本脚本的目标属性设置。这种算是一定程度上的脚本复用
    1. 宏定义
      1. 公共设置语句:
        1. ADD_COMPILE_DEFINITIONS(expr):expr 支持 VAR 或者 VAR=VALUE
        2. ADD_DEFINITIONS(expr):expr 支持 -DVAR /DVAR 或者 -DVAR=VALUE /DVAR=VALUE
      2. 目标属性设置:
        1. TARGET_COMPILE_DEFINITIONS(target, expr):expr 格式同 ADD_COMPILE_DEFINITIONS
    2. 头文件路径
      1. 公共设置语句:
        1. INCLUDE_DIRECTORIES(expr):expr 支持本脚本目录的相对路径,可以写多条路径,空格或换行分隔
      2. 目标属性设置:
        1. TARGET_INCLUDE_DIRECTORIES(target, expr)
    3. 库路经
      1. 公共设置语句:
        1. LINK_DIRECTORIES(expr):expr 支持本脚本目录的相对路径,可以写多条路径,空格或换行分隔
      2. 目标属性设置:
        1. TARGET_LINK_DIRECTORIES(target, expr)
      1. 公共设置语句:
        1. LINK_LIBRARIES(expr):expr 支持库名,可以不带 .lib 或 .a,多个库以空格或换行分隔
      2. 目标属性设置:
        1. TARGET_LINK_LIBRARIES(target, expr),推荐使用这个
    4. 设置输出文件路径

      SET_TARGET_PROPERTIES(mylib PROPERTIES
          OUTPUT_NAME mylib
          DEBUG_OUTPUT_NAME mylib${DEBUG_LIBNAME_SUFFIX}
          RUNTIME_OUTPUT_DIRECTORY ../../lib)

      注意事项:有些库喜欢在debug库的后面加上 d 或者 _d,请自行设置 DEBUG_LIBNAME_SUFFIX 的缺省值,在 cmake gui 内也可以直接改写
       

    5. 其它属性
      脚本属性和命令列表请查看:cmake commands 和 cmake properties
    6. 定制构建配置
      我们自身的 debug/release 版本和预制的不同,所以需要定制配置
      ADD_DEFINITIONS(-DNDEBUG)

      SET(CMAKE_CONFIGURATION_TYPES "release;debug" CACHE STRING "" FORCE)
      SET(CMAKE_BUILD_TYPE release CACHE STRING "Choose the type of build, options are: release debug." FORCE)

      SET(CMAKE_CXX_FLAGS_RELEASE "/O2 /Oy- /Oi /Ot /Zi")
      SET(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG")
      SET(CMAKE_SHARED_LINKER_FLAGS_RELEASE "/DEBUG")

      SET(CMAKE_CXX_FLAGS_DEBUG "/Od /Oy- /Zi")
      SET(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG")
      SET(CMAKE_SHARED_LINKER_FLAGS_DEBUG "/DEBUG")

      1. debug: 宏定义 -DNDEBUG,禁用所有优化(/Od),为EXE/DLL生成调试信息(/DEBUG),调试信息格式:程序数据库(/Zi)

      2. release: 宏定义 -DNDEBUG,优化2级(/O2),速度优先(/Ot),启用内部函数(/Oi),不省略栈指针,为EXE/DLL生成调试信息(/DEBUG),调试信息格式:程序数据库(/Zi)
      3. VS 链接 crt 模式选择:cmake3.15 内引入了新的语句可以保证可执行文件都是用 /MD 方式链接crt:SET(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
    7. 判断当前构建的配置

      IF("$<CONFIG>" STREQUAL "release")
          TARGET_COMPILE_DEFINITIONS(WebNativeDemo PRIVATE SK_RELEASE)
      ELSE()
          TARGET_COMPILE_DEFINITIONS(WebNativeDemo PRIVATE SK_DEBUG)
      ENDIF()

      上面是根据配置来定义宏,$<CONFIG> 是 cmake 的 generator expression 机制,请看下面的 4.d 段落
    8. 生成 macOS .app Bundle

      FILE(GLOB resources_skin_root "${CMAKE_CURRENT_SOURCE_DIR}/WebNativeDemo/skin/*")
      FILE(GLOB resources_library ${CMAKE_BINARY_DIR}/../out/debug/*.dylib ${CMAKE_BINARY_DIR}/../out/debug/*.so)
      SOURCE_GROUP("Library" FILES ${resources_library})
      ADD_EXECUTABLE(WebNativeDemo MACOSX_BUNDLE ${sources} ${resources_skin_root} ${resources_library})
      SET_SOURCE_FILES_PROPERTIES(${resources_skin_root} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/skin")
      SET_SOURCE_FILES_PROPERTIES(${resources_library} PROPERTIES MACOSX_PACKAGE_LOCATION "Libraries")

       

      SET_TARGET_PROPERTIES(WebNativeDemo PROPERTIES
          MACOSX_BUNDLE TRUE
          MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/xcode/WebNativeDemo/Info.plist
          BUILD_RPATH "@executable_path/../Libraries"
          INSTALL_RPATH "@executable_path/../Libraries"
          XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH "NO")

      如果想要添加文件到 .app 中,首先需要通过 FILE 和 LIST 收集文件列表,然后将文件列表放入 ADD_EXECUTABLE 声明中,最后通过SET_SOURCE_FILES_PROPERTIES指定文件列表在 app 中的位置。
      上面将 skin 下所有文件放入了 .app/Contents/Resources/skin 下,将依赖的动态链接库放入了 .app/Contents/Libraries 下,因为编译好的 WebNativeDemo 在 .app/Contents/MacOS 下,所以还将构建和安装时的 RPATH 指定为动态链接库的相对目录。
      BUILD_RPATH 是普通调试时的 run path 指定,如果走了 INSTALL 步骤,就需要也同时指定 INSTALL_RPATH
      我在 RPATH 这里试验了 @loader_path 和 $ORIGIN 都没有效果
  4. 构建目标的安装设置
    1. 设置目的目录
      SET(CMAKE_INSTALL_PREFIX "${EXTERNAL_PATH}/mylib" CACHE PATH "..." FORCE)
    2. 安装公共头文件
      INSTALL(DIRECTORY ../include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mylib)
      这里需要注意一点:../include/ 最后面的斜线不要丢掉,否则会把整个 include 复制为 ${CMAKE_INSTALL_INCLUDEDIR}/mylib/include
    3. 安装静态库、动态库、可执行文件

      IF(WIN32)
          IF(CMAKE_GENERATOR_PLATFORM STREQUAL "x64")
              SET(path_platform "x64")
          ELSE()
              SET(path_platform "x86")
          ENDIF()
      ELSEIF(APPLE)
      ELSEIF(LINUX)
      ENDIF()

      INSTALL(TARGETS mylib
          RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}/${path_platform}/$<CONFIG>
          LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/${path_platform}/$<CONFIG>
          ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/${path_platform}/$<CONFIG>)
      注意事项:上面一块是随OS不同生成不同的安装路径,windows上就沿用传统子路径,apple macos 可以考虑使用 mac_x86, mac_x64

    4. 安装可能的调试符号

      INSTALL(FILES $<TARGET_PDB_FILE:${PROJECT_NAME}>
          DESTINATION ${CMAKE_INSTALL_BINDIR}/${path_platform}/$<CONFIG> OPTIONAL)

      注意事项:
      1. 这里$<> 是cmake 的一种叫 generator expression 的机制,随用户选择构建的配置不同而自动变化。该机制细节请查看:cmake generator expressions
      2. $<TARGET_PDB_FILE:${PROJECT_NAME}> 的意思是找项目输出的 pdb 文件,没有配置 pdb 输出时为空。这些内置变量请查看:cmake generator expressions: variables
      3. OPTIONAL 是提示这个文件如果不存在,不要报错
  5. cmake GUI的使用(以cmake3.15为例)
    1. 如何生成 vs2015 64bit项目?
      选好源码CMakeLists.txt所在目录和构建目录后,点击Configure按钮,会出现提示框如右图:
    2. 在 Platform for generator 内选取 x64 即可。
    3. 如何生成支持 xp 的项目?
      在Configure弹出的提示框内,Optional toolset to use 下面的输入框内填写 v140_xp 即可
  6. 对开源库编译出的 sdk 目录结构的建议
    第三方库输出 sdk 目录结构基本上有 3 个子目录:bin, lib, include,另外可能还有些资源目录。
    1. bin 放置可执行文件(dll, exe, so, dylib)和调试符号文件(pdb)
      这个目录下面需要分 OS 和配置,典型的目录结构是:
      1. bin
      2.   |-x86
      3.   |   |-release
      4.   |   |-debug
      5.   |-mac-x86_64
      6.   |   |-release
      7.   |   |-debug
    2. lib 放置导入库或静态库(lib, a)
      这个目录下面也需要分 OS 和配置,目录结构和 bin 雷同
    3. include 放置头文件(h)
      像 zlib,jsoncpp 之类较小且做了全平台适配的项目,可以只用一套头文件,但是像 skia, openssl 这种有头文件生成机制的项目就不能这样,因为它们每个平台都对应有不同的同名头文件,怎样既不包含重复头文件又可以满足跨平台需求?我现在是这样做:
      1. include
      2.   |-openssl   #此目录下放置所有固定不变头文件
      3.   |-x86
      4.   |   |-openssl   #此目录下按原目录层次放置平台相关头文件
      5.   |       |-opensslconf.h  
      6.   |-mac-x86_64
      7.       |-openssl
      8.           |-opensslconf.h
        我们最后在写包含路径时,只需要多加一行 openssl/include/${PLATFORM} 就够了,在使用时,仍然只需要 #include <openssl/xxxx.h>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值