gcc & make & cmake

编译

安装 gcc (多版本共存)

  • 阿里云镜像:http://mirrors.aliyun.com/gnu/gcc/

参考博客: Linux下编译安装GCC 4.9.4 - Caosiyang's Blog

# 下载gcc源码解压后的目录假定为gcc-4.9.4
cd gcc-4.9.4
sh ./contrib/download_prerequisites
cd ..
mkdir build-gcc-4.9.4
cd build-gcc-4.9.4
# gcc-4.9.4将会安装至/usr/local/gcc-4.9.4/目录下
../gcc-4.9.4/configure --prefix=/usr/local/gcc-4.9.4/ --enable-checking=release --enable-languages=c,c++ --disable-multilib
make -j4
make install

关于头文件与库的搜索路径

C_INCLUDE_PATH  # C 头文件库搜索路径, 备注: 系统本身的不在这个变量里
CPLUS_INCLUDE_PATH  # C++ 头文件库搜索路径, 备注: 系统本身的不在这个变量里
LD_LIBRARY_PATH  # 动态链接库搜索路径, 备注: 系统本身的不在这个变量里
LIBRARY_PATH  # # 静态链接库搜索路径, 备注: 系统本身的不在这个变量里

为什么通常会需要配置 LD_LIBRARY_PATH 而不需要配置 C_INCLUDE_PATHCPLUS_INCLUDE_PATH, 例如:

  • 安装 CUDA

  • 安装 openCV

  • 安装 tensorRT

关于 ldconfig

注:以下均为简单理解,并非准确理解

在 Linux 下,默认动态链接库的搜索路径保存在 /etc/ld.so.conf 中,默认动态链接库的访问使用缓存机制,存放在 /etc/ld.so.cache (二进制格式),如果在默认路径下添加了动态链接库,则需要使用 ldconfig 更新默认路径里的动态链接库至 /etc/ld.so.cache。具体来说,可能会遇到需要使用 ldconfig 命令的场景例如安装 mysql 时,默认会将 mysql 的库文件安装到 /usr/local/mysql/lib,这个目录一般是在默认的动态链接库搜索路径下,因此由于缓存机制的存在,如果在不执行 ldconfig 时,在需要使用 mysql 相关的动态链接库时,会报找不到库的错误。

ldconfig -v 用于查看已经缓存的动态链接库。

另外,ldconfig 是系统层面的一些机制,在用户层面,也可以配置 LD_LIBRARY_PATH 来添加库目录

示例(杂录)

例子1

目录结构及文件内容

main.cc
cheader/
  - MathFunctions.cc
  - MathFunctions.h

// main.cc文件
#include "MathFunctions.h"

编译方法

单条指令编译

gcc main.cc cheader/MathFunctions.cc -I cheader/ -o Demo
  • -I 选项用于增加include目录

  • -o 选项用于指定编译输出的文件位置

先编译链接库,再利用链接库编译应用

# 编译动态链接库: 两种都可以
gcc -shared -o cheader/libMathFunctions.so -fPIC cheader/MathFunctions.cc
# cd cheader && gcc -shared -o libMathFunctions.so -fPIC MathFunctions.cc && cd ..

# 使用动态链接库
# 方式一:此处main.cc与cheader/libMathFunctions.so顺序不能乱
gcc -I cheader -o Demo main.cc cheader/libMathFunctions.so
# 方式二:(待修改)可以找到链接库但找不到符号?
gcc -I cheader -o Demo -Lcheader -l MathFunctions main.cc
  • -L用于增加静态/动态链接库的搜索路径,-l用于指定静态/动态链接库的具体名称(注意不含lib前缀及文件扩展名)。stackoverflow问答的解释

  • 可以使用 gcc 的组件 nm 命令查看 .so 文件中的符号。stackoverflow

    nm --demangle --defined-only --extern-only cheader/libMathFunctions.so
    # 或
    nm -D cheader/libMathFunctions.so

编译步骤、动态链接库与静态链接库

