spdlog日志库源码:CMake构建项目

发布时间 2023-07-17 09:50:31作者: 明明1109

spdlog项目构成

Github源码:https://github.com/gabime/spdlog

spdlog项目采用CMake构建,其一级目录结构如下

$ tree -L 1
.
├── CMakeLists.txt             // 根目录CMake文件
├── INSTALL                    // 安装说明
├── LICENSE                    // license声明文件
├── README.md                  // 项目介绍文档
├── appveyor.yml               // 用于自动化构建, Windows构建平台的配置
├── bench                      // benchmark, 用于综合测试
├── build                      // 用于存放编译过程产生的中间文件
├── cmake                      // 用于存放与cmake构建项目有关的文件
├── example                    // 用户例程
├── include                    // 头文件根目录
├── logos                      // 用于存放logo
├── scripts                    // 存放脚本文件
├── src                        // 源码目录
└── tests                      // 单元测试目录

本文主要分析项目构建、源码(src、include目录下),例程(example),单元测试(tests)。其他部分,必要时也解析。


CMake构建

分析一个CMake构建的项目的源码构成,不得不分析CMake文件。spdlog的CMake文件,从位置上讲,分为三部分:
1)根目录CMakeLists.txt,CMake命令入口,负责整个项目构建的主体配置;
2)cmake目录,包含多个cmake文件,定义了专门的CMake函数,适配IDE环境,.h输入文件;
3)源码子目录下的CMakeLists.txt文件,用于子目录下的目标构建;

因为3)通常比较简单,这里主要讲1)和2)。

根目录CMakeLists.txt

cmake版本要求

# 指明对cmake的最低、最高版本要求
# cmake_minimum_required(VERSION)调用了cmake_policy(VERSION)
cmake_minimum_required(VERSION 3.10...3.21)

include专用cmake文件

#  ---------------------------------------------------------------------------------------
# Start spdlog project
#  ---------------------------------------------------------------------------------------
include(cmake/utils.cmake)
include(cmake/ide.cmake)

spdlog_extract_version()

# 设置项目名、版本、语言
project(spdlog VERSION ${SPDLOG_VERSION} LANGUAGES CXX)
message(STATUS "Build spdlog: ${SPDLOG_VERSION}")

# 使用GNU标准安装目录
# CMake会根据CMAKE_INSTALL_PREFIX变量, 构建出绝对路径, 指明BINDIR、LIBDIR、INCLUDEDIR等各种路径
include(GNUInstallDirs)

include命令是直接将指定文件包含进当前cmake文件中。
utils.cmake 包含一些工具函数,比如spdlog_extract_version,用于从.h文件中提取spdlog版本信息;
ide.cmake 用于支持IDE,以相对路径方式将头文件目录定义为变量,这样其他cmake文件只需要引用变量即可;

spdlog通过GNUInstallDirs指明库使用GNU标准安装目录,即生成多个跟安装库有关的路径的变量,形如CMAKE_INSTALL_<dir>CMAKE_INSTALL_FULL_<dir>,而在后续通过install指令将库安装到对应路径即可。

设置默认build类型

CMAKE_BUILD_TYPE默认值设为"Release"。还有另一个常用值"Debug"。

# ---------------------------------------------------------------------------------------
# Set default build to release
# ---------------------------------------------------------------------------------------
if(NOT CMAKE_BUILD_TYPE)
    # cache 缓存条目, 可以通过CMake GUI的add按钮增加. 缓存条目本质上可以跨层级进行传递的变量, 类似于全局变量
    # FORCE 默认情况下缓存条目的值不会被覆盖, 除非使用了选项FORCE
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose Release or Debug" FORCE)
endif()

注意:set + CACHE + FORCE命令,可以让该值通过CMake GUI显示并修改。CMake GUI如下图所示:

编译器配置

spdlog支持多个编译器平台,在这部分指明。支持C++11,对于其他编译器如MSVC、CygWin,有额外配置。

