一、简介#

MSYS2 is a collection of tools and libraries providing you with an easy-to-use environment for building, installing and running native Windows software.

msys2是一个在 Windows 操作系统上模拟类 Unix 目录的一套编译工具链与链接库的集合。其功能与vcpkg有重叠之处,但 msys2 提供的功能远比 vcpkg 丰富。

截至撰写本文之时,LLVM 工具链最新版本为 18.1.6。

二、前置知识#

在了解 msys2 的用途之前,请确保已知悉下述知识点。

1. 编译工具链#

(1) GCC#

The GNU Compiler Collection includes front ends for C, C++, Objective-C, Fortran, Ada, Go, D and Modula-2 as well as libraries for these languages (libstdc++,...).

GCC,全称 GNU 编译器集合,是由开源社区维护的一套编译工具链(gcc 只是其中一个用于 C 语言的编译程序),它最常用于各个 Linux 发行版中,其在 Windows 平台下也有移植版本,例如 MinGW。

(2) LLVM#

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.

LLVM 不是缩写,该工具链本身就叫这个名字。在不同平台上的表现与 GCC 各有优劣。它既可在类 Unix 环境下使用,也为 Windows 平台提供支持。

(3) MSVC#

该编译工具链由微软提供,与 Visual Studio 绑定,主要在 Windows 平台上使用。使用 Visual Studio 进行编译时,若没有设置使用第三方工具链,则默认使用 MSVC 进行编译。

2. C/C++ 运行时#

C/C++ 存在标准库,这些标准库在在各个平台上对外提供的 API 均一致。例如,stdio.h头文件下定义的printf()函数,不论在 Windows 平台中还是在各个 Linux 发行版中,不论是使用 gcc、clang 还是使用 msvc 编译,其返回值、参数类型均一致。但在统一的 API 之下,是各个平台、各个编译工具链各自不同的实现,这些实现往往会被编译为动态链接库,这些动态链接库也被称为运行时。在特定平台下使用特定编译工具链编译的二进制文件,只要使用了标准库,就会链接到这些运行时。也因此,在没有该平台 / 该编译工具链提供的运行时的环境下,执行该二进制文件(或执行链接到此文件的可执行文件)将发生找不到动态链接库的错误。

下面以 C++ 为例,演示这种错误。

#include <iostream>

int main() {
    std::cout<<"Hello, world!\n";
    return 0;
}

LLVM 使用的 C++ 标准库是其自己实现的 libc++,GCC 使用的 C++ 标准库则是 libstdc++,因此使用 clang++ 编译后的二进制文件(或链接到此文件的可执行文件)不能在没有由 LLVM 工具链提供的链接库的 Linux 发行版中执行。实际上,把所需的动态链接库复制一份,放在二进制文件所在目录下后,二进制文件也能正常运行。

三、MSYS2 基础#

msys2 的安装请参考官方网站,此处不做赘述。

无法更新密钥/可信数据库?

如果你是根据官方教程安装的 msys2,那么大概率在安装时会卡在:

==> Updating trust database...

如果你对你的网络有信心,则什么都不要做等待安装完成即可;否则就断网。

如果你是通过Scoop安装的,则大概率在首次运行 msys2 时会遇到:

gpg: error retrieving '[email protected]' via WKD: Connection timed out
gpg: error reading key: Connection timed out
gpg: refreshing 1 key from hkps://keyserver.ubuntu.com
gpg: key F40D263ECA25678A: "Alexey Pavlov (Alexpux) <[email protected]>" not changed
gpg: Total number processed: 1
gpg:              unchanged: 1

直接按 CTRL+C 退出进程,再关闭 msys2 后重新打开即可。

1. 运行环境选择#

由于运行时的差异,msys2 为不同的工具链提供了不同的运行环境。

环境名架构工具链C 运行时C++ 运行时
msysx64GCCcygwinlibstdc++
ucrt64x64GCCucrtlibstdc++
clang64x64LLVMucrtlibc++
mingw64x64GCCmsvcrtlibstdc++

msys2 中也有支持 x86/ARM64 架构的环境,此处未列举。各个环境之间的具体差异,请参考官方网站。如果想使用 GCC 工具链,推荐使用 ucrt64 环境;如果想使用 LLVM 工具链,推荐使用 clang64 环境。msys 环境中安装的库会共享至所有环境,因此不推荐使用该环境,除非是安装 git、cmake 等第三方通用工具链。

2. 命令行环境#

打开 cmd(或者 PowerShell),执行 clang64(或者 ucrt64)命令,即可进入 msys2 环境的 shell。该 shell 类似于 bash,兼容常用的 bash 命令,并且 msys2 内置了很多常用的程序,例如 grep、xargs 等。

从命令行启动 msys2 时,除 msys 环境外,其他环境均会继承此命令行下的所有环境变量,包括 PATH。因此如果在 Windows 上也安装了其他编译工具链,则在 msys2 的环境内也可以使用该工具链。

$ where clang
C:\Users\Engin\software\Scoop\global\apps\msys2\2024-05-07\clang64\bin\clang.exe
C:\Users\Engin\software\Scoop\global\apps\mingw-winlibs-llvm-ucrt\current\bin\clang.exe