编译步骤拆解为:

  • 编译预处理(pre-processing):将 #include 处理好,宏展开等。使用 -E 指定,生成文件后缀名习惯用 .i

  • 编译(compiling):转换为汇编代码。使用 -S 指定,生成文件后缀名习惯用 .s

  • 汇编(assembling):将汇编代码转换为目标文件。使用 -c 指定,生成文件后缀名习惯用 .o

  • 链接(linking):将目标文件进行链接,最终生成可执行文件

    • 可以把多个目标文件打包为一个作为函数库,这一过程可以借助 ar 命令来完成。函数库分为动态链接库与静态链接库

参考资料:

  • 不确定好坏的资料:https://tldp.org/HOWTO/Program-Library-HOWTO/index.html

Make

# 下面的命令用于观察具体的执行命令
# 然而使用 CMake 生成 Makefile, 接下来使用 make -n 命令以获取具体步骤, 往往不会深入到 gcc 命令
make --dry-run  # make -n
make VERBOSE=1
make --dry-run VERBOSE=1

CMake

资源:

项目结构与 cmake 基本命令

CMake 用于生成 Makefile (如果使用 make 的话), 典型的编译步骤如下

# 步骤一: 配置(Configure)
# 生成 Makefile 文件 (准确地说会根据平台不同有所变化)
mkdir build && cd build && cmake ..
# 或者是
cmake -B build -S .. && cd build
# 加上下面的参数, 会额外生成 compile_commands.json, 可以观察到相应的编译命令
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..

# 步骤二: 构建(Build)
# (1) 可以直接用 make 命令
make
# 只展示编译步骤不执行, 但得不到想要的 gcc 编译命令
make --dry-run
# 或者简写为
make -n

# (2) 或者用 cmake 命令
cmake --build .
# 只展示编译步骤不执行, 但得不到想要的 gcc 编译命令
cmake --build . -- --dry-run

# 步骤三: 安装(Install), 可选, cmake --build 命令在 cmake>=3.15 才能使用, 之前的版本只能用 make --install
# 如果在配置环节不指定 -DCMAKE_INSTALL_PREFIX=/path/to/you/wish, 默认会安装到 /usr/local/bin, /usr/local/include, /usr/local/lib 这些路径
cmake --install .

# 指定安装目录既可以在 Configure 阶段指定 -DCMAKE_INSTALL_PREFIX=/path/to/you/wish
# 也可以在 Install 阶段
cmake --install . --prefix "/path/to/you/wish"

# (TODO: 不确定含义) 如果使用 multi-configuration tools, 可以使用这个
cmake --install . --config Release

# 也可以用下面的方式代替 cmake --install . 但 cmake --install . 更具兼容性和移植性
# cmake --build . --target install

一种典型的项目组织如下

root/
  - src/      # 源文件
  - include/  # 头文件
  - build/    # out-of-tree build
  - CMakeLists.txt

备注: 有些项目源文件和头文件不会分离, 而是放在同一级目录下, 也是常见的

CMakeLists.txt 语法

重要语法索引 (cheetsheet)

