Cmake-Cookbook学习笔记

Cmake-Cookbook

相关资料整理:
原仓库:https://github.com/dev-cafe/cmake-cookbook
陈晓伟对CMake-Cookbook的中文翻译:https://github.com/xiaoweiChen/CMake-Cookbook
CMake-Cookbook中文目录
官方文档: https://cmake.org/cmake/help/v3.12/manual/cmake-buildsystem.7.html#object-libraries

写在前面,常用的一些cmake命令
一、编译项目

mkdir build
cd build
cmake .. 

二、构建项目生成可执行文件

cmake --build . --config Release

备注:等同于Cmakelists.txt中set(CMAKE_BUILD_TYPE Release CACHE STRING “Build type” FORCE)。在build时,利用config设置编译类型。
三、安装
设置安装路径并执行安装命令

cmake -DCMAKE_INSTALL_PREFIX=E:\cmakeoutput  ..
cmake --build . --target install --config Release

1、cmake中相关路径

```bash
CMAKE_SOURCE_DIR: 顶级cmakelists.txt的文件夹目录。
CMAKE_BINRAY_DIR: 对应cmake的build的目录,主要是运行时生成的文件目录。
CMAKE_CURRENT_SOURCE_DIR: 一般来说,一个工程会有多个cmakelists.txt文件,对应当前文件目录。
CMAKE_CURRENT_BINARY_DIR: 对应build里的目录。
CMAKE_MODULE_PATH: api(include/find_package)包含别的cmake文件时的搜索目录。
CMAKE_PREFIX_PATH: api(find_libray/path)包含模块时的搜索目录。
CMAKE_INSTALL_PREFIX: 调用install相关函数,要生成/保存的根目录路径。
PROJECT_BINARY_DIR和PROJECT_SOURCE_DIR是等价的。也就是当前源码的目录
CMAKE_SIZEOF_VOID_P  空指针的大小 EQUAL 8表示64位 EQUAL 4表示32位
CMAKE_HOST_SYSTEM_PROCESSOR 运行CMake的CPU的名称

2、cmake常用变量、函数、语法

变量

project(recipe-04 VERSION 2.0.1 LANGUAGES C)
PROJECT_VERSION_MAJOR #2
PROJECT_VERSION_MINOR #0
PROJECT_VERSION_PATCH #1
PROJECT_VERSION #cmake中的版本号变量
CMAKE_CXX_COMPILER_ID 编译器ID
CMAKE_SYSTEM_NAME 当前系统名字
BUILD_SHARED_LIB 是一个全局变量,主要是用于控制cmake是否可以生成动态so

语法

set 设置参数,包含一般/缓存/环境变量。
unset 取消设置参数
EXISTS #判断文件是否存在
list 针对列表操作,比如针对文件列表/参数列表/编译列表的增加删除这些。
list(APPEND _sources Message.hpp Message.cpp) 向_source变量添加源文件
string 针对字符串的操作,如大小写,查找,正则表达式匹配等。
message 打印消息,可以跟踪测试修改。
add_compile_options:不同平台一般来说有不同的编译设置。
add_definitions:添加预处理器定义。
include_directories: 如visual studio里的,头文件搜索目录,在当前项目以及当前项目用add_subdirectory添加的项目都会应用。
target_sources(helloworld xxx.cpp)#向依赖库中添加源文件
target_include_directories:针对指定目标的include_directories。
link_libraries: 添加库文件路径,注意是全路径,如果是本方案的项目,直接使用项目名就行。在当前项目以及当前项目用add_subdirectory添加的项目都会应用。
target_link_libraries:指定目标的link_directories。
add_library:添加库,根据参数生成静态或是动态库。
add_executable:添加执行文件。
set_target_properties:指定项目一些具体编辑器里的属性,如生成lib/dll的目录。
target_compile_definitions: 添加预处理器定义

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
  message(STATUS "Configuring on/for Linux")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  message(STATUS "Configuring on/for Windows")
configure_file 可将cmake中的变量值传到.h.in文件变量中,然后生成转换成.h文件
configure_file(foo.h.in foo.h @ONLY)  cmake-cookbook 2章4节
find_program # This command is used to find a program. 

file:

file 针对文件操作,如收集文件列表,读写文件等。
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/helloworld.cpp helloworld) #读取当前目录下的helloworld.cpp文件为helloworld变量
file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path )

TO_NATIVE_PATH模式将cmake样式 转换为带有特定于平台的斜杠的本机路径(\在Windows和/或其他地方)。
语法:

# 字符串中添加变量
 target_compile_definitions(hello-world PUBLIC "COMPILER_NAME=\"${CMAKE_CXX_COMPILER_ID}\"")
# 如果`MPI_FOUND`为真,那么` $<BOOL:${MPI_FOUND}>`的值将为1
$<$<BOOL:${MPI_FOUND}>:MPI::MPI_CXX>
foreach(<loop_var> <items>)
  <commands>
endforeach()

其中< items >是用分号或空格分隔的项列表。foreach和匹配的endforeach之间的所有命令都被记录,而没有被调用。一旦endforeach被求值,记录的命令列表将为< items >中的每个项调用一次。在每次迭代开始时,变量< loop_var >将被设置为当前项的值。`

2.1 add_custom_command、add_custom_target

1. 使用`add_custom_command`编译目标,生成输出文件。
2. `add_custom_target`的执行没有输出。
3. 构建目标前后,`add_custom_command`的执行可以没有输出。

add_custom_target添加一个具有给定名称的目标,以执行给定命令。目标没有输出文件,即使命令试图创建具有目标名称的文件,也始终被视为过期。使用add_custom_command()命令生成具有依赖项的文件。默认情况下,任何内容都不依赖于自定义目标。使用add_dependencies()命令向其他目标或从其他目标添加依赖项。