由于作者是通过 Scoop 软件安装的 msys2 与 LLVM(UCRT 运行时)工具链,此处通过 where 命令查找到的 clang 二进制文件所在路径有两个,一个是由 msys2 的 clang64 环境提供的工具链,另一个则是不依赖 msys2 的独立的 LLVM 工具链。

虽然 msys2 允许使用非 msys2 的工具链,但不推荐这么做。因为这么做可能会导致工具链找不到由 msys2 提供的链接库;也有可能会因为 msys2 提供的链接库的运行时与工具链的运行时不一致导致报错。

若不希望继承当前命令行下的环境变量,则请使用如下命令:

> msys2 -clang64

其中,clang64应当被替换为你所希望使用的环境名。

3. 包管理器与工具链安装#

包管理器是类 Unix 系统的特色,不能不品尝。msys2 使用 pacman 作为包管理器,更新也是通过 pacman 更新。有关 pacman 使用方法请自行搜索,此处不做赘述。msys2 的包名统一按照mingw-w64-[clang/ucrt-][架构]-[软件名]格式命名,且在任一环境下均可以安装其他所有环境的包(但非此环境下的包不能使用),因此请一定确保输入的包名正确。以 clang64、ucrt64 和 mingw64 环境为例,安装对应工具链的命令分别是:

$ pacman -S mingw-w64-clang-x86_64-toolchain

另外,推荐使用 clang64 环境的原因在于mingw-w64-clang-x86_64-clang包和mingw-w64-clang-x86_64-clang-tools-extra包。前者不仅提供了编译器等基础工具,还提供了用于代码格式化的 clang-format;而后者提供了用于静态代码检查的语言服务器 clangd(可用于 vscode)。

四、CMake 基础#

CMake是一个生成构建文件的工具,支持 Windows 和类 Unix 平台。构建文件,即类似于 Makefile 文件、Visual Studio 的工程文件等记录如何编译项目的文件。有了这些文件以后,构建工具即可根据这些文件正确编译整个项目。为什么不直接写构建文件?因为构建文件过于复杂,不是一般人能够维护的。

CMake 的关键字甚至语法都不需要逐个记住,只需活用搜索引擎查找用法即可。CMake 在开始之前请确保已经安装对应环境下的 CMake 以及所需要使用的第三方库。截至撰写本文之时,CMake 最新版为 3.29.5。

$ pacman -S mingw-w64-clang-x86_64-cmake

1. 前置知识#

pkg-config is a helper tool used when compiling applications and libraries.

pkg-config 是用于查找已安装的第三方链接库的工具。它常用于为编译器提供正确的链接选项。该工具在安装 msys2 的工具链时就已经安装了。

$ pkg-config --libs lua
-llua -lm

上述命令查找了 lua 库,pkg-config 给出了正确的链接选项。

2. 基本使用#

首先,在工程根目录下创建一个名为CMakeLists.txt的文本文件,CMake 将会根据此文件生成构建文件。一个CMakeLists.txt文件样例如下:

# 设置 CMake 最低版本要求,不必一定是当前使用版本。
cmake_minimum_required(VERSION 3.29.3)

# 修改 CMake 提供的内置变量
set(CMAKE_C_STANDARD 17)                # 设置 C 语言标准为 C17
set(CMAKE_CXX_STANDARD 17)              # 设置 C++ 标准为 C++17
set(CMAKE_EXPORT_COMPILE_COMMANDS on)

# 设置项目名(ritsu)、项目版本号(0.1.0)、使用语言(C、C++)
project(ritsu VERSION 0.1.0 LANGUAGES C CXX)

# 查找 pkg-config,通过 pkg-config 查找并配置第三方库
find_package(PkgConfig REQUIRED)
pkg_search_module(TESSERACT REQUIRED tesseract) # 查找 tesseract 库
pkg_search_module(LUA REQUIRED lua)             # 查找 lua 库

# 添加头文件搜索目录
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src ${TESSERACT_INCLUDE_DIRS} ${LUA_INCLUDE_DIRS})

# 查找文件
# 以递归方式查找所有位于当前工程目录的 src 文件夹下的 .c 文件
# 查找结果将赋值给 ALL_SOURCES 变量
file(GLOB_RECURSE ALL_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c)

# 生成可执行文件
add_executable(${PROJECT_NAME} ${ALL_SOURCES})

# 链接第三方库
target_link_libraries(${PROJECT_NAME} PRIVATE ${TESSERACT_LIBRARIES} ${LUA_LIBRARIES})

在 CMake 的语法中,${}中的字符将被视为变量,CMake 在执行时会替换为此变量的真实值。CMAKE 内置的常用变量有:

  • PROJECT_NAME,当前项目的名称,仅在 project()语句之后可用。
  • CMAKE_CURRENT_SOURCE_DIR,当前CMakeLists.txt 所处目录
  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY,静态链接库生成目录
  • CMAKE_LIBRARY_OUTPUT_DIRECTORY,动态链接库生成目录
  • CMAKE_RUNTIME_OUTPUT_DIRECTORY,可执行文件生成目录
  • CMAKE_C_STANDARD,C 语言的标准(例如 C17)
  • CMAKE_CXX_STANDARD,C++ 的标准(例如 C++17)
  • CMAKE_EXPORT_COMPILE_COMMANDS ,是否导出编译时的命令,这些命令可以为 clangd 提供静态代码检查的依据。