初学先概览, 回头再复习. TODO: 此部分内容来自 GPT, 回头再做确认(似乎不太靠谱, 很多出现在 CMake 官方 tutorial 中的命令这里没有, 许多这里出现的, CMake 官方 tutorial 中没有).

  • 项目信息

    • cmake_minimum_required(VERSION X.Y): 指定 CMake 的最低版本要求。

    • project(<project_name>): 定义项目的名称,可以可选地指定语言(C、C++等)。

  • 源文件与目标

    • add_executable(<target> <source1> <source2> ...): 创建一个可执行目标。

    • add_library(<target> <source1> <source2> ...): 创建一个库目标(静态库或共享库)。

    • aux_source_directory(<directory> <variable>): 查找指定目录中的所有源文件,并将它们的名称存入变量。

  • 依赖关系

    • target_link_libraries(<target> <library1> <library2> ...): 指定目标所依赖的库。

    • add_subdirectory(<directory>): 添加子目录,通常用于包含其他 CMakeLists.txt 文件。

  • 变量和选项

    • set(<variable> <value>): 定义或设置变量。

    • option(<option_name> <description> <default>): 定义一个布尔选项,用户可以选择该选项。

    • if(<condition>) ... endif(): 条件语句,用于控制构建时的行为。

    • foreach(<var> <items>) ... endforeach(): 迭代语句,用于遍历列表中的每一项。

  • 包含和查找模块

    • find_package(<package_name> [REQUIRED]): 查找并加载指定的包。

    • include_directories(<directory>): 添加包含目录。

    • link_directories(<directory>): 添加链接目录。

  • 生成规则和安装

    • install(TARGETS <target> DESTINATION <directory>): 指定安装目标及其安装位置。

    • file(<command> <args>): 处理文件的多种操作,例如复制、删除、生成等。

  • 配置和构建选项

    • set(CMAKE_BUILD_TYPE <type>): 设置构建类型,例如 Debug 或 Release。

    • set(CMAKE_CXX_STANDARD <version>): 设置 C++ 标准。

  • 脚本与宏

    • macro(<name> <args>) ... endmacro(): 定义一个宏,可以多次调用。

    • function(<name> <args>) ... endfunction(): 定义一个函数,具有局部作用域。

  • 自定义命令和目标

    • add_custom_command(...): 定义自定义构建命令。

    • add_custom_target(...): 定义自定义构建目标。

  • 其他

    • message(<message>): 打印消息到标准输出。

    • include(<module>): 包含其他 CMake 文件。

add_library, add_executable 分别表示创建一个库/可执行文件以及其所需要的源文件, 它们之后可能会跟上 target_link_libraries, target_include_directories, target_compile_definitions, 分别表示为了生成这个库/可执行文件需要的其他库名,头文件目录,编译时宏定义.

这类 target_xxx 的命令还有:

  • target_compile_features: 用于指定 C++ 标准, 例如: target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

  • target_compile_options: 直接指定编译选项, 但需要手动解决跨平台问题. 例如: target_compile_options(tutorial_compiler_flags INTERFACE -Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused). 所谓跨平台, 指的是例如指定 C++ 标准, 使用 gcc 编译器, 写法是 gcc -std=c++11 ..., 而 MSVC 编译器的写法是 cl /std:c++11. 因此一般来说, 使用 target_compile_options 时可能会见到下面的写法:

# 下面的生成器表达式语法需要 >=3.15
cmake_minimum_required(VERSION 3.15)

# 这个是一个“虚拟”的库,用于添加编译选项
add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

# COMPILE_LANG_AND_ID 是 cmake 一个内置的生成器表达式, 生成器表达式的语法是 $<xxx:yyy>, 而 COMPILE_LANG_AND_ID 的语法是
# $<COMPILE_LANG_AND_ID:lang, compiler_id>
# 其中 lang 代表编程语言, compiler_id 是编译器名(可以有多个,用逗号隔开), 如果检查到编译器, 这一项的值为"1",否则为"0"

set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")

# "$<1:...>" 的值为 "...", "$<0:...>" 的值为空字符串 ""
target_compile_options(tutorial_compiler_flags INTERFACE
  "$<${gcc_like_cxx}:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>"
  "$<${msvc_cxx}:-W3>"
)

特殊变量: PROJECT_SOURCE_DIR, PROJECT_BINARY_DIR, CMAKE_CURRENT_SOURCE_DIR

官方 Tutorial 笔记

链接 (本文写作时的版本为cmake==3.31.0-rc2): https://cmake.org/cmake/help/latest/guide/tutorial

Step1: A Basic Starting Point

主要介绍了单个源文件和头文件的情形, 但内容其实比较多

(1) 首先介绍了 project, cmake_minimum_required, add_executable 命令, 以及使用 cmake 命令行工具进行配置和构建

