1. 为什么放着包管理器不用,非要自己编译 wxWidgets
先说个真实场景。某开发者在某公司做桌面端工具链,公司内部有个小项目要在 Deepin 上跑起来,这个项目依赖 wxWidgets 的某些新 API。刚开始图省事,直接在系统里apt install libwxgtk3.0-gtk3-dev,装完一编译,链接倒是过了,代码却跑不起来——仔细一查,仓库里给的 wxWidgets 版本还是 3.0.x,项目在 Ubuntu 上用 3.1.x 的 API 写得非常顺手,部分接口和枚举在 3.0 里根本没有,连wxRichTextCtrl的若干新特性都是残缺的。折腾了一个下午,最后只能老老实实从源码编译 wxWidgets。
这件事基本点明了"为什么不用 apt 装完就完事"的核心:你想用新特性,但仓库不给你新版本。Deepin 基于 Debian 体系,Debian 的稳定策略偏保守,wxWidgets 这种基础库通常不会轻易升级大版本。只要你的项目跨平台、依赖的 wxWidgets 特性又比较多,几乎必然会遇到"系统库够用但不完全够用"的尴尬。
除了版本落后,还有几个场景也必须走源码编译:
- 需要定制编译参数。仓库里的二进制包是通用构建,没有开启 WebView、没有 STC 编辑器组件、没有富文本的某些附加模块。你想用这些模块,就得自己加上
--enable-webview、--enable-stc这些 configure 选项重新编。 - 需要静态链接。把 wxWidgets 和你的程序编成一个独立的静态二进制,方便丢到别的 Linux 机器上跑。发行版自带的 .so 根本没法做这事情,因为人家没提供对应的静态库。
- 需要调试符号和带断言版本。自己在 debug 模式下编一套,程序崩了能直接看到 wxWidgets 内部堆栈。发行版包通常只有 release 版,调试起来经常"黑盒"。
另外,企业内部网络环境往往不能直接连外网apt,搞一个离线包很麻烦。从源码编一个 wxWidgets 出来,目录一拷就能带到内网用,省心得多。
所以你看,源码编译不是"闲着没事折腾",而是被需求倒逼的。从时间成本上算,第一次编译大概要 20 到 40 分钟,之后每次重编增量编译只要几分钟,跟你在排查 API 差异上花的时间比,太划算了。
2. 编译前的准备:依赖、工具链与关键决策
2.1 先检查工具链,别第一步就翻车
Deepin 出厂一般带了 GCC/G++,但版本可能很老。wxWidgets 3.1/3.2 版本开始对编译器有要求,推荐 C++11 以上,如果你要用 C++17 特性的接口,GCC 8 以下会比较痛苦。开工前先把这些命令跑一遍:
gcc --version g++ --version make --version cmake --version pkg-config --version如果某个没装,用sudo apt install build-essential cmake pkg-config补上。pkg-config相当重要,wxWidgets 的 configure 脚本要依赖它来探测 GTK、GL 等库,它缺失的话后面会看到一堆莫名其妙的报错。
2.2 依赖包列表,按需装但要装全
wxWidgets 在 Linux 下最常见的后端是 GTK,其次还有 Motif、X11 纯后端。绝大多数项目用的是 GTK,Deepin 也默认带 GTK 环境。为了不出意外,我建议把下面这些包一起装上:
sudo apt install libgtk-3-dev libgl1-mesa-dev libglu1-mesa-dev \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ libnotify-dev libwebkit2gtk-4.0-dev \ libcurl4-openssl-dev libsecret-1-dev \ libsdl2-dev libtiff-dev libpng-dev libjpeg-dev \ libsm-dev libice-dev libxpm-dev这里我解释下每个包的用意。libgtk-3-dev是必须的,wxWidgets 的 GTK 后端全部建立在它上面;libgl1-mesa-dev和libglu1-mesa-dev是 OpenGL 支持,你要用wxGLCanvas就必须有;libgstreamer系列提供音视频播放能力,wxMediaCtrl依赖它们;libwebkit2gtk-4.0-dev是wxWebView的 WebKit 后端;libcurl和libsecret不是核心依赖,但有些辅助模块会用到。
也许有人会问,把这些都装上是不是过度?我的经验是:如果你不确定项目会不会用到某个模块,就都装。源码编译最怕的是 configure 阶段探测不到某些库然后静默禁用对应模块,最后你用到的时候才发现功能是空壳。与其来回补依赖重编,不如一次装全。
2.3 获取源码:稳定版 tarball 还是 Git 分支
获取 wxWidgets 源码一般两条路:
- 到官网下载稳定版 tarball,比如
wxWidgets-3.2.6.tar.bz2; - 直接用 Git 拉取官方仓库,切到你想要的版本分支。
git clone --branch WX_3_2_6 https://github.com/wxWidgets/wxWidgets.git wxWidgets-3.2.6用 Git 的好处是以后想升级可以直接 pull,想切到开发版分支也很方便。但注意一点:从 Git 拉下来的源码没有生成 configure 脚本,需要先跑autoreconf -f -i,因为 configure 文件是发布时生成的,仓库里不会带。tarball 里自带 configure,可以跳过这一步。这是很常见的坑,有不少人拉完源码直接./configure然后报错找不到文件,就是这个原因。
2.4 关键决策:GTK2 还是 GTK3
wxWidgets 在 Linux 下有两个主要后端:GTK2 和 GTK3。Deepin 20 以后的系统默认是 GTK3,但有些老的系统仍然装得有 GTK2 的开发包。configure 脚本会自动探测系统里的 GTK 版本,它的默认优先级倾向于 GTK2(因为老版本 wxWidgets 最成熟的路径就是 GTK2)。
问题在于:如果你系统里 GTK2 和 GTK3 的开发包同时存在,configure 可能默认选到 GTK2,编出来的程序外观、行为跟 Deepin 的现代桌面风格很不搭,而且新版本中不少 GTK2 相关的修复和特性不会生效。所以我在配置时都会主动加上:
--with-gtk=3强制指定后端。这也是编译前必须想清楚的决策之一,后面我会再提一次,因为很多人就是在这里吃了暗亏。
3. 真正的编译过程:configure、make、make install 逐步拆解
3.1 autoreconf 生成 configure 脚本
如果你是用 Git 拉的源码,进入目录后先执行:
cd wxWidgets-3.2.6 autoreconf -f -i这一步需要系统装了autoconf、automake、libtool。没装的话sudo apt install autoconf automake libtool。执行时间取决于机器性能,一般几分钟内跑完。跑完之后目录下会多出configure这个脚本。
如果你用的是官网 tarball,这步直接跳过。
3.2 configure 参数详解,每项都有讲究
我的推荐配置如下:
mkdir build-gtk3 && cd build-gtk3 ../configure --prefix=/opt/wxwidgets/3.2.6 \ --with-gtk=3 \ --enable-shared \ --enable-unicode \ --enable-webview \ --enable-stc \ --disable-monolithic \ --with-libpng \ --with-libjpeg \ --with-libtiff \ --with-zlib逐项说明:
--prefix=/opt/wxwidgets/3.2.6:指定安装路径。我习惯把不同版本装在不同目录,比如/opt/wxwidgets/3.2.5和/opt/wxwidgets/3.2.6,方便切换和共存。你也可以装到~/wxlibs,但后面要手动配置环境变量,不如/opt干净。--enable-shared:编译动态库。如果只做内部工具、要到处拷贝二进制,可以改成--disable-shared走静态库;两者都编也可以,就是体积大、时间久。--enable-unicode:必须开启。现在还有谁用窄字符的 wxWidgets?不开 unicode 的话字符串处理会出现各种诡异编码问题。--enable-webview:开启 wxWebView 模块。刚才提到 WebKit 的开发包装了才有意义。--enable-stc:启用 Scintilla 文本编辑器组件,做代码编辑器类工具必需。--disable-monolithic:这个参数比较微妙。默认情况下 wxWidgets 编译成多个独立的小库(如libwx_base、libwx_gtk3u_core等),叫非单体构建;如果开启--enable-monolithic,就编成一个超级大库。官方推荐默认的非单体方式,因为这样最终链接时只链接你实际用到的库,二进体积更小。但如果你发现自己项目里出现了莫名其妙的重复符号、未定义引用问题,可以考虑改成 monolithic 模式统一库,虽然体积大但链接简单。我平时用非单体,没出过问题。- 后面四个
--with-*基本都是默认开启,写出来是提醒你不要因为某些原因把它们显式禁掉——有项目为了压缩体积禁用 libpng 之类,结果程序一跑图片全花。
configure 阶段最后会输出一个汇总表格,列出哪些模块被启用、哪些被禁用。不要急着往下走,先看一下 WEBVIEW、STC、MEDIACTRL 等关键模块的状态。如果显示 no,说明依赖探测失败,需要回头补装库再重新 configure。
3.3 编译和安装:内存与时间的博弈
configure 通过后,直接开始编译:
make -j$(nproc)注意-j后面的数字。$(nproc)会读取 CPU 逻辑核心数,现代机器普遍 8 核 16 线程,直接全开。但这有个隐患:每开一个编译任务,GCC 大概吃掉 1 到 2GB 内存。如果机器只有 8GB 内存,-j16很可能在中途因为内存耗尽被系统杀进程,表现为"编译到一半突然 Killed"。所以 8GB 内存的机器建议-j4,16GB 内存可以-j8,大户随意。
第一次全量编译耗时大概 20 到 40 分钟,看 CPU 性能。之后你改代码增量编译就很快,基本分钟级别。
编译完成后安装:
sudo make install安装到/opt/wxwidgets/3.2.6之后,目录结构大概是:
/opt/wxwidgets/3.2.6/ ├── bin/wx-config ├── lib/ │ ├── libwx_baseu-3.2.so │ ├── libwx_gtk3u_core-3.2.so │ └── ... └── include/wx-3.2/wx/看到wx-config和一堆.so文件就算基本成功了。
3.4 编译用到的路径与资源选择补充
有人问过,能不能直接在源码目录运行./configure而不是新建 build 目录?可以,但官方推荐 out-of-source 构建,也就是刚才我用的mkdir build-gtk3 && cd build-gtk3这种方式。好处是源码目录保持干净,可以同时建多个 build 目录做不同配置(比如一个 GTK3 debug、一个 GTK2 release),互不干扰。如果只有一份源码目录,以后想换配置还得 make distclean,很烦。
还有一个细节是环境变量LDFLAGS和CPPFLAGS。默认情况下--prefix指定的路径会自动加入编译搜索路径,不需要手动设置。但如果 configure 探测某些库失败,你可以临时指定:
CPPFLAGS="-I/usr/include/gtk-3.0" LDFLAGS="-L/usr/lib/x86_64-linux-gnu" ../configure ...这种情况在 Deepin 上比较少见,真遇到了也都是依赖缺失,不是路径问题。
4. 编译中最容易踩的坑:完整排查链路还原
这一章本该叫"常见错误",但我更想还原真实的踩坑过程。因为我见过太多人卡在某个报错半天出不来,最后发现只是个很蠢的原因。希望这个排查思路对你有帮助。
4.1 坑一:configure 静默选择了 GTK2
某开发者 A 第一次在 Deepin 上编译 wxWidgets,跑完 configure 后随手划走输出,直接 make,也没看汇总表。编完之后写了个测试窗口程序:
#include <wx/wx.h> class MyApp : public wxApp { public: virtual bool OnInit() { wxFrame* frame = new wxFrame(nullptr, wxID_ANY, wxT("Hello")); frame->Show(); return true; } }; wxIMPLEMENT_APP(MyApp);编译命令:
g++ hello.cpp $(wx-config --cxxflags --libs) -o hello跑起来发现窗口外观跟系统其它 GTK3 程序明显有差异,而且在高分屏下的缩放行为异常。查了半天才发现,configure 阶段因为系统里同时存在 GTK2 和 GTK3 的开发包,自动选了 GTK2,编译结果里全都带gtk2字样。
解决办法是加上--with-gtk=3重新配置,并且 configure 完后必须肉眼确认输出里写着GTK+ 3相关字样而不是GTK+ 2。这一步极其关键,我建议所有人在./configure之后都养成看一眼关键模块的习惯,别管输出多长都别省。
4.2 坑二:缺少 OpenGL 头文件导致 GL 相关模块失败
某次我在干净环境里编译,配置时没有装libgl1-mesa-dev,configure 输出里 OpenGL 相关是no。我当时没当回事,项目里没打算用 wxGLCanvas,结果后面做缩放截图功能时依赖了 wxGraphicsContext 的某些加速路径,运行时性能极差,还伴随 X11 错误。
排查过程很折腾。先怀疑是自己代码问题,逐行核对;后来怀疑系统驱动,重装了 Mesa;最后才偶然发现编译出的 wxWidgets 库文件里根本没有 GL 相关的符号。用nm命令验证:
nm -D /opt/wxwidgets/3.2.6/lib/libwx_gtk3u_core-3.2.so | grep -i gl输出里光秃秃什么都没。这时候回看 configure 的汇总,OpenGL 一行清楚地写着 no——只是当时没看。
教训就是:configure 阶段的汇总表比报错信息更值得你花时间。报错至少是显性的,静默禁用模块才是最阴险的坑。
4.3 坑三:内存不足导致的 "Killed"
编译 wxWidgets 本身不算重负载,但-j开太猛也会出事。某同学在 8GB 内存的办公本上直接make -j8,编译到 60% 左右终端弹出一行Killed,整个进程消失。他第一反应以为代码问题,重试了好几次都在同一个位置被杀,后来才发现是内存耗尽。
解决办法很简单:
make -j2或者先把make clean再降低并行度重编。这里还有个锦囊:如果你卡在内存不足上,可以临时加一块 swap:
sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile编完之后看情况可以sudo swapoff /swapfile再删掉。这台机器如果以后还要编其他大项目,留着这块 swap 也不亏。
4.4 坑四:升级系统库之后,程序链接到了旧 ABI
这个坑属于最隐晦的一类。编译安装完 wxWidgets 后,我用它编了一个内部工具,一切正常。但某天系统升级了一波软件包,Deepin 自带的旧版 libwx 库版本发生了变化,我重新编译程序时发现:wx-config --cxxflags明明指向/opt/wxwidgets/3.2.6/include/wx-3.2,但ldd一看,程序链接的还是系统库/usr/lib/x86_64-linux-gnu/libwx_baseu-3.0.so.0。
根源在于wx-config的搜索顺序。如果我把PKG_CONFIG_PATH和LD_LIBRARY_PATH没设置,程序运行时动态链接器会优先找系统路径下的同名 .so,于是出现"编译时用的新头文件、运行时跑的老库"的 ABI 不匹配,表现就是各种undefined symbol或者内存错乱。
解决办法是写环境变量,并且要么写进~/.bashrc,要么写成项目内的一个env.sh脚本:
export WX_CONFIG=/opt/wxwidgets/3.2.6/bin/wx-config export PATH=/opt/wxwidgets/3.2.6/bin:$PATH export LD_LIBRARY_PATH=/opt/wxwidgets/3.2.6/lib:$LD_LIBRARY_PATH这里我特别提醒:LD_LIBRARY_PATH 只对当前 shell 有效,如果开了新终端忘了 source,程序又是另一种崩溃方式。最稳妥的是把自定义库路径写进/etc/ld.so.conf.d/:
echo "/opt/wxwidgets/3.2.6/lib" | sudo tee /etc/ld.so.conf.d/wxwidgets.conf sudo ldconfig这样所有用户的程序都能正确找到新库,不用每次手动设置太多环境变量。但如果你同时装了多个版本且经常切换,那还是用env.sh的方式更灵活。
4.5 坑五:链接时报 "undefined reference" 与模块枚举
还有一个高频链接问题。项目代码里用到了wxStyledTextCtrl,但 configure 时没有--enable-stc,编译的时候头文件可能还在(因为头文件是随包一起的),链接时才报undefined reference。
这类错误出现时,第一反应不该是去翻代码,而是去检查你编译的 wxWidgets 到底启用了哪些模块。用wx-config --list可以看得清清楚楚:
/opt/wxwidgets/3.2.6/bin/wx-config --list输出里如果对应模块没有列出,那你的程序怎么改都白搭,回到 configure 阶段去补对应模块开启。
5. 安装后的验证与项目接入实践
5.1 先做个最小验证,别急着接大项目
装完之后我写的第一个验证程序只有十几行,就是上面那个 Hello 窗口,主要是确认三件事:
wx-config --version输出的是不是你要的版本;- 编出来的程序能正常弹窗,而且窗口样式符合 GTK3 观感;
- 用
ldd确认实际链接库路径来自你的安装目录。
/opt/wxwidgets/3.2.6/bin/wx-config --version g++ hello.cpp $(/opt/wxwidgets/3.2.6/bin/wx-config --cxxflags --libs) -o hello ldd hello | grep wx看到输出里libwx_gtk3u_core-3.2.so => /opt/wxwidgets/3.2.6/lib/...才算真的成功。很多人编译通过但程序一跑就崩,十有八九是这里没确认。
5.2 在 CMake 项目中使用自定义 wxWidgets
如果你的项目用 CMake 管理,那么不一定要手动拼wx-config参数。CMake 里自带 FindwxWidgets 模块:
cmake_minimum_required(VERSION 3.10) project(MyApp) set(CMAKE_CXX_STANDARD 17) # 告诉 CMake 使用我们指定的 wx-config set(wxWidgets_CONFIG_EXECUTABLE "/opt/wxwidgets/3.2.6/bin/wx-config") set(wxWidgets_USE_UNICODE ON) set(wxWidgets_USE_STATIC OFF) find_package(wxWidgets REQUIRED COMPONENTS core base adv net aui stc webview) include_directories(${wxWidgets_INCLUDE_DIRS}) add_executable(MyApp main.cpp) target_link_libraries(MyApp ${wxWidgets_LIBRARIES})这里有个坑:CMake 的find_package(wxWidgets)默认会调用wx-config --libs,而且会缓存结果。如果你曾经用系统版 wxWidgets 配置过同一个构建目录,换到自定义版之后需要把CMakeCache.txt删了重新生成,否则缓存里还是旧的库路径,链接时又跑回系统库。删缓存这个操作虽然简单,但非常容易被忽略。
5.3 不依赖 wx-config 的 pkg-config 方式
新版 wxWidgets 也提供 pkg-config 支持(前提是 configure 时探测到了 pkg-config)。安装后你会在/opt/wxwidgets/3.2.6/lib/pkgconfig/下看到wxgtk3u-3.2.pc之类的文件。使用方式:
export PKG_CONFIG_PATH=/opt/wxwidgets/3.2.6/lib/pkgconfig:$PKG_CONFIG_PATH g++ hello.cpp $(pkg-config --cflags --libs wxgtk3u-3.2) -o hello这种方式好处是跟其他依赖库的下发方式统一,坏处是 Deepin 上有些用户忘了设置PKG_CONFIG_PATH,pkg-config 找遍系统也找不到新装的 wxWidgets,报错信息还很抽象("Package wxgtk3u-3.2 was not found")。相比之下wx-config方式更直接,我还是推荐两者都以wx-config为主。
5.4 给 IDE 配置你的自定义 wxWidgets
如果你用 Qt Creator 或 VSCode 做日常开发,需要让 IDE 的智能提示和索引找到自定义头文件。
以 Qt Creator 为例,在项目的.pro或 CMake 配置里指定编译参数位置。VSCode 则在.vscode/c_cpp_properties.json中,includePath里加上:
"/opt/wxwidgets/3.2.6/include/wx-3.2", "/opt/wxwidgets/3.2.6/lib/wx/include/gtk3-unicode-3.2"第二个路径是 wx 编译时生成的平台配置头文件目录,少了它,IDE 里飘红一大片。很多人在 VSCode 里配 wxWidgets 时只加了第一个路径,结果wx/wx.h都找不到,就是因为第二个路径缺失。这个细节在官方文档里不太起眼,但实际开发中非常关键。wx-config --cxxflags输出里其实已经包含了这两个路径,直接复制就行。
6. 多版本共存、自动化构建与我的使用建议
6.1 多版本共存不打架的目录规划
前面提过--prefix的设计价值,这里展开讲。如果你同时在做两个项目:一个依赖 wxWidgets 3.0(老项目),一个依赖 3.2(新项目),且两个项目在同一台 Deepin 机器上编译,怎么办?
最蠢的方式是装一个、卸一个。正确做法是每个版本独立 prefix:
../configure --prefix=/opt/wxwidgets/3.0.5 --with-gtk=3 ... make -j8 sudo make install cd ../build-gtk3-3.2 ../configure --prefix=/opt/wxwidgets/3.2.6 --with-gtk=3 ... make -j8 sudo make install使用时按项目切换wx-config的 PATH。我推荐写两个切换脚本,而不去改全局环境变量:
use-wx3.2.sh:
export PATH=/opt/wxwidgets/3.2.6/bin:$PATH export LD_LIBRARY_PATH=/opt/wxwidgets/3.2.6/lib:$LD_LIBRARY_PATHuse-wx3.0.sh同理。每个项目构建前先 source 对应脚本,然后wx-config --version确认当前生效版本。小心别把两个版本的 LD_LIBRARY_PATH 同时设置,否则运行时动态链接器会按目录顺序找库,版本一混,程序就起不来了。
6.2 自动化构建脚本的思路
如果你经常要在新环境重编 wxWidgets,可以写成脚本。某开发者把整个流程抽象成一段 bash,每次拿到一台新 Deepin 机器,跑一遍脚本就能装好,包括依赖安装、源码下载、编译和安装:
#!/bin/bash set -e WX_VERSION="3.2.6" PREFIX="/opt/wxwidgets/$WX_VERSION" BUILD_DIR="$HOME/build-wx-$WX_VERSION" # 1. 安装依赖 sudo apt update sudo apt install -y build-essential cmake pkg-config autoconf automake libtool \ libgtk-3-dev libgl1-mesa-dev libglu1-mesa-dev \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ libnotify-dev libwebkit2gtk-4.0-dev libcurl4-openssl-dev \ libtiff-dev libpng-dev libjpeg-dev # 2. 获取源码 if [ ! -d "$BUILD_DIR/src" ]; then git clone --branch WX_3_2_6 --depth 1 https://github.com/wxWidgets/wxWidgets.git "$BUILD_DIR/src" fi # 3. 编译 cd "$BUILD_DIR/src" autoreconf -f -i mkdir -p "$BUILD_DIR/build" cd "$BUILD_DIR/build" ../configure --prefix="$PREFIX" --with-gtk=3 --enable-shared \ --enable-unicode --enable-webview --enable-stc \ --disable-monolithic make -j$(nproc) sudo make install echo "wxWidgets $WX_VERSION installed to $PREFIX"这段脚本我在几台机器上跑过,基本稳定。唯一需要注意的是git clone --depth 1只拉了指定分支的最近提交,如果后续想切到别的分支,得重新 clone 或用git fetch --unshallow,否则仓库不完整。
6.3 我的总体建议
聊了这么多,最后说点实际的取舍。如果你的项目只是临时用一下 wxWidgets,或者完全跟着发行版走的旧项目,直接用apt install libwxgtk3.0-gtk3-dev就够了,没必要为那些用不到的新特性付出 40 分钟编译时间。
但如果你跟我一样,在 Deepin 上做跨平台 GUI 工具开发,源码编译几乎是必经之路。版本可定制、模块可选、ABI 可控,这些收益远大于一次性编译投入。而且经历过一次之后你会有自己的 build 目录和脚本,下次再遇到新的 Deepin 环境,一条命令跑完,不用再痛苦地跟系统库的版本限制较劲。
最后分享一个我个人的小习惯:永远在 configure 阶段把汇总表截图或复制到 build 笔记里。不管是隔了几个月回来看当时开了哪些模块,还是排查某个库为什么静默消失了,这份笔记都能让你少走很多弯路。编译 wxWidgets 本身不难,难的是让它按照你需要的方式构建,而这份记录就是你控制整个构建过程的起点。