# ---------------------------------------------------------------------------------------
# Compiler config
# ---------------------------------------------------------------------------------------
# CMAKE_CXX_STANDARD值为0
if(NOT CMAKE_CXX_STANDARD)
    # 设置c++ 11 支持
    set(CMAKE_CXX_STANDARD 11)
    # 设置开启标准要求
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
endif()

# make sure __cplusplus is defined when using msvc and enable parallel build
if(MSVC)
    # Win10 + MSVC, ${CMAKE_CXX_FLAGS}输出:
    # /DWIN32 /D_WINDOWS /EHsc /Zc:__cplusplus /MP
    string(APPEND CMAKE_CXX_FLAGS " /Zc:__cplusplus /MP")
endif()

# 关闭c++ 扩展
set(CMAKE_CXX_EXTENSIONS OFF)

# CYGWIN: Windows上运行的类UNIX模拟环境
# MSYS: Minimal GNU(POSIX)system on Windows,是一个小型的GNU环境. 类似于CYGWIN
if(CMAKE_SYSTEM_NAME MATCHES "CYGWIN" OR CMAKE_SYSTEM_NAME MATCHES "MSYS")
    set(CMAKE_CXX_EXTENSIONS ON)
endif()

判断当前项目是否为spdlog

用户可能以多种方式引入spdlog,可能是库,也可能是以源码形式。那么,构建时,CMake如何判断呢?
spdlog是通过当前cmake文件所在路径,是否为顶层路径,来判断的。因为如果是以头文件形式include,spdlog必然不会位于项目的根目录。而结果记录在自定义变量SPDLOG_MASTER_PROJECT。

# ---------------------------------------------------------------------------------------
# Set SPDLOG_MASTER_PROJECT to ON if we are building spdlog
# ---------------------------------------------------------------------------------------
# Check if spdlog is being used directly or via add_subdirectory, but allow overriding
if(NOT DEFINED SPDLOG_MASTER_PROJECT)
    # CMAKE_CURRENT_SOURCE_DIR 当前正在处理的源目录的路径
    # CMAKE_SOURCE_DIR 源树的顶层目录
    # 也就是说, 目前没有定义SPDLOG_MASTER_PROJECT, 但当前CMakeLists位于顶层目录,
    # 依然将SPDLOG_MASTER_PROJECT定义为ON(BOOL类型值 ON/OFF)
    if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
        set(SPDLOG_MASTER_PROJECT ON)
    else()
        set(SPDLOG_MASTER_PROJECT OFF)
    endif()
endif()

选项开关

选项开关通过option来指定,用于控制编译流程。

AddressSanitizer是一个快速检测内存的工具,只能用于gcc/clang。如果想要使用,就需要配合打开一些编译选项。

关于AddressSanitizer更详细内容,可以参考这篇文章:AddressSanitizer使用介绍

# ---------------------------------------------------------------------------------------
# Set SPDLOG_MASTER_PROJECT to ON if we are building spdlog
# ---------------------------------------------------------------------------------------
# Check if spdlog is being used directly or via add_subdirectory, but allow overriding
if(NOT DEFINED SPDLOG_MASTER_PROJECT)
    # CMAKE_CURRENT_SOURCE_DIR 当前正在处理的源目录的路径
    # CMAKE_SOURCE_DIR 源树的顶层目录
    # 也就是说, 目前没有定义SPDLOG_MASTER_PROJECT, 但当前CMakeLists位于顶层目录,
    # 依然将SPDLOG_MASTER_PROJECT定义为ON(BOOL类型值 ON/OFF)
    if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
        set(SPDLOG_MASTER_PROJECT ON)
    else()
        set(SPDLOG_MASTER_PROJECT OFF)
    endif()
endif()