(2) 然后使用 set 命令配置 cmake 内置的变量 CMAKE_CXX_STANDARDCMAKE_CXX_STANDARD_REQUIRED, 从而实现对 C/C++标准 的管控

(3) 最后补充介绍了 project 中可以定义项目的版本号, 在这之后的 CMakeLists.txt 内容里就可以使用 <PROJECT-NAME>_VERSION_MAJOR<PROJECT-NAME>_VERSION_MINOR 内置变量, 为了将这两个变量变得可以在源码中使用(源码中用宏来获取这两个变量), 可以采用如下方案: 配置 config.h.in, 在这个模板文件中使用 @<PROJECT-NAME>_VERSION_MAJOR@ 的语法用于宏值的定义, 并且在 CMakeLists.txt 中使用 configure_file 命令使得 cmake 在构建项目时生成真正的 config.h 文件, 最后使用 target_include_directories 让可执行文件的编译过程正确地 include 出于 PROJECT_BINARY_DIRconfig.h 文件.

Step2: Adding a Library

主要介绍了多个源文件的情形, 且库代码位于单独的目录内, 且包含有自己的 CMakeLists.txt 文件.

(1) 内层 CMakeLists.txt 引入了 add_library 命令, 而外层的 CMakeLists.txt 为了使得最终的 main 程序能链接库, 需要使用 add_subdirectory 用于执行得到库, 使用 target_link_libraries 来让 main 链接到库, 使用 target_include_directories 使得 main 在编译时能识别到库的头文件

(2) 演示了如何利用 option 来为 cmake 在配置阶段增加 -D<OPTION_NAME>=<value> 来控制编译行为. 在官方的例子中, 场景是这样的, 我们的库是一个计算 sqrt 的函数, 而 main 程序希望实现如下功能: 如果希望使用我们自己实现的数学库, 那么就编译出该数学库, 并且 main 程序链接并使用其实现; 如果希望使用官方的数学库 cmath, 那么就不编译我们自己的数学库. 具体实现方式如下: TODO

Step3: Adding Usage Requirements for a Library

主要是优化 Step1 和 Step2 中的一些写法: 在 Step1 中, C/C++标准 的管控是使用 set 命令全局管控 CMAKE_CXX_STANDARDCMAKE_CXX_STANDARD_REQUIRED 来实现的, 这样就不能实现不同模块采用不同的 C/C++标准; 在 Step2 中, 外层的 main 不仅需要链接库, 还需要通过 target_include_directories 添加库文件目录.

Step4: Adding Generator Expressions

本部分感觉不是核心用法, Generator Expressions 是 CMakeLists.txt 的脚本特性

Step5: Installing and Testing

引入 cmake 项目安装, 以及单元测试, TODO: 此处代码移除

# 首先 config
mkdir build && cd build && cmake ..
# 然后可以 install/test/pack
ctest
cpack

Step6: Adding Support for a Testing Dashboard

本部分感觉不是核心用法, 单元测试可以更花哨, 可以有个仪表盘来展示

Step7: Adding System Introspection

本部分与 Step6 都使用了 include 语法, 来引入 /path/to/cmake/Modules 中的其他“脚本”文件

Step8: Adding a Custom Command and Generated File

本部分实现了一个特殊功能: 首先编写一段 C 代码, 其执行后会生成一个头文件, 整个项目的构建过程是先得到这个头文件 (通过自定义命令来实现: add_custom_command), 然后其他的库/可执行文件可能需要使用这个头文件

Step9: Packaging an Installer

本部分介绍了项目打包, 具体介绍了使用 cpack 的打包方式, 打包为 .tar.gz 文件(二进制安装), 操作比较简单, CMakeLists.txt 加几行配置即可. 其他的项目打包和发布方式待探索

Step10: Selecting Static or Shared Libraries

前序步骤所有的库都是使用的静态库(当然也可以在 add_library 是设定为动态库, 但前面没有明确提到), 本部分介绍怎么“优雅”地配置以生成动态链接库

