第 2 章:工程骨架 —— CMake 如何拼出不同的 Transmission
同一仓库要在 Linux、Windows、macOS、Android 和嵌入式环境里构建不同产品。理解 CMake 不是“安装前置知识”,而是理解模块边界最直接的办法。
一、顶层构建在做四件事
根 CMakeLists.txt 的工作可以归纳为:定义产品矩阵、探测平台能力、选择依赖来源、组装目标。
1.1 功能开关不是简单布尔值
GTK、Qt、macOS 与部分系统能力使用 tr_auto_option:值可以是 ON/OFF/AUTO。AUTO 允许构建系统“找到依赖就开,找不到就跳过”;显式 ON 则把缺依赖升级为配置错误。对跨发行版软件来说,这比大量固定预设更稳健。
主要开关包括:
- 产品:
ENABLE_DAEMON、ENABLE_GTK、ENABLE_QT、ENABLE_MAC、ENABLE_CLI、ENABLE_UTILS; - 核心能力:
ENABLE_UTP、ENABLE_NLS、WITH_INOTIFY、WITH_KQUEUE、WITH_SYSTEMD; - 工程质量:
ENABLE_TESTS、ENABLE_WERROR、RUN_CLANG_TIDY; - 资产:
REBUILD_WEB与INSTALL_WEB。
1.2 “系统依赖或 vendored 依赖”是发行友好策略
USE_SYSTEM_* 允许发行版维护者链接系统库,也允许普通用户使用 third-party/ 中的固定副本。libevent、DHT、µTP、fmt、fast_float、small、utf8cpp 等依赖都遵循这一模式。
这不是重复建设:系统包方便安全更新与统一维护,vendored 副本保证从源码构建的可重复性。
二、目标依赖图
核心库由 libtransmission/CMakeLists.txt 定义为静态库,产品目标链接它和各自 UI/平台依赖。
libtransmission-app 是很薄的共享应用层,提供展示模式、转换器、favicon cache 等可复用逻辑;它没有把 GUI 统一成一个跨平台框架。GTK、Qt 与 Cocoa 仍保留各自的原生交互风格。
三、为什么核心仍提供 C API
虽然内部已大量使用 C++20,公共边界仍是 transmission.h 中的 C 风格函数与 opaque pointer:tr_session*、tr_torrent*、tr_ctor*。
这种设计有三个现实收益:
- ABI 表面积小:内部类、模板和容器不暴露给调用方;
- 跨语言容易:Objective-C++、C++ GUI 与其他 FFI 都能调用;
- 重构自由度高:
tr_session内部从裸指针改成unique_ptr,不必让所有前端同步修改类型。
代价是 API 看起来比现代 C++ 冗长,错误返回与所有权约定要靠命名和文档维持。
四、平台差异被压在哪些位置
| 能力 | 统一接口 | 平台实现 |
|---|---|---|
| 文件系统 | file.h / file-utils.h | file-posix.cc、file-win32.cc |
| Peer socket | peer-socket.h | TCP 与 µTP 两套适配器 |
| 子进程 | subprocess.h | subprocess-posix.cc、subprocess-win32.cc |
| Watch directory | watchdir.h | inotify、kqueue、Win32、generic polling |
| 加密 | crypto-utils.h | CommonCrypto、OpenSSL、mbedTLS、wolfSSL、fallback |
| 字符串平台桥 | string-utils.h | 通用 C++ 与 macOS .mm |
正确的可移植层通常长这样:业务层依赖一个窄接口,编译系统只选择一个实现。Transmission 大体遵守这一点,因此 Peer Manager 不需要知道 Windows 文件句柄和 POSIX fd 的差别。
五、Web UI 为什么默认不重建
仓库同时保存 web/src/ 与生成后的 web/public_html/。默认 REBUILD_WEB=OFF,构建本地客户端不要求安装 npm;只有开发 Web UI 或发布新资产时才运行 esbuild。
这体现了桌面软件常见的“源资产与发布资产并存”策略:
- 发行包构建不被 Node 工具链卡住;
- daemon 可以直接安装并托管静态文件;
- Web 开发者仍可在
web/独立 lint/build。
代价是生成物可能与源码不同步,所以发布流程必须承担一致性检查。
六、依赖不是平铺的
把第三方库按责任分组,比背名字有用:
| 责任 | 代表依赖 | 被谁使用 |
|---|---|---|
| 事件与 HTTP 服务 | libevent | Session 事件循环、RPC server、socket 回调 |
| HTTP 客户端 | libcurl | Tracker HTTP、webseed、端口测试、blocklist 下载 |
| P2P 扩展 | dht、libutp | 去中心化发现、UDP 传输 |
| 网络环境 | miniupnpc、libnatpmp、libpsl | 端口映射、域名策略 |
| 数据与性能 | libdeflate、fast_float、wide-integer | 压缩、解析、数值工具 |
| 基础 C++ | fmt、sigslot、small、utf8cpp | 格式化、信号、小容器、UTF-8 |
| 测试 | GoogleTest | 单元与模块测试 |
七、从构建图推导出的架构事实
- 内核是产品族的唯一强依赖中心。UI 间没有依赖,避免形成“大一统前端层”。
- RPC 是能力,不是 daemon 专属模块。RPC server 由 Session 持有,本地 GUI 也能开启 Web/RPC。
- Qt 的远程模式不是另一套业务逻辑。它复用 RPC 客户端,把本地与远端响应统一成
tr_variant。 - Web 是被内核托管的静态客户端。HTTP 服务与 RPC 在同一个
tr_rpc_server中路由。 - 测试链接核心而非启动完整 UI。这使协议和数据结构测试更快、更稳定。
八、建议的最小构建阅读法
如果要确认某个源文件最终进入哪个产品,不要猜:
- 在所属目录的
CMakeLists.txt找target_sources; - 沿
target_link_libraries向上看目标; - 回根 CMake 看对应
ENABLE_*条件; - 再看
target_compile_definitions判断平台分支。
这条路径也适合排查“代码明明改了为什么没生效”:常见原因不是函数没被调用,而是当前构建根本选了另一套平台实现或关闭了目标。
源码锚点
CMakeLists.txt:全局选项、版本与第三方库策略libtransmission/CMakeLists.txt:核心源文件和链接依赖libtransmission-app/CMakeLists.txt:共享应用层gtk/CMakeLists.txt、qt/CMakeLists.txt:GUI 目标web/package.json:Web 工具链