# option 用于控制编译流程
# option格式: option(<variable> "<help_text>" [value])
# value定义选项默认状态, 值一般是OFF或ON, 除去ON, 其他所有值默认OFF
# 该选项为ON, 代表打开所有SPDLOG_BUILD_XXX选项都打开

# SPDLOG_BUILD_ALL 控制是否编译所有目标, 如example, tests, bench
option(SPDLOG_BUILD_ALL "Build all artifacts" OFF)

# SPDLOG_BUILD_SHARED 控制是否编译为静态库(.lib/.a)
option(SPDLOG_BUILD_SHARED "Build shared library" OFF)

# precompiled headers option 控制是否预编译头文件, 能加快编译速度; 但如果文件有修改, 就会需要重新编译.
option(SPDLOG_ENABLE_PCH "Build static or shared library using precompiled header to speed up compilation time" OFF)

# build position independent code
option(SPDLOG_BUILD_PIC "Build position independent code (-fPIC)" OFF)

# FIXME: 默认值OFF?
# example options
# SPDLOG_BUILD_EXAMPLE 控制是否编译example目录下的例程
option(SPDLOG_BUILD_EXAMPLE "Build exmpale" ${SPDLOG_MASTER_PROJECT})
# SPDLOG_BUILD_EXAMPLE_HO 控制是否以头文件形式, 编译exmample目录下的例程
option(SPDLOG_BUILD_EXAMPLE_HO "Build head only exmaple" OFF)

# testing options
# 控制是否编译test目录下的例程
option(SPDLOG_BUILD_TESTS "Build tests" OFF)
option(SPDLOG_BUILD_TESTS_HO "Build header only example" OFF)

# 需要安装google benchmark 测试工具 (需要安装google benckmark)
# bench options
# 控制释放编译bench目录下的例程
option(SPDLOG_BUILD_BENCH "Build benchmarks (Requires https://github.com/google/benchmark.git to be installed)" OFF)

# AddressSanitizer 是一个快速的内存错误检测工具 (需要安装: https://github.com/google/sanitizers/wiki/AddressSanitizer)
# sanitizer options 控制是否使用AddressSanitizer
option(SPDLOG_SANITIZE_ADDRESS "Enable address sanitizer in tests" OFF)

# warning options 控制编译警告信息
option(SPDLOG_BUILD_WARNINGS "Enable compiler warnings" OFF)

# install options 控制是否安装库. 当以外部链接库方式引入时, 必须打开此选项
option(SPDLOG_INSTALL "Generate the install target" ${SPDLOG_MASTER_PROJECT})
# SPDLOG_USE_STD_FORMAT 控制是否使用std::format
# std::format 要求C++20, 而且只有部分编译器实现, 查看: https://en.cppreference.com/w/cpp/compiler_support
option(SPDLOG_USE_STD_FORMAT "Use std::format instead of fmt library. No compile-time format string checking." OFF)
# SPDLOG_FMT_EXTERNAL 控制使用外部fmt库取代spdlog内置的
# fmt库官网: https://fmt.dev/latest/index.html
option(SPDLOG_FMT_EXTERNAL "Use external fmt library instead of bundled" OFF)
# SPDLOG_FMT_EXTERNAL_HO 控制使用外部fmt库(头文件方式)取代spdlog内置的
option(SPDLOG_FMT_EXTERNAL_HO "Use external fmt header-only library instead of bundled" OFF)
# SPDLOG_NO_EXCEPTIONS 遇到异常时, 直接调用abort()终止进程, 也就是说不抛出异常
option(SPDLOG_NO_EXCEPTIONS "Compile with -fno-exceptions. Call abort() on any spdlog exceptions" OFF)

# 不允许同时开启的2个选项, 因为是互斥的
if(SPDLOG_FMT_EXTERNAL AND SPDLOG_FMT_EXTERNAL_HO)
    # FATAL_ERROR: 产生 CMake Error,会停止编译系统的构建过程
    message(FATAL_ERROR "SPDLOG_FMT_EXTERNAL and SPDLOG_FMT_EXTERNAL_HO are mutually exclusive")