Step11: Adding Export Configuration

TODO: 似乎是让本项目在其他项目中能使用, TODO

Step12: Packaging Debug and Release

TODO

单文件

引入: cmake_minimum_required, project, add_executable, aux_source_directory

最简单的情况:

  • 一个源文件和头文件, 用于编译得到一个可执行文件(引入 cmake_minimum_required, project, add_executable)

  • 多个源文件和头文件在同一目录, 最终得到一个可执行文件(引入 aux_source_directory)

库与链接

引入: add_library, target_link_libraries, target_include_directories, add_subdirectory

# cmake_minimum_required
# project (Demo)
add_subdirectory(math)

# math/CMakeLists.txt 包含如下内容
# aux_source_directory(. DIR_LIB_SRCS)
# add_library (MathFunctions ${DIR_LIB_SRCS})

add_executable(Demo main.cc)
target_link_libraries(Demo MathFunctions)
# 类似地, 也有一个 target_include_directories 来指定 include 目录
# target_include_directories(Demo PUBLIC "${PROJECT_BINARY_DIR}")

注意: add_executable 用于指定可执行文件依赖的源文件, 而 target_link_libraries 用于指定可执行文件依赖的库文件. 并且 add_executable 必须在 target_link_libraries 之前. 虽然按照标准的 gcc 编译流程, 编译步骤确实是:

(1) 先编译并打包得到库文件 libMathFunctions.a (2) 使用源文件 main.c 编译得到 main.o (3) 使用 main.olibMathFunctions.a 链接得到 Demo

但 CMakeLists.txt 的语法里不关心 main.o 这种中间产物, 它只关系为了生成 Demo, 它需要哪些源文件 (对应于 add_executable), 哪些库文件 (对应于 target_link_libraries)

add_subdirectory 命令表示“执行”子目录的 CMakeLists.txt: 父目录的 CMakeLists.txtadd_subdirectory 之前通过 set 定义的变量, 在子目录中的 CMakeLists.txt 是可见的, 并且在子目录 CMakeLists.txt 中定义的变量, 在父目录 add_subdirectory 之后, 对父目录的 CMakeLists.txt 也是可见的

configure_file

configure_file: 根据模板文件生成宏定义头文件, 模板文件名通常是 config.h.in, 而宏定义头文件名一般是 config.h, 在使用 cmake 生成 Makefile 时, 会自动得到这个 config.h (与编译选项无关). 在 C/C++ 源代码中可以去通过 #include "config.h" 从而确定 config.h 中的宏定义

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
# 自动生成如下变量: Demo_VERSION_MAJOR 是 1, Demo_VERSION_MINOR 是 0
project(Demo VERSION 1.0)

set(USE_MYMATH ON)

configure_file (config.h.in config.h)
# 也可以更明确地这么写, PROJECT_SOURCE_DIR 和 PROJECT_BINARY_DIR 是预定义变量
# configure_file ("${PROJECT_SOURCE_DIR}/config.h.in" "${PROJECT_BINARY_DIR}/config.h")

# 添加可执行文件的源文件
add_executable(Demo demo.cxx)
# 明确添加 include 目录
target_include_directories(Demo PUBLIC "${PROJECT_BINARY_DIR}")

demo.cxx

#include <iostream>
#include "config.h"

int main(int argc, char* argv[])
{
#ifdef USE_MYMATH
    std::cout << "USE_MYMATH defined" << std::endl;
#else
    std::cout << "USE_MYMATH undefined" << std::endl;
#endif
    std::cout << " Version " << VERSION_MAJOR << "." << VERSION_MINOR << std::endl;
    return 0;
}

config.h.in 的写法如下:

