前言

  最近在处理Linux交叉编译,多工程结合的问题,决定从头认真学一下构建工具Makefile、CMAKE,此文为记录,学习路径为gcc->Makeflie->CMAKE。

编译原理

  首先,所有的文件本质上都是一串字符串,它的后缀只是方便人类,对计算机并不是那么重要(尤其是Linux),完整的编译过程如下:
alt text
由上图易知主要有四个步骤:

  1. 预处理(*.i -E):进行字符段的替换,如include、define
  2. 编译(*.s -S):将文本文件翻译为汇编文件
  3. 汇编(*.o -C):将.s文件转化为可重定位目标程序(relocatable object program),或者说机器语言
  4. 链接(可执行文件):链接所有.o文件,产生最终的可执行文件

GCC

  GCC(GNU Compiler Collection),是一个开源编译软件集合,在官方手册中能看到其最权威的介绍。在Linux中,直接使用apt install gcc即可;而在Windows中,使用Mingw,其是GCC(GNU Compiler Collection)在Windows平台上的移植版本,Mingw32是精简包,64则更全面些,还包括的POSIX的Pthread库,推荐MinGW64下载),同时记得配置环境变量。
常见组成部分:

  • g++:默认链接标准C++库,同时兼容C
  • gcc:编译C

常见软件:

  • ar: 这是一个程序,可通过从文档中增加、删除和析取文件来维护静态库文件。通常使用该工具是为了创建和管理连接程序使用的静态库文档。该程序是 binutils 包的一部分
  • ld: GNU 连接程序。该程序将目标文件的集合组合成可执行程序。该程序是 binutils 包的一部分

GCC默认头文件搜索路径:

1
echo | gcc -v -x c -E -

在遇到头文件、库找不到的时候,可以使用此命令检查

C语言

编译一个C语言程序,你可能看到这些后缀:

  • .a:静态库(Static object library(archive))
  • .so:动态库/运行时库(Shared object library)
  • .c
  • .h
  • .i
  • .s
  • .o

例程:

1
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, char** argv)
{
printf("Hello World");
return 0;
}

  可以在命令行中键入以下指令,两部分是等效的,但拆分过程有助于理解,作为参数的文件名字可有多个,一起执行
1
2
3
4
5
6
gcc -E main.c -o main.i # 仅预处理
gcc -S main.c -o main.s # 预处理并产生汇编文件
gcc -c main.c # 预处理、汇编、编译成.o
gcc main.o -o exec # 生成目标可执行文件
------------------------
gcc main.c -o exec # 其实可以直接一步到位

静态库