某些第三方库会提供 CMake 的模块文件,可以直接使用find_package()导入;但若某些库没有提供,则推荐使用 pkg-config。pkg-config 提供了 CMake 模块文件,使得 CMake 可以通过 pkg-config 查找并加载第三方库信息。其使用格式如下:

pkg_search_module(<标识符> REQUIRED <库名>)

第三方库的头文件目录将会赋值给<标识符>_INCLUDE_DIRS变量,链接库则会赋值给<标识符>_LIBRARIES变量。

编写完CMakeLists.txt文件后,在当前目录下执行如下命令即可完成编译:

$ cmake -G "MinGW Makefiles" .  # 最后的 "." 表示指定当前文件夹为 CMakeLists.txt 所在的工程目录
$ mingw32-make                  # 如果安装了 make,则可以直接使用make

mingw32-make 是由 MinGW 提供的 make 的 Windows 平台移植版。如果安装了 msys 环境下的 make 包,则-G选项可以使用"Unix Makefiles"或者"MSYS Makefiles"值。该选项指定 CMake 应该为何种构建系统生成构建文件。

命令执行完毕后,可在CMAKE_RUNTIME_OUTPUT_DIRECTORY变量对应的目录下找到可执行文件(如果没有设置此变量,则应该处于当前工程根目录下),同时会注意到 CMake 会在工程目录的根目录生成了不少缓存文件,这严重影响了工程目录的结构。这一问题将在下一小节得到解决。

3. 预设#

当需要编译出不同的版本的二进制文件时(例如需要 Windows 版和 Linux 版),频繁修改CMakeLists.txt不是明智的选择。此处推荐使用 CMake 的预设。CMakePresets.json即为保存预设的文件,存储格式为 JSON,需要与CMakeLists.txt处于同一目录下。一个 CMake 预设文件的样例如下:

{
    // CMake 版本要求
    "cmakeMinimumRequired": {
        "major": 3,
        "minor": 29,
        "patch": 3
    },

    // 预设
    "configurePresets": [
        {
            "binaryDir": "${sourceDir}/build", // 指定 CMake 缓存文件以及构建文件生成路径
            "cacheVariables": {
                // 初始 CMake 变量,可在 CMakeLists.txt 中直接使用
                // 也可修改默认的 CMake 变量
                "CMAKE_ARCHIVE_OUTPUT_DIRECTORY": "${sourceDir}/lib",
                "CMAKE_C_STANDARD": "17",
                "CMAKE_EXPORT_COMPILE_COMMANDS": "on",
                "CMAKE_LIBRARY_OUTPUT_DIRECTORY": "${sourceDir}/bin",
                "CMAKE_RUNTIME_OUTPUT_DIRECTORY": "${sourceDir}/bin"
            },
            "hidden": true, // 不允许直接使用
            "name": "base" // 预设名
        },
        {
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug" // debug 模式,禁用优化并为二进制文件附加调试信息
            },
            "hidden": true,
            "inherits": "base", // 继承自 base 预设
            "name": "debug"
        },
        {
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release" // release 模式,默认启用 O2 优化
            },
            "hidden": true,
            "inherits": "base",
            "name": "release"
        },
        {
            "cacheVariables": {
                "CMAKE_C_COMPILER": "clang" // 指定 C 语言编译器
            },
            "generator": "MinGW Makefiles", // 指定为何种构建系统生成构建文件
            "hidden": true,
            "name": "windows"
        },
        {
            "description": "Windows Debug Configuration",
            "displayName": "Windows Debug",
            "inherits": ["debug", "windows"],
            "name": "win-dbg"
        },
        {
            "description": "Windows Debug Configuration",
            "displayName": "Windows Debug",
            "inherits": ["release", "windows"],
            "name": "win-rel"
        }
    ],

    // 预设的版本
    "version": 8
}

在 CMake 的预设中,${sourceDir}会被替换为工程目录的路径。类似的宏扩展请参考官方文档 - Macro Expansion,此处不做赘述。使用 CMake 预设时,命令行参数需要添加--preset参数,命令样例如下:

$ mkdir build && cd build   # 创建构建文件缓存目录并切换到此目录
$ cmake .. --preset win-dbg # 以 win-dbg 预设生成构建文件
$ mingw32-make              # 编译项目

按照上述方式生成的构建文件将会全部处于build目录下,因此也必须在该目录下执行 mingw32-make 命令。

4. CTest#

CTest 是 CMake 自带的一个用于单元测试的程序。正确编写CMakeLists.txt文件并编译项目后,在构建文件所在路径执行 ctest 命令即可执行所有单元测试并查看结果。

# ...
# 省略部分代码
# ...

# 此处仅当构建 debug 版本时才启用单元测试
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    enable_testing()

    # 添加子项目,test 文件夹内存放单元测试代码
    add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/test)
endif()