1. 这套老版本组合到底在解决什么问题
如果你在搜索引擎里敲下"CGAL 4.14 + VS2013 + Boost 1.70 + Qt 5.8"这串关键词,大概率不是出于好奇,而是被某个遗留项目逼到了墙角。这套组合的每一个版本号都透着一股"上一个时代"的气息:VS2013 是 2013 年的编译器,Qt 5.8 发布于 2017 年初,CGAL 4.14 是 2019 年的版本,而 Boost 1.70 同样是 2019 年的产物。把它们凑在一起,通常意味着你手上有一个不能轻易升级的工程——可能是依赖了某个只提供 VS2013 二进制库的第三方组件,也可能是整个团队的构建服务器还锁死在旧工具链上。
CGAL(Computational Geometry Algorithms Library)本身是一个计算几何领域的重量级 C++ 库,提供了三角剖分、凸包、布尔运算、网格生成、多边形处理等大量算法。它的特点是模板元编程用得极深,对编译器的 C++ 标准支持有硬性要求,同时又强依赖 Boost 的多个组件。Qt 在这里通常承担的是可视化交互的角色,比如把 CGAL 算出来的三角网格用 QGLWidget 或 QGraphicsView 渲染出来,再配上一些参数调节的界面。所以这套组合的本质是:用 CGAL 做几何计算内核,用 Qt 做前端展示,用 Boost 补齐基础设施,全部跑在 VS2013 这个老编译器上。
为什么这件事值得单独写一篇?因为这套组合的搭建过程里,坑的密度远高于现代工具链。VS2013 对 C++11 的支持是"部分支持",CGAL 4.14 虽然官方文档说支持 VS2013,但实际编译时会遇到大量模板实例化失败、constexpr支持不完整、noexcept推导异常等问题。Boost 1.70 和 VS2013 之间也存在微妙的兼容性边界——Boost 从某个版本开始逐渐放弃对 VS2013 的官方测试,1.70 恰好卡在一个"还能用但需要手动打补丁"的位置。Qt 5.8 相对独立,但它和 VS2013 的配合需要选对安装包(msvc2013 版本),否则会出现链接期符号不匹配。
这篇文章适合三类人:第一类是被遗留项目绑住、必须在这套环境上把工程跑起来的一线开发者;第二类是想理解"老工具链下如何做依赖管理"的技术负责人;第三类是对 CGAL 感兴趣但被它的编译复杂度劝退、想找一个可复现路径的学习者。我会把整个搭建过程拆成可操作的步骤,同时把每一步背后的"为什么"讲清楚,让你不仅能把环境搭起来,还能在遇到变体问题时知道往哪个方向排查。
2. 四个组件的版本咬合关系与选型逻辑
2.1 为什么是这四个版本,而不是别的组合
先看一张版本对照表,把每个组件的角色和约束条件列清楚:
| 组件 | 版本 | 角色 | 对 VS2013 的兼容状态 | 关键约束 |
|---|---|---|---|---|
| Visual Studio | 2013 (VC12) | 编译器与 IDE | 自身 | 仅支持部分 C++11 |
| CGAL | 4.14 | 计算几何算法库 | 官方声明支持 | 需要 Boost 1.58+ |
| Boost | 1.70 | 基础设施库 | 可用但需注意 | 1.70 是最后几个较好支持 VC12 的版本 |
| Qt | 5.8 | GUI 与可视化 | 需选 msvc2013 包 | 与 CGAL 无直接依赖 |
这张表里最关键的信息是:CGAL 4.14 要求 Boost 最低 1.58,但并没有限制上限。那为什么偏偏选 1.70?因为 Boost 1.71 之后,部分库开始使用 VS2013 不支持的 C++14 特性,编译会直接报错。1.70 是一个"甜点版本"——它包含了 CGAL 4.14 需要的所有 Boost 组件(比如Boost.Graph、Boost.PropertyMap、Boost.Thread),同时还没有引入那些会卡死 VC12 的新语法。如果你手头只有 Boost 1.72 或更高,要么降级,要么就得去改 Boost 源码里的宏判断,工作量不小。
Qt 5.8 的选择逻辑不太一样。Qt 和 CGAL 之间没有编译期依赖,Qt 只负责把 CGAL 的输出画出来。所以 Qt 版本的选择相对自由,5.8 之所以常见,是因为它是最后一个提供完整 msvc2013 预编译包的 LTS 附近版本。再往后的 Qt 5.9、5.12 虽然也有 msvc2013 包,但部分模块的二进制兼容性开始出现细微问题。如果你只是用 Qt 的 GUI 部分(QtWidgets、QtGui、QtOpenGL),5.8 足够稳定。
2.2 CGAL 的 header-only 特性与实际编译负担
CGAL 从 5.0 开始才全面转向 header-only,4.14 时代还有部分模块需要编译成库。但即便在 4.14,核心的几何算法(如Triangulation_2、Triangulation_3、Convex_hull_2)都是 header-only 的,你只需要在代码里#include对应的头文件,不需要额外链接.lib。这带来一个好处:你不需要单独编译 CGAL 的库文件,省掉了一大块构建时间。
但 header-only 也有代价。CGAL 的模板实例化非常重,一个简单的Delaunay_triangulation_2实例化可能就会让编译器吃掉几百 MB 内存,编译时间以分钟计。在 VS2013 上这个问题更严重,因为 VC12 的模板编译器在处理深层嵌套模板时效率不高,而且对auto和decltype的支持不完整,CGAL 内部的一些类型推导会退化成显式类型声明,进一步增加编译负担。
实际经验是:把 CGAL 相关的代码单独放在一个静态库项目里,只暴露必要的接口给 Qt 主工程。这样做的好处是 CGAL 的模板实例化只发生一次,Qt 工程不需要重复编译那些头文件。如果你把所有代码混在一个工程里,每次改一行 Qt 界面代码都要重新实例化一遍 CGAL 模板,开发体验会非常糟糕。
2.3 Boost 1.70 的编译策略:全量还是按需
Boost 1.70 在 VS2013 下的编译有两种策略。第一种是跑bootstrap.bat然后b2全量编译,生成所有库的.lib。第二种是只编译 CGAL 实际用到的那几个库。我推荐第二种,原因很直接:全量编译 Boost 在 VS2013 上大约需要 40 到 60 分钟,而且会生成好几个 GB 的中间文件。CGAL 4.14 实际依赖的 Boost 库并不多,主要是:
Boost.Thread(用于并行算法)Boost.System(错误码)Boost.Graph(部分算法内部使用)Boost.PropertyMap(图算法辅助)Boost.Atomic(线程安全)
你可以用b2 --with-thread --with-system --with-graph --with-atomic这样的参数只编译需要的库。但要注意,CGAL 的某些头文件会间接包含其他 Boost 头文件,如果编译时报"找不到 xxx.lib",再补编译对应的库即可。这种"按需编译、逐步补齐"的方式比一次性全量编译更可控。
提示:Boost 1.70 在 VS2013 下编译时,
Boost.Thread可能会报一个关于boost::chrono的警告,这是已知问题,不影响功能。如果遇到链接错误,检查是否同时链接了boost_thread和boost_chrono。
3. 从零搭建:环境准备与依赖安装的完整链路
3.1 VS2013 的安装与 C++11 支持确认
VS2013 的安装本身没有太多坑,但有一个关键点:必须安装 Update 5。VS2013 的早期版本(RTM、Update 1-4)在 C++11 支持上有一些 bug,特别是对variadic templates和alias templates的处理。CGAL 4.14 的某些头文件会用到这些特性,如果编译器版本不对,会报出莫名其妙的语法错误。Update 5 是 VS2013 的最后一个累积更新,也是兼容性最好的版本。
安装完成后,打开 VS2013 的命令行工具("Developer Command Prompt for VS2013"),运行cl /?确认编译器版本。你应该看到类似Microsoft (R) C/C++ Optimizing Compiler Version 18.00.40629的输出,其中40629是 Update 5 的版本号。如果是更早的版本号,建议重新安装 Update 5。
另外,VS2013 默认不开启/std:c++11这样的标准选项(那个年代还没有这个开关),它默认就是 C++11 的部分支持模式。你不需要额外加编译选项,但需要在代码里避免使用 VS2013 不支持的 C++11 特性,比如constexpr的复杂用法、noexcept的推导、以及std::make_unique(这个在 VS2013 里没有,需要自己写一个或者用 Boost 的替代)。
3.2 Boost 1.70 的下载、解压与编译参数
Boost 1.70 的源码包可以从官方渠道获取,解压到一个路径中不含空格和中文的目录,比如D:\libs\boost_1_70_0。路径里有空格会导致b2工具的某些脚本解析出错,这是很多人第一次编译 Boost 时踩的坑。
编译步骤如下:
# 进入 Boost 根目录 cd D:\libs\boost_1_70_0 # 运行 bootstrap,生成 b2.exe bootstrap.bat # 编译需要的库,toolset 指定 msvc-12.0(对应 VS2013) b2 toolset=msvc-12.0 --with-thread --with-system --with-graph --with-atomic --with-chrono link=static runtime-link=static address-model=64 stage这里有几个参数需要解释。toolset=msvc-12.0明确告诉 Boost 用 VS2013 的编译器,因为b2默认可能会找到你机器上更高版本的 VS。link=static和runtime-link=static表示生成静态库并静态链接 C 运行时,这样做出来的程序不依赖 VC 运行时 DLL,部署更方便。address-model=64生成 64 位库,如果你的项目是 32 位,改成32。stage表示把生成的库文件集中放到stage\lib目录下,方便后续引用。
编译完成后,检查stage\lib目录,你应该看到类似libboost_thread-vc120-mt-s-1_70.lib的文件。文件名里的vc120表示 VS2013,mt表示多线程,s表示静态运行时。这些命名规则很重要,后面在 VS 里配置链接器时需要对应上。
3.3 CGAL 4.14 的获取与 CMake 配置要点
CGAL 4.14 的源码包解压后,目录结构里有一个CMakeLists.txt。虽然 CGAL 核心是 header-only 的,但官方推荐用 CMake 来管理依赖和生成 VS 工程。你需要先安装 CMake(建议 3.10 以上版本),然后在 CGAL 源码目录下创建一个build文件夹,运行 CMake GUI。
CMake 配置时有几个关键变量需要设置:
BOOST_ROOT:指向 Boost 的根目录,比如D:\libs\boost_1_70_0BOOST_LIBRARYDIR:指向 Boost 的库目录,比如D:\libs\boost_1_70_0\stage\libCMAKE_INSTALL_PREFIX:CGAL 的安装目录,比如D:\libs\CGAL-4.14-installCGAL_DIR:如果你之前已经安装过 CGAL,这里指向安装目录
点击 Configure 后,CMake 会检测 Boost 的版本和组件。如果一切正常,你会看到Boost_FOUND为TRUE,并且列出了找到的 Boost 库。如果报错说找不到某个 Boost 库,回到上一步确认是否编译了对应的组件。
Configure 通过后,点击 Generate 生成 VS2013 的解决方案文件。打开CGAL.sln,编译INSTALL项目(通常只需要编译这一个项目),CGAL 的头文件会被复制到CMAKE_INSTALL_PREFIX指定的目录。这个安装目录里主要是include文件夹,里面是 CGAL 的所有头文件。
注意:CGAL 4.14 的 CMake 脚本在 VS2013 下可能会报一个关于
CGAL_USE_GMP的警告。如果你不需要精确的有理数运算,可以忽略这个警告;如果需要,得额外编译 GMP 和 MPFR 库,那又是另一个大坑,建议先不启用。
3.4 Qt 5.8 的安装与 VS2013 插件的配置
Qt 5.8 的安装包需要选择qt-opensource-windows-x86-msvc2013-5.8.0.exe这个版本。安装时勾选Qt 5.8下的msvc2013 64-bit(或 32-bit,取决于你的项目架构)。安装完成后,你还需要安装 Qt VS Tools 插件,让 VS2013 能够识别 Qt 的工程文件。
Qt VS Tools 的版本需要和 VS2013 匹配。较新的 Qt VS Tools 可能不支持 VS2013,你需要找 2.x 版本的插件。安装插件后,在 VS2013 的菜单栏会出现 "Qt VS Tools" 菜单,里面可以设置 Qt 的版本路径。添加一个 Qt 版本,指向C:\Qt\Qt5.8.0\5.8\msvc2013_64(根据你的实际安装路径调整)。
配置完成后,新建一个 Qt 工程测试一下。如果能看到 Qt 的工程模板,并且编译一个简单的窗口程序能跑起来,说明 Qt 环境就绪。
4. 把四个组件串起来:工程配置与第一个可运行示例
4.1 VS2013 工程的头文件路径与库路径设置
现在假设你已经有了一个 VS2013 的 C++ 工程,想在里面同时使用 CGAL、Boost 和 Qt。打开工程的属性页,需要配置以下几个地方:
C/C++ -> 常规 -> 附加包含目录,添加:
D:\libs\CGAL-4.14-install\include D:\libs\boost_1_70_0 C:\Qt\Qt5.8.0\5.8\msvc2013_64\include C:\Qt\Qt5.8.0\5.8\msvc2013_64\include\QtWidgets C:\Qt\Qt5.8.0\5.8\msvc2013_64\include\QtGui C:\Qt\Qt5.8.0\5.8\msvc2013_64\include\QtCore C:\Qt\Qt5.8.0\5.8\msvc2013_64\include\QtOpenGL链接器 -> 常规 -> 附加库目录,添加:
D:\libs\boost_1_70_0\stage\lib C:\Qt\Qt5.8.0\5.8\msvc2013_64\lib链接器 -> 输入 -> 附加依赖项,添加:
libboost_thread-vc120-mt-s-1_70.lib libboost_system-vc120-mt-s-1_70.lib libboost_graph-vc120-mt-s-1_70.lib libboost_atomic-vc120-mt-s-1_70.lib libboost_chrono-vc120-mt-s-1_70.lib Qt5Core.lib Qt5Gui.lib Qt5Widgets.lib Qt5OpenGL.lib这里最容易出错的是 Boost 库的文件名。如果你编译 Boost 时用的是runtime-link=static,文件名里会有-s;如果是runtime-link=shared,则没有-s,但需要额外链接libboost_thread-vc120-mt-1_70.lib这样的动态版本。库文件名必须和编译时的参数完全对应,否则会出现 LNK2019 未解析符号的错误。
4.2 一个最小的 CGAL + Qt 示例:凸包计算与显示
为了验证环境是否真的可用,我建议从一个最小的示例开始:用 CGAL 计算一组随机点的凸包,然后用 Qt 把点和凸包画出来。这个示例足够简单,但涵盖了 CGAL 算法调用、Qt 界面、以及两者之间的数据传递。
先看 CGAL 部分的代码:
// convex_hull.h #pragma once #include <vector> #include <CGAL/Exact_predicates_inexact_constructions_kernel.h> #include <CGAL/convex_hull_2.h> typedef CGAL::Exact_predicates_inexact_constructions_kernel K; typedef K::Point_2 Point2; std::vector<Point2> computeConvexHull(const std::vector<Point2>& input);// convex_hull.cpp #include "convex_hull.h" std::vector<Point2> computeConvexHull(const std::vector<Point2>& input) { std::vector<Point2> result; CGAL::convex_hull_2(input.begin(), input.end(), std::back_inserter(result)); return result; }这段代码里,Exact_predicates_inexact_constructions_kernel是 CGAL 里最常用的内核之一。它的特点是"判定精确、构造近似"——在比较两个点是否相等、判断点是否在直线左侧这类谓词运算上使用精确算术,而在计算交点坐标这类构造运算上使用浮点数。这样做的好处是凸包算法的正确性有保证(不会因为浮点误差把凸包算错),同时性能又比全精确内核好很多。
Qt 部分,创建一个QWidget子类,重写paintEvent:
// hull_widget.h #pragma once #include <QWidget> #include <QVector> #include <QPointF> class HullWidget : public QWidget { Q_OBJECT public: explicit HullWidget(QWidget* parent = nullptr); void setData(const QVector<QPointF>& points, const QVector<QPointF>& hull); protected: void paintEvent(QPaintEvent* event) override; private: QVector<QPointF> m_points; QVector<QPointF> m_hull; };// hull_widget.cpp #include "hull_widget.h" #include <QPainter> HullWidget::HullWidget(QWidget* parent) : QWidget(parent) {} void HullWidget::setData(const QVector<QPointF>& points, const QVector<QPointF>& hull) { m_points = points; m_hull = hull; update(); } void HullWidget::paintEvent(QPaintEvent*) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); painter.fillRect(rect(), Qt::white); // 画所有点 painter.setPen(Qt::NoPen); painter.setBrush(Qt::blue); for (const auto& p : m_points) { painter.drawEllipse(p, 3, 3); } // 画凸包 painter.setPen(QPen(Qt::red, 2)); painter.setBrush(Qt::NoBrush); if (m_hull.size() > 1) { for (int i = 0; i < m_hull.size(); ++i) { painter.drawLine(m_hull[i], m_hull[(i + 1) % m_hull.size()]); } } }在主窗口里,生成随机点,调用 CGAL 计算凸包,然后把结果转成QPointF传给HullWidget。这里有一个细节:CGAL 的Point_2坐标是double类型,Qt 的QPointF也是double,所以转换很直接,用CGAL::to_double(p.x())即可。
4.3 编译时最常见的三类错误与排查顺序
第一次编译这个示例,你大概率会遇到错误。根据我的经验,错误集中在三类:
第一类:找不到头文件。报错信息是Cannot open include file: 'CGAL/xxx.h'。这说明附加包含目录没配对,或者 CGAL 的安装路径不对。检查CGAL-4.14-install\include下是否有CGAL文件夹,如果没有,说明 CMake 的 INSTALL 步骤没成功。
第二类:链接错误 LNK2019。报错信息是unresolved external symbol,通常跟 Boost 或 Qt 的库有关。先检查库文件名是否和实际文件一致,再检查库目录是否配对了。如果错误涉及boost::thread相关的符号,确认是否链接了libboost_thread和libboost_chrono。
第三类:模板实例化错误 C2783 或 C2893。这类错误通常出现在 CGAL 的头文件里,报错信息很长,涉及模板参数推导失败。这往往是因为 VS2013 的 C++11 支持不完整,某些 CGAL 头文件里的auto或decltype用法无法被正确解析。解决办法是升级到 VS2013 Update 5,或者在 CGAL 头文件里手动替换那些不支持的语法(不推荐,工作量大)。如果实在无法解决,考虑换用 CGAL 4.11 或 4.12,它们对 VS2013 的兼容性更好。
排查顺序建议是:先解决头文件问题,再解决链接问题,最后处理模板错误。因为头文件问题会导致编译直接中断,链接问题在编译通过后才会出现,模板错误则可能被前两类问题掩盖。
5. 让工程真正可用:性能调优与日常开发中的经验
5.1 编译速度优化:预编译头与模板实例化控制
CGAL 工程的编译速度是一个绕不开的话题。在 VS2013 上,一个中等规模的 CGAL 工程全量编译可能需要 10 到 20 分钟。有几个手段可以显著改善:
使用预编译头(stdafx.h)。把 CGAL、Boost、Qt 的常用头文件都放进stdafx.h,然后在工程设置里开启预编译头。这样这些头文件只需要编译一次,后续的编译会快很多。但要注意,预编译头里不要放模板实例化很重的头文件,否则预编译头本身就会编译很久。
把 CGAL 代码隔离到静态库。前面提过,CGAL 的模板实例化只发生一次,Qt 工程只链接静态库,不直接包含 CGAL 头文件。这样做还有一个好处:静态库可以单独编译,不需要每次改 Qt 界面都重新编译 CGAL 部分。
控制模板实例化的数量。CGAL 的每个内核和算法组合都会产生独立的模板实例。如果你同时用了Exact_predicates_inexact_constructions_kernel和Exact_predicates_exact_constructions_kernel,编译器会为两套内核各生成一份代码。尽量统一使用一种内核,除非确实需要精确构造。
5.2 运行时性能:内核选择与算法参数
CGAL 的内核选择对运行时性能影响很大。Exact_predicates_inexact_constructions_kernel在大多数场景下是最佳选择,因为它的谓词运算是精确的(不会出现浮点误差导致的错误判定),而构造运算是近似的(速度快)。但如果你需要精确的交点坐标,比如做布尔运算,那就得用Exact_predicates_exact_constructions_kernel,代价是速度慢一个数量级。
另一个影响性能的因素是算法的参数。比如Delaunay_triangulation_2在插入大量点时,如果点的分布很不均匀,三角剖分的质量会下降,后续的网格生成会更慢。可以在插入点之前先做一次空间排序,或者使用spatial_sort函数对点进行预处理。
Qt 这边的性能瓶颈通常在渲染。如果你要画几十万个点,用QPainter::drawEllipse逐个画会非常慢。这时候应该改用QGraphicsView框架,或者直接用 OpenGL 渲染。Qt 5.8 的QOpenGLWidget已经比较稳定,配合QOpenGLBuffer和QOpenGLShaderProgram可以做到百万级点的实时渲染。
5.3 版本升级的边界:什么时候该放弃这套组合
这套组合的寿命是有限的。VS2013 已经停止支持多年,CGAL 的新版本(5.x 之后)逐渐放弃对 VS2013 的兼容,Boost 也在持续提高编译器门槛。如果你发现以下信号,说明该考虑升级了:
- CGAL 的新功能(比如某些 3D 算法)在 4.14 里没有,而升级 CGAL 又要求 VS2015 以上
- Boost 的某个新库(比如
Boost.Beast、Boost.JSON)在 1.70 里没有,而项目又需要 - Qt 的新特性(比如 Qt 5.12 的
QML改进)无法使用 - 编译时间越来越长,开发效率明显下降
升级路径通常是:VS2013 -> VS2015 或 VS2017,CGAL 4.14 -> CGAL 5.x,Boost 1.70 -> Boost 1.75+,Qt 5.8 -> Qt 5.12 或 5.15。每一步升级都需要重新验证依赖关系,工作量不小,但长期来看是值得的。
如果暂时不能升级,至少要把工程结构做好隔离:CGAL 相关的代码放在独立的静态库里,Qt 界面代码不直接依赖 CGAL 头文件,Boost 的使用限制在少数几个模块里。这样将来升级时,只需要替换静态库的实现,界面代码基本不用动。
6. 几个容易被忽略的细节与我的实际体会
6.1 字符集与运行库的一致性
VS2013 工程默认使用 Unicode 字符集,而 Boost 和 CGAL 的某些库在编译时可能用的是多字节字符集。如果出现链接错误,检查工程属性里的"字符集"设置是否和 Boost 编译时的一致。另外,运行库选项(/MT还是/MD)也必须和 Boost 编译时的runtime-link参数对应。我见过太多因为/MT和/MD混用导致的 LNK2005 错误,排查起来很费时间。
6.2 Qt 的 moc 与 CGAL 头文件的冲突
Qt 的 moc(元对象编译器)会扫描头文件里的Q_OBJECT宏,生成额外的 C++ 代码。如果某个头文件同时包含了Q_OBJECT和 CGAL 的头文件,moc 可能会因为无法解析 CGAL 的模板语法而报错。解决办法是把 CGAL 相关的代码放在单独的类里,这个类不继承QObject,也不包含Q_OBJECT宏。Qt 的界面类只调用这个类的接口,不直接包含 CGAL 头文件。
6.3 调试模式下的性能陷阱
CGAL 在 Debug 模式下会开启大量断言检查,运行速度比 Release 模式慢几十倍。如果你在 Debug 模式下测试一个包含几万个点的三角剖分,可能会等上几分钟。建议在开发阶段就用 Release 模式做性能测试,Debug 模式只用来排查逻辑错误。另外,VS2013 的 Debug 模式对迭代器的检查也很严格,CGAL 的某些算法在 Debug 下会触发迭代器越界断言,而在 Release 下正常运行。遇到这种情况,先确认是不是真的越界,如果不是,可以在 CGAL 的配置里关闭部分断言。
6.4 一个关于 Boost 1.70 的小坑
Boost 1.70 在 VS2013 下编译Boost.Thread时,可能会报一个关于boost::chrono::steady_clock的警告。这个警告不影响功能,但如果你开启了"将警告视为错误"(/WX),编译会失败。解决办法是在 Boost 的编译参数里加上cxxflags=/wd4996来禁用特定警告,或者在工程设置里关掉/WX。我个人的习惯是关掉/WX,因为 CGAL 和 Boost 的头文件里总会有一些无害的警告,逐个处理不现实。
这套环境搭建下来,最深的体会是:老工具链的问题不在于单个组件有多难搞,而在于它们之间的交互边界模糊。CGAL 说支持 VS2013,但没说支持到什么程度;Boost 说兼容 VC12,但没保证所有库都能编译;Qt 说提供 msvc2013 包,但没承诺和 CGAL 的模板代码和平共处。真正的工作量在于把这些模糊地带一个个试出来,然后找到稳定的配置组合。一旦跑通了,这套环境其实相当可靠,毕竟这些版本都是经过大量项目验证的。