静态库(.a/.lib)本质是多个.o文件的归档包,在链接时一起组合到最终的可执行程序,但具体的函数接口需要配套的.h文件暴露出来,这样在编译阶段才不会报错,而在链接阶段,仅需要.a文件中的二进制代码和符号表来完成地址重定位。

  1. 编译为可重定位目标程序
    1
    gcc -c xxx.c 
  2. 编译静态库
    约定俗成静态库名字:lib+库名,如liboperation.a
    1
    ar -r [lib自定义库名.a] [.o] [.o]
  3. 使用(链接)
    直接使用一步到位的命令:
    1
    gcc main.c liboperation.a -o exec 
      从上面的过程可以看出,所谓的静态库就是预先编译了一些.c源程序,打包成一个库,方便我们调用和管理。
      具体介绍一下ar,使用ar -h可查看具体使用方法,其中[]是可选项,{}是必选项
    1
    2
    3
    4
    5
    6
    Usage: ar [emulation options] [-]{dmpqrstx}[abcDfilMNoPsSTuvV] [--plugin <name>] [member-name] [count] archive-file file...
    ar -M [<mri-script]
    ```
    ### 动态库
    动态库(.so/.dll)仅在运行时才去加载,并不会被打包进入可执行文件中,仅留下去哪找动态库的标记,一样要配套的.h文件来暴露接口。
    1. 同样要编译出<kbd>可重定位目标程序</kbd>,但这次要加个参数<kbd>-fpic </kbd>(意为生成位置无关代码,确保动态库的可被加载到内存任意位置)
    gcc -c -fpic xxx.c
    1
    2
    3
    2. 编译动态库
    ```shell
    gcc -shared [.o] [.o] -o [lib自定义库名.so]
  4. 链接
    -wl:gcc传递参数给链接器ld的语法,表示将后面的参数交给链接器
    rpath:库路径写入可执行文件的内部信息中,运行时优先寻找
    1
    gcc [.c] -o [可执行文件名] -l[库名] -Wl,rpath=[库路径]
    注:如libmydyn.so 简化为 mydyn,编译器会自动补全 lib 前缀和 .so 后缀
    -L:指定动态库的搜索路径

    C++

    编译一个C++语言程序,你可能看到这些后缀:
  • .a:静态库(Static object library(archive))
  • .c .cc .cp .cpp .cxx .c++
  • .h:C/C++都可以用
  • .li
  • .s
  • .o
    至于其他部分,和C部分一模一样,只是gcc变为了g++,同时g++是向C兼容的,毕竟C可以看作C++的一个子集

    Makefile

  • GNU Make 官方网站:https://www.gnu.org/software/make/
  • GNU Make 官方文档下载地址:https://www.gnu.org/software/make/manual/
  • Makefile Tutorial:https://makefiletutorial.com/
      make 指令会在当前目录下找到一个名字叫 Makefilemakefile 的文件,如果找到,它会找文件中第一个目标文件(target),并把这个文件作为最终的目标文件,如果 target 文件不存在,或是 target 文件依赖的 .o 文件(prerequities)的文件修改时间要比 target 这个文件新,就会执行后面所定义的命令 command 来生成 target 这个文件,如果 target 依赖的 .o 文件(prerequisties)也存在,make 会在当前文件中找到 target 为 .o 文件的依赖性,如果找到,再根据那个规则生成 .o 文件

    基本格式

    1
    2
    targets: prerequisties
    [Tab键] command
    不得使用空格替代Tab,makefile语法对缩进有严格规定

.PHONY:伪目标,由上面流程可知,目标现存文件,如果文件存在则不会执行,用于像clean这种目标。

变量

  有三种定义变量的方式,取变量使用$()

示例

  将上述的Helloworld例程保存为test.c,下列的文件保存为Makefile,而后调用

1
make

即可在同目录下看见编译出的可执行文件out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 编译器设置
CC = gcc
# 编译选项
CFLAGS = -Wall -Wextra -std=c99

# 目标文件
TARGET = out

# 源文件
SRCS = test.c

# 默认目标
all: $(TARGET)

# 编译目标
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $@ $^
@echo "编译完成: $(TARGET)"

# 清理目标
clean:
rm -f $(TARGET)
@echo "已清理: $(TARGET)"

# 伪目标声明
.PHONY: all clean

CMAKE

  CMake是一个构建工具,相比于其他的编程语言,CMake更复杂,也更关注底层细节,正是由于CMake的出现,使得C++真正实现了跨平台,官方参考手册

学习的必要性

  • CMake是学习C/C++的必经之路
  • CMake不仅可以管理C/C++
  • 支持生成几乎所有主流IDE的项目
  • 真正的跨平台:支持Windows、Linux、macOS、Cygwin等

  缺点也很明显,作为一个高级工具,CMake是一门语言,具有一定的学习成本,而且版本的更新差别比较大。推荐至少使用版本3.20。

Windows中使用CMake构建

  CMake官网,在Windows下,默认使用的编译器是MSVC,使用cmake --help可以查看本平台的Generators,带星号的就是默认的,例如我的是* Visual Studio 17 2022,一般来说我们更经常使用的是MinGW Makefiles

1
2
3
4
5
cmake --version
cmake -B build_msvc
cmake --build build_msvc

cmake -B build_MinGw -G "MinGW Makefiles"

以下是一个非常简单的例子:
1
2
3
cmake_minimum_required(VERSION 3.16.2)
project(Hello)
add_executable(Hello hello.cpp)

1
2
3
4
5
6
7
#include <iostream>

int main(int argc, char **argv)
{
std::cout << "hello" << std::endl;
return 0;
}

  add_executable即为项目添加可执行文件,第一个参数name是可执行文件的名字,与项目名无关系,相关的指令官方文档中同样说明得很详细。