add_custom_target(unpack-eigen
  ALL
  COMMAND
    ${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/eigen-eigen-5a0156e40feb.tar.gz
  COMMAND
    ${CMAKE_COMMAND} -E rename eigen-eigen-5a0156e40feb eigen-3.3.4
  WORKING_DIRECTORY
    ${CMAKE_CURRENT_BINARY_DIR}
  COMMENT
    "Unpacking Eigen3 in ${CMAKE_CURRENT_BINARY_DIR}/eigen-3.3.4"
  )
add_dependencies(linear-algebra unpack-eigen)#指定可执行目标对自定义目标的依赖关系
add_custom_command(
  OUTPUT
  	${wrap_BLAS_LAPACK_sources}
  COMMAND
  	${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
  COMMAND
  	${CMAKE_COMMAND} -E touch ${wrap_BLAS_LAPACK_sources}
  WORKING_DIRECTORY
  	${CMAKE_CURRENT_BINARY_DIR}
  DEPENDS
  	${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
  COMMENT
  	"Unpacking C++ wrappers for BLAS/LAPACK"
  VERBATIM
  )

2.2 CMakePrintHelpers模块

include(CMakePrintHelpers)
  cmake_print_properties(
    TARGETS MPI::MPI_CXX
    PROPERTIES INTERFACE_LINK_LIBRARIES
    )

2.3 include(GNUInstallDirs)模块

此模块帮助设置.lib .dll .pdb文件的输出路径

include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})

2.4 execute_process执行命令

类似于cmd命令之类,whoami是返回系统用户名

execute_process(COMMAND <cmd1> [<arguments>]
                [COMMAND <cmd2> [<arguments>]]...
                [WORKING_DIRECTORY <directory>]
                [TIMEOUT <seconds>]
                [RESULT_VARIABLE <variable>]
                [RESULTS_VARIABLE <variable>]
                [OUTPUT_VARIABLE <variable>]
                [ERROR_VARIABLE <variable>]
                [INPUT_FILE <file>]
                [OUTPUT_FILE <file>]
                [ERROR_FILE <file>]
                [OUTPUT_QUIET]
                [ERROR_QUIET]
                [COMMAND_ECHO <where>]
                [OUTPUT_STRIP_TRAILING_WHITESPACE]
                [ERROR_STRIP_TRAILING_WHITESPACE]
                [ENCODING <name>]
                [ECHO_OUTPUT_VARIABLE]
                [ECHO_ERROR_VARIABLE]
                [COMMAND_ERROR_IS_FATAL <ANY|LAST>])

cmake中获取git信息

execute_process(          # 执行一个子进程
        COMMAND ${GIT_EXECUTABLE} log -1 --pretty=format:%h # 命令
        OUTPUT_VARIABLE ${_git_hash}        # 输出字符串存入变量
        OUTPUT_STRIP_TRAILING_WHITESPACE    # 删除字符串尾的换行符
        ERROR_QUIET                         # 对执行错误静默
        WORKING_DIRECTORY                   # 执行路径
          ${CMAKE_CURRENT_SOURCE_DIR}
        )

2.5 macro自定义宏、function函数

(1)${ARGC}保存给定宏的所有参数数量。

(2)${ARGV} 保存给定宏的所有参数列表。

(3)${ARGN}保存超过最后一个预期参数的参数列表,即额外参数列表。

macro(宏名称 参数1 参数2 ...)
	...
endmacro()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(colors) #通过include引用colors.cmake

2.5.1 cmake_parse_arguments模块

cmake_parse_arguments模块官方解释

function(add_catch_test)
  set(options)
  set(oneValueArgs NAME COST)
  set(multiValueArgs LABELS DEPENDS REFERENCE_FILES)
  cmake_parse_arguments(add_catch_test
    "${options}"
    "${oneValueArgs}"
    "${multiValueArgs}"
    ${ARGN}
    )
  message(STATUS "defining a test ...")
  message(STATUS "    NAME: ${add_catch_test_NAME}")
  message(STATUS "    LABELS: ${add_catch_test_LABELS}")
  message(STATUS "    COST: ${add_catch_test_COST}")
  message(STATUS "    REFERENCE_FILES: ${add_catch_test_REFERENCE_FILES}")

2.6 include_guard

CMake | include_guard命令详解

2.7set_target_properties

cmakecookb-10.1
设置依赖库的属性:

set_target_properties(message-shared
  PROPERTIES
    POSITION_INDEPENDENT_CODE 1
    SOVERSION ${PROJECT_VERSION_MAJOR}
    OUTPUT_NAME "message"
    DEBUG_POSTFIX "_d"
    PUBLIC_HEADER "Message.hpp"
    MACOSX_RPATH ON
    WINDOWS_EXPORT_ALL_SYMBOLS ON
  )

POSITION_INDEPENDENT_CODE 1:设置生成位置无关代码所需的编译器标志。
SOVERSION ${PROJECT_VERSION_MAJOR} : 这是动态库提供的应用程序编程接口(API)版本。在设置语义版本之后,将其设置为与项目的主版本一致。
DEBUG_POSTFIX “_d”:这告诉CMake,如果我们以Debug配置构建项目,则将_d后缀添加到生成的动态库。
PUBLIC_HEADER “Message.hpp”:我们使用这个属性来设置头文件列表(本例中只有一个头文件),声明提供的API函数。这主要用于macOS上的动态库目标,也可以用于其他操作系统和目标
MACOSX_RPATH ON:这将动态库的install_name部分(目录)设置为macOS上的@rpath。
WINDOWS_EXPORT_ALL_SYMBOLS ON:这将强制在Windows上编译以导出所有符号。

2.8 CMake COMMADN

在C++、Python混合编程中。Python模块在${CMAKE_CURRENT_BINARY_DIR}中创建,为了让Python的test.py脚本找到它,我们使用一个自定义环境变量传递相关的路径,该环境变量用于在test.py中设置path变量。请注意,如何将命令设置为调用CMake可执行文件本身,以便在执行Python脚本之前设置本地环境。这为我们提供了平台独立性,并避免了环境污染:
cmake-cookbook 9.6

add_test(
     NAME
       python_test
     COMMAND
       ${CMAKE_COMMAND} -E env ACCOUNT_MODULE_PATH=${CMAKE_CURRENT_SOURCE_DIR}
                           ACCOUNT_HEADER_FILE=${CMAKE_CURRENT_SOURCE_DIR}/account/account.h
                           ACCOUNT_LIBRARY_FILE=$<TARGET_FILE:account>
      ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/account/test.py
   )

cookbook-10.1

add_test(
  NAME test_shared
  COMMAND $<TARGET_FILE:hello-world_wDSO>
  )

2.9GenerateExportHeader规定动态库只公开最小的符号

GenerateExportHeader可规定动态库只公开最小的符号,从而限制代码中定义的对象和函数对外的可见性。我们希望在默认情况下,动态库定义的所有符号都对外隐藏。

include(GenerateExportHeader)
generate_export_header(account
  BASE_NAME account
  )

cmakecookbook-10.2

include(GenerateExportHeader)
generate_export_header(message-shared
  BASE_NAME "message"
  EXPORT_MACRO_NAME "message_EXPORT"
  EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h"
  DEPRECATED_MACRO_NAME "message_DEPRECATED"
  NO_EXPORT_MACRO_NAME "message_NO_EXPORT"
  STATIC_DEFINE "message_STATIC_DEFINE"
  NO_DEPRECATED_MACRO_NAME "message_NO_DEPRECATED"
  DEFINE_NO_DEPRECATED
  )
  • BASE_NAME:设置生成的头文件和宏的名称。
  • EXPORT_MACRO_NAME:设置导出宏的名称。
  • EXPORT_FILE_NAME:设置导出头文件的名称。
  • DEPRECATED_MACRO_NAME:设置弃用宏的名称。这是用来标记将要废弃的代码,如果客户使用该宏定义,编译器将发出一个将要废弃的警告。
  • NO_EXPORT_MACRO_NAME:设置不导出宏的名字。
  • STATIC_DEFINE:用于定义宏的名称,以便使用相同源编译静态库时使用。
  • NO_DEPRECATED_MACRO_NAME:设置宏的名称,在编译时将“将要废弃”的代码排除在外。
  • DEFINE_NO_DEPRECATED:指示CMake生成预处理器代码,以从编译中排除“将要废弃”的代码。

设置可执行文件的属性:

set_target_properties(hello-world_wDSO
    PROPERTIES
    MACOSX_RPATH ON
    SKIP_BUILD_RPATH OFF
    BUILD_WITH_INSTALL_RPATH OFF
    INSTALL_RPATH "${message_RPATH}"
    INSTALL_RPATH_USE_LINK_PATH ON
  )

SKIP_BUILD_RPATH OFF:告诉CMake生成适当的RPATH,以便能够在构建树中运行可执行文件。
UILD_WITH_INSTALL_RPATH OFF:关闭生成可执行目标,使其RPATH调整为与安装树的RPATH相同。在构建树中不运行可执行文件。
INSTALL_RPATH “${message_RPATH}”:将已安装的可执行目标的RPATH设置为先前的路径。
INSTALL_RPATH_USE_LINK_PATH ON:告诉CMake将链接器搜索路径附加到可执行文件的RPATH中。

3、引入第三方库

find_package(LibaryName)根据对应CMAKE_MODULE_PATH找到对应的Find<LibaryName>.cmake,一般来说,有如下三下变量(你也可以定义成别的名字)<LibaryName>_FOUND 是否找到库
<LibaryName>_INCLUDE_DIR <LibaryName>_INCLUDES 库头文件目录
<LibaryName>_LIBRARY <LibaryName>_LIBRARIES 库链接文件路径
如果我们引用的第三方库没有提供Find<LibaryName>.cmake,我们可以自己写,只需要填充上面上面变量,就可以使用find_package,实际一般用如下几个函数确定这三个变量,而这几个函数默认都会去CMAKE_PREFIX_PATH查找:
FIND_PACKAGE_HANDLE_STANDARD_ARGS:<LibaryName>_FOUND
find_path:获得<LibaryName>_INCLUDE_DIR目录。
find_library:获得<LibaryName>_LIBRARY 目录。
结合前面的include_directories/link_libraries引用对应的<LibaryName>_INCLUDE_DIR/<LibaryName>_INCLUDE_DIR就引入第三方库了。

4、静态库与动态库

静态库和动态库最本质的区别就是:该库是否被编译进目标(程序)内部。

分别介绍:

静态(函数)库
一般扩展名为(.a或.lib),这类的函数库通常扩展名为libxxx.a或xxx.lib 。
这类库在编译的时候会直接整合到目标程序中,所以利用静态函数库编译成的文件会比较大,这类函数库最大的优点就是编译成功的可执行文件可以独立运行,而不再需要向外部要求读取函数库的内容;但是从升级难易度来看明显没有优势,如果函数库更新,需要重新编译。
动态函数库
动态函数库的扩展名一般为(.so或.dll),这类函数库通常名为libxxx.so或xxx.dll 。
与静态函数库被整个捕捉到程序中不同,动态函数库在编译的时候,在程序里只有一个“指向”的位置而已,也就是说当可执行文件需要使用到函数库的机制时,程序才会去读取函数库来使用;也就是说可执行文件无法单独运行。这样从产品功能升级角度方便升级,只要替换对应动态库即可,不必重新编译整个可执行文件。

5、Gcc、G++区别

https://blog.csdn.net/bit_clearoff/article/details/53965514
Windows中我们常用vs来编译编写好的C和C++代码;vs把编辑器,编译器和调试器等工具都集成在这一款工具

6、CMAKE_TOOLCHAIN_FILE交叉编译

指定.cmake文件的路径
CMAKE_TOOLCHAIN_FILE

7、Cmake Option

https://blog.csdn.net/chouhuan1877/article/details/100808689?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control
以下代码表示如果USE_LIBRARYONMAKE_STATIC_LIBRARY默认值为OFF,否则MAKE_SHARED_LIBRARY默认值为ON

option(USE_LIBRARY "Compile sources into a library" OFF)
include(CMakeDependentOption)
CMakeDependentOption
cmake_dependent_option(
	MAKE_STATIC_LIBRARY "Compile sources into a static library" OFF
	"USE_LIBRARY" ON
	)
cmake_dependent_option(
	MAKE_SHARED_LIBRARY "Compile sources into a shared library" ON
	"USE_LIBRARY" ON
	)	

8、cmake COMPILE_FLAGS

VERBOSE=1,详细输出cmake生成的makefile调用的确切g++命令
-O3
-O2
CXX_STANDARD 14
CXX_EXTENSIONS OFF
CXX_STANDARD_REQUIRED ON
POSITION_INDEPENDENT_CODE 1
Release —— 在编译器中使用命令: -O3 -DNDEBUG 可选择此版本。
Debug ——在编译器中使用命令: -g 可选择此版本。
MinSizeRel—— 最小体积版本。在编译器中使用命令:-Os -DNDEBUG可选择此版本。
RelWithDebInfo—— 既优化又能调试。
原文链接:https://blog.csdn.net/weixin_42089190/article/details/106420270
-fPIC 添加fPIC选项生成的动态库,是位置无关。这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能运行。通常的方法是获取指令指针的值,加上一个偏移得到全局变量/函数的地址。添加 -fPIC选项的源文件对于它引用的函数头文件编写有较宽松的尺度。比如只需要包含声明的函数的头文件,即使没有相应的 C 文件来实现,编译成 .so 库照样可以通过。
fPIC 解释
position-independent code (PIC):用于生成位置无关代码。位置无关代码,可以理解为代码无绝对跳转,跳转都为相对跳转。生成动态库时,需要加上-fPIC选项。
-Wall

9、Norma Variables和Cache Variables

两种变量的理解Norma Variables Cache Variables
CMake语法—缓存变量(Cache Variable)

10、在目录中查找所有源文件

aux_source_directory用法

aux_source_directory(<dir> <variable>)

11、超级构建ExternalProject_Add添加/在线下载外部项目并编译

设置外部cmake项目的编译输出位置

   set_property(DIRECTORY PROPERTY EP_BASE ${CMAKE_BINARY_DIR}/subprojects)

设置编译好FFTW3.dll和头文件的安装输出位置

   set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/stage)

