Skip to content

第 2 章:工程骨架 —— CMake 如何拼出不同的 Transmission

同一仓库要在 Linux、Windows、macOS、Android 和嵌入式环境里构建不同产品。理解 CMake 不是“安装前置知识”,而是理解模块边界最直接的办法。

一、顶层构建在做四件事

CMakeLists.txt 的工作可以归纳为:定义产品矩阵、探测平台能力、选择依赖来源、组装目标。

1.1 功能开关不是简单布尔值

GTK、Qt、macOS 与部分系统能力使用 tr_auto_option:值可以是 ON/OFF/AUTOAUTO 允许构建系统“找到依赖就开,找不到就跳过”;显式 ON 则把缺依赖升级为配置错误。对跨发行版软件来说,这比大量固定预设更稳健。

主要开关包括:

  • 产品:ENABLE_DAEMONENABLE_GTKENABLE_QTENABLE_MACENABLE_CLIENABLE_UTILS
  • 核心能力:ENABLE_UTPENABLE_NLSWITH_INOTIFYWITH_KQUEUEWITH_SYSTEMD
  • 工程质量:ENABLE_TESTSENABLE_WERRORRUN_CLANG_TIDY
  • 资产:REBUILD_WEBINSTALL_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*

这种设计有三个现实收益:

  1. ABI 表面积小:内部类、模板和容器不暴露给调用方;
  2. 跨语言容易:Objective-C++、C++ GUI 与其他 FFI 都能调用;
  3. 重构自由度高tr_session 内部从裸指针改成 unique_ptr,不必让所有前端同步修改类型。

代价是 API 看起来比现代 C++ 冗长,错误返回与所有权约定要靠命名和文档维持。

四、平台差异被压在哪些位置

能力统一接口平台实现
文件系统file.h / file-utils.hfile-posix.ccfile-win32.cc
Peer socketpeer-socket.hTCP 与 µTP 两套适配器
子进程subprocess.hsubprocess-posix.ccsubprocess-win32.cc
Watch directorywatchdir.hinotify、kqueue、Win32、generic polling
加密crypto-utils.hCommonCrypto、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 服务libeventSession 事件循环、RPC server、socket 回调
HTTP 客户端libcurlTracker HTTP、webseed、端口测试、blocklist 下载
P2P 扩展dht、libutp去中心化发现、UDP 传输
网络环境miniupnpc、libnatpmp、libpsl端口映射、域名策略
数据与性能libdeflate、fast_float、wide-integer压缩、解析、数值工具
基础 C++fmt、sigslot、small、utf8cpp格式化、信号、小容器、UTF-8
测试GoogleTest单元与模块测试

七、从构建图推导出的架构事实

  1. 内核是产品族的唯一强依赖中心。UI 间没有依赖,避免形成“大一统前端层”。
  2. RPC 是能力,不是 daemon 专属模块。RPC server 由 Session 持有,本地 GUI 也能开启 Web/RPC。
  3. Qt 的远程模式不是另一套业务逻辑。它复用 RPC 客户端,把本地与远端响应统一成 tr_variant
  4. Web 是被内核托管的静态客户端。HTTP 服务与 RPC 在同一个 tr_rpc_server 中路由。
  5. 测试链接核心而非启动完整 UI。这使协议和数据结构测试更快、更稳定。

八、建议的最小构建阅读法

如果要确认某个源文件最终进入哪个产品,不要猜:

  1. 在所属目录的 CMakeLists.txttarget_sources
  2. 沿 target_link_libraries 向上看目标;
  3. 回根 CMake 看对应 ENABLE_* 条件;
  4. 再看 target_compile_definitions 判断平台分支。

这条路径也适合排查“代码明明改了为什么没生效”:常见原因不是函数没被调用,而是当前构建根本选了另一套平台实现或关闭了目标。

源码锚点

文档采用 CC BY-SA 4.0;源码片段保留 Transmission 上游许可。