在Linux中构建

  1. 使用包管理工具安装
    1
    sudo apt-get install cmake
  2. 编译源码安装
      下载源码,自行编译结果
    1
    2
    3
    4
    5
    6
    7
    8
    sudo apt install build-essential
    sudo wget https://cmake.org/files/v3.28/cmake-3.28.0.tar.gz
    tar -zxvf cmake-3.28.0.tar.gz
    cd cmake-3.28.0
    ./configure
    sudo make
    sudo make install
    cmake --version

    使用

      与在Windows中使用的指令一致,在生成后可以发现build目录下还有个CMakeCache.txt文件,这是CMake的缓存机制,如果发现定义的变量没有及时生效,可以rm -rf ./build,重新cmake

CMake的语法

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.16)

message("hello
world")

message(${CMAKE_VERSION})
1
2
touch first.cmake
cmake -P first.cmake

  在命令行中,使用:

1
2
3
mkdir build
cd build # cmake生成的配置文件在终端命令行的所在目录下
cmake ..

变量

  变量的种类:CMake预定义的、自定义,变量存储的都是字符串,对于变量的操作有:setunset以及list方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
cmake_minimum_required(VERSION 3.16)

# 设置变量
set(var1 "test") # 不加双引号也一样,有的系统使用双引号还要加反义字符
message(${var1})
message(--------)

# 设置多个值
set(Listvalue a1 a2)
message(${Listvalue})
set(Listvalue a3;a4) # 等同上面这个例子,也说明了变量可覆盖
message(${Listvalue})
message(--------)

# 查询\设置环境变量,如:PATH
message($ENV{PATH})
set(ENV{CXX} "g++")
message($ENV{CXX})
message(--------)

# 删除变量
unset(ENV{CXX})
# message($ENV{CXX}) # 由于变量未定义,解除注释运行会报错

# List方法
set(Listvalue a1 a2 a3) # 对比
message(${Listvalue})
unset(Listvalue)
## 追加项
list(APPEND port p1 p2 p3)
message(${port})
## 查长度
list(LENGTH port len) # 操作名称,操作对象,返回的变量
message(${len})
## 查index
list(FIND port p2 index) # 操作名称,操作对象,查找的值,返回的变量
message(${index})
## 删除
list(REMOVE_ITEM port p1) # 操作名称,操作对象,删除的项
message(${port})
## 插入项
list(APPEND port p5)
list(INSERT port 2 p4) # 插入项
message(${port})
## 反转
list(REVERSE port)
message(${port})
## 排序:按照ASCII码排序
list(SORT port)
message(${port})

  同时CMake也预先定义了些变量供我们使用,如CMAKE_PROJECT_NAMECMAKE_PROJECT_VERSION等CMAKE开头的变量,更多变量可查看官方手册

流程控制

  1. 条件控制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    cmake_minimum_required(VERSION 3.16)

    set(flag TRUE)
    # set(flag FALSE)

    #条件控制
    if(flag)
    message(ok)
    else()
    message(false)
    endif()
    # 非
    if(NOT flag)
    message(ok)
    else()
    message(false)
    endif()
    # 或
    if(NOT flag OR flag)
    message(ok)
    else()
    message(false)
    endif()
    # 与
    if(NOT flag AND flag)
    message(ok)
    else()
    message(false)
    endif()

    # 大小判断
    if(1 LESS 2)
    message("小于")
    else()
    message("大于")
    endif()

    if("ok" LESS 233)
    message("less") # 条件语句的比较操作默认是基于字符串的字典序
    else()
    message("larger")
    endif()

    if(1 EQUAL "1")
    message("EQUAL") # 会输出等于,这是因为存储的都是字符串
    endif()
  2. 循环
      在CMake中实现循环有两种方法,分别是foreachwhile,其中比较推荐for,因为相比较while具有更确定的执行次数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    cmake_minimum_required(VERSION 3.18)

    foreach(var RANGE 3)
    message(${var})
    endforeach(var RANGE 3)

    message(---------)

    set(My_List 1 2 3)

    foreach(var IN LISTS My_List ITEMS 4 f) # 遍历并且追加
    message(${var})
    endforeach(var IN LISTS My_List 4 f)

    message(---------)

    # zip,拼接,注意这是3.18版本才引入的特性
    set(L1 one two three four)
    set(L2 1 2 3 4 5)

    foreach(num IN ZIP_LISTS L1 L2)
    message("word = ${num_0}, num = ${num_1}")
    endforeach(num IN ZIP_LISTS L1 L2)
    for运行测试

函数

  定义函数的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.16)

