CMake 入门 - 知乎0. 序CMake 是一个跨平台的开源构建工具,使用 CMake 能够方便地管理依赖多个库的目录层次结构并生成 makefile 和使用 GNU make 来编译和连接程序。 1. 构建单个文件1.1 使用 GCC 编译假设现在我们希望编写一个函…https://zhuanlan.zhihu.com/p/149828002CMake实践应用专题 - 知乎介绍CMake工程应用实战经验。https://www.zhihu.com/column/c_1369781372333240320gcc分别链接静态库和动态库生成可执行文件_不#曾&轻听的博客-CSDN博客本文分别通过链接静态库和动态库来编译可执行文件,在使用gcc编译的过程中了解链接静态库和动态库的区别与联系,同时深入理解Linux系统上是如何将源程序一步步的编译组装成可执行文件的。目录(一)库文件(二)编译时链接静态库文件1.源程序2.编译静态库文件libx2.a(一)库文件(二)编译时链接静态库文件1.源程序main.c(主函数)#include"sub1.h"#include"sub2.h"#include
gcc将源文件编译成可执行文件或者库文件。而当需要编译的东西很多时,需要说明先编译什么,后编译什么,这个过程成为构建,常用的工具是make,对应的定义构建过程的文件为makefile,而编写makefile对于大型项目比较复杂,通过cmke就可以通过更加简洁的语法定义构建的流程,cmake定义构建过程的文件为cmakelists.txt.
2.cmake的语法核心概念
变量:cmake中使用set,unset命令设置或者取消设置变量
# Set variable
set(AUTHOR_NAME Farmer)
set(AUTHOR "Farmer Li")
set(AUTHOR Farmer\ Li)
# Set list
set(SLOGAN_ARR To be) # Saved as "To;be"
set(SLOGAN_ARR To;be)
set(SLOGAN_ARR "To;be")
set(NUM 30) # Saved as string, but can compare with other number string
set(FLAG ON) # Bool value
# set( ... CACHE [FORCE])
set(CACHE_VAR "Default cache value" CACHE STRING "A sample for cache variable")
# set(ENV{} [])
set(ENV{ENV_VAR} "$ENV{PATH}")
message("Value of ENV_VAR: $ENV{ENV_VAR}")
常用的脚本命令:
1.消息打印,即message命令,其实就是打印log,用来打印不同信息,
message([] "message text" ...)
2.条件分支
set(EMPTY_STR "")
if (NOT EMPTY_STR AND FLAG AND NUM LESS 50 AND NOT NOT_DEFINE_VAR)
message("The first if branch...")
elseif (EMPTY_STR)
message("EMPTY_STR is not empty")
else ()
message("All other case")
endif()
3.列表操作
list也是cmake的一个命令,有很多有用的子命令,比较常用的有append:往列表中添加元素,length:获取列表元素个数,join:将列表元素用指定的分隔符连接起来。
set(SLOGAN_ARR To be) # Saved as "To;be"
set(SLOGAN_ARR To;be)
set(SLOGAN_ARR "To;be")
set(WECHAT_ID_ARR Real Cool Eengineer)
list(APPEND SLOGAN_ARR a) # APPEND sub command
list(APPEND SLOGAN_ARR ${WECHAT_ID_ARR}) # Can append another list
list(LENGTH SLOGAN_ARR SLOGAN_ARR_LEN) # LENGTH sub command
# Convert list "To;be;a;Real;Cool;Engineer"
# To string "To be a Real Cool Engineer"
list(JOIN SLOGAN_ARR " " SLOGEN_STR)
message("Slogen list length: ${SLOGAN_ARR_LEN}")
message("Slogen list: ${SLOGAN_ARR}")
message("Slogen list to string: ${SLOGEN_STR}\n")
4.文件操作
CMake的file
命令支持的操作比较多,可以读写、创建或复制文件和目录、计算文件hash、下载文件、压缩文件等等。 使用的语法都比较类似,以笔者常用的递归遍历文件为例,下面是获取src目录下两个子目录内所有c文件的列表的示例:
file(GLOB_RECURSE ALL_SRC
src/module1/*.c
src/module2/*.c
)
GLOB_RECURSE表示执行递归查找,查找目录下所有符合指定正则表达式的文件。
5.配置文件生成
使用configure_file
命令可以将配置文件模板中的特定内容替换,生成目标文件。
set(VERSION 1.0.0)
configure_file(version.h.in "${PROJECT_SOURCE_DIR}/version.h")
6.执行系统命令
使用execute_process命令可以执行一条或者顺序执行多条系统命令。
7.查找库文件
通过find_library在指定的路径和相关默认路径下查找指定名字的库。
find_library ( name1 [path1 path2 ...])
8.include其他模块
include(CPack) # 开启打包功能
include(CTest) # 开启测试相关功能
3.CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(CMakeTemplate VERSION 1.0.0 LANGUAGES C CXX DESCRIPTION "A cmake template project")
##--------------------- Version file ---------------------------------------##
configure_file(src/c/cmake_template_version.h.in "${PROJECT_SOURCE_DIR}/src/c/cmake_template_version.h")
# Specified the language standard
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)
##--------------------- Compile Options ------------------------------------##
# Configure compile options
add_compile_options(-Wall -Wextra -pedantic -Werror)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c++11")
# Set build type
# set(CMAKE_BUILD_TYPE Debug) # Use `cmake -DCMAKE_BUILD_TYPE=Debug` more better
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
# Compile options for Debug variant
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
# Compile options for Release variant
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
message(STATUS "Compile options for c: ${CMAKE_C_FLAGS}")
message(STATUS "Compile options for c++: ${CMAKE_CXX_FLAGS}")
##--------------------- Global Macros --------------------------------------##
add_definitions(-DDEBUG -DREAL_COOL_ENGINEER)
##--------------------- Include directories --------------------------------##
include_directories(src/c)
##--------------------- Source files ---------------------------------------##
file(GLOB_RECURSE MATH_LIB_SRC
src/c/*.c
)
##--------------------- Build target ---------------------------------------##
option(USE_IMPORTED_LIB "Use pre compiled lib" OFF)
if (USE_IMPORTED_LIB)
# add_library(math STATIC IMPORTED)
# set_property(TARGET math PROPERTY IMPORTED_LOCATION "./lib/libmath.a")
find_library(LIB_MATH_DEBUG mathd HINTS "./lib")
find_library(LIB_MATH_RELEASE math HINTS "./lib")
add_library(math STATIC IMPORTED GLOBAL)
set_target_properties(math PROPERTIES
IMPORTED_LOCATION "${LIB_MATH_RELEASE}"
IMPORTED_LOCATION_DEBUG "${LIB_MATH_DEBUG}"
IMPORTED_CONFIGURATIONS "RELEASE;DEBUG"
)
add_subdirectory(src/c/nn)
else()
# Build math lib
add_subdirectory(src/c/math)
add_subdirectory(src/c/nn)
endif()
# Merge library
if (APPLE)
set(MERGE_CMD libtool -static -o)
add_custom_command(OUTPUT libmerge.a
COMMAND libtool -static -o libmerge.a $ $
DEPENDS math nn)
else()
add_custom_command(OUTPUT libmerge.a
COMMAND ar crsT libmerge.a $ $
DEPENDS math nn)
endif()
add_custom_target(_merge ALL DEPENDS libmerge.a)
add_library(merge STATIC IMPORTED GLOBAL)
set_target_properties(merge PROPERTIES
IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/libmerge.a
)
# Build demo executable
add_executable(demo src/c/main.c)
target_link_libraries(demo PRIVATE merge)
##--------------------- Build unit tests -----------------------------------##
option(CMAKE_TEMPLATE_ENABLE_TEST "Whether to enable unit tests" ON)
if (CMAKE_TEMPLATE_ENABLE_TEST)
message(STATUS "Unit tests enabled")
enable_testing()
add_subdirectory(third_party/googletest-release-1.10.0 EXCLUDE_FROM_ALL)
include_directories(third_party/googletest-release-1.10.0/googletest/include)
add_executable(test_add test/c/test_add.cc)
add_executable(test_minus test/c/test_minus.cc)
add_executable(test_gtest_demo test/c/test_gtest_demo.cc)
target_link_libraries(test_add math gtest gtest_main)
target_link_libraries(test_minus math gtest gtest_main)
target_link_libraries(test_gtest_demo math gtest gtest_main)
add_test(NAME test_add COMMAND test_add)
add_test(NAME test_minus COMMAND test_minus)
add_test(NAME test_gtest_demo COMMAND test_gtest_demo)
endif()
##--------------------- Install and Package target -------------------------##
# Install
if (NOT USE_IMPORTED_LIB)
install(TARGETS math nn demo
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
PUBLIC_HEADER DESTINATION include)
file(GLOB_RECURSE MATH_LIB_HEADERS src/c/math/*.h)
install(FILES ${MATH_LIB_HEADERS} DESTINATION include/math)
endif()
# Package, These variables should set before including CPack module
set(CPACK_GENERATOR "ZIP")
set(CPACK_SET_DESTDIR ON) # 支持指定安装目录
set(CPACK_INSTALL_PREFIX "RealCoolEngineer")
include(CPack)
1.设置版本项目
2.指定编程语言版本
为了在不同机器上更加统一,最好指定语言的版本,比如声明C使用c99标准,c++使用c++11标准。这里设置的变量都是CMAKE_开头(包括project命令自动设置的变量),这类变量都是CMake的内置变量,正是通过修改这些变量的值来配置cmake构建的行为。
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)
3.配置编译选项
通过add_compile_options命令可以为所有编译器配置编译选项;设置CMAKE_C_FLAGS可以配置c编译器的编译选项;设置变量CMAKE_CXX_FLAGS可配置针对c++编译器的编译选项。
add_compile_options(-Wall -Wextra -pedantic -Werror)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c++11")
4.配置编译类型
通过设置CMAKE_BUILD_TYPE来配置编译类型,可设置为Debug、Release等。
set(CMAKE_BUILD_TYPE Debug)
如果设置类型为Debug,那么对于c编译器,CMake会检查是否有针对此变异类型的编译选项CMAKE_C_FLAGS_DEBUG,如果有,则将它的配置内容加到CMAKE_C_FLAGS中。针对不同的编译类型可以设置不同的编译选项,如果对于DEbug版本,开启调试信息,不进行代码优化
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
对于Release版本,不包含调试信息,优化等级设置为2
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
5.添加全局宏定义
通过add_definitions可以添加全局的宏定义
add_definitions(-DDEBUG -DREAL_COOL_ENGINEER)
6.添加include目录
通过include_directoories来设置头文件的搜索目录
include_directories(src/c)
4.静态库,动态库和可执行文件
库文件:本质上来说库是一种可执行的二进制形式,可以被操作系统载入内存执行。一些公用函数制作成函数库,供其他程序使用,函数库分为静态库和动态库两种。静态库在程序编译时会被链接到目标代码中,程序运行时不再需要改静态库,静态库的名称一般为libxxx.a,xxx是该lib的名称,动态库在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入,因此程序运行时还需要动态库存在。动态库的名字一般为libxxx.so.major.minor,xxx是该lib的名称,major是主版本号,minor是副版本号。
ldd 可执行文件 可以查看一个可执行依赖的动态库
后缀名.o 就是object, 也就相当于windows下编译的obj文件, 俗称目标文件,该文件是指源代码经过编译程序产生的且能被cpu直接识别二进制代码。由编译器生成,具体的生成方法在不同的开发环境上是不同的.
在linux上,cpp可以先编译成.o文件,有了.o之后可以编译静态库.a,也可以编译动态库.so,编译之后在连接为可执行文件,.a链接可执行文件后可删除,.so链接可执行文件后还要保存,并且动态库链接比静态库麻烦一点在于,由于是运行时才被载入,所以载入的路径就需要注意,一般生成生成的.so文件是在生成目录下的,程序运行时会在/usr/lib或者/usr/lib64等目录中查找需要的动态库文件,若找到,则载入动态库,否则报错,不过在cmake编译时可以指定要编译成so的文件的地址。
5.c/cpp的编译
c和cpp文件是无法直接运行的,需要使用编译工具将.c等源文件转化为可执行文件,例如.exe是windows上被计算机运行,现实中用到的很多软件都由成百上千源代码文件组成,将这些源代码文件最终转化为可执行文件的过程,被称为构建build。复杂软件的构建过程会包括一些列活动,构建大型软件确实非常麻烦,一般都会有一些工具辅助完成上述工作,编译这一阶段一般分为四步,预处理,编译,汇编和链接。
gcc是一系列编译器的集合,支持cpp,go等语言,编译器也不是只有gcc一种。gcc对大量内容进行包装,隐藏了复杂步骤。
1.预处理:预编译主要处理源代码中以#开始的预编译指令,只要规则如下:处理#include预编译指令,将被包含的文件插入到该预编译指令的位置,这是一个递归的过程,如果被包含的文件还包含了其他文件,会递归完成这个过程。处理条件预编译指定,例如#if,#ifdef,#else。删除#define,展开所有宏定义。添加行号和文件名标示。
2.编译:编译的过程主要是进行词法分析、语法分析、语义分析,只进行编译,不汇编,可以生成硬件平台相关的汇编语言。gcc中使用了一个角cc1的工具。
3.汇编:变成二进制的机器码,但是它不能执行,因为它缺少系统运行所必须的库,比如C中的printf对应的汇编语言puts函数,系统还不知道puts函数在内存中的具体位置,如果使用了外部函数或者变量,还需要链接。
4.链接:有两种方式,一种是静态链接,一种是动态链接,静态链接把所有依赖的第三方库函数都打包到了一起,导致最终的可执行文件非常大,动态链接并不将那些库文件直接拿出来,而是在运行时用到了再去读取,对汇编产生的.o文件链接之后就产生了一个真正的可执行文件。动态链接库会在namespec前加上前缀lib
,最终会被命名为libnamespec.so。
不同操作系统的动态链接库文件稍有不同,linux称之为共享目标文件shared object,文件后缀为.so,windows的动态链接库Dynamic link library文件后缀为.dll。
无论何种操作系统,动态链接生成的目标文件中凡是涉及第三方库的函数调用都是地址无关的,这里的地址是进程在内存上的虚拟地址。
6. so
多个可执行文件可以共享使用系统中的共享库。每个可执行文件都更小,占用的磁盘空间也相对较小。共享库之间的隔离决定了共享库可以进行小版本的代码升级,重新编译并部署到操作系统上,并不影响它被可执行文件调用。静态链接库的任何函数有了改动,除了静态链接库本身需要重新编译构建,依赖这个函数的左右可执行文件都需要重新编译构建一遍。现在一般都是用so。
但是共享库也有缺点:1.直接迁移目标文件比较麻烦,要保证文件所需要的so都在,共享库的接口不能动。
如果linux程序报错提示缺少某个库,可以用ldd检测依赖哪个so,把相应的so拷贝到/usr/lib/或者/usr/lib64中去。如果找不到,就用环境变量LD_LIBRARY_PATH来调整。
刚才提到,Linux的动态链接库绝大多数都在/lib
和/usr/lib
下,操作系统也会默认去这两个路径下搜索动态链接库。另外,/etc/ld.so.conf
文件里可以配置路径,/etc/ld.so.conf
文件会告诉操作系统去哪些路径下搜索动态链接库。这些位置的动态链接库很多,如果链接器每次都去这些路径遍历一遍,非常耗时,Linux提供了ldconfig
工具,这个工具会对这些路径的动态链接库按照SONAME规则创建软连接,同时也会生成一个缓存Cache到/etc/ld.so.cache
文件里,链接器根据缓存可以更快地查找到各个.so
文件。每次在/lib
和/usr/lib
这些路径下安装了新的库,或者更改了/etc/ld.so.conf
文件,都需要调用ldconfig
命令来做一次更新,重新生成软连接和Cache。但是/etc/ld.so.conf
文件和ldconfig
命令最好使用root账户操作。非root用户可以在某个路径下安装库文件,并将这个路径添加到/etc/ld.so.conf
文件下,再由root用户调用一下ldconfig
。
对于非root用户,另一种方法是使用LD_LIBRARY_PATH
环境变量。LD_LIBRARY_PATH
存放着若干路径。链接器会去这些路径下查找库。非root可以将某个库安装在了一个非root权限的路径下,再将其添加到环境变量中。
7.examples
./cmake-template
├── CMakeLists.txt
├── src
│ └── c
│ ├── cmake_template_version.h
│ ├── cmake_template_version.h.in
│ ├── main.c
│ └── math
│ ├── add.c
│ ├── add.h
│ ├── minus.c
│ └── minus.h
└── test
└── c
├── test_add.c
└── test_minus.
math变成静态库,编译main.c为可执行文件,依赖math静态库
1.编译静态库
file(GLOB_RECURSE MATH_LIB_SRC
src/c/math/*.c
)
add_library(math STATIC ${MATH_LIB_SRC})
将src/c/math下的源文件编译为静态库,使用file命令获取math下所有的.c文件,通过add_library编译为math的静态库,库的类型有static指定,动态库的话为shared。
2.编译可执行文件
add_executable(demo src/c/main.c)
target_link_libraries(demo math)
通过add_executable构建可执行程序,但是对于可执行文件来说,有时候还会依赖其他的库,则需要使用target_link_libraries来声明构建此可执行文件需要链接的库。main.c使用了src/c/math下实现的一些函数结构,所以依赖于前面的math库。
要注意直到add_library就生成so文件就可以了,后续调用so文件就可以了,add_exectable是生成可执行文件了,通过外部args可执行文件能够快速测起来。
8.模块化构建
CMakeLists.txt是定义一个目录的构建系统的,所以对于模块化构建,其实就是分别为每一个子模块目录编写一个CMakeLista.txt,在其父目录中导入子目录的构建系统生成对应的目标,以便在父目录中使用。
将math目录视为子模块,为其单独定义构建系统,整个项目依赖math模块的编译结果。
1.定义子目录的构建系统
只要定义目录的构建系统,都是在此目录下创建一个CMakeLists.txt文件。
cmake_minimum_required(VERSION 3.12)
project(CMakeTemplateMath VERSION 0.0.1 LANGUAGES C CXX)
aux_source_directory(. MATH_SRC)
message("MATH_SRC: ${MATH_SRC}")
add_library(math STATIC ${MATH_SRC})
子目录一般也有自己的project,如果有需要也可以指定自己的版本号,aux_source_directory,改可以搜索指定目录(第一个参数)下的所有源文件,将源文件的列表保存到指定的变量(第二个参数)。
2.包含子目录
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
其中source_dir
就是要包含的目标目录,该目录下必须存在一个CMakeLists.txt
文件,一般为相对于当前CMakeLists.txt
的目录路径,当然也可以是绝对路径。
add_subdirectory(src/c/math)
# Build demo executable
add_executable(demo src/c/main.c)
target_link_libraries(demo math)
9.导入编译好的目标文件
在前面介绍的命令add_subdirectory
其实是相当于通过源文件来构建项目所依赖的目标文件,但是CMake也可以通过命令来导入已经编译好的目标文件。
导入库文件,使用add_library,通过指定IMPORTED选项表明这是一个导入的库文件,通过设置其属性指明其路径:
add_library(math STATIC IMPORTED)
set_property(TARGET math PROPERTY
IMPORTED_LOCATION "./lib/libmath.a")
对于库文件路径,也可以使用find_library来找。
导入成功后,就可以将该库链接到其他目标上。