endif()

# std::format跟外部fmt库不能同时开启
if(SPDLOG_USE_STD_FORMAT AND SPDLOG_FMT_EXTERNAL_HO)
    message(FATAL_ERROR "SPDLOG_USE_STD_FORMAT and SPDLOG_FMT_EXTERNAL_HO are mutually exclusive")
endif()

if(SPDLOG_USE_STD_FORMAT AND SPDLOG_FMT_EXTERNAL)
    message(FATAL_ERROR "SPDLOG_USE_STD_FORMAT and SPDLOG_FMT_EXTERNAL are mutually exclusive")
endif()

# misc tweakme options
if (WIN32) # Windows平台
    # 宽字符支持选项
    option(SPDLOG_WCHAR_SUPPORT "Support wchar api" OFF)
    option(SPDLOG_WCHAR_FILENAMES "Support wchar filenames" OFF)
else()
    set(SPDLOG_WCHAR_SUPPORT OFF CACHE BOOL "non supported option" FORCE)
    set(SPDLOG_WCHAR_FILENAMES OFF CACHE BOOL "non supported option" FORCE)
endif ()

# 检查操作系统名称, 如果是Linux平台, 就开启SPDLOG_CLOCK_COARSE选项
if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
    # 指定CLOCK_REALTIME_COARSE选项后, clock_gettime() 粗粒度获取时间, 精度由ns变为ms
    # 另一个选项CLOCK_MONOTONIC_COARSE
    option(SPDLOG_CLOCK_COARSE "Use CLOCK_REALTIME_COARSE instead of the regular clock," OFF)
else()
    set(SPDLOG_CLOCK_COARSE OFF CACHE BOOL "non supported option" FORCE)
endif ()

# 阻止子进程继承log file的文件描述符. 如果是类Unix系统, 打开文件时, 指定O_CLOEXEC选项
option(SPDLOG_PREVENT_CHILD_FD "Prevent from child processes to inherit log file descriptors" OFF)
# 阻止查询thread id. 如果是Linux系统, 通过系统调用__NR_gettid来读取thread id
option(SPDLOG_NO_THREAD_ID "prevent spdlog from query the thread id on each log call if thread id is not needed" OFF)
# 阻止thread local存储
option(SPDLOG_NO_TLS "prevent spdlog from using thread local storage" OFF)
# 阻止以atomic<T>存储 log level
option(SPDLOG_NO_ATOMIC_LEVELS
        "prevent spdlog from using of std::atomic log levels (use only if your code never modifies log levels concurrently"
        OFF)
# 关闭default logger, default logger在程序中属于registry class 数据成员
option(SPDLOG_DISABLE_DEFAULT_LOGGER "Disable default logger creation" OFF)

# clang-tidy是一个静态代码分析框架
if (${CMAKE_VERSION} VERSION_GREATER "3.5")
    option(SPDLOG_TIDY "run clang-tidy" OFF)
endif ()

if (SPDLOG_TIDY)
    set(CMAKE_CXX_CLANG_TIDY "clang-tidy")
    # 开启后,其生成的文件compile_commands.json,包含所有编译单元所执行的指令
    # 在生成期间, 决定是否启用编译命令输出
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
    message(STATUS "Enabled clang-tidy")
endif ()

if (SPDLOG_BUILD_PIC)
    # 该值是POSITION_INDEPENDENT_CODE的默认值, 用于初始化所有目标上的POSITION_INDEPENDENT_CODE属性
    # POSITION_INDEPENDENT_CODE是否创建与位置无关的目标, 打开后生成编译选项 -fPIC
    # https://blog.csdn.net/zhizhengguan/article/details/115323750
    set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif ()

# 搜索Threads库, 要求必须存在
find_package(Threads REQUIRED)
message(STATUS "Build type: " ${CMAKE_BUILD_TYPE})