简介:这是一套面向量化交易系统开发者与 C++ 后端工程师的 WonderTrader 编译依赖集合,主要用于在 Ubuntu 环境下补齐回测、实盘和数据服务组件构建时缺失的第三方头文件,缓解依赖版本不一致、路径配置繁琐以及从源码逐个拉取耗时等问题。压缩包合计约两千个文件,其中一千八百五十六个 hpp 头文件、一百四十四个 h 头文件,整体约十四点五兆,主要提供 Boost 等常用基础库以及容器、格式化、序列化、时间处理与文档解析等接口,目录紧凑,便于随工程归档和复用。依赖基于 gcc 十一点四版本的 Ubuntu 环境编译整理,使用者只需按说明调整构建脚本中的依赖路径变量,即可接入由 Git 管理的现有项目,减少环境搭建与编译排错成本。已有二百四十二人学习,适合需要快速复现构建环境、核对头文件引用关系并排查编译链路问题的读者参考。
1. 一次链接失败的下午
A同学在 Ubuntu 上装 WonderTrader 的依赖库,前面 apt 装得挺顺,boost、yaml-cpp、zmq 一股脑全用系统源,cmake 也过了,最后链接阶段炸了:undefined reference to boost::filesystem::path::...刷了满屏。他以为是 boost 没装上,删了重装三遍,问题照旧——真正的原因是系统里同时躺着两个 boost,CMake 找到的是头文件新、库文件旧的那一套。
这类依赖库环境在量化、行情、回测这类 C++ 项目里极其常见,Ubuntu 上尤其容易踩坑:系统源自带的库版本普遍偏保守,而框架又要 C++17、要某个最低版本的 boost、要 protobuf 和 zmq 能对得上 ABI。装不好不是跑不起来这么简单,是编译能过、一跑就崩,或者回测结果对不上,排查起来跟黑匣子一样。这篇记录适合两类人:第一次在 Ubuntu 上从零搭 WonderTrader 依赖库的,以及装到一半翻车、想知道该回退到哪一步的。
2. 依赖清单与版本取舍:Ubuntu 上 WonderTrader 到底要装哪些库
动手之前先把清单和版本定死,这是整件事里最省时间的一步。依赖库这种东西,装的时候图快随便 apt 一把梭,后面编译报错就得一个个反查,返工成本远高于前期多花二十分钟确认版本。我一般会把依赖分成三层来看:编译期要的、链接期要的、运行期要的,三层混在一起想,就很容易出现「装是装上了,但链接器找不到」这种玄学问题。
2.1 依赖分层:工具链、C++ 库、运行时三块分开算
第一层是工具链,包括 gcc/g++、make、cmake、git、pkg-config。这一层决定了你能不能编译 C++17 代码、CMakeLists 里的cmake_minimum_required能不能被满足。WonderTrader 这类框架的构建脚本通常要求 CMake 3.13 以上,GCC 9 以上,这两个门槛在 Ubuntu 20.04 及以后的版本里系统源自带就能过,不需要额外折腾。
第二层是 C++ 库本身,也是真正容易出事的部分:
- boost:文件系统、线程、正则、时间日期这几块用得最多,是整个依赖里体量最大、编译最慢的一个;
- yaml-cpp:配置文件解析,仓位、合约、调度这些配置都走它;
- zmq(libzmq + cppzmq):行情和交易通道的底层消息队列;
- protobuf:部分模块的序列化,和 zmq 经常成对出现;
- fmt / glog / spdlog:格式化与日志,属于编译期头文件依赖偏多的那类;
- openssl、curl:网络请求和加密;
- libmariadb-dev 或 libmysqlclient-dev:数据落库;
- python3-dev:Python 侧接口编译时要用的头文件。
第三层是运行期,主要是 libstdc++ 的版本、动态库搜索路径、以及 Python 侧的 numpy 之类。这一层最容易在「编译机器上好好的,换台机器就起不来」的时候暴露出来。
提示:不要一上来就
apt install libboost-all-dev。这个包会把 boost 的所有组件和开发头文件全塞进 /usr,看起来省事,实际上是后面版本冲突的最大来源。
2.2 Ubuntu 20.04 与 22.04 的版本对照
不同 Ubuntu 版本系统源自带的库版本差异不小,下面这张表是我在两类机器上实际会对照着看的内容。表里的版本是系统源大致的自带水平,用来做决策参考,不是让你严格对齐——真正要求以项目 CMakeLists 和 README 里写的最低版本为准。
| 组件 | 20.04 系统源大致版本 | 22.04 系统源大致版本 | 我的选择 | 理由 |
|---|---|---|---|---|
| gcc / g++ | 9.x | 11.x | 用系统自带 | 都满足 C++17,不需要额外装 |
| cmake | 3.16 | 3.22 | 用系统自带 | 满足 3.13 的最低要求 |
| boost | 1.71 | 1.74 | 源码编译或系统源二选一,不能混 | 混用必然出现头文件与库不匹配 |
| yaml-cpp | 0.6.2 | 0.7.0 | 源码编译固定到 0.6.x | 0.7 改过接口,老代码容易编译失败 |
| fmt | 6.x | 8.x | 源码编译并锁定版本 | fmt 大版本之间 ABI 不兼容 |
| protobuf | 3.6 | 3.12 | 系统源 | 只做序列化,够用 |
| libzmq | 4.3.x | 4.3.x | 系统源 | 版本稳定,不需要自编 |
| openssl | 1.1.1 | 3.0 | 跟随系统,不跨版本混用 | 3.0 与 1.1.1 的 API 有差异 |
| mariadb client | 10.3 | 10.6 | 系统源 | 驱动层兼容性好 |
这里有个反直觉的点:新版本系统不一定更省事。22.04 的 openssl 3.0 会让一些老代码在编译时直接报deprecated甚至找不到符号,而 20.04 的 boost 1.71 在部分模板写法上又比 1.74 宽松。所以不要去追「最新版」,要追「项目验证过的那个区间」。
2.3 安装前缀约定:别把依赖装进 /usr
这一条是我踩过最多次的坑,也是整个安装流程里最值得坚持的一条纪律:所有自己编译的依赖统一装到一个独立前缀,我习惯用/opt/wt-deps。
原因很直接。apt 装的东西归包管理器管,你自己make install装到 /usr/local 甚至 /usr 下的东西,包管理器不知道,升级系统时可能被覆盖,出问题也没法apt remove干净卸载。全部收进/opt/wt-deps之后,好处立刻显现:想换版本,改个目录名就行;想彻底清掉,rm -rf /opt/wt-deps一步到位;两套 boost 想并存做对比测试,装到/opt/wt-deps-boost174就完事,互不干扰。
目录结构我一般统一成这样,后面所有命令都基于这个约定:
sudo mkdir -p /opt/wt-deps sudo chown -R "$USER":"$USER" /opt/wt-deps mkdir -p /opt/wt-deps/{include,lib,bin,lib/pkgconfig,src}第一行建目录,第二行把属主改成当前用户,避免后面每次make install都 sudo;第三行一次性把 include、lib、bin、pkgconfig 和源码缓存目录建好。后面src用来放各依赖的源码包,装完不删,出问题能直接回源码目录重编,比重新下载快得多。
3. 系统层准备:apt 依赖、编译工具链与 boost 的装法
系统层这一步的目标很明确:把工具链和能用 apt 解决的库一次性装齐,剩下的难啃的(主要是 boost 和 yaml-cpp)走源码。顺序不能反,先装源码库再补 apt 依赖,中间任何一次 cmake 失败都会让你怀疑是不是前面的源码库没编对。
3.1 基础工具链与 apt 依赖一次装齐
先把软件源更新到最新索引,然后把编译工具链和能走系统源的库全部装完。这一步建议一条命令跑完,避免装到一半发现漏了 pkg-config 又回头补:
sudo apt update sudo apt install -y \ build-essential gcc g++ make cmake git pkg-config \ libssl-dev libcurl4-openssl-dev \ libzmq3-dev libprotobuf-dev protobuf-compiler \ libmariadb-dev libmariadb-dev-compat \ python3-dev python3-pip \ libglog-dev逐段说一下为什么是这些。build-essential一次性带上 gcc、g++、make 和 libc 开发头文件;pkg-config虽然不起眼,但后面判断依赖有没有装对全靠它;libssl-dev和libcurl4-openssl-dev是网络层的开发包,注意要装-dev结尾的,只装运行时库编译时会报找不到头文件;libzmq3-dev同时带上 libzmq 和 cppzmq 的头文件;libmariadb-dev-compat提供 mysql 兼容的符号链接,很多老代码还在找mysql.h,装了这个就不用改源码;python3-dev是给 Python 接口编译准备的。
装完立刻验证一下,别等到编译时才暴露问题:
gcc --version | head -1 cmake --version | head -1 pkg-config --modversion libzmq pkg-config --modversion protobuf四条命令分别确认编译器、CMake 和两个关键库的版本。如果pkg-config报Package xxx was not found,说明对应的-dev包没装成功,或者 pkgconfig 文件路径不对,先解决这个再往下走。
3.2 boost:用系统源还是源码编译,怎么选
boost 是这套依赖里唯一需要认真做决策的。两条路都能走通,但适用场景不同。
用系统源的做法是sudo apt install libboost-all-dev,优点是五分钟搞定,缺点也明显:版本被锁死在系统源那个版本上,而且会把 boost 的所有组件(包括你根本用不到的)全部塞进 /usr,头文件和库文件都跟系统绑在一起。如果你只是本地跑一遍验证,这条路完全够用。
源码编译的适用场景是:项目明确要求某个 boost 最低版本、需要跟别人对齐 ABI、或者要开特定的编译选项(比如-fPIC编静态库)。我的习惯是——只要这台机器后面还要编译别的依赖库、还要打产物,就走源码编译,因为可控。
这里有个血泪经验:boost 用系统源还是源码,整个环境必须统一,不能一半一半。系统里如果 apt 装过 boost,/usr/include/boost和/usr/lib/x86_64-linux-gnu/libboost_*.so都在,CMake 的FindBoost默认会先找系统路径,结果就是头文件用了/opt/wt-deps里的新版本、库链接了/usr里的旧版本,编译能过,链接报 undefined reference,或者运行时段错误。这就是第 1 章里那个下午的真相。
判断机器上是否已经混装,用这两条命令:
dpkg -l | grep -i boost ls /usr/include/boost/version.hpp 2>/dev/null && grep BOOST_LIB_VERSION /usr/include/boost/version.hpp第一条列出所有通过 apt 装的 boost 相关包,第二条直接看系统头文件里的版本号。只要第二条有输出,而你又打算用源码编译的 boost,就得先决定是卸掉系统包还是干脆放弃源码编译。
3.3 boost 源码编译:b2 参数逐条拆开
确定走源码之后,流程是下载、解压、bootstrap、b2 install。下面的命令以 boost 1.7x 系列为例,具体小版本换成你需要的即可,参数写法是通用的:
cd /opt/wt-deps/src tar -xzf boost_1_7x_0.tar.gz cd boost_1_7x_0 # 生成 b2,只保留需要的组件,别全编 ./bootstrap.sh \ --prefix=/opt/wt-deps \ --with-libraries=system,filesystem,thread,regex,date_time,chrono # 正式编译安装 ./b2 -j"$(nproc)" \ variant=release \ link=static \ runtime-link=shared \ threading=multi \ cxxflags="-fPIC" \ install--prefix指定安装前缀,必须是/opt/wt-deps,跟前面的目录约定对齐;--with-libraries是关键,只编这几个组件能把编译时间从半小时压到几分钟,boost 全量编译在低配机器上真的很折磨;-j$(nproc)用满所有核,但如果你机器内存小于 8G,建议改成-j2,boost 的模板实例化很吃内存,并行太高会直接 OOM。
link=static表示编静态库,runtime-link=shared表示运行时库(libstdc++)动态链接,两者不冲突;threading=multi开多线程支持,WonderTrader 这种框架基本都要;cxxflags="-fPIC"是给后面可能被编进共享库做准备,不加的话后面链接动态库会报relocation R_X86_64_32 against ... can not be used when making a shared object,这个错误信息很难直接联想到是 boost 编译参数的问题。
装完检查一下产物,这一步别省:
ls /opt/wt-deps/lib/libboost_filesystem* ls /opt/wt-deps/include/boost/version.hpp第一条看静态库有没有生成,第二条确认头文件到位。如果lib目录空的,八成是b2 install那步权限不够或者前缀写错,回头看输出里有没有Permission denied。
4. git 拉源码与逐库构建:顺序、脚本与路径配置
依赖库装完一轮之后,真正容易乱的阶段来了:源码拉取和逐库编译。多个依赖库之间存在隐式依赖关系(比如 cppzmq 依赖 libzmq、某些库依赖 fmt),顺序错了会连着报错,看起来像库本身有问题,其实只是编早了。
4.1 git 的配置与克隆习惯
拉源码之前先把 git 的基础配置做掉,尤其是网络不稳定的时候,默认配置会让人误以为是仓库挂了:
git config --global http.postBuffer 524288000 git config --global core.compression 0 git config --global advice.detachedHead false第一条把 HTTP 传输缓冲区调到 500MB 左右,大仓库克隆时能减少中途断流;第二条关闭传输压缩,用带宽换稳定性,在网络抖动明显的时候有效;第三条关掉 detached HEAD 的提示噪音,因为依赖库经常直接 checkout 某个 tag 而不是主分支。
克隆依赖源码时,我一般只拉需要的深度,不要--recursive一把梭:
cd /opt/wt-deps/src git clone --depth 1 --branch <tag> <repo-url> yaml-cpp git clone --depth 1 --branch <tag> <repo-url> fmt--depth 1是浅克隆,只拿最新一个提交,仓库体积能小一个数量级,编译依赖根本用不到历史记录;--branch <tag>直接把版本锁定在某个 tag 上,而不是跟着主分支漂。这一点在依赖库上特别重要:主分支今天能编过,下周可能就因为一次不兼容改动编不过,而你的项目代码没动过,排查方向会被彻底带偏。<repo-url>换成你实际使用的仓库地址,<tag>换成项目文档里指定或你验证过的版本标签。
4.2 编译顺序与可复用的构建脚本
依赖库之间大致遵循「底层先编、上层后编」的顺序。我在 Ubuntu 上常用的顺序是:openssl/curl 走 apt 不用管,然后是 boost、yaml-cpp、fmt、protobuf、zmq、最后是主项目。这个顺序的理由是 boost 最独立、yaml-cpp 和 fmt 不依赖别的第三方库、protobuf 和 zmq 用 apt 版本,冲突面最小。
每个库的编译命令高度相似,写成脚本比手敲稳。下面是我常用的通用构建脚本,放在/opt/wt-deps/src/build_dep.sh:
#!/usr/bin/env bash # 用法: ./build_dep.sh <源码目录> [额外的 cmake 参数...] set -euo pipefail SRC_DIR="$1"; shift PREFIX="/opt/wt-deps" BUILD_DIR="${SRC_DIR}/build" cmake -S "${SRC_DIR}" -B "${BUILD_DIR}" \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="${PREFIX}" \ -DCMAKE_PREFIX_PATH="${PREFIX}" \ -DCMAKE_CXX_FLAGS="-fPIC" \ "$@" cmake --build "${BUILD_DIR}" -j"$(nproc)" cmake --install "${BUILD_DIR}"set -euo pipefail让脚本在任何一步失败时立刻停下,而不是带着错误继续往下跑;-DCMAKE_BUILD_TYPE=Release是必须的,Debug 版本编出来的库性能差一个量级,回测跑起来会慢得离谱;-DCMAKE_PREFIX_PATH指向自定义前缀,保证这个库依赖其他自编库时能找到;-DCMAKE_CXX_FLAGS="-fPIC"同样是给共享库做准备;"$@"把额外参数透传进去,方便针对不同库追加开关。
以 yaml-cpp 为例,实际调用是这样:
cd /opt/wt-deps/src ./build_dep.sh ./yaml-cpp \ -DYAML_CPP_BUILD_TESTS=OFF \ -DYAML_CPP_BUILD_TOOLS=OFFYAML_CPP_BUILD_TESTS=OFF跳过单元测试编译,省时间;YAML_CPP_BUILD_TOOLS=OFF不编命令行工具,依赖场景用不上。这两个开关在依赖库编译里属于标准操作,编测试和工具纯属浪费。
4.3 让编译器找到它们:环境变量、ldconfig 与 pkg-config
装完之后最容易卡住的一步是「库明明在 /opt/wt-deps/lib 里,编译就是找不到」。原因是 Linux 下链接期和运行期用的是两套完全不同的搜索路径,很多人只配了其中一个。
链接期(也就是编译和链接的时候)靠的是LIBRARY_PATH、CMAKE_PREFIX_PATH、CMAKE_LIBRARY_PATH这几个;运行期(程序跑起来的时候)靠的是LD_LIBRARY_PATH和/etc/ld.so.conf.d/里的配置。只设LD_LIBRARY_PATH,编译照样报cannot find -lxxx,这是典型误区。
我一般两套都配齐。先写环境变量文件:
cat >> ~/.bashrc <<'EOF' export WT_DEPS=/opt/wt-deps export PATH=$WT_DEPS/bin:$PATH export LD_LIBRARY_PATH=$WT_DEPS/lib:$LD_LIBRARY_PATH export LIBRARY_PATH=$WT_DEPS/lib:$LIBRARY_PATH export CMAKE_PREFIX_PATH=$WT_DEPS:$CMAKE_PREFIX_PATH export PKG_CONFIG_PATH=$WT_DEPS/lib/pkgconfig:$PKG_CONFIG_PATH EOF source ~/.bashrcPATH让/opt/wt-deps/bin下的工具优先;LD_LIBRARY_PATH管运行期动态库查找;LIBRARY_PATH管链接期静态库查找,这一条是最多人漏的;CMAKE_PREFIX_PATH让 CMake 的find_package优先在这个前缀下找;PKG_CONFIG_PATH让pkg-config能读到自编库的.pc文件。
再把运行期路径固化到系统里,避免每次开新终端都要 source:
echo "/opt/wt-deps/lib" | sudo tee /etc/ld.so.conf.d/wt-deps.conf sudo ldconfig ldconfig -p | grep -E "boost_filesystem|yaml-cpp|fmt"/etc/ld.so.conf.d/下的配置文件会被ldconfig读取,写进去之后系统级生效;ldconfig刷新缓存;最后一条验证缓存里能不能查到这几个库。如果查不到,要么路径写错了,要么那个库根本没有.so文件(只有静态库.a,这种情况就得靠静态链接,不走动态查找)。
5. 避坑与排查:五条编译链接阶段的真实翻车记录
依赖库这套东西,装的过程顺不顺很大程度上取决于你踩过多少坑。下面这五条是我在不同机器上反复遇到的,按「现象 → 原因 → 解决」记下来,出问题时可以直接对号入座。
5.1 编译与链接阶段的三条
第一条:undefined reference to boost::filesystem::...
现象:boost 头文件找得到,编译阶段没有任何报错,链接时一整屏都是 boost 相关的 undefined reference。
原因:链接器找不到 boost 的库文件,或者找到了但不匹配。最常见的是系统里存在两个 boost(系统源一个、自编一个),头文件用了新的、库用了旧的,符号对不上;其次是 CMake 里find_package(Boost)没写COMPONENTS,只找到了基础组件,filesystem、system 这些没被加进链接列表。
解决:先确认版本是否统一,用 3.2 节里的两条命令查系统 boost;然后确保 CMake 里显式声明组件,比如find_package(Boost REQUIRED COMPONENTS system filesystem thread),并且加-DBoost_NO_SYSTEM_PATHS=ON -DBOOST_ROOT=/opt/wt-deps,强制只用自编版本。
第二条:编译通过,运行时报error while loading shared libraries: libxxx.so.x: cannot open shared object file
现象:可执行文件编出来了,直接跑立刻退出,提示找不到某个.so。
原因:运行期库搜索路径没配。编译时靠CMAKE_PREFIX_PATH和LIBRARY_PATH找到了库,运行期这两个变量完全不起作用,程序只认LD_LIBRARY_PATH和 ldconfig 缓存。
解决:按 4.3 节把/etc/ld.so.conf.d/wt-deps.conf写好并sudo ldconfig;临时调试可以在命令前加LD_LIBRARY_PATH=/opt/wt-deps/lib ./your_binary验证是不是这个原因。
第三条:relocation R_X86_64_32 against ... can not be used when making a shared object; recompile with -fPIC
现象:主项目要编成共享库,链接依赖库时报这个错,信息写得很吓人。
原因:依赖库编译时没加-fPIC。位置无关代码是共享库的前提,静态库默认不加这个选项,一旦被链进.so就报错。
解决:回退,重新编译那个依赖库并加上-fPIC。boost 用cxxflags="-fPIC",CMake 项目用-DCMAKE_CXX_FLAGS="-fPIC",装完再重新链接主项目。这也是 3.3 和 4.2 节里一开始就把-fPIC写进去的原因。
5.2 运行与环境阶段的两条
第四条:version GLIBCXX_3.4.29 not found或version CXXABI_1.3.13 not found
现象:程序在编译机器上跑得好好的,拷到另一台 Ubuntu 机器上就报这个,或者在同一台机器上用不同 conda 环境跑时报。
原因:GCC 版本不一致导致 libstdc++ 版本不一致。用高版本 GCC 编出来的东西,运行机上的 libstdc++ 版本太老,没有对应的符号版本。conda 环境自带一套 libstdc++,激活环境后会覆盖系统的那一套,所以同一台机器也可能出现。
解决:两个方向,一是统一编译机和运行机的 GCC 版本;二是编译时静态链接 libstdc++,在链接参数里加-static-libstdc++ -static-libgcc,把运行时依赖收进可执行文件。第二种更省事,代价是产物体积变大,但在部署环节能省掉一整类问题。
第五条:make -j16跑到一半进程被杀,报virtual memory exhausted: Cannot allocate memory
现象:编译 boost 或某个模板很重的库,进度条走到一半直接退出,没有任何具体错误。
原因:并行数开太大,每个编译单元都在做模板实例化,内存瞬间被吃满。低配机器上-j$(nproc)是个陷阱,核多不代表内存够。
解决:降到-j2或-j4重试;如果机器实在紧张,临时加一块 swap 也能救急。这个问题的隐蔽性是它不报编译错误,只报 OOM,很容易被误判成代码有问题。
6. 装完怎么验证:从 ldd 到一次最小链接
依赖库全装完之后,不要急着去编主项目,先花五分钟做一轮独立验证。因为主项目编译一旦失败,你没法判断是项目代码的问题还是依赖环境的问题,前面所有工作就白做了。
第一步查动态库依赖能不能全部解析:
ldd ./your_binary | grep "not found"ldd列出目标程序依赖的所有共享库及其解析结果,grep "not found"筛出没找到的。没有输出就是全部解析成功。如果有输出,说明那个库的运行期路径没配好,回 4.3 节检查/etc/ld.so.conf.d/和ldconfig。
第二步写一个最小 CMake 工程,把 boost、yaml-cpp、fmt 同时链一遍。工程不用做任何业务逻辑,能编过、能跑起来输出一行字就算成功。这一步的价值在于它把「头文件路径」「库路径」「链接顺序」三个问题一次性暴露出来,比在主项目里大海捞针强得多。
第三步查 pkg-config 是否认得自编的库:
pkg-config --cflags --libs yaml-cpp fmt正常会输出一串-I/opt/wt-deps/include -L/opt/wt-deps/lib -lyaml-cpp之类的内容。如果报Package not found,说明PKG_CONFIG_PATH没生效或者那个库没生成.pc文件,前者改环境变量,后者在 CMake 里加-DCMAKE_INSTALL_PREFIX重装一遍即可(大部分库默认会生成)。
第四步做一次「干净 shell 测试」:新开一个终端窗口,不 source 任何东西,直接跑你的可执行文件。这一步是验 ldconfig 有没有真正生效——如果只有 source 过.bashrc的终端能跑,说明运行期路径是靠LD_LIBRARY_PATH撑着的,换人换机器就崩。真正的验证标准是:全新的 shell 里不加任何环境变量也能跑起来。
我吃过一次亏之后养成习惯:每装完一套依赖环境,必走一遍「ldd 无 not found + 最小工程链接 + 干净 shell 启动」这三关,任何一关过不了就先解决,绝不带着隐患往主项目上冲。那次的教训是主项目编了两小时才失败,最后发现是 fmt 的头文件路径被系统版本覆盖了,早五分钟验证就能省下两小时。这套流程希望能帮你少走一段弯路。
本文还有配套的精品资源,点击获取