做 C++ 后端的人,最怕的不是协程写不好,而是编译环境里躺着两个“看起来差不多”的开发库。Drogon 项目接 MySQL 的时候,我就在 libmariadb-dev 和 libmysqlclient-dev 的冲突上整整折腾了两个晚上。今天把这份 Drogon MySQL 兼容补丁整理出来,给所有被类似构建问题卡住的人一个可以直接照抄的答案:这两个开发库到底为什么会打架、怎么诊断、补丁怎么写、构建怎么跑。内容不长,但每一步都是实打实测过的思路,适合刚接触 Drogon 的人,也适合在 CI 里被依赖关系折磨的工程老手。
1. 冲突到底冲突在哪
1.1 两个开发库的“血缘关系”
先说清楚这俩库是什么关系。libmysqlclient-dev 是传统 MySQL 客户端的开发包,由 Oracle 维护的 MySQL 分支或与其协议兼容的实现演化而来;libmariadb-dev 则是 MariaDB 官方提供的客户端开发库。MariaDB 当年从 MySQL 分叉出来,API 层面保持了高度兼容,连最常见的几个 C API 函数名都完全一致。
问题就出在这个“高度兼容”上。两个开发包都向外提供mysql.h、mysql_version.h这些同名头文件,也都提供mysql_init、mysql_real_query、mysql_store_result等同名符号。表面上是同一套接口,底层实现却各自独立。在 Debian 系发行版里,libmariadb-dev和libmysqlclient-dev通常会被标记为互相冲突,原因是它们会向系统写入相同路径的文件。一旦因为某些间接依赖导致两个包都被塞进环境里,编译期就会陷入“头文件来自一个库,二进制库来自另一个库”的混乱局面。
下面这个表可以快速帮你理清两者的差异:
| 维度 | libmysqlclient-dev | libmariadb-dev |
|---|---|---|
| 常见配置命令 | mysql_config | mariadb_config |
| 典型头文件 | mysql.h, mysql_version.h | mysql.h, mariadb_version.h |
| 典型动态库 | libmysqlclient.so | libmariadb.so |
| 包冲突关系 | 与 libmariadb-dev 冲突 | 与 libmysqlclient-dev 冲突 |
| API 风格 | MySQL C API | 兼容 MySQL C API |
不同小版本发行版的头文件路径可能略有差别,但核心矛盾是一致的:这两个包都想“独占”MySQL C API 资源。
1.2 冲突在 Drogon 构建中的具体表现
Drogon 是一个基于 C++ 的异步 Web 框架,它内置了数据库后端支持。开启 MySQL 后端时,Drogon 的 CMake 构建逻辑会去系统里找一个mysql.h和一套客户端库。正常情况下这没什么问题,但一旦环境里同时存在 MariaDB 和 MySQL 两套开发文件,Drogon 的查找路径就可能“串线”。
最常见的是下面这类编译错误:
In file included from /usr/include/mysql/mysql.h:70, from /path/to/drogon/orm_lib/src/mysql_impl.cpp:31: /usr/include/mariadb/mysql.h:40:9: error: redefinition of ‘typedef struct st_mysql MYSQL’这种“redefinition of typedef”的报错,基本就是头文件被拆成了两半:某个模块先引入了 MySQL 的头文件,另一个模块又通过不同路径引入了 MariaDB 的头文件。两边都定义了相同结构体,编译器直接罢工。
更隐蔽的是链接期错误。比如 CMake 找到了 MariaDB 的头文件,却在链接阶段找到了libmysqlclient.so。由于两个库的符号版本、内部实现细节、依赖的辅助库不完全一致,链接器可能报出“undefined reference to `mysql_init@'”这类诡异问题,或者干脆链接成功,但程序运行后内存布局对不上,出现偶发崩溃。这类问题比编译错误更烦人,因为它不是每次都必现。
1.3 影响范围远不止 Drogon
不要觉得这是 Drogon 独有的毛病。所有使用 MySQL C API 的 C/C++ 库都可能踩雷,包括一些 ORM、连接池,甚至手写数据库客户端的项目。但 Drogon 的特殊之处在于它的构建系统会同时拉入头文件、动态库以及额外的线程和 SSL 依赖,一旦某个环节被错误库污染,后续执行cmake --build时就会反复横跳。
所以我把解决方案封装成了一个独立补丁模块,目的是让 Drogon 在遇到这种双开发库环境时,不再靠 CMake 的“好运”,而是明确地告诉构建系统:你到底应该用哪一套头文件和库。这套逻辑可以单独放进 Drogon 源码树,也可以放进你自己的工程里通过CMAKE_MODULE_PATH注入,后面会详细说。
2. 动手前先摸清底细
2.1 看当前到底装了什么
不要急着写补丁,先搞清楚机器上实际安装了哪些包。Debian/Ubuntu 系可以用下面的命令快速过滤:
dpkg -l | grep -E 'libmysqlclient|libmariadb|default-libmysqlclient|mariadb-client-core'如果看到libmysqlclient-dev和libmariadb-dev同时存在,就说明环境已经处在冲突状态。有些发行版还会安装default-libmysqlclient-dev,它可能是 MySQL 或 MariaDB 的一个过渡包,同样需要留意。
另外,dpkg -l只能看到安装状态,不能完整展示文件归属。要确认头文件到底归哪个包管,可以继续执行:
dpkg -S /usr/include/mysql/mysql.h输出会告诉你当前/usr/include/mysql/mysql.h属于libmariadb-dev还是libmysqlclient-dev。如果这条命令没有输出,说明头文件不在系统标准路径,或者这个路径被手动覆盖过。
2.2 看配置命令指向哪里
MySQL 和 MariaDB 各自提供了配置命令:mysql_config和mariadb_config。它们可以输出该开发库的 include 路径、库路径、编译选项和版本信息,是我们将要使用的关键工具。
which mysql_config mariadb_config mysql_config --version mysql_config --include mysql_config --libs mariadb_config --version mariadb_config --cc mariadb_config --include mariadb_config --libs注意观察输出。如果mysql_config --include指向/usr/include/mariadb,而mysql_config --libs输出的是-lmariadb,说明mysql_config这个命令实际上是由 MariaDB 提供的包装脚本,系统里所有看似“MySQL”的配置其实都偏向 MariaDB。反之亦然。
这里有个经验:永远不要凭命令名字猜库来源。一定要看dpkg -S $(which mysql_config)的输出,或者看mysql_config --version的返回值。
2.3 看 CMake 缓存里残留了什么
Drogon 的 CMake 构建过程会把搜到的路径和结果写进build/CMakeCache.txt。如果之前配置过 MySQL 后端,后来又换了库,缓存里可能残留旧路径。
grep -iE 'MYSQL|MARIADB' build/CMakeCache.txt重点看MYSQL_INCLUDE_DIR、MYSQL_LIBRARY、MySQL_*这类变量。如果它们的来源互相矛盾,比如 include 路径是/usr/include/mysql,但 library 路径里带着libmariadb.so,那就太明显了。
不要直接在CMakeCache.txt里手动改路径,因为 CMake 重新运行时会用新的探测结果覆盖,手动改反而容易制造新旧混合状态。正确做法是删除build目录后重新配置,或者用cmake -U 'MYSQL_*'清除相关缓存变量。
3. 补丁的设计与原理
3.1 设计思路:不硬刚,只统一
面对两个开发库的冲突,有些人会尝试通过修改 Drogon 源码里的#include <mysql.h>为#include <mariadb/mysql.h>来规避问题。我不推荐这个路径。一是 Drogon 是第三方框架,直接改源码会让后续升级变得痛苦;二是某些头文件的内部依赖链很复杂,强行替换 include 名称会把 MySQL 和 MariaDB 的宏混在一起,反而制造更多不确定性。
我的补丁思路很简单:不让 CMake 靠“运气”去搜头文件和库,而是强制它通过同一个配置命令同时拿到 include 路径和链接库。这样做的好处是,无论你用 MariaDB 还是 MySQL,得到的头文件和库一定来自同一个客户端库,不会出现“头文件在左、库在右”的分裂局面。
如果一定要在物理机上同时保留两个开发包给其他项目用,那就别让 Drogon 直接去系统路径里探索。在容器或独立安装前缀里只暴露需要的那个包,再通过配置命令指向它。这样既解决了 Drogon 的构建,也不影响其他项目。
3.2 核心模块:DrogonMysqlCompat.cmake
下面这个 CMake 模块是整个兼容补丁的核心。它先找mariadb_config或mysql_config,然后无条件使用该命令的--include和--libs输出,不靠find_path和find_library的猜测逻辑。
# cmake/DrogonMysqlCompat.cmake # 处理 libmariadb-dev 与 libmysqlclient-dev 冲突的兼容模块。 # 用法: # include(DrogonMysqlCompat) # drogon_setup_mysql(DROGON_MYSQL_INCLUDE_DIRS DROGON_MYSQL_LIBRARIES) function(drogon_setup_mysql OUT_INCLUDE_DIRS OUT_LIBRARIES) # 优先使用 mariadb_config,其次使用 mysql_config。 # 不要同时把两个配置命令都塞进搜索列表, # 务必保证 include 路径和链接库来自同一个客户端库。 find_program(MYSQL_CONFIG_EXECUTABLE NAMES mariadb_config mysql_config DOC "Path to mariadb_config or mysql_config" ) if(NOT MYSQL_CONFIG_EXECUTABLE) message(FATAL_ERROR "找不到 mariadb_config/mysql_config,请先安装 libmariadb-dev 或 libmysqlclient-dev") endif() message(STATUS "Drogon MySQL compat: using ${MYSQL_CONFIG_EXECUTABLE}") # 分别取出 include 参数和链接参数 execute_process( COMMAND "${MYSQL_CONFIG_EXECUTABLE}" --include OUTPUT_VARIABLE MYSQL_INCLUDE_ARGS OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY ) execute_process( COMMAND "${MYSQL_CONFIG_EXECUTABLE}" --libs OUTPUT_VARIABLE MYSQL_LIB_ARGS OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY ) # 把字符串按空格拆成 CMake 列表,方便后面赋给 target separate_arguments(MYSQL_INCLUDE_ARGS NATIVE_COMMAND "${MYSQL_INCLUDE_ARGS}") separate_arguments(MYSQL_LIB_ARGS NATIVE_COMMAND "${MYSQL_LIB_ARGS}") # 回传给调用方 set(${OUT_INCLUDE_DIRS} "${MYSQL_INCLUDE_ARGS}" PARENT_SCOPE) set(${OUT_LIBRARIES} "${MYSQL_LIB_ARGS}" PARENT_SCOPE) message(STATUS "Drogon MySQL include: ${MYSQL_INCLUDE_ARGS}") message(STATUS "Drogon MySQL libs: ${MYSQL_LIB_ARGS}") endfunction()这个函数的关键点是把两个execute_process放在同一个配置命令后面。因为mariadb_config --include和mariadb_config --libs必然来自同一套库,所以不会出现跨库混用的问题。如果两个 dev 包共存,但路径被某些环境变量干扰了,只要你最终选择的命令是对的,头文件和库就不会打架。
3.3 如何让 Drogon 接上这套逻辑
有了模块之后,还要让 Drogon 的 CMake 在构建 MySQL 后端时调用它。假设你的 Drogon 源码在/path/to/drogon,补丁的核心 diff 大致如下:
--- a/CMakeLists.txt +++ b/CMakeLists.txt @@ if(BUILD_MYSQL) - find_package(MySQL REQUIRED) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + include(DrogonMysqlCompat) + drogon_setup_mysql(DROGON_MYSQL_INCLUDE_DIRS DROGON_MYSQL_LIBRARIES) + + target_include_directories(drogon PUBLIC ${DROGON_MYSQL_INCLUDE_DIRS}) + target_link_libraries(drogon PUBLIC ${DROGON_MYSQL_LIBRARIES}) endif()不同版本的 Drogon 对 target 名称和 CMake 结构略有差异,但思路不变:不要再用原来那个模糊的find_package(MySQL),改为显式调用我们的兼容函数。如果你的环境里既有 MariaDB 也有 MySQL,还可以通过给 CMake 传参锁定执行路径,例如:
cmake -S . -B build -DBUILD_MYSQL=ON -DMYSQL_CONFIG_EXECUTABLE=/usr/bin/mariadb_configfind_program在有外部变量传入时,会优先使用外部指定的路径,不会再去 PATH 里找第一个。这个特性非常适合 CI 环境统一固定库来源。
3.4 为什么这样有效
传统find_path加find_library的方式,本质上是分两条独立路径去搜索。如果系统中出现两套同 API 的开发库,搜索顺序稍有变化,结果就会不一致。补丁改成“配置命令驱动”之后,实际上把两个查询合并成了一个原子操作:你要么全用 MariaDB,要么全用 MySQL,不存在中间态。
对于 Drogon 这种高度依赖异步 IO 的框架来说,头文件版本和库版本不一致不仅影响编译,更影响运行期内存布局。比如MYSQL结构体大小不同,mysql_store_result返回的字段结构在客户端库内部被分配,但你用的是头文件里声明的旧结构体,程序可能拿到错误数据。这种问题不像编译错误那么直白,排查起来非常痛苦。所以花半小时把构建逻辑做干净,比省这几分钟更划算。
4. 实操步骤:完整跑一遍兼容构建
4.1 先清理可能“打架”的环境
如果你现在正处于冲突状态,不要急着写补丁。先决定你要用哪套库。这一步选错,后面所有步骤都白做。
我通常建议在干净容器里操作。如果必须在物理机上处理,先卸载掉不想用的那个开发包:
sudo apt purge libmysqlclient-dev libmariadb-dev sudo apt autoremove然后只安装你需要的那个:
# 如果项目偏向 MariaDB sudo apt install libmariadb-dev # 或者,如果偏向 MySQL sudo apt install libmysqlclient-dev有的发行版里,安装libmariadb-dev时需要同步安装libmariadb-dev-compat,因为它提供了某些应用需要的兼容 symlink。按你的发行版提示操作即可。
这里还有一个备选方案:如果你两个包都不舍得卸载,那就用 Docker 把构建环境隔离起来。容器里只装其中一个 dev 包,物理机上的混乱不会影响你。
4.2 准备 Drogon 构建依赖
Drogon 构建依赖不止 MySQL。你需要一个能编译 C++17 的编译器、CMake、OpenSSL 开发库、jsoncpp 开发库等。在 Debian/Ubuntu 上大致是:
sudo apt install build-essential cmake libssl-dev libjsoncpp-dev zlib1g-dev uuid-dev然后把 Drogon 仓库克隆下来,进入源码根目录:
git clone <drogon-repo-url> drogon cd drogon4.3 放置兼容模块
在源码根目录下创建cmake目录,把DrogonMysqlCompat.cmake放进去。
mkdir -p cmake然后把 3.2 节的完整 CMake 函数存成cmake/DrogonMysqlCompat.cmake。建议用cat写入,避免编辑器自动加空格或转义字符。
4.4 修改 CMakeLists.txt 并应用补丁
找到CMakeLists.txt里关于 MySQL 后端的段落。不同版本写法不同,但通常会看到类似if(BUILD_MYSQL)的结构。把原来的find_package(MySQL REQUIRED)替换成对兼容模块的调用,具体 diff 可以参考 3.3 节。
如果你不想改动 Drogon 的源码,也可以在自己的工程里覆盖CMAKE_MODULE_PATH,然后在cmake目录下提供一个同样名为DrogonMysqlCompat.cmake的文件。但要注意,Drogon 自身的CMakeLists.txt仍然需要被修改,因为你得让它调用这个函数,而不是原来的查找逻辑。
应用完成后,建议用git diff看一遍改动,确认没有误改到其他 CMake 配置。
4.5 配置并编译
回到 Drogon 源码根目录,开始配置构建。重点是把 MySQL 后端打开:
cmake -S . -B build -DBUILD_MYSQL=ON -DCMAKE_BUILD_TYPE=Release如果兼容模块被正确启用,你会看到类似这样的输出:
-- Drogon MySQL compat: using /usr/bin/mariadb_config -- Drogon MySQL include: -I/usr/include/mariadb -- Drogon MySQL libs: -L/usr/lib/x86_64-linux-gnu -lmariadb -lpthread -ldl -lz看到include的前缀和libs中的库名一致,就可以确认没有跨库混用。然后执行编译:
cmake --build build -j$(nproc)如果编译中途报错,不要慌,回到第 4.1 步重新确认包环境,再清掉build目录重来。大部分问题都出在缓存污染,而不是代码本身。
4.6 编写一个最小验证程序
编译通过后,最好写一个最小程序验证 Drogon 的数据库连接池真的能连上 MySQL 或 MariaDB。
#include <drogon/drogon.h> #include <iostream> int main() { drogon::app().addListener("127.0.0.1", 8080); drogon::app().loadConfigFile("./config.json"); auto db = drogon::app().getDbClient(); auto result = db->execSqlSync("select 1 as ok"); if (!result.empty()) { std::cout << "DB connect OK, row count=" << result.size() << std::endl; } return 0; }为了跑通,你还要在config.json里配置数据库地址、用户名和密码。不过这一步只是验证运行时的库是否一致,测试通过就能说明补丁生效了。
5. 常见问题与避坑清单
5.1 问题速查表
下面这个表是我个人在实际操作中整理出来的高频问题,按优先级排列:
| 现象 | 可能原因 | 排查命令 | 解决办法 |
|---|---|---|---|
| 编译报 redefinition of typedef | 多个同名头文件被先后引入 | dpkg -S /usr/include/mysql/mysql.h | 只保留一套 dev 包,或让 include 路径统一 |
| 链接阶段 undefined reference to mysql_init | 头文件与库版本不匹配 | mysql_config --version对比实际库版本 | 用兼容模块强制使用同一个配置命令 |
| 运行时提示找不到 libmariadb.so 或 libmysqlclient.so | 动态库不在默认搜索路径 | `ldd ./build/exe | grep -E 'mysql |
cmake重新配置后变量没变 | CMakeCache 残留旧值 | `grep -iE 'MYSQL | MARIADB' build/CMakeCache.txt` |
| 同一个项目在 CI 与本地结果不同 | 两个环境的 mysql_config 指向不同 | 打印mysql_config --version | 在 CI 中固定MYSQL_CONFIG_EXECUTABLE变量 |
5.2 实操经验与避坑建议
第一,不要在同一个build目录里反复切换 MySQL 后端和 MariaDB 后端。CMake 缓存对这套东西的“记忆”比你想象中顽固,切换前先删除build目录。我试过很多次,只改 CMake 变量不删目录,最后总会漏掉某个隐藏变量。
第二,补丁函数里应优先使用mariadb_config还是mysql_config,取决于你项目里哪个库是“主”。如果你还有另一个项目用 libmysqlclient-dev,不要在这个环境里混用。最干净的做法是在 Drogon 的构建容器里只暴露需要的那套配置命令。如果物理机上的/usr/bin/mysql_config其实是 MariaDB 包装脚本,那也没问题,关键是保持它在整个构建过程中稳定不变。
第三,不要试图通过修改mysql.h的 include 宏来绕过冲突。比如给编译器传-Dmysql.h=mariadb/mysql.h之类的操作,短期可能骗过编译器,长期一定会在某个子模块里把两个头文件同时引入,造成更奇怪的错误。
第四,CI 场景下,建议每次都打印出mysql_config的路径和版本,并把MYSQL_CONFIG_EXECUTABLE写进 CI 变量。少了这一步,等哪天 runner 镜像更新了包,构建可能会无声无息地切到另一套库。
第五,链接静态库时也要注意符号冲突。如果你同时链接了某个用 MariaDB 编译的静态库和另一个用 MySQL 编译的静态库,最终二进制内可能有重复符号定义。这时候别在 CMake 层面纠结,先确认整个依赖树里混用了几套客户端库,该换则换。
这套补丁和排查方法,不只适用于 Drogon。以后你写任何基于 MySQL C API 的 C++ 组件,遇到类似“哪个库来负责”的模糊问题时,第一反应都应该是:
- 先确认一个配置命令;
- 再让 include 和 libs 全部从这一个命令生成;
- 最后固定到构建脚本里,不留给系统“自由发挥”的空间。
我个人现在的规矩是:任何 C++ 项目只要涉及 MySQL/MariaDB 客户端库,都会在 CMake 层写一个类似的原子查找函数,并且把mysql_config的路径打印在配置日志最显眼的位置。这套 Drogon MySQL 兼容补丁只是这个思路的一个具体落地。只要环境里出现 libmariadb-dev 和 libmysqlclient-dev 的影子,就用它把问题掐死在配置阶段。