function(My_func FirstArg)
message("MyFunc Name: ${CMAKE_CURRENT_FUNCTION}")
message("FirstArg :${FirstArg}")
set(FirstArg "New")
message("FirstArg again:${FirstArg}")
message("ARGV0 ${ARGV0}")
message("ARGV1 ${ARGV1}")
message("ARGV2 ${ARGV2}") # 可以传多个,与接受不匹配也可以调用
endfunction(My_func FirstAg)

set(FirstArg "first value")
My_func(${FirstArg} "value") # 可以传多个,与接受不匹配也可以调用
message("FirstArg :${FirstArg}") # 体现了函数的作用域范围

作用域

  CMake有两种作用域:

  1. 函数作用域
  2. 文件夹作用域
    使用add_subdirectory()命令执行嵌套目录中的CMakeLists.txt列表文件时,子CMakeLists会继承父CMakeLists的变量,可使用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    cmake_minimum_required(VERSION 3.16)

    project(scope)

    function(Outfunc)
    message("-> Out: ${Var}")
    set(Var 2)
    Infunc()
    message("<- Out: ${Var}")
    endfunction(Outfunc)

    function(Infunc)
    message("-> In: ${Var}")
    set(Var 3)
    message("<- In: ${Var}")
    endfunction(Infunc)

    set(Var 1)
    message("->Global:${Var}")
    Outfunc()
    message("<-Global:${Var}")
    执行结果

      其实不推荐写宏,不方便阅读,因为它过于灵活了,会读就行,CMake的宏也相当于替换,但是有类似函数的传参。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    cmake_minimum_required(VERSION 3.16)

    # 使用宏相当于替换
    macro(Test myVar)
    set(myVar "new value")
    message("argument: ${myVar}")
    endmacro(Test myVar)

    set(myVar "first value")
    message("myVar: ${myVar}")
    Test("value")
    message("myVar: ${myVar}")

    # 打印顺序为:
    # myVar: first value
    # argument: value 这是因为是传参,不是变量
    # myVar: new value

    生成器表达式

      基本语法:$<操作符条件:需要包含的文件路径>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    ## 定义静态库目标canstack
    add_library(canstack STATIC
    $<TARGET_OBJECTS:Utils_OBJ>
    $<TARGET_OBJECTS:CanSM_OBJ>
    $<TARGET_OBJECTS:CanNm_OBJ>
    $<TARGET_OBJECTS:Nm_OBJ>
    $<TARGET_OBJECTS:RingBuffer_OBJ>
    $<TARGET_OBJECTS:PduR_OBJ>
    $<TARGET_OBJECTS:CanTp_OBJ>
    $<TARGET_OBJECTS:Com_OBJ>
    $<TARGET_OBJECTS:Dcm_OBJ>
    $<TARGET_OBJECTS:Dem_OBJ>
    )# 这是CMake的生成器表达式,通过这种方式可以将多个对象库的目标文件合并到一个静态库中

    ## 设置头文件包含路径
    target_include_directories(canstack PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> # 有个变量引用
    $<INSTALL_INTERFACE:include>
    $<INSTALL_INTERFACE:include/General>
    $<INSTALL_INTERFACE:include/Configs>
    )# 第一个是仅在项目构建时生效,后三个是在项目安装后生效,区别在于前者用来编译本地库
    还可嵌套,用于实现条件编译,内层返回真假从而决定外层是否有效:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    add_executable(myapp
    main.cpp
    $<$<CONFIG:Debug>:${DEBUG_FILES}> # Debug 模式包含这些文件
    )
    --------------------
    add_executable(myapp
    main.cpp
    $<$<AND:$<PLATFORM_ID:Windows>,$<CONFIG:Debug>>:src/win_debug.cpp>
    )

    CMake构建项目的方式

    直接写入源码路径的方式

      适用于小工程,直接使用add_executable,也是最简单的方式,第一个参数是可执行文件名,后面的是所依赖的文件;project指定名字与所用的语言。
    1
    2
    3
    4
    5
    ├── animal
    │ ├── dog.cpp
    │ └── dog.h
    ├── CMakeLists.txt
    └── main.cpp
    1
    2
    3
    4
    5
    cmake_minimum_required(VERSION 3.16)

    project(Animal CXX)
    add_executable(Animal main.cpp animal/dog.cpp)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    #include "animal/dog.h"

    int main(int argc, char **argv)
    {
    std::cout << "hello world" << std::endl;

    Dog dog;
    std::cout << dog.barking() << std::endl;

    return 0;
    }
    1
    2
    3
    4
    5
    6
    #include "dog.h"

    std::string Dog::barking()
    {
    return std::string("dog wang wang");
    }
    1
    2
    3
    4
    5
    6
    7
    8
    #pragma once
    #include <string>

    class Dog
    {
    public:
    std::string barking();
    };

    CMakeList嵌套(最常用)

      首先是调用子目录CMake变量的方法来构建整个工程:使用include方法可以引入子目录中的cmake后缀的配置文件,将配置加入add_executable中即可,相当于在上面直接配置文件变量的基础上使用子CMakeLists的变量管理子目录中的文件,这种方法类似Makefile的处理方法在CMake中并不常用。同样给出一个极简的例子:
    1
    set(animal_sources animal/dog.cpp animal/cat.cpp)
    1
    2
    include(animal/animal.cmake)
    add_executable(Animal main.cpp $animal_sources)
      接下来再发掘一下CMake的功能,使其更好用:
  • add_library(名字 类型):定义一个库,类型包括STATIC、SHARED、OBJECT,生成的库名为lib[name]
  • target_include_directories(目标 路径) :设置头文件路径
  • target_source
  • add_subdirectory():添加子模块目录,自动在该目录下查找CMakeLists.txt文件
  • target_link_libraries(目标 所链接的库):在主应用中链接库,从而能够使用

  同样给出一个简单的子模块例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cmake_minimum_required(VERSION 3.22)
