
Windows 上敲代码、Linux 上出二进制这件事在 Qt6 时代比 Qt5 顺手不少但真动手还是得掉几层皮。前段时间接了个桌面小工具的活客户明确要求交付 Linux 版可执行文件而我自己的主力开发机是 WindowsQt Creator 用了七八年快捷键都长在手上了不可能为了一个交付换个系统重头熟悉。中间折腾了三四天把libxcb-cursor0缺失、GLIBC 版本倒挂、打包后Could not load the Qt platform plugin xcb、中文变方框这些坑挨个踩了一遍。这篇就把整套流程摊开讲从环境搭建、CMake 工程写法、构建脚本、依赖打包到远程调试能直接抄的部分我都贴出来需要你自己按项目改的地方我会标清楚。这篇适合手里有 Qt6 工程、需要出 Linux 版本、但主力开发机是 Windows 的人。Linux 底子好一点跟起来更轻松没有也不至于卡死关键命令我都会解释它在干什么。整条链路的核心思路就一句话Windows 负责写和看Linux 负责编和跑别指望在 Windows 上直接吐出一个能跑的 Linux 二进制那条路性价比太低后面会讲为什么。1. 先想清楚Windows 写代码、Linux 出包到底有几种落地形态1.1 三种主流形态以及它们各自适合谁把这套流程落地本质上就是回答一个问题编译动作在哪台机器、哪个内核上执行。市面上能用的方案我归纳成三类区别不在工具在于你对隔离程度和启动成本的取舍。第一类是WSL2。它是 Windows 内置的一个轻量级虚拟机跑的是真 Linux 内核文件系统、system call、动态链接器都是原生的。装完就能用启动一两秒和 Windows 侧的文件互访也最方便/mnt/c直接挂在里面。个人开发者、单机作业、需要频繁在两边来回切文件的场景这个是首选。第二类是独立虚拟机或远程构建机。VirtualBox、VMware 里跑一个 Ubuntu或者干脆有一台局域网内的 Linux 主机通过 SSH 把源码推过去编译。这种方案的好处是环境彻底干净发行版版本随你挑能精确控制 GLIBC 版本后面会讲这为什么重要适合团队协作或者需要给多个目标平台出包的情况。第三类是容器。Windows 上跑 Docker Desktop用ubuntu:20.04这类镜像做构建环境docker run -v把源码挂进去出来就是成品。这套东西最大的价值是可复现——把 Dockerfile 写进仓库一年后任何人 checkout 下来都能编出一样的包。缺点是 Windows 上 Docker Desktop 的资源占用和文件挂载性能都不算好大规模构建会肉疼。我自己的选择是 WSL2 日常开发加一个容器做最终出包两者互补成本也不高。1.2 为什么不推荐在 Windows 上直接交叉编译出 Linux 二进制很多人第一反应是我装一套x86_64-linux-gnu-gcc交叉工具链配个 CMake toolchain file是不是就能在 Windows 上直接编出 Linux 可执行文件了技术上可以实践上不建议。原因有三层。第一层是Qt6 本身没提供官方的 Windows 主机到 Linux 目标的交叉编译支持。Qt 官方给出的交叉编译路径主要是 Linux 主机到嵌入式设备Boot2QtWindows 主机交叉编译 Linux 桌面版不在支持列表里。你要自己从源码交叉编译整套 Qt6那个工作量比项目本身还大。第二层是依赖链太深。哪怕 Qt6 编出来了你还得把 xcb、fontconfig、freetype、libpng、harfbuzz、libGL 这一长串底层库全部交叉编译一遍版本还得对得上。任何一个对不上运行时就是一堆undefined symbol。第三层是调试体验断档。在 Windows 上交叉编译出来的二进制你没法本地跑只能拷到 Linux 上再试出了问题再回来改一轮迭代十几分钟。而 WSL2 里改完直接cmake --build然后./app五秒钟看到结果。结论除非你有非常特殊的合规或流水线要求否则把这部分精力花在 WSL2 或容器上回报率高得多。1.3 一张对照表帮你五分钟定方案方案上手成本构建性能环境一致性调试体验推荐场景WSL2低较好ext4 内中好个人开发、单机作业独立虚拟机中好好中需要精确控制发行版版本局域网远程机中好好好Remote SSH团队共享构建机Docker 容器中高较好Linux 宿主机时最佳极好中CI、可复现出包Windows 直接交叉编译极高差差差基本不推荐这张表我建议你按我现在最缺什么来选缺时间选 WSL2缺一致性选容器缺一台好机器就找台旧笔记本装 Ubuntu 挂局域网。2. 把 Linux 构建环境塞进 WSL2 的几个关键动作2.1 发行版选择与 Qt6 依赖清单装 WSL2 现在的命令很简单管理员权限打开 PowerShellwsl --install -d Ubuntu-24.04 wsl --set-default-version 2 wsl -l -vwsl -l -v输出里的 VERSION 必须是 2如果是 1用wsl --set-version Ubuntu-24.04 2转过来。这个一定要确认WSL1 的文件系统和网络栈跟 Linux 差得远跑 Qt 会有各种玄学问题。发行版版本这里有个取舍。Ubuntu 24.04 的 GLIBC 是 2.39Ubuntu 22.04 是 2.35Ubuntu 20.04 是 2.31。编译机越老产出的二进制兼容性越广这个规则后面第 6 节会详细展开。如果你只给自己机器用24.04 就行如果要给一堆不确定环境的客户交付用 20.04 或 22.04 更稳。依赖包这块Qt6 的桌面版比 Qt5 的依赖干净一些但 xcb 相关的一组库一个都不能少sudo apt update sudo apt install -y build-essential cmake ninja-build pkg-config git \ libgl1-mesa-dev libglu1-mesa-dev libegl1-mesa-dev \ libxkbcommon-dev libxkbcommon-x11-dev \ libxcb1-dev libxcb-cursor-dev libxcb-icccm4-dev libxcb-image0-dev \ libxcb-keysyms1-dev libxcb-randr0-dev libxcb-render-util0-dev \ libxcb-shape0-dev libxcb-xinerama0-dev libxcb-xkb-dev \ libfontconfig1-dev libfreetype6-dev \ fonts-noto-cjk gdblibxcb-cursor-dev这个包单独拎出来说因为它是 Qt 6.5 之后新加的硬依赖很多老教程里没有。少了它在 WSL 里跑任何 Qt6 GUI 程序都会直接报Could not load the Qt platform plugin xcb而且报错信息完全不会告诉你是哪个包缺了只能靠QT_DEBUG_PLUGINS1一点点扒。我第一次踩这个坑花了一个多小时。fonts-noto-cjk是给中文界面兜底的Linux 默认字体包里基本不含中文字形不加这个你界面上的中文全是方框。2.2 源码放 /mnt/c 还是 ext4性能、权限、符号链接的三重差异这是 WSL2 用户最容易忽略、但影响最大的一个决定。Windows 的 C 盘在 WSL 里的挂载点是/mnt/c走的是 9p 或 virtiofs 这类跨系统文件共享协议。它的随机小文件读写性能大概是 ext4 的十分之一到五分之一。CMake 的 configure 阶段会疯狂地 stat 成千上万个文件项目稍大一点configure 时间能从几秒变成一两分钟构建阶段更夸张一个中等规模的项目能差出五分钟以上。所以第一条建议源码放 WSL 自己的文件系统里也就是~/work/这种路径下然后 Windows 侧通过\\wsl$\Ubuntu-24.04\home\you\work访问或者干脆用 VS Code 的 Remote-WSL 直接在 WSL 里打开项目。第二条是权限。/mnt/c默认挂载不带 metadatachmod 改了也不生效所有文件看起来都是 777 或者一个固定的映射权限。你要是写 CI 脚本.sh文件忘记chmod x或者设了也没用./build.sh就报 Permission denied。解决办法是在/etc/wsl.conf里加[automount] options metadata,umask22,fmask11 [interop] appendWindowsPath false改完wsl --shutdown重启 WSL 才生效。第三条是符号链接。很多开源库的源码包里带 symlinkWindows 上解压会把它变成普通文件或者直接丢掉。Windows 侧创建 symlink 需要开发者模式或者管理员权限跨系统边界的时候尤其容易出问题。源码放 ext4 里就完全不用操心这个。我的目录组织大概是这样的~/work/ # 所有源码ext4 ~/Qt/ # Qt6 安装目录ext4 ~/build/ # 构建输出ext4 /mnt/c/Users/me/Download/ # 只用来放下载的安装包2.3 用 aqtinstall 装一套干净的 Qt6WSL 里装 Qt6 有三条路apt 装qt6-base-dev版本老、官方 online installer要图形界面虽然 WSLg 支持但很别扭、aqtinstall命令行最干净。aqtinstall 是个 Python 包直接从 Qt 官方下载站拉安装包不用图形界面python3 -m venv ~/.venv/qt source ~/.venv/qt/bin/activate pip install -U aqtinstall aqt install-qt linux desktop 6.7.3 linux_gcc_64 \ -m qtdeclarative qtquick3d qtshadertools qtimageformats qtsvg装完在~/Qt/6.7.3/gcc_64。这个路径待会儿要作为CMAKE_PREFIX_PATH传给 CMake。几个细节linux_gcc_64是目标平台标识Linux 桌面 64 位-m后面跟的是额外模块qtdeclarative提供 QML 支持用了 Quick 就必须装qtsvg如果你要显示 SVG 图标也得加。没装的模块在 CMake 里find_package会直接失败别指望它会自己补。装完验证一下source ~/.venv/qt/bin/activate aqt list-qt linux desktop --modules 6.7.3 linux_gcc_64能看到模块列表就说明仓库信息拉取正常。2.4 VS Code Remote 的连接方式如果不用 Qt CreatorVS Code Remote-WSL 是目前体验最顺的组合。装三个扩展WSL、C/C、CMake Tools。然后CtrlShiftP选WSL: Connect to WSL连上之后打开~/work/yourproject整个工作区就跑在 Linux 侧了。CMake Tools 需要配一下settings.json{ cmake.generator: Ninja, cmake.buildDirectory: ${workspaceFolder}/build, cmake.configureArgs: [ -DCMAKE_PREFIX_PATH/home/you/Qt/6.7.3/gcc_64, -DCMAKE_BUILD_TYPERelWithDebInfo ], cmake.buildArgs: [-j, 8] }RelWithDebInfo这个构建类型我强烈建议在开发和调试阶段用它开-O2但保留调试符号性能接近 Release又能正常打断点看变量。很多人用 Debug 调试 Qt 程序遇到性能问题时发现 Debug 下和 Release 下的行为完全不同白白浪费排查时间。如果你更习惯 Qt Creator也可以在 Windows 侧装 Qt Creator然后配置一个远程设备指向 WSL 里的 IP。不过这个配置过程比 VS Code 麻烦而且 Qt Creator 的 WSL 集成一直不算稳定我不太推荐。3. Qt6 的 CMake 工程跨平台时哪几行必须重写3.1 qt_standard_project_setup 之后还得亲手补的东西Qt6 推的 CMake API 比 Qt5 时代清晰太多了一个最简的CMakeLists.txt长这样cmake_minimum_required(VERSION 3.21) project(HelloQt6 VERSION 1.0.0 LANGUAGES CXX) find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui Widgets) qt_standard_project_setup() qt_add_executable(appmain src/main.cpp src/mainwindow.cpp src/mainwindow.h ) target_link_libraries(appmain PRIVATE Qt6::Widgets) install(TARGETS appmain RUNTIME DESTINATION bin)qt_standard_project_setup()在 6.3 版本引入它替你干了三件事设CMAKE_AUTOMOC/AUTOUIC/AUTORCC为 ON、把 C 标准拉到 17、设置CMAKE_RUNTIME_OUTPUT_DIRECTORY之类的默认值。看起来省事但有几个它不管的地方必须你自己动手。第一C 标准如果需要高于 17比如用到 C20 的concepts得在调用它之前设set(CMAKE_CXX_STANDARD 20)否则会被它覆盖。第二它不会帮你在 CMake 和 Qt6 版本不一致时报警。find_package(Qt6 6.5 REQUIRED)里的版本号建议写成你实际用的最低版本别写REQUIRED就完事不然同事用 6.2 的机器配置会直接崩。第三它不开任何警告选项。这跟跨平台直接相关因为 GCC 和 MSVC 的警告集完全不同你在 Windows 上编译零警告的代码到 GCC 下可能一堆-Wunused-parameter。建议这么写target_compile_options(appmain PRIVATE $$CXX_COMPILER_ID:MSVC:/utf-8 /W4 /permissive- $$CXX_COMPILER_ID:GNU:-Wall -Wextra -Wno-unused-parameter )/utf-8这个 MSVC 选项待会儿在第 3.4 节展开讲它和中文乱码直接相关。3.2 平台条件分支if(UNIX) 还是生成器表达式跨平台项目里最常写的就是平台分支。CMake 提供两种写法用错了会有坑。if(WIN32)这种是配置期判断在 CMake configure 阶段就确定下来只有当前平台的那一支会被写进构建系统。生成器表达式$$PLATFORM_ID:Linux:...是生成期判断值会原样写进构建文件由生成器Ninja/Make在构建时展开。两者在单平台构建时结果一样但有以下区别需要给不同平台链不同库用if()更直观。需要在同一个 target 上根据平台加编译选项生成器表达式的可读性更好。做多配置生成器Visual Studio、Xcode时生成器表达式能跟配置类型Debug/Release组合if()不行。实际项目里我的写法是混着用if(WIN32) target_sources(appmain PRIVATE src/platform/win32_util.cpp) target_link_libraries(appmain PRIVATE winmm) elseif(UNIX AND NOT APPLE) target_sources(appmain PRIVATE src/platform/linux_util.cpp) target_link_libraries(appmain PRIVATE dl pthread) target_compile_definitions(appmain PRIVATE PLATFORM_LINUX1) endif() target_compile_definitions(appmain PRIVATE $$CONFIG:Debug:ENABLE_VERBOSE_LOG1 )有一个坑必须提醒UNIX在 macOS 上也是 TRUE。很多人写if(UNIX)想处理 Linux 特有逻辑结果 mac 上也走了同一支链了一堆 Linux 专有的库。正确的判断是if(UNIX AND NOT APPLE)或者直接用if(CMAKE_SYSTEM_NAME STREQUAL Linux)后者最明确。3.3 RPATH让二进制自己找到 libQt6Core.soLinux 的动态链接器找库的顺序是RPATH→LD_LIBRARY_PATH→/etc/ld.so.cache→/lib和/usr/lib。注意 RPATH 排在LD_LIBRARY_PATH前面这点跟很多人的直觉相反。默认情况下 CMake 编出来的二进制的 RPATH 指向构建目录的绝对路径也就是你cmake --build那个目录。这意味着你把二进制拷到别的机器上它找不到libQt6Core.so.6直接报error while loading shared libraries。正确做法是在CMakeLists.txt里写set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) set(CMAKE_INSTALL_RPATH $ORIGIN/../lib)这三行的含义需要掰开说。第一行让构建产物和安装产物用不同的 RPATH开发时指向构建目录里的 Qt安装时才用$ORIGIN相对路径。第二行设为 FALSE 是防止 CMake 把 Qt 安装目录的绝对路径也塞进去那样等于没脱敏。第三行是关键$ORIGIN会被动态链接器替换成可执行文件所在目录所以最终目录结构是yourapp/ ├── bin/appmain └── lib/ ├── libQt6Core.so.6 ├── libQt6Gui.so.6 └── platforms/libqxcb.so注意$ORIGIN在 CMake 里不要写成${ORIGIN}后者会被当变量展开成空字符串你会得到一个看起来正常但完全没用的 RPATH。这个坑我见过至少三次。验证 RPATH 有没有写对用readelf -d build/appmain | grep -i path输出里看到$ORIGIN/../lib就对了。3.4 编码与换行符中文乱码和 bad interpreter 的根因这两个问题跟 Qt 本身没关系纯粹是 Windows 和 Linux 的文本约定不同但跨平台开发一定会撞上。编码问题。Windows 上的 MSVC 默认按系统区域设置简体中文环境下是 GBK解读源文件而 GCC 默认按 UTF-8。同样的src/main.cpp里写了一句QStringLiteral(保存成功)MSVC 编出来是乱码字符GCC 编出来正常。解决办法有两个我建议两个都上target_compile_options(appmain PRIVATE $$CXX_COMPILER_ID:MSVC:/utf-8)然后在项目根目录加.editorconfigroot true [*] charset utf-8 end_of_line lf insert_final_newline true/utf-8这个 MSVC 参数同时设置了源码字符集和执行字符集为 UTF-8是解决 Windows 侧中文问题最省事的办法。另外文件不要存成 UTF-8 with BOM除非你确定整个工具链都能正确处理BOM 有时会让脚本解释器把#!行读错。换行符问题。Windows 是 CRLFLinux 是 LF。源码文件混着来通常没事编译器不在乎。但shell 脚本必须用 LF否则你在 WSL 里跑./build.sh会看到bash: ./build.sh: /bin/bash^M: bad interpreter: No such file or directory^M就是多出来的 CR。解决办法是在项目里加.gitattributes* textauto eollf *.sh text eollf *.bat text eolcrlf *.png binary *.so binary这样 Git 在 checkout 时会自动把 shell 脚本转成 LF。已经搞混的项目可以用dos2unix批量修sudo apt install dos2unix然后find . -name *.sh -exec dos2unix {} \;。4. 从源码到可交付的 Linux 包完整构建与打包链路4.1 一键构建脚本长什么样环境搭好之后我习惯在项目根目录放一个scripts/build-linux.sh把所有参数固定下来#!/usr/bin/env bash set -euo pipefail QT_DIR${QT_DIR:-$HOME/Qt/6.7.3/gcc_64} BUILD_DIR${BUILD_DIR:-build-linux} BUILD_TYPE${BUILD_TYPE:-RelWithDebInfo} JOBS$(nproc) cmake -S . -B $BUILD_DIR \ -G Ninja \ -DCMAKE_BUILD_TYPE$BUILD_TYPE \ -DCMAKE_PREFIX_PATH$QT_DIR \ -DCMAKE_INSTALL_PREFIX$PWD/stage cmake --build $BUILD_DIR -j $JOBS cmake --install $BUILD_DIRset -euo pipefail这三件事在构建脚本里非常重要-e让任何命令失败立刻退出-u让未定义变量直接报错防止$QT_DIR拼错导致-DCMAKE_PREFIX_PATH空着-o pipefail让管道里任何一环失败都能被捕获。少写一个你的 CI 就可能在编译失败的情况下继续往下走最后打出一个全是旧文件的包。CMAKE_INSTALL_PREFIX设成$PWD/stage所有安装产物落到项目内的一个目录方便后续打包和清理也不会污染系统。这在容器环境下尤其重要因为容器的/usr/local是临时的。4.2 ldd 查不出来的依赖怎么办构建成功不等于能跑。第一次在 WSL 里执行刚编出来的二进制最常见的报错就是缺库。用ldd查ldd build-linux/appmain | grep not found但ldd有个局限它只看直接和间接的共享库依赖看不到运行时才dlopen加载的插件。Qt 的平台插件、图片格式插件、SQL 驱动全是dlopen加载的ldd完全查不出来。这种情况下要用QT_DEBUG_PLUGINS1QT_DEBUG_PLUGINS1 ./stage/bin/appmain 21 | head -50它会打印每一个被尝试加载的插件、搜索的路径、失败的原因。如果你看到一串Found metadata in lib ... but the plugin is not loadable通常是因为插件本身依赖的某个库缺了输出里会接着告诉你缺哪个。这是排查xcb插件问题最有效的手段比看各种论坛帖子快得多。还有一个更彻底的方案是用linuxdeploy的--appdir模式它会用ldd加一堆人工规则把该拷的库都拷齐。这个下一节说。4.3 qt_generate_deploy_app_scriptQt6.3 之后的官方方案Qt 6.3 引入了官方的部署脚本生成器这是我目前最推荐的方式因为它不用你手写一堆拷贝逻辑install(TARGETS appmain RUNTIME DESTINATION bin ) qt_generate_deploy_app_script( TARGET appmain OUTPUT_SCRIPT deploy_script NO_UNSUPPORTED_PLATFORM_ERROR ) install(SCRIPT ${deploy_script})NO_UNSUPPORTED_PLATFORM_ERROR的作用是在 Windows 或 macOS 上配置时不要报错直接跳过。这样同一份CMakeLists.txt在三个平台上都能正常 configure不用写平台分支。这个脚本会自动处理 Qt 库、平台插件、QML 模块如果用了 Quick、图片格式插件、翻译文件的拷贝并设置好 RPATH。cmake --install build --prefix /tmp/pkg之后/tmp/pkg就是一个可以直接拷贝到任何同架构 Linux 上运行的目录。有几个注意点。第一qt_generate_deploy_app_script必须在install(TARGETS)之后调用顺序反了会报错说找不到 target。第二它依赖find_package(Qt6 ... COMPONENTS Core ...)已经执行过相关组件都得列全。第三QML 项目里如果你用了运行时才 import 的模块比如动态Loader加载的它扫不出来得用qt_import_qml_plugins(appmain)手动补。4.4 AppImage 与 deb两种交付物的选择产物形态上Linux 桌面有个痛点没有统一的包管理生态。所以交付方式要看你客户是谁。AppImage是单文件可执行双击就能跑不用安装不依赖系统里的 Qt。用linuxdeploy打wget -O linuxdeploy.AppImage \ https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage chmod x linuxdeploy.AppImage ./linuxdeploy.AppImage --appdir AppDir \ --executable stage/bin/appmain \ --desktop-file packaging/app.desktop \ --icon-file packaging/app.png \ --plugin qt \ --output appimage--plugin qt会去拉linuxdeploy-plugin-qt它负责把 Qt 的插件和 QML 模块按正确结构摆好。生成的.AppImage可以直接发给用户。AppImage 需要 FUSE 才能运行WSL 里通常没有所以本地测试要用./YourApp.AppImage --appimage-extract-and-run加这个参数它会把自身解压到临时目录再跑。这个参数在服务器和容器环境里也一样需要记得写进你的测试文档。deb包适合 Ubuntu/Debian 系的企业用户他们习惯apt install。用 CPack 打set(CPACK_GENERATOR DEB) set(CPACK_PACKAGE_NAME helloqt6) set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) set(CPACK_DEBIAN_PACKAGE_MAINTAINER youexample.com) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_PACKAGE_FILE_NAME helloqt6-${PROJECT_VERSION}-linux-amd64) include(CPack)CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON会自动扫描二进制依赖并生成Depends字段但这只在你有dpkg-shlibdeps的环境里有效WSL 的 Ubuntu 里自带。注意它扫出来的依赖是系统包名比如libqt6core6不是 Qt 官方安装包里那套所以如果你的用户系统里没装 Qt6 的运行时deb 装完也跑不起来。稳妥做法是把 Qt 库一起打进 deb或者干脆用静态部署 AppImage。我的实际做法是内部测试给 AppImage正式交付给 deb 加一段 postinst 脚本自己设LD_LIBRARY_PATH。两种都留着按客户要求选。5. 在 Windows 上给 Linux 进程打断点调试链路怎么搭5.1 gdbserver VS Code 的最小可用配置WSL2 里跑应用、Windows 侧的 VS Code 打断点这套链路搭起来比想象中简单。第一步WSL 里装 gdb 和 gdbserversudo apt install gdb gdbserver第二步在 WSL 里启动带调试服务的程序gdbserver :2345 ./stage/bin/appmain第三步在 VS CodeRemote-WSL 模式的工作区里建.vscode/launch.json{ version: 0.2.0, configurations: [ { name: Remote Debug (gdbserver), type: cppdbg, request: launch, program: ${workspaceFolder}/stage/bin/appmain, miDebuggerServerAddress: localhost:2345, miDebuggerPath: /usr/bin/gdb, cwd: ${workspaceFolder}/stage/bin, setupCommands: [ { description: Enable pretty-printing, text: -enable-pretty-printing, ignoreFailures: true }, { description: Set library search path, text: set solib-search-path ${workspaceFolder}/stage/bin/../lib, ignoreFailures: true } ] } ] }solib-search-path那一条很关键。因为你的 Qt 库是相对 RPATH 部署的gdb 默认会去系统路径找符号找不到就会显示一堆??。设了这个之后断在 Qt 内部的栈也能看到函数名。第四步VS Code 里按 F5它会连上 gdbserver 并接管进程。断点、单步、查看变量、调用栈全部可用。一个使用上的小技巧调试阶段先让程序自己跑起来gdbserver --attach :2345 pid停在某个状态下再连接比从main开始一行行走要高效得多。Qt 程序的启动流程很长从main走到你的业务代码可能要经过几千行框架代码。5.2 Qt 类型的美化打印gdb 默认不认识QString、QList查看的时候只能看到内部的d_ptr指针非常难受。Qt 官方提供了一套 Python 美化打印脚本在$QT_DIR/share/qt6/printers目录下。让 gdb 自动加载# ~/.gdbinit python import sys sys.path.insert(0, /home/you/Qt/6.7.3/gcc_64/share/qt6/printers) from qt6printers import register_qt6_printers register_qt6_printers(None) end加载之后QString会显示成字符串内容QListT会显示成数组QMap会显示成键值对。这个提升是质变级别的尤其是排查字符串处理逻辑的时候。注意路径里的qt6printers模块名在 Qt 6 里就是qt6printers.pyQt5 时代叫qt5printers网上很多老文章里的模块名是错的直接抄会 ImportError。5.3 QML 与 qDebug 日志回流到 Windows纯 C 的 Widgets 程序一条qDebug()就够了输出会直接出现在 gdbserver 的终端里。但如果用了 Qt Quick情况会复杂一些因为 QML 引擎的日志走的是另一条路。最省事的办法是在main.cpp里装一个消息处理器把日志同时写到文件#include QFile #include QTextStream #include QDateTime static void messageHandler(QtMsgType type, const QMessageLogContext ctx, const QString msg) { static QFile logFile(QStringLiteral(app.log)); if (!logFile.isOpen()) { logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text); } const QString line QStringLiteral([%1][%2] %3\n) .arg(QDateTime::currentDateTime().toString(Qt::ISODate)) .arg(static_castint(type)) .arg(msg); logFile.write(line.toUtf8()); logFile.flush(); fprintf(stderr, %s, line.toLocal8Bit().constData()); } int main(int argc, char *argv[]) { qInstallMessageHandler(messageHandler); // ... }写到文件的好处是 WSL 和 Windows 之间的文件系统互通你在 Windows 侧用任何编辑器都能实时看日志不用切终端。logFile.flush()那一行别省不然程序崩溃时最后几行日志会丢在缓冲区里而那几行往往就是崩溃原因。如果要调试 QML 本身比如绑定不生效、组件没渲染可以加-qmljsdebuggerport:65530,block启动参数然后让 Qt Creator 或者 Qt 官方的 QML Debugger 连上去。这套东西我用得不多因为大多数 QML 问题看日志加二分注释就能定位。5.4 WSLg 环境下 GUI 调试的注意事项Windows 11 自带 WSLgWSL 里的 GUI 程序会直接以窗口形式显示不需要额外配 X Server。这对调试非常友好——你可以直接看到界面渲染结果。但有三个点要注意。第一首次运行会慢。WSLg 第一次启动 compositor 要几秒你会觉得程序卡住了其实是在初始化显示。第二次之后就正常了。第二缩放和高 DPI。WSLg 默认的 DPI 可能和 Windows 侧不一致Qt 程序在高分屏上可能界面很小或者很大。可以通过环境变量强制export QT_SCALE_FACTOR1.5或者在main.cpp里设置QGuiApplication::setHighDpiScaleFactorRoundingPolicy()。第三字体渲染差异。WSLg 用的字体渲染引擎和 Windows 原生不同同一个界面在 WSLg 里和真实 Linux 机器上看起来可能有细微差别。如果客户对界面像素级还原有要求最终验收一定要在真实 Linux 环境里跑一遍。6. 踩过的坑清单这些现象背后其实是同一类问题6.1 Could not load the Qt platform plugin xcb这是 Qt6 在 Linux 上最高频的报错没有之一。它出现的原因至少有五种现象根因解法完全找不到插件部署时没拷platforms/目录检查bin/../plugins/platforms/libqxcb.so是否存在找到插件但加载失败缺libxcb-cursor0apt install libxcb-cursor0只有 WSL 里出错缺libxkbcommon-x11-0装对应的运行时包部署后报错开发时正常RPATH 没设或设错readelf -d检查重设CMAKE_INSTALL_RPATH报错信息指向别的插件环境变量污染检查QT_PLUGIN_PATH和QT_QPA_PLATFORM_PLUGIN_PATH排查这套东西的唯一正解是QT_DEBUG_PLUGINS1它会告诉你插件搜索了哪些路径、每个路径下找到了什么、加载失败的具体原因。我强烈建议把这个环境变量加到你的默认调试流程里能省掉大量猜测。另外要注意一个环境变量的优先级问题如果系统里已经装了qt6-base的 apt 包QT_PLUGIN_PATH可能被设成了系统路径导致你的私有部署被忽略。用env | grep QT_检查一下有的话在启动脚本里unset掉。6.2 GLIBC_2.34 not found 与越老越保险的构建机这是交付环节最隐蔽的坑。你的程序在 WSL 的 Ubuntu 24.04 上跑得好好的拷到客户的 Ubuntu 20.04 机器上立刻报/lib/x86_64-linux-gnu/libc.so.6: version GLIBC_2.34 not found原因是GLIBC 是向后兼容但不向前兼容的在老版本 GLIBC 上编的程序能在新版本上跑反过来不行。你的程序链接了 2.39 版本的符号客户机器只有 2.31自然报错。判断你的二进制需要多高的 GLIBCobjdump -T stage/bin/appmain | grep GLIBC_ | sed s/.*GLIBC_// | sort -V -u | tail -5输出里最高的那个版本号就是你的最低要求。解法有三个。最彻底的是在目标最低版本的系统里构建比如用ubuntu:20.04容器跑一遍构建流程这就是第 1.1 节提到的容器的最大价值。其次是动态加载而不是直接链接把用到新符号的功能用dlopen延迟加载运行时判断版本再决定用不用这个改造成本很高。最后是静态链接 libstdc-static-libstdc -static-libgcc能解决一部分GLIBCXX_3.4.29 not found的问题但解决不了GLIBC_2.34 not found因为后者是 libc 本身的符号。我的项目最终就是用了一个Dockerfile基于ubuntu:20.04做发布构建日常开发还在 24.04 的 WSL 里。这个双轨制用了半年很稳。FROM ubuntu:20.04 RUN apt-get update DEBIAN_FRONTENDnoninteractive apt-get install -y \ build-essential cmake ninja-build git python3-pip \ libgl1-mesa-dev libxkbcommon-dev libxcb-cursor0 COPY . /src WORKDIR /src RUN ./scripts/build-linux.sh cmake --install build-linux --prefix /out6.3 文件名大小写Windows 上的侥幸Linux 上的报错Windows 的 NTFS 默认大小写不敏感#include MyHeader.h找不到myheader.h也能编过。Linux 的 ext4 大小写敏感同样的代码直接fatal error: MyHeader.h: No such file or directory。这个坑的隐蔽在于它不会在你改代码时出现而是在你新加一个文件或者从 Windows 拷一批文件进来时突然爆发。而且报错信息指向的头文件名看起来明明存在只是大小写不同很容易让人怀疑编译器。两个应对手段。一是在 Windows 侧开启目录大小写敏感需要管理员 PowerShellfsutil file setCaseSensitiveInfo D:\projects\myapp enable这样你在 Windows 上编译时也会遇到同样的报错问题提前暴露。注意这个属性只对当前目录生效子目录要递归设而且对挂载的 WSL 路径不生效。二是在 CI 里加一道检查用一条命令扫出所有不一致的 includefind . -name *.cpp -o -name *.h | xargs grep -h ^#include | \ sed s/.*#include \(.*\).*/\1/ | sort -u | while read f; do find . -name $(basename $f) | grep -q . || echo MISSING: $f done更简单粗暴的做法是给项目定个规矩所有文件名一律小写加下划线。团队里立了这个规矩之后这类问题基本绝迹。6.4 中文显示成方框与字体依赖链程序能跑起来界面也出来了但所有中文都是方框。这不是编码问题编码问题会显示乱码字符而不是方框是字体缺失。Qt 在 Linux 上通过 fontconfig 查找字体系统里没有中文字形的字体时它就渲染成方框。装上 CJK 字体就行sudo apt install fonts-noto-cjk fonts-wqy-zenhei fonts-wqy-microhei fc-cache -fvfc-cache -fv那一步别忘装完字体不刷新缓存fontconfig 还是找不到。但这里有个更深的问题你的交付包里不能假设用户系统装了中文字体。给客户部署的时候如果对方是个精简的服务器版 Linux一样会出方框。稳妥做法是在main.cpp里内置字体int main(int argc, char *argv[]) { QApplication app(argc, argv); int fontId QFontDatabase::addApplicationFont(:/fonts/NotoSansCJKsc-Regular.otf); if (fontId ! -1) { QStringList families QFontDatabase::applicationFontFamilies(fontId); if (!families.isEmpty()) { QFont font(families.first(), 10); app.setFont(font); } } // ... }把字体文件加到resources.qrc里编译进二进制。一个完整的思源黑体 CJK 子集大概两三兆对桌面应用完全可以接受。这样无论用户系统有什么字体界面都不会出方框。顺便说一个我踩过的小坑addApplicationFont的路径必须以:/开头走 Qt 资源系统如果你写成相对文件路径打包成 AppImage 之后因为工作目录变化会加载失败界面又是方框而你在开发机上完全复现不出来。6.5 一个容易被忽略的收尾检查正式交付前我习惯在干净的容器里跑一遍冒烟测试这是唯一能保证用户拿到就能跑的办法docker run --rm -v $PWD/stage:/app ubuntu:20.04 bash -c apt-get update -qq apt-get install -y -qq libgl1 libxkbcommon-x11-0 libxcb-cursor0 /dev/null QT_QPA_PLATFORMoffscreen /app/bin/appmain --self-test QT_QPA_PLATFORMoffscreen让 Qt 在无显示设备的环境里也能初始化适合做自动化冒烟测试。如果你的程序支持一个--self-test参数做基本功能自检整个交付流程就闭环了。我现在的做法是每个项目都实现一个--self-test哪怕只是加载一下配置、创建一下窗口、退出来也比人工点一遍靠谱。这套东西跑通之后Windows 写代码、Linux 出包的流程就真的顺了。我实际用下来最大的体会是别试图让两边的环境看起来一样而是让每条职责边界清晰——Windows 负责编辑、版本控制、看日志Linux 负责编译、链接、跑测试中间的同步靠 Git 和挂载目录。边界一旦清晰绝大部分跨平台问题都会变成一次性的配置工作而不是反复出现的幽灵 bug。