cmake-cookbo-8.1


ExternalProject_Add(${PROJECT_NAME}_core
  SOURCE_DIR
    ${CMAKE_CURRENT_LIST_DIR}/src
  CMAKE_ARGS
    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
    -DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS}
    -DCMAKE_CXX_STANDARD_REQUIRED=${CMAKE_CXX_STANDARD_REQUIRED}
  CMAKE_CACHE_ARGS
    -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
  BUILD_ALWAYS
    1
  INSTALL_COMMAND
    ""
  )

可以支持在线下载第三方库FFTW3且编译输出到指定目录下——cookbook8.3、8.4

if(FFTW3_FOUND)
  get_property(_loc TARGET FFTW3::fftw3 PROPERTY LOCATION)
  message(STATUS "Found FFTW3: ${_loc} (found version ${FFTW3_VERSION})")
  add_library(fftw3_external INTERFACE)  # dummy
else()
  message(STATUS "Suitable FFTW3 could not be located. Downloading and building!")

  include(ExternalProject)
  ExternalProject_Add(fftw3_external
    URL
      http://www.fftw.org/fftw-3.3.8.tar.gz
    URL_HASH
      MD5=8aac833c943d8e90d51b697b27d4384d
    DOWNLOAD_NO_PROGRESS
      1
    UPDATE_COMMAND
      ""
    LOG_CONFIGURE
      1
    LOG_BUILD
      1
    LOG_INSTALL
      1
    CMAKE_ARGS
      -DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
      -DBUILD_TESTS=OFF
    CMAKE_CACHE_ARGS
      -DCMAKE_C_FLAGS:STRING=$<$<BOOL:WIN32>:-DWITH_OUR_MALLOC>
    )

  include(GNUInstallDirs)

  set(
    FFTW3_DIR ${STAGED_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake/fftw3
    CACHE PATH "Path to internally built FFTW3Config.cmake"
    FORCE
    )
endif()
````FetchContent`模块,它将提供需要的声明、查询和填充依赖项函数,也可以支持在线下载第三方库且编译输出到指定目录下
```bash
  include(FetchContent)#首先包括`FetchContent`模块
   FetchContent_Declare(#声明内容——名称、存储库位置和要获取的精确版
     googletest
     GIT_REPOSITORY https://github.com/google/googletest.git
     GIT_TAG release-1.8.0
   )
   FetchContent_GetProperties(googletest)#查询内容是否已经被获取/填充
   add_subdirectory(
      ${googletest_SOURCE_DIR}
      ${googletest_BINARY_DIR}
      )

12、cmake -D -G -P -Wdey 及编译器相关

https://cmake.org/cmake/help/v3.14/manual/cmake.1.html?highlight=open#command-line-tool-mode
cmake -G的G应该是Generator的意思,该语句可以设置IDE,比如Xcode
cmake -D 是用来设置编译器的
-D 用于传入配置项option,更新或创建缓存入口

cmake -D PYTHON_EXECUTABLE=/custom/location/python ..  #指定python解释器的位置

有时需要将-D PYTHON_EXECUTABLE-D PYTHON_LIBRARY-D PYTHON_INCLUDE_DIR传递给CMake CLI

-E cmake -E [ …] 命令行工具模式
-Wdev 禁止开发者告警,也会忽略过时信息告警
-P 执行cmake脚本
指定编译类型
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ^
cmake -D CMAKE_CXX_COMPILER=clang++ …
windows 指定编译器架构
cmake -G “Visual Studio 16 2019” -A x64 ^

#判断编译器类型,如果是gcc编译器,则在编译选项中加入c++11支持
if(CMAKE_COMPILER_IS_GNUCXX)
    add_compile_options(-std=c++11)
    message(STATUS "optional:-std=c++11")   
endif(CMAKE_COMPILER_IS_GNUCXX)

下面是对Visual Studio的CMake调用,将为Release和Debug配置生成一个构建树。然后,您可以使--config标志来决定构建这两个中的哪一个:

```shell
$ mkdir -p build
$ cd build
$ cmake .. -G"Visual Studio 12 2017 Win64" -D CMAKE_CONFIGURATION_TYPES="Release;Debug"
$ cmake --build . --config Release

target_compile_definitions(hello-world PUBLIC “COMPILER_NAME=”${CMAKE_CXX_COMPILER_ID}“”)
将编译类型储存为CACHE变量,使用 FORCE 选项覆盖现有条目。

set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)

13、add_library、target_link_libraries、target_include_directories

创建库并链接库的方式一:
# generate an object library from sources
add_library(message-objs  # 添加message对象
  OBJECT
    Message.hpp
    Message.cpp
  )

# this is only needed for older compilers
# but doesn't hurt either to have it
set_target_properties(message-objs
  PROPERTIES
    POSITION_INDEPENDENT_CODE 1
  )
add_library(message-shared
  SHARED
    $<TARGET_OBJECTS:message-objs>
  )
set_target_properties(message-shared
  PROPERTIES
    OUTPUT_NAME "message"
  )
add_library(message-static
  STATIC
    $<TARGET_OBJECTS:message-objs>
  )
set_target_properties(message-static
  PROPERTIES
    OUTPUT_NAME "message"
  )
target_link_libraries(helloworld message-static)  # 链接静态库

STATIC库是目标文件的归档文件,在链接其它目标的时候使用。SHARED库会被动态链接(动态链接库),在运行时会被加载。MODULE库是一种不会被链接到其它目标中的插件,但是可能会在运行时使用dlopen-系列的函数。默认状态下,库文件将会在于源文件目录树的构建目录树的位置被创建,该命令也会在这里被调用。

  • STATIC:创建静态库,也就是对象文件的存档,用于链接其他目标时使用,例如:可执行文件
  • SHARED:创建共享库,也就是可以动态链接并在运行时加载的库
  • OBJECT:创建对象库,也就是对象文件不需要将它们归档到静态库中,也不需要将它们链接到共享对象中

MODULE选项将生成一个插件库,也就是动态共享对象(DSO),没有动态链接到任何可执行文件,但是仍然可以在运行时加载。由于我们使用C++来扩展Python,所以Python解释器需要能够在运行时加载我们的库。使用MODULE选项进行add_library,可以避免系统在库名前添加前缀(例如:Unix系统上的lib)。

cmake : add_library详解STATIC(静态库)/SHARED(动态库)/MODULE(模块库)
创建库并链接库的方式二:

list(APPEND _sources Message.hpp Message.cpp)
add_library(message STATIC ${_sources})
target_link_libraries(hello-world message)

14、CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS

cmake官方解释
通过在C++类中减少对显式DLL导出标记的需求,这简化了移植项目到Windows。

15、CMake: Public VS Private VS Interface

官方文档: https://cmake.org/cmake/help/v3.12/manual/cmake-buildsystem.7.html#object-libraries
CMake: Public VS Private VS Interface解释与作用

  • PRIVATE,编译定义将只应用于给定的目标,而不应用于相关的其他目标。
  • INTERFACE,对给定目标的编译定义将只应用于使用它的目标。
  • PUBLIC,编译定义将应用于给定的目标和使用它的所有其他目标。

另一种解释:CMake 中的 PUBLIC,PRIVATE,INTERFACE

  • 以target_link_libraries(A B)命令为例,从理解的角度解释:
  • PRIVATE: 依赖项B仅链接到目标A,若有C链接了目标A,C不链接依赖项B
  • INTERFACE:依赖项B并不链接到目标A,若有C链接了目标A,C会链接依赖项B
  • PUBLIC :依赖项B链接到目标A,若有C链接了目标A,C也会链接依赖项B
cmake target_link_libraries()<PUBLIC|PRIVATE|INTERFACE> 的区别
#如果目标的头文件.h中包含了依赖的头文件(源文件间接包含),那么这里就是PUBLIC
#如果目标仅源文件中包含了依赖的头文件,那么这里就是PRIVATE
#如果目标的头文件包含依赖,但源文件未包含,那么这里就是INTERFACE

此外还有

target_include_directories():指定目标包含的头文件路径。
target_link_libraries():指定目标链接的库。
target_compile_options():指定目标的编译选项。

一个例子:cmake:target_** 中的 PUBLIC,PRIVATE,INTERFACE
包含CMakeLists.txt子目录:

一、
# defines targets and sources
add_subdirectory(src)
# defines targets and sources
二、
include(src/CMakeLists.txt)
include(external/CMakeLists.txt)

16、编译特性和语言标准

https://cmake.org/cmake/help/latest/manual/cmake-compile-features.7.html
编译功能要求可以通过target_Compile_features()命令指定。例如,如果必须使用cxx_constexpr功能的编译器支持编译目标:

add_library(mylib requires_constexpr.cpp)
target_compile_features(mylib PRIVATE cxx_constexpr)

例如,如果在项目的头文件中广泛使用C++ 11特性,那么客户端必须使用不小于C++ 11的编译器模式。可通过以下代码请求:

target_compile_features(mylib PUBLIC cxx_std_11)
编译器:

AppleClang: Apple Clang for Xcode versions 4.4+.

Clang: Clang compiler versions 2.9+.

GNU: GNU compiler versions 4.4+.

MSVC: Microsoft Visual Studio versions 2010+.

SunPro: Oracle SolarisStudio versions 12.4+.

Intel: Intel compiler versions 12.1+.

16.1 try_compile、 check__compiles_function编译检查

  • CMAKE_REQUIRED_FLAGS:设置编译器标志。
  • CMAKE_REQUIRED_DEFINITIONS:设置预编译宏。
  • CMAKE_REQUIRED_INCLUDES:设置包含目录列表。
  • CMAKE_REQUIRED_LIBRARIES:设置可执行目标能够连接的库列表。
    使用check_cxx_source_compiles需要include CheckCXXSourceCompiles模块
try_compile(
    omp_taskloop_test_1
    ${_scratch_dir}
    SOURCES
      ${CMAKE_CURRENT_SOURCE_DIR}/taskloop.cpp
    LINK_LIBRARIES
      OpenMP::OpenMP_CXX
    )
  message(STATUS "Result of try_compile: ${omp_taskloop_test_1}")

  include(CheckCXXSourceCompiles)
  file(READ ${CMAKE_CURRENT_SOURCE_DIR}/taskloop.cpp _snippet)
  set(CMAKE_REQUIRED_LIBRARIES OpenMP::OpenMP_CXX)
  check_cxx_source_compiles("${_snippet}" omp_taskloop_test_2)
  unset(CMAKE_REQUIRED_LIBRARIES)

检查编译器标志


list(APPEND CXX_BASIC_FLAGS "-g3" "-O1")
include(CheckCXXCompilerFlag)
set(ASAN_FLAGS "-fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_REQUIRED_FLAGS ${ASAN_FLAGS})
check_cxx_compiler_flag(${ASAN_FLAGS} asan_works)
unset(CMAKE_REQUIRED_FLAGS)
if(asan_works)
  string(REPLACE " " ";" _asan_flags ${ASAN_FLAGS})
  add_executable(asan-example asan-example.cpp)
  target_compile_options(asan-example
    PUBLIC
      ${CXX_BASIC_FLAGS}
      ${_asan_flags}
    )
  target_link_libraries(asan-example
    PUBLIC
      ${_asan_flags}
    )
endif()

17、target_compile_definitions及预处理器

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-02 LANGUAGES CXX)
add_executable(hello-world hello-world.cpp)

# let the preprocessor know about the system name
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
  target_compile_definitions(hello-world PUBLIC "IS_LINUX")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  target_compile_definitions(hello-world PUBLIC "IS_WINDOWS")
endif()

#pragma once是一个比较常用的C/C++预处理指令,只要在头文件的最开始加入这条预处理指令,就能够保证头文件只被编译一次。

C/C++文件中的预处理指令#define,#ifdef,#ifndef,#endif…可以知道当前是在哪个操作系统

#include <cstdlib>
#include <iostream>
#include <string>
std::string say_hello() {
#ifdef IS_WINDOWS
  return std::string("Hello from Windows!");
#elif IS_LINUX
  return std::string("Hello from Linux!");
#else
  return std::string("Hello from an unknown system!");
#endif
}
int main() {
  std::cout << say_hello() << std::endl;
  return EXIT_SUCCESS;
}

18、查询运行CMake的主机系统的系统信息及处理器指令集检测

Query host system specific information.
查询主机系统特定信息

cmake_host_system_information(RESULT <variable> QUERY <key> ...)

cmake官方

19、find_package命令找到Python解释器

使用find_package(EXACT)关键字,限制CMake检测特定的版本

  • find_file:在相应路径下查找命名文件
  • find_library:查找一个库文件
  • find_package:从外部项目查找和加载设置
  • find_path:查找包含指定文件的目录
  • find_program:找到一个可执行程序
  • find_package(PythonInterp REQUIRED)
  • PYTHONINTERP_FOUND:是否找到解释器
  • PYTHON_EXECUTABLE:Python解释器到可执行文件的路径
  • PYTHON_VERSION_STRING:Python解释器的完整版本信息
  • PYTHON_VERSION_MAJOR:Python解释器的主要版本号
  • PYTHON_VERSION_MINOR :Python解释器的次要版本号
  • PYTHON_VERSION_PATCH:Python解释器的补丁版本号
  • 使用以下工具进行调试:
include(CMakePrintHelpers)
cmake_print_variables(_status _hello_world)

以下代码在cmake中执行python代码

execute_process(
  COMMAND
    ${PYTHON_EXECUTABLE} "-c" "print('Hello, world!')" #-c应该表示接下来输入字符串
  RESULT_VARIABLE _status
  OUTPUT_VARIABLE _hello_world
  ERROR_QUIET
  OUTPUT_STRIP_TRAILING_WHITESPACE
  #根据您执行的命令,它可能会为变量设置尾随空格。
  #如果这种行为不可取,可以使用选项OUTPUT_STRIP_Training_WHITESPACE作为“execute_process”函数的最后一个参数
  )
execute_process(
  COMMAND
    ${PYTHON_EXECUTABLE} "-c" "import ${_module_name}; print(${_module_name}.__version__)"
  OUTPUT_VARIABLE _stdout
  ERROR_VARIABLE _stderr
  OUTPUT_STRIP_TRAILING_WHITESPACE
  ERROR_STRIP_TRAILING_WHITESPACE
  )
Message("OUTPUT_VARIABLE=${OUTPUT_VARIABLE}")
if(_stderr MATCHES "ModuleNotFoundError")
  message(STATUS "Module ${_module_name} not found")
else()
  message(STATUS "Found module ${_module_name} v${_stdout}")
endif()

FindPackageHandleStandardArgs模块提供了用于实现Find_package()调用的Find模块中的函数。

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(NumPy
  FOUND_VAR NumPy_FOUND
  REQUIRED_VARS NumPy
  VERSION_VAR _numpy_version
  )

查找第三方库的指定模块

# 查找Boost库的filesystem模块
find_package(Boost 1.54 REQUIRED COMPONENTS filesystem)
# target_link_libraries(hello-world
  PUBLIC
    Boost::filesystem
  )
#如果Boost库安装在非标准位置,可以在配置时使用`BOOST_ROOT`变量传递Boost安装的根目录
cmake -D BOOST_INCLUDEDIR=/custom/boost/include -DBOOST_LIBRARYDIR=/custom/boost/lib  

有四种方式可用于找到依赖包:

  1. 使用由包供应商提供CMake文件 <package>Config.cmake<package>ConfigVersion.cmake<package>Targets.cmake,通常会在包的标准安装位置查找。
  2. 无论是由CMake还是第三方提供的模块,为所需包使用find-module
  3. 使用pkg-config,如本节的示例所示。
  4. 如果这些都不可行,那么编写自己的find模块,Cmake-CookBook3章10节有一个自定义的find模块。
    添加CMAKE_MODULE_PATH路径
    list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake_modules)

19.1find_package和pkgconfig

cmake使用指定opencv版本
find_package()函数使用前设置_DIR。我们可以在调用cmake时将这个目录传给cmake。由于其优先级最高,因此cmake会优先从该目录中寻找,这样我们就可以随心所欲的配置cmake使其找到我们希望它要找到的包。

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(CMAKE_MODULE_PATH "/my/custom/module/path;${CMAKE_MODULE_PATH}")

Ubuntu 16.04 + OpenCV 自定义环境变量 pkg-config / PKG_CONFIG_PATH
find_package Cmake官方解释
stackoverflow对find_package默认搜索路径的解释
pkg-config相关命令详解说明
Windows中使用pkg-config程序

20、test测试enable_testing、add_test()

Cmake-CookBook 4章2节catch测试模块
option(ENABLE_UNIT_TESTS “Enable unit tests” ON)

 $ mkdir -p build
   $ cd build
   $ cmake ..
   $ cmake --build .
   $ ctest #执行cmake中的测试命令

ctest -C debug #设置ctest的configure模式
ctest -T memcheck#检测内存泄漏

option(ENABLE_UNIT_TESTS "Enable unit tests" ON)
if(ENABLE_UNIT_TESTS) #启动ENABLE_UNIT_TESTS模块
  include(fetch_git_repo.cmake)

  fetch_git_repo(
    googletest
    ${CMAKE_BINARY_DIR}/_deps
    https://github.com/google/googletest.git
    release-1.8.0
  )
  # when building with Visual Studio
  set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
  # Prevent GoogleTest from using PThreads
  set(gtest_disable_pthreads ON CACHE BOOL "" FORCE)
  add_subdirectory(
    ${googletest_SOURCE_DIR}
    ${googletest_BINARY_DIR}
    )
  set_tests_properties(example PROPERTIES WILL_FAIL true)#将属性`WILL_FAIL`设置为`true`
  set_tests_properties(example PROPERTIES TIMEOUT 10)#为测试指定时限,设置为10秒,超过了这个设置时间则失败
  set_tests_properties(j PROPERTIES COST 4.5) #

ctest --parallel 4#在4个内核上运行测试,还可以使用环境变量CTEST_PARALLEL_LEVEL将其设置为所需的级别
ctest core 1: aeeeiiiiiii core 2: bfffjjjjjjjjj core 3: cggg core 4: dhhhhh#设置测量顺序
set_tests_properties(j PROPERTIES COST 4.5)
测试属性label

set_tests_properties(
     feature-a
     feature-b
     feature-c
     PROPERTIES
     	LABELS "quick"
     )
 # 结果:
long     =   0.62 sec*proc (3 tests)
quick    =   0.28 sec*proc (3 tests)
ctest -R feature #运行名称中包含feature的程序测试时间
ctest -L long #运行所有长label的测试时间
ctest --help#查看使用方法

定义了一个文本固件,并将其称为my-fixture。我们为安装测试提供了FIXTURES_SETUP属性,并为清理测试了FIXTURES_CLEANUP属性,并且使用FIXTURES_REQUIRED,我们确保测试feature-afeature-b都需要安装和清理步骤才能运行。将它们绑定在一起,可以确保在定义良好的状态下,进入和离开相应的步骤。

set_tests_properties(
  setup
  PROPERTIES
    FIXTURES_SETUP my-fixture
  )
set_tests_properties(
  feature-a
  feature-b
  PROPERTIES
    FIXTURES_REQUIRED my-fixture
  )
set_tests_properties(
  cleanup
  PROPERTIES
    FIXTURES_CLEANUP my-fixture
  )
# Result:
    Start 1: setup
1/4 Test #1: setup ............................   Passed    0.09 sec
    Start 2: feature-a
2/4 Test #2: feature-a ........................   Passed    0.04 sec
    Start 3: feature-b
3/4 Test #3: feature-b ........................   Passed    0.04 sec
    Start 4: cleanup
4/4 Test #4: cleanup ..........................   Passed    0.04 sec

20.1 macro自定义宏+add_test测试

add_executable(cpp_test test.cpp)
target_link_libraries(cpp_test sum_integers)

macro(add_catch_test _name _cost)
  math(EXPR num_macro_calls "${num_macro_calls} + 1")
  message(STATUS "add_catch_test called with ${ARGC} arguments: ${ARGV}")

  set(_argn "${ARGN}")
  if(_argn)
    message(STATUS "oops - macro received argument(s) we did not expect: ${ARGN}")
  endif()

  add_test(
    NAME
      ${_name}
    COMMAND
      $<TARGET_FILE:cpp_test> #执行cpp_test,及参数设置
      [${_name}] --success --out  #cpp_test  --success --out 1.log --durations yes 可直接在cmd中运行
      ${PROJECT_BINARY_DIR}/tests/${_name}.log --durations yes
    WORKING_DIRECTORY
      ${CMAKE_CURRENT_BINARY_DIR}
    )

  set_tests_properties( 
    ${_name}
    PROPERTIES
      COST ${_cost}  #通过将此属性设置为浮点值来明确定义测试的成本
    )
endmacro()

set(num_macro_calls 0)

add_catch_test(short 1.5)
add_catch_test(long 2.5 extra_argument)

message(STATUS "in total there were ${num_macro_calls} calls to add_catch_test")

21 install安装

CMAKE_INSTALL_PREFIX表示要安装的路径
当只要求安装库,我们可以执行以下步骤:

cmake -D COMPONENT=lib -P cmake_install.cmake

构建树、安装树
官方解释:
ARCHIVE
Static libraries are treated as ARCHIVE targets, except those marked with the FRAMEWORK property on OS X (see FRAMEWORK below.) For DLL platforms (all Windows-based systems including Cygwin), the DLL import library is treated as an ARCHIVE target.

LIBRARY
Module libraries are always treated as LIBRARY targets. For non- DLL platforms shared libraries are treated as LIBRARY targets, except those marked with the FRAMEWORK property on OS X (see FRAMEWORK below.)

RUNTIME
Executables are treated as RUNTIME objects, except those marked with the MACOSX_BUNDLE property on OS X (see BUNDLE below.) For DLL platforms (all Windows-based systems including Cygwin), the DLL part of a shared library is treated as a RUNTIME target.
PUBLIC_HEADER
Any PUBLIC_HEADER files associated with a library are installed in the destination specified by the PUBLIC_HEADER argument on non-Apple platforms. Rules defined by this argument are ignored for FRAMEWORK libraries on Apple platforms because the associated files are installed into the appropriate locations inside the framework folder.

   install(
     TARGETS
       message-shared
       hello-world_wDSO
     ARCHIVE
       DESTINATION ${INSTALL_LIBDIR}
       COMPONENT lib
     RUNTIME
       DESTINATION ${INSTALL_BINDIR}
       COMPONENT bin
     LIBRARY
       DESTINATION ${INSTALL_LIBDIR}
       COMPONENT lib
     PUBLIC_HEADER
       DESTINATION ${INSTALL_INCLUDEDIR}/message
       COMPONENT dev
     )

将message_RPATH设置为{CMAKE_INSTALL_PREFIX}/{INSTALL_BINDIR} , 这里我在windwos下构建,$CMAKE_INSTALL_PREFIX表示路径是C:\Program Files (x86)。这里message_RPATH= $ORIGIN\ . .\lib,帮助可执行文件寻找链接项。参考https://docs.oracle.com/cd/E19957-01/806-0641/appendixc-16/index.html

file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
if(APPLE)
  set(_rpath "@loader_path/${_rel}")
else()
  set(_rpath "\$ORIGIN/${_rel}")
message(STATUS "${_rpath}")
endif()
file(TO_NATIVE_PATH "${_rpath}/${INSTALL_LIBDIR}" message_RPATH)

设置可执行文件相关输出。
SKIP_BUILD_RPATH是一个布尔值,指定是否跳过允许目标从生成树运行的RPATH的自动生成。如果在创建目标时设置了变量CMAKE_SKIP_BUILD_RPATH,则该属性由该变量的值初始化。
BUILD_WITH_INSTALL_RPATH是一个布尔值,指定是否将生成树中的目标链接到INSTALL_RPATH。这优先于SKIP_BUILD_RPATH,避免了安装前重新链接的需要。
INSTALL_RPATH_USE_LINK_PATH是一个布尔值,如果设置为true,则会将链接器搜索路径中以及项目外部的目录附加到INSTALL_RPATH。如果在创建目标时设置了变量CMAKE_INSTALL_RPATH_USE_LINK_PATH,则该属性由变量CMAKE_INSTALL_RPATH_USE_LINK_PATH的值初始化。

set_target_properties(hello-world_wDSO
  PROPERTIES
    MACOSX_RPATH ON
    SKIP_BUILD_RPATH OFF # 编译时加上RPATH,通过RPATH解决cmake动态编译后找不到动态链接库问题 
    BUILD_WITH_INSTALL_RPATH OFF #编译时RPATH不使用安装的RPATH 
    INSTALL_RPATH "${message_RPATH}"
    INSTALL_RPATH_USE_LINK_PATH ON#安装的执行文件不加上RPATH
  )

符号可见性:

  • CXX_VISIBILITY_PRESET hidden:这将隐藏所有符号,除非显式地标记了其他符号。当使用GNU编译器时,这将为目标添加fvisibility=hidden标志。
  • VISIBILITY_INLINES_HIDDEN 1:这将隐藏内联函数的符号。如果使用GNU编译器,这对应于-fvisibility-inlines-hidden

Windows上,这都是默认行为。WINDOWS_EXPORT_ALL_SYMBOLS属性为ON来覆盖它。

   set_target_properties(message-shared
     PROPERTIES
       POSITION_INDEPENDENT_CODE 1
       CXX_VISIBILITY_PRESET hidden
       VISIBILITY_INLINES_HIDDEN 1
       SOVERSION ${PROJECT_VERSION_MAJOR}
       OUTPUT_NAME "message"
       DEBUG_POSTFIX "_d"
       PUBLIC_HEADER "Message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h"
       MACOSX_RPATH ON
     )

CMake编写install export 生成寻找链接项的.cmake文件

涉及的相关第三方库

Sanitizers 静态和动态代码分析的非常有用的工具。通过使用适当的标志重新编译代码并链接到必要的库,可以检查内存错误(地址清理器)、未初始化的读取(内存清理器)、线程安全(线程清理器)和未定义的行为(未定义的行为清理器)相关的问题。与同类型分析工具相比,Sanitizers带来的性能损失通常要小得多,而且往往提供关于检测到的问题的更详细的信息。缺点是,代码(可能还有工具链的一部分)需要使用附加的标志重新编译
https://github.com/arsenm/sanitizers-cmake
valgrind linux的内存泄漏检查工具。Memcheck这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值