# Enable CMake support for ASM and C languages
enable_language(C ASM)

# CAN_Stack include paths
set(CAN_Stack_Include_Dirs
${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/INCLUDE
${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/INCLUDE/Configs ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/INCLUDE/General

${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/utils

)

set(CAN_Stack_Src
${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/utils/util_tick.c
${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/Cfg/util_platform_adapter.c
)

add_library(CAN_Stack OBJECT)
target_include_directories(CAN_Stack PUBLIC ${CAN_Stack_Include_Dirs})
target_sources(CAN_Stack PRIVATE ${CAN_Stack_Src})

target_link_libraries(CAN_Stack PUBLIC stm32cubemx)

  这个例子是创建了一个链接文件(.o)的集合,对应add_library参数里的OBJECT,而后在主CMakeLists.txt中包含与引用:
1
2
3
4
5
6
7
add_subdirectory(cmake/CAN_stack)

target_link_libraries(${CMAKE_PROJECT_NAME}
stm32cubemx
CAN_Stack
# Add user defined libraries
)

CMake与共享库

  当项目是库(静态库 / 动态库),且需要:

  1. 被其他项目引用(例如,作为第三方库提供给别人使用);
  2. 安装到系统标准路径(如 usr/local/lib、usr/local/include),符合系统的目录规范;
  3. 支持 find_package(canstack) 语法(让其他项目一键找到并链接你的库),其通过.cmake配置文件查找第三方库

  此时就需要学习这些命令,首先是install命令,其可以理解为是复制到指定地方的命令,确保库的文件、头文件、配置文件被正确安装,其他项目才能方便地复用,其通用格式为:

install(<子命令> <待安装内容> DESTINATION <安装路径> [其他参数])

  • <子命令>:指定待安装的内容类型(如 TARGETS 对应库 / 可执行文件,EXPORT 对应目标配置文件,DIRECTORY 对应目录,FILES 对应单个文件)。
  • DESTINATION:指定安装路径(相对路径,默认基于 CMAKE_INSTALL_PREFIX,如 DESTINATION lib 对应 安装目录/lib)。

  而后介绍一下find_package,他是CMake中用来查找和导入外部库的命令,实现自动定位系统中已安装或者指定路径的库,以下为相关文件介绍:

  • PackageNameTargets.cmake:记录库的目标信息(路径、编译路径等)
  • PackageNameConfig.cmake.in:手动创建的模板文件
  • PackageNameConfig.cmake:find_package的入口文件,指导如何找到库,即Targets.cmake文件
  • PackageNameConfigVersion.cmake:记录库的版本信息,支持版本检查

  find_package有两种查找库的方式,Config模式和Module模式,前者针对支持CMake的库,即库本身基于CMake构建并安装了配置文件,其工作流程如下:

  • CMake 会在预设路径(系统目录、自定义路径)中搜索 PackageNameConfig.cmakepackagename-config.cmake(区分大小写取决于库的定义)。
  • 找到配置文件后,自动导入库的目标(如 OpenCV::Core),并设置相关变量(如 PackageName_INCLUDE_DIRS、PackageName_LIBRARIES)

实际项目示例

  首先是给出模板文件.cmake.in的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
@PACKAGE_INIT@

# 导入目标配置(由 install(EXPORT) 生成的 CAN_StackTargets.cmake)
include("${CMAKE_CURRENT_LIST_DIR}/CAN_StackTargets.cmake")

# 声明依赖(如果 CAN_Stack 依赖 stm32cubemx,需要确保它能被找到)
if(NOT stm32cubemx_FOUND)
find_dependency(stm32cubemx REQUIRED)
endif()

# 标记库已成功找到
set(CAN_Stack_FOUND TRUE)


而后是CMakeList.txt的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
cmake_minimum_required(VERSION 3.22)

project(CAN_Stack LANGUAGES C ASM)
# Enable CMake support for ASM and C languages
enable_language(C ASM)

add_library(CAN_Stack STATIC) # 生成的是静态库

# CAN_Stack include paths
target_include_directories(CAN_Stack PUBLIC
# 构建时:使用相对路径(基于当前 CMakeLists.txt 所在目录)
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/INCLUDE>

# 安装时:使用安装目录下的相对路径(与 install(DIRECTORY) 对应)
$<INSTALL_INTERFACE:include>

)

set(CAN_Stack_Src
${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/utils/util_tick.c
# ${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/Cfg/util_platform_adapter.c

)


#target_include_directories(CAN_Stack PUBLIC ${CAN_Stack_Include_Dirs})
target_sources(CAN_Stack PRIVATE ${CAN_Stack_Src})

target_link_libraries(CAN_Stack PUBLIC stm32cubemx)

# -------------------------- 安装和导出配置(关键步骤) --------------------------

# 1. 安装目标文件和头文件
install(TARGETS CAN_Stack
EXPORT CAN_StackTargets # 标记该目标需要导出
ARCHIVE DESTINATION lib # 静态库安装路径(若后续改为 STATIC 库)
LIBRARY DESTINATION lib # 动态库安装路径(若改为 SHARED 库)
RUNTIME DESTINATION bin # 可执行文件路径(若有)
INCLUDES DESTINATION include # 头文件搜索路径
)

# 2. 安装头文件(复制到安装目录的 include 下)
install(DIRECTORY ../../Middlewares/CAN_Stack/INCLUDE/
DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
install(DIRECTORY ../../Middlewares/CAN_Stack/utils/
DESTINATION include/utils
FILES_MATCHING PATTERN "*.h"
)
install(DIRECTORY ../../Middlewares/CAN_Stack/Cfg/
DESTINATION include/Cfg
FILES_MATCHING PATTERN "*.h"
)
install(DIRECTORY ../../Middlewares/CAN_Stack/CanNm/
DESTINATION include/CanNm
FILES_MATCHING PATTERN "*.h"
)

install(DIRECTORY ../../Middlewares/CAN_Stack/GenData/ # 源码中 GenData 的路径
DESTINATION include/GenData # 安装到目标路径(必须与 INTERFACE 路径一致)
FILES_MATCHING PATTERN "*.h" # 只安装 .h 头文件(避免复制无关文件)
)

# 3. 导出目标配置文件(供 find_package 使用)
install(EXPORT CAN_StackTargets
FILE CAN_StackTargets.cmake
NAMESPACE CAN_Stack:: # 命名空间,避免冲突
DESTINATION lib/cmake/CAN_Stack # 配置文件安装路径
)

# 4. 生成并安装 Config.cmake 和 ConfigVersion.cmake
include(CMakePackageConfigHelpers)

# 生成 Config.cmake(根据模板)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/CAN_StackConfig.cmake.in # 模板路径
${CMAKE_CURRENT_BINARY_DIR}/CAN_StackConfig.cmake # 生成路径
INSTALL_DESTINATION lib/cmake/CAN_Stack # 安装路径
)

# 生成 ConfigVersion.cmake(版本信息)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/CAN_StackConfigVersion.cmake
VERSION 1.0.0 # 替换为实际版本号
COMPATIBILITY SameMajorVersion # 版本兼容性规则
)

# 安装生成的配置文件
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/CAN_StackConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/CAN_StackConfigVersion.cmake
DESTINATION lib/cmake/CAN_Stack
)

  配置完成相关文件后,使用如下指令:
1
2
3
mkdir build_can_stack && cd build_can_stack
cmake ../cmake/CAN_stack/ -DCMAKE_INSTALL_PREFIX=../install -G "MinGW Makefiles" -DCMAKE_TOOLCHAIN_FILE=-DCMAKE_TOOLCHAIN_FILE="../cmake/gcc-arm-none-eabi.cmake"
cmake --build . --target install

  而后在工程的主CMakeList.txt中,使用find_package命令即可找到并使用自创的库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
cmake_minimum_required(VERSION 3.22)

# 获取项目根目录
get_filename_component(ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
#导入native工具链配置
include(${ROOT_PATH}/cmake/toolchains/native.cmake)

# set(TI_MIN_CMAKE_VERSION "3.15.0" CACHE STRING INTERNAL)
# set(TICLANG_ARMCOMPILER "$ENV{TICLANG_ARMCOMPILER}")
# include(${ROOT_PATH}/cmake/toolchains/ticlang.cmake)


project(CANStackTest) # 定义项目名称
enable_testing() # 启用CMake的测试功能

# 生成名为CANStackTest的可执行文件
add_executable(CANStackTest main.c
src/example_nm_autosar.c
src/example_nm_osek.c
src/example_com.c

canstack_user_configs/util_platform_adapter.c
canstack_user_configs/Nm_Cbk.c
canstack_user_configs/Nm_Lcfg.c
canstack_user_configs/Com_Lcfg.c
canstack_user_configs/GenData/Com_Signal_Cfg_BLE.c
)

# 设置可执行文件的头文件包含路径
target_include_directories(CANStackTest PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/canstack_user_configs>
) # $<BUILD_INTERFACE:>是CMake的生成式表达式,用于指定“仅在项目构建阶段生效的路径”,而不用安装到系统目录

# 查找并链接canstack库
find_package(CanStack 2.0 REQUIRED PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../output/can_stack/cmake/native)#查找已安装的canstack库,要求2.0,并通过PATHS显式指定搜寻路径
# find_package(CanStack 2.0 REQUIRED PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../output/can_stack/cmake/ticlang/m3)
#
target_link_libraries(CANStackTest PRIVATE CanStack::canstack -lpthread) # 链接库

# 注册测试用例,配合前面的enable_testing()使用
add_test(
NAME CANStackTest
COMMAND CANStackTest
)


  在查找库的依据就是在库名后面加字符串,如一个名为CAN_Stack的静态库,那设置CAN_Stack_DIR变量find_package就会优先去这找,这就是CMake的配置文件模式,指定这个目录是去找XXXConfig.cmake文件,
1
2
set(CAN_Stack_DIR ${CMAKE_CURRENT_SOURCE_DIR}/install/lib/cmake/CAN_Stack)
find_package(CAN_Stack 1.0 REQUIRED)

配置踩坑说明

  在配置库的时候,应该让用户配置与库本身的实现代码分立,在使用库target_include_directories时,特别是用到install命令时,应该分开用BUILD_INTERFACEINSTALL_INTERFACE,生成器表达式来构建以确保能够在不同情况下都能正确找到头文件,注:生成器表达式不支持列表。
  在更新配置后,应清除对应的buildinstall文件夹重新编译以确保修改内容生效。
  关于路径:${CMAKE_CURRENT_SOURCE_DIR}/../../Middlewares/CAN_Stack/INCLUDE意为退后两级目录再进入,而不是传统完整的目录

OpenCV

  OpenCV官方releases下载opencv_contrib
  配置参考:OpenCV安装教程:Windows 安装 Visual Studio + OpenCV + OpenCV contrib