如何用 Python、CMake 和 Docker 统一 C++ 跨平台构建?
结论
可以用 Python 提供统一的构建入口,用 CMake Presets 管理构建参数,并通过 Docker 提供工具链环境。不同目标需要不同的执行方式:Linux 上的 MinGW-w64 可以直接生成 Windows 程序;Windows 原生 MSVC 构建需要 Windows 执行环境。Wine 可以运行部分 Windows 工具,但不能保证完整的 MSVC、Windows SDK 和 Qt 工具链都能正常工作。
建议先统一配置、命令和产物目录,再逐项接入工具链。工具链仍需安装,可以集中放在容器镜像或独立目录中,减少宿主机上的配置工作。
构建环境应该怎样划分?
原问题把本机构建、交叉编译、容器和 Wine 放在同一组需求中。这几项解决的问题各不相同:
- 本机构建和交叉编译决定编译器运行的平台与产物运行的平台是否一致。
- Docker 用来隔离工具链和依赖。
- Wine 用来运行 Windows 可执行文件。
- Python 负责选择配置和启动进程。
执行环境可以这样安排:
| 目标 | 执行方式 | 主要条件 |
|---|---|---|
| Linux x86_64 | x86_64 Linux 容器内运行 GCC | 容器包含对应 Qt 和依赖 |
| Linux aarch64 | aarch64 Linux 容器内构建 | 宿主机为 aarch64,或已配置跨架构模拟 |
| Linux aarch64 | x86_64 容器内运行交叉编译器 | 配置 aarch64 工具链、sysroot 和目标依赖 |
| Windows MinGW | Linux 容器内运行 MinGW-w64 | 使用匹配的 Windows Qt 和依赖 |
| Windows MSVC | Windows 主机、虚拟机或远程构建节点 | 初始化 MSVC 与 Windows SDK 环境 |
如果宿主机是 x86_64,docker run --platform linux/arm64 只指定镜像平台,不会自动为宿主机安装 QEMU/binfmt 支持。下面的示例采用 aarch64 容器内构建的方式,运行前需要准备相应的执行能力。
1. 让 CMake 负责解析 Presets
不要在 Python 中重新实现 CMakePresets.json 的解析规则。preset 可能涉及继承、宏展开、环境变量和文件包含,直接读取几个 JSON 字段容易漏掉这些语义。
Python 只需要调用:
cmake --list-presets
cmake --preset linux-x64
cmake --build --preset linux-x64
配置 preset 和构建 preset 是两类对象,可以使用相同名称建立对应关系。
下面的示例使用 preset schema version 3,需要 CMake 3.21 或更高版本。如果项目自身或 Qt 要求更高版本,应按更高要求准备环境。
{
"version": 3,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "native",
"inherits": "base"
},
{
"name": "linux-x64",
"inherits": "base"
},
{
"name": "linux-arm64",
"inherits": "base"
},
{
"name": "windows-mingw",
"inherits": "base",
"toolchainFile": "${sourceDir}/cmake/toolchains/mingw-x64.cmake"
}
],
"buildPresets": [
{"name": "native", "configurePreset": "native"},
{"name": "linux-x64", "configurePreset": "linux-x64"},
{"name": "linux-arm64", "configurePreset": "linux-arm64"},
{"name": "windows-mingw", "configurePreset": "windows-mingw"}
]
}
这里的 linux-arm64 没有指定交叉编译工具链,因为它会在 aarch64 容器中运行该架构的编译器。如果选择在 x86_64 容器内交叉编译,需要另外配置 toolchain file 和目标 sysroot。
2. 为 MinGW 配置明确的交叉编译环境
创建 cmake/toolchains/mingw-x64.cmake:
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
set(CMAKE_FIND_ROOT_PATH
/usr/x86_64-w64-mingw32
/opt/qt/target)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
这些路径是示例约定,必须与实际镜像中的工具链布局一致。这样的配置让 CMake 查找目标平台的库和头文件,同时允许构建期间使用 Linux 上的可执行工具。
使用 Linux 版 MinGW-w64 编译 Windows 程序不需要 Wine。如果构建过程需要执行 Windows 程序,例如代码生成器、配置探测程序或测试程序,才需要进一步考虑 Wine。
3. 用 Python 选择执行环境
可以将下面的 build.py 放在项目根目录。它提供交互菜单,也支持直接指定配置;本机模式使用当前进程环境,容器模式设置固定的工作目录和 Qt 查找路径。
示例中的镜像需要提前准备,并通过环境变量指定实际名称。这里不假设某个现成镜像已经包含所需工具链。
#!/usr/bin/env python3
import argparse
import os
from pathlib import Path
import subprocess
ROOT = Path(__file__).resolve().parent
PROFILES = {
"native": {},
"linux-x64": {
"image_env": "CXX_IMAGE_X64",
"platform": "linux/amd64",
},
"linux-arm64": {
"image_env": "CXX_IMAGE_ARM64",
"platform": "linux/arm64",
},
"windows-mingw": {
"image_env": "CXX_IMAGE_MINGW",
"platform": "linux/amd64",
},
}
def execute(profile, command):
config = PROFILES[profile]
if not config:
subprocess.run(command, cwd=ROOT, check=True)
return
image = os.environ.get(config["image_env"])
if not image:
raise SystemExit(
f"Set {config['image_env']} to a prepared toolchain image"
)
docker_command = [
"docker", "run", "--rm",
"--platform", config["platform"],
"--user", f"{os.getuid()}:{os.getgid()}",
"--env", "HOME=/tmp",
"--env", "CMAKE_PREFIX_PATH=/opt/qt/target",
"--volume", f"{ROOT}:/work",
"--workdir", "/work",
image,
*command,
]
subprocess.run(docker_command, check=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--profile", choices=list(PROFILES))
parser.add_argument(
"--jobs", type=int, default=os.cpu_count() or 1
)
args = parser.parse_args()
if args.jobs < 1:
parser.error("--jobs must be positive")
profile = args.profile
if profile is None:
names = list(PROFILES)
for index, name in enumerate(names, start=1):
print(f"{index}. {name}")
try:
index = int(input("Select configuration: "))
if not 1 <= index <= len(names):
raise ValueError
profile = names[index - 1]
except ValueError:
raise SystemExit("Invalid selection")
execute(profile, ["cmake", "--preset", profile])
execute(profile, [
"cmake", "--build", "--preset", profile,
"--parallel", str(args.jobs),
])
if __name__ == "__main__":
try:
main()
except subprocess.CalledProcessError as error:
raise SystemExit(error.returncode)
容器分支面向 Linux 宿主机,使用 os.getuid()、os.getgid() 设置输出文件的所有者。镜像中的工具和依赖需要允许该用户读取、执行。
假设团队已经构建并发布了对应镜像,可以按下面的方式调用。镜像名只是占位示例,需要替换:
export CXX_IMAGE_X64='your-registry/cxx-linux-x64:toolchain-v1'
export CXX_IMAGE_ARM64='your-registry/cxx-linux-arm64:toolchain-v1'
export CXX_IMAGE_MINGW='your-registry/cxx-windows-mingw:toolchain-v1'
python3 build.py
python3 build.py --profile linux-x64
python3 build.py --profile linux-arm64 --jobs 4
python3 build.py --profile windows-mingw
Python 使用参数列表启动进程,并通过 check=True 在命令失败时停止构建。配置和编译在同一类容器环境中执行,项目统一挂载到 /work,可以避免 CMake 缓存中记录的路径在两个阶段发生变化。
4. Qt 必须按宿主工具与目标库分别准备
Qt 项目通常比普通 C++ 项目多一层约束:构建时执行的工具需要能在构建环境中运行,最终链接的库则必须属于目标平台。
例如,在 Linux 上用 MinGW-w64 构建 Windows 应用时:
moc、rcc等构建工具需要有可在当前构建环境运行的版本。- Qt 库和其他第三方库需要面向 Windows,并与所选 MinGW 工具链兼容。
- MSVC 版 Qt 不能直接当作 MinGW 版 Qt 使用。
示例把目标 Qt 的安装前缀设为 /opt/qt/target,并通过 CMAKE_PREFIX_PATH 提供给 CMake。实际项目还需要按 Qt 版本和构建方式配置宿主工具。对于需要宿主 Qt 的 Qt 6 交叉编译配置,可以通过 QT_HOST_PATH 指定宿主 Qt 安装位置,例如:
cmake --preset windows-mingw -DQT_HOST_PATH=/opt/qt/host
该命令应在准备好这些目录的构建环境中执行。如果项目需要这个参数,应将其加入对应 preset;只在一次手动命令中设置,不能代替团队共享配置。
安装 Qt 后,还要核对目标架构、编译器、Qt 模块和第三方依赖是否匹配,才能判断环境是否可用。
5. Wine 与 MSVC 应作为独立执行配置接入
如果需要通过 Wine 执行某个 Windows 工具,建议为该工具链单独准备镜像,并明确设置 WINEPREFIX、工具路径和必要的持久化挂载。先验证具体工具,再接入统一入口。
不要给所有 Windows 目标默认启用 Wine。交叉编译过程中出现 try_run() 或执行目标程序的步骤时,能否使用 Wine 取决于程序本身及其依赖;GUI、驱动或系统服务相关程序尤其需要单独验证。
MSVC 原生构建适合交给 Windows 构建节点。Windows 侧先通过 Developer Command Prompt 或 VsDevCmd.bat 初始化环境,再执行相同形式的 CMake preset 命令。Linux 上的 Python 入口可以调用远程任务或 CI 作业,保留统一的操作入口。
在 Wine 下运行 MSVC 的可用性需要针对具体工具链版本验证,不能将其视为有保证的通用方案。
6. 共享配置时需要保留哪些边界?
仓库中适合保存 CMakePresets.json、toolchain file、Python 入口和镜像构建定义。开发者个人的安装路径可以放进不提交的 CMakeUserPresets.json;便携式工具链则通过固定目录或环境变量接入。
共享配置时还需要注意:
- 每个目标使用独立构建目录。切换编译器、架构或工具链版本后,应使用新目录或清理对应的旧缓存。
- 容器内路径保持一致。本机与容器不要共用包含不同绝对路径的 CMake 构建目录。
- 团队共享镜像应固定版本;需要严格复现时,记录镜像 digest,并管理工具链和依赖版本。
- 编译成功与运行验证分开处理。Windows 产物应在合适的 Windows 环境验证,aarch64 产物应在对应架构环境验证。
- 第三方工具链、Windows SDK 和 Qt 的获取与分发需要符合各自许可条件。
这套结构可以先覆盖 Linux x86_64、Linux aarch64 和 Windows MinGW,再通过同一个 Python 入口扩展 Windows MSVC 构建节点。各平台的工具链差异由镜像和 preset 处理,调用方式保持一致。
备注:内容仅供参考。
