1. 说明
这篇笔记用于说明如何使用cmake构建Linux驱动,这样可以方便地将driver和app作为一个整体统一构建。
2. 示例
首先来看一个代码示例,为了简化起见,我直接在驱动目录下进行构建而没有作为子目录添加到软件工程内。
cmake_minimum_required (VERSION 3.0.0)
project(driver_demo)
# 由于我的系统更新过内核,所以这里需要设置内核源代码路径
set(KDIR /home/linux/linux-5.12)
# 定义编译函数/方法,驱动编译过程主要在这个函数进行
function(compile_module obj)
set(TARGET_NAME ${obj})
# 添加目标,obj即为模块名称。由于我在build目录下构建,所以这里选择将.c和.h文件拷贝到该目录下
add_custom_target(${TARGET_NAME} ALL cp -f ${CMAKE_CURRENT_SOURCE_DIR}/*.c ${CMAKE_CURRENT_BINARY_DIR}/
COMMAND cp -f ${CMAKE_CURRENT_SOURCE_DIR}/*.h ${CMAKE_CURRENT_BINARY_DIR}/
COMMAND echo "compiling module ${obj}.ko..."
)
# 设置依赖,相当于Makefile中的 $(MODULE_NAME)-objs += demo_main.o
set(depend_objlist "demo_main.o")
# 设置编译命令
add_custom_command(TARGET ${TARGET_NAME}
POST_BUILD
COMMAND echo "obj-m := ${obj}.o" > ${CMAKE_CURRENT_BINARY_DIR}/Makefile
COMMAND echo "${obj}-objs:=${depend_objlist}" >>${CMAKE_CURRENT_BINARY_DIR}/Makefile
COMMAND make -C ${KDIR} M=${CMAKE_CURRENT_BINARY_DIR} modules
)
endfunction()
# 调用编译函数
compile_module(demo)
这里写了一个简单的demo driver,除了demo_main.c文件外不依赖于其他.c。注意,在编译驱动或其他内核模块时,虽然在本地目录进行make,但在实际编译的过程中会进入Linux kernel,所以如何重新编译过系统内核,需要在cmake文件中指定kernel source code的路径。
编译
mkdir build && cd build
cmake ..
make or make demo
3. 命令解析
function
这个命令类似于函数封装,可以将一系列命令封装成一个方法,后续只要调用该方法就行。其格式如下:
function(<name> [<arg1> ...])
<commands>
endfunction()
- 以function()开头,endfunction()结束
- name : 函数名称
- arg… : 该函数使用的参数
- commands : 函数内部调用的命令,只有函数被调用时,这些命令才会运行
除了使用固定的arg变量,还可以使用argc和argv来表示可变参数,此时argc代表参数个数,argv则是参数列表,里面的值分别用argv0, argv1 …表示。
add_custom_target
这个命令用于添加一个始终会被构建的目标,该目标可以在make中执行,类似于make all/clean之类的命令。比如上述示例中,make demo的demo既是模块名称,也是目标名称。
这个命令与后面要说的add_custom_command有紧密联系,其格式如下:
add_custom_target(Name [ALL] [command1 [args1...]]
[COMMAND command2 [args2...] ...]
[DEPENDS depend depend depend ... ]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[JOB_POOL job_pool]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS]
[SOURCES src1 [src2...]])
- Name : 目标名称,注意Name并不是可以任意设置的,有些cmake内置的命令或关键字不能使用,如:ALL, install, clean等
- ALL : 表示此目标会被添加进默认的构建目标。(不是很明白这句话的意思,但似乎每一个add_custom_target都会有一个这样的参数)
- COMMAND : 表示在构建时要运行的命令(该命令可以是shell命令,也可以是可执行文件),如果有多条,那么会按顺序依次执行
- DEPENDS : 引用add_custom_command命令创建的依赖。(没用过,也不懂)
- BYPRODUCTS : 指定命令要生成的文件
- WORKING_DIRECTORY : 指定本命令运行时对应的目录
- COMMENT : 在执行命令时输出的信息,类似于debug…
- JOB_POOL : Specify a pool for the Ninja generator. Incompatible with USES_TERMINAL, which implies the console pool. Using a pool that is not defined by JOB_POOLS causes an error by ninja at build time.
- VERBATIM : 对于有转义字符时,建议使用此参数,确保该字符会被正确转义。否则,解析时可能因平台差异而不同。
- USES_TERMINAL : 赋予命令直接访问终端的权限。
- COMMAND_EXPAND_LISTS : 扩展COMMAN参数中的列表,包括使用生成器表达式创建的列表。
- SOURCES : 执行命令执行所需的额外的源文件
注: 我在测试这个命令的时候,发现如果有多个add_custom_target,那么生成的Makefile只能执行其中一个命令。
add_custom_command
这个命令用于为当前的编译系统添加编译规则,有两个主要用法,一种生成输出文件,另一种针对目标命令。上例中就是第二种情形。
生成文件
格式如下:
add_custom_command(OUTPUT output1 [output2 ...]
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[MAIN_DEPENDENCY depend]
[DEPENDS [depends...]]
[BYPRODUCTS [files...]]
[IMPLICIT_DEPENDS <lang1> depend1
[<lang2> depend2] ...]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[DEPFILE depfile]
[JOB_POOL job_pool]
[VERBATIM] [APPEND] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS])
针对目标
第二种方式对于构建目标非常有用,可以作为目标的一部分,在构建之前或之后执行。注意,目标必须与此命令在同一目录(同一CMakeLists.txt)下。
add_custom_command(TARGET <target>
PRE_BUILD | PRE_LINK | POST_BUILD
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS])
- PRE_BUILD : 在构建目标之前运行
- PRE_LINK : 在源代码编译之后但是链接二进制文件或静态库之前运行
- POST_BUILD : 在目标构建之后运行