// 这是注释:
// 如果在 CMakeLists.txt 的逻辑里, USE_MYMATH 变量的值为 ON, 那么生成的 config.h 里将包含 #define USE_MYMATH
// 如果在 CMakeLists.txt 的逻辑里, USE_MYMATH 变量的值为 OFF, 那么生成的 config.h 里将包含 /* #undef USE_MYMATH */
#cmakedefine USE_MYMATH
// 生成的 config.h 中包含宏和值: VERSION_MAJOR:1, VERSION_MINOR:0
#define VERSION_MAJOR @Demo_VERSION_MAJOR@
#define VERSION_MINOR @Demo_VERSION_MINOR@

自定义编译选项

TODO: 搞完整

  • option + add_definitions

  • option + config.h.in + include "config.h"

引入: option, add_definition, if

前面已经看到过 cmake 像这样 -DCMAKE_EXPORT_COMPILE_COMMANDS=ON 的选项, 事实上, cmake 可以自定义编译选项. 涉及到的相关 cmake 语法有

  • option: 增加一个 cmake 的编译选项, 也就是增加一个这种用法 cmake -D<选项名>=<值>

  • add_definitions: 将编译选项直接传递给编译器, 也就是利用 gcc 的 -D<宏名>=<值> 参数, 例如: gcc -DDEBUG main.c -o main

cmake -DUSE_MYMATH=ON ..

为此需要在 CMakeLists.txt 中包含如下内容

# USE_MYMATH 的默认设置为 ON, 也就是定义 USE_MYMATH 这个宏
option (USE_MYMATH "Use provided math implementation" ON)
add_definitions(-DUSE_MYMATH)

# 根据宏来确定编译方式
if (USE_MYMATH)
  include_directories ("${PROJECT_SOURCE_DIR}/math")
  add_subdirectory (math)
  set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)

C 代码中可以这样使用

#include <stdio.h>
#include <stdlib.h>

#ifdef USE_MYMATH
  #include "MathFunctions.h"
#else
  #include <math.h>
#endif

int main(int argc, char *argv[])
{
    if (argc < 3){printf("Usage: %s base exponent \n", argv[0]); return 1;}
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
#ifdef USE_MYMATH
    printf("Now we use our own Math library. \n");
    double result = power(base, exponent);
#else
    printf("Now we use the standard library. \n");
    double result = pow(base, exponent);
#endif
    printf("%g ^ %d is %g\n", base, exponent, result);
    return 0;
}

安装与测试

CMake 官方 Tutorial-5

mkdir Step5_build
mkdir Step5_install
cd Step5_install
# 设置为 ON 或者 OFF 会决定 libSqrtLibrary.a 是否被安装
cmake -DUSE_MYMATH=OFF ../Step5
cmake --build .
cmake --install . --prefix=/path/to/Step5_install

# libtutorial_compiler_flags.a 不被安装是因为它被这样声明为了 INTERFACE
# add_library(tutorial_compiler_flags INTERFACE)

TODO: 基本已确认

关于 cmake 命令与 CMakeLists.txt 内的相对路径问题: CMakeLists.txt 的写法只需关注它本身与源码的相对路径, 而不必关心 cmake 命令行的参数. 这一点和 python 有区别 (python 代码里的 import 需要与 python 作为启动脚本时的当前目录需要相配)

CMakeLists.txt
main.cc
MathFunctions.cc
MathFunctions.h

CMakeLists.txt 的文件内容如下:

cmake_minimum_required (VERSION 2.8)
project (Demo2)

# 查找目录下的所有源文件, 并将名称保存到 DIR_SRCS 变量, 在这个例子里是 main.cc MathFunctions.cc
aux_source_directory(. DIR_SRCS)

# 指定生成目标, 表示生成的可执行文件名为 Demo
# 在这个例子里, 等价于 add_executable(Demo main.cc MathFuctions.cc)
add_executable(Demo ${DIR_SRCS})

使用 cmake .mkdir build && cd build && cmake .. 或者 cmake -B build -S . 均可以成功, 后两者的 Makefile 在 build/Makefile

CMAKE_CURRENT_SOURCE_DIR 总是指向 CMakeLists.txt 所在目录, 而 CMAKE_CURRENT_BINARY_DIR 总是指向生产的文件目录

Last updated