首页 / 博客 / Swift 6.4 duplic
ENGINEERING_BLOG · 2026.09.08

Swift 6.4 duplicate module name:2026 远程 Mac CI 修复

截至 2026 年 9 月 8 日,Apple 的 Xcode 27 beta 6 已搭载 Swift 6.4;其发布说明明确要求同一次 Swift dependency scan 可达的 Clang module 使用唯一名称。(developer.apple.com)

本周建议动作:先保留稳定工具链,不要重建节点或全量清缓存;用失败构建的首个有效诊断,反查两个同名 module.modulemap 的来源、搜索路径和依赖版本。 如果冲突来自你自己的声明,合并或重命名;如果来自暂时无法修改的第三方 SDK,则生产线维持稳定版本,Swift 6.4 作为隔离验证线。

这篇文章适合维护 Swift、Objective-C、C/C++ 混合依赖和二进制 SDK 的开发者,也适合负责远程 Mac 构建节点、依赖缓存和 Xcode 工具链切换的 DevOps 工程师。如果你需要决定“现在修、暂缓升级,还是保留双版本 CI”,下面的取证顺序可以直接用于故障单和变更评审。

SECTION 01 先用时间线确认:这是模块重名,还是普通编译失败?

不要从最终退出码开始判断。先保存一次成功构建和一次失败构建的完整信息,包括活动 Xcode 路径、Swift 编译器版本、SDK 选择、完整 xcodebuild 命令、Package.resolved、依赖提交号以及远程节点的环境变量。

Xcode 27 beta 6 的系统要求页面显示,该版本需要 macOS Tahoe 26.4 或更高版本,并使用 Swift 6.4 编译器;Xcode 26.6 则对应 Swift 6.3。这个差异足以让同一份项目在两个节点上走不同的依赖扫描路径,但不能单凭版本差异认定某个依赖必然不兼容。(developer.apple.com)

先把日志分成四类:

日志现象 故障边界 第一取证动作
duplicate module name 或同名 Clang module 诊断 模块声明或搜索路径冲突 找出所有可达的 module.modulemap
redefinition、类型重复定义 头文件被多次声明、宏环境不同或模块边界错误 对照重复类型的头文件入口和编译宏
module not found 搜索路径缺失、SDK 或依赖未进入扫描范围 检查 -I-F、SDKROOT 和依赖生成目录
链接阶段 ld、符号未定义 通常已越过模块扫描阶段 转查库搜索路径、架构和链接参数

Apple 的说明把这次变化限定在 Swift dependency scanner 对 Clang module 的处理上:过去扫描器可能容忍重复名称,新版扫描要求同一次扫描可达的模块名称唯一。它并不等同于 Swift module、静态库或链接库名称必须全部唯一。(developer.apple.com)

⚠️ 不要把“清理 DerivedData 后暂时通过”当成修复证据。它只能说明缓存状态发生了变化,不能证明两个模块声明已经消失。

SECTION 02 项目自有声明重复:从 module.modulemap、Bridging Header 和参数入手

第一条故障线是项目自己把同一个 Clang module 声明了两次。常见组合包括:仓库内有自定义 module.modulemap,某个依赖又携带一份同名声明;Bridging Header 引入了一组头文件,同时 Header Search Paths 又把同一组头文件作为模块入口暴露;脚本还额外追加了 -fmodule-map-file=

module.modulemap 不是普通配置文件。它把一组 C 或 Objective-C 头文件映射为逻辑模块,并通过模块名、头文件入口和导出规则提供给 Swift 使用。Swift Package Manager 的系统库目标也可以通过该文件暴露 C 库;自定义文件存在时,SwiftPM 会优先使用它,而不是随意生成另一份映射。(github.com)

在远程 Mac CI 上,建议先生成一份不修改工作区的证据:

set -o pipefail

xcode-select -p
xcrun swift --version
xcrun --sdk macosx --show-sdk-path

find "${WORKSPACE}" "${DEPENDENCY_ROOT}" \
  \( -name module.modulemap -o -name module.private.modulemap -o -name module.map \) \
  -print 2>/dev/null | sort

grep -RInE '^[[:space:]]*(framework[[:space:]]+)?module[[:space:]]+' \
  "${WORKSPACE}" "${DEPENDENCY_ROOT}" 2>/dev/null \
  | tee "${ARTIFACT_DIR}/module-declarations.txt"

命令中的 ${WORKSPACE}${DEPENDENCY_ROOT}${ARTIFACT_DIR} 必须替换为你的实际路径。不要直接使用全盘 find /,否则系统 SDK、工具链缓存和无关目录会混入结果,反而难以判断哪些文件真的进入了本次构建。

找到同名模块后,逐项比较:

  1. 模块声明名称是否完全相同,包括大小写和点号分层。
  2. headerumbrella 指向的入口是否相同。
  3. 两份文件是否来自同一个依赖的复制目录,还是来自两个不同组件。
  4. 构建命令是否同时传入了显式 -fmodule-map-file= 和隐式搜索路径。
  5. Bridging Header 是否仍然需要这些头文件,还是可以改为显式模块导入。

优先选择合并声明、移除重复复制或重命名自有模块。不要随机删除某个目录,也不要把第三方文件直接覆盖进生产仓库;那样可能让本地构建通过,却使下一个节点缺少必要头文件。

SECTION 03 第三方源码和二进制 SDK:区分“互相重名”与“冒充系统模块”

第二条故障线来自依赖树。你需要分别检查 Swift Package、vendored source、XCFramework、手工拖入的 SDK,以及脚本生成的中间目录。二进制 SDK 即使没有明显的源码目录,也可能在 Framework 的 Modules/module.modulemap 中携带模块声明。

这两类冲突的修复方式不同:

  • 两个第三方组件使用相同的自定义模块名:查看依赖方是否已有兼容版本;如果没有,优先隔离其中一个组件的头文件搜索路径,而不是修改二进制内部内容。
  • 第三方组件重新声明系统或 SDK 模块:确认它是否把系统模块的 module.modulemap 复制到了自己的头文件目录;这种情况通常应由上游发布修复版本,生产仓库不宜长期维护临时补丁。
  • 同一依赖被源码和二进制形式同时接入:检查 Package.swift、Build Phases、脚本参数和 Framework Search Paths,避免同时加载两个分发形态。
  • 自有封装模块与第三方模块误用同名:重命名自有模块通常比改第三方接口更容易回滚,也更容易在依赖锁文件中留下清晰记录。

Clang 官方文档说明,模块映射文件通常与头文件放在一起,隐式模块搜索会寻找 module.modulemap;也可以通过 -fmodule-map-file= 显式加载指定文件。这个边界很重要:你要追踪的是“哪些文件对扫描可见”,不是简单统计仓库里有多少个同名文件。(github.com)

依赖来源 重点检查位置 更稳妥的处理
Swift Package 的 C / Objective-C target Sources/<Target>/module.modulemap、生成头文件目录 校对 target 名称与模块名,避免重复接入
XCFramework 各架构目录下的 Modules/module.modulemap 暂时隔离验证,不直接修改发布包
手工集成 SDK Framework Headers、Modules、Build Phases 删除重复复制阶段,保留唯一入口
仓库自有封装 includeHeaders、Bridging Header 合并声明或重命名自有模块
系统 SDK SDKROOT 对应的 Framework 和 Headers 不复制系统模块,改为正确引用 SDK

如果你需要长期运行多套 Xcode 和依赖组合,先阅读 VPSNIX 的帮助中心,把节点登录方式、权限和重置流程固定下来,再把构建故障定位到依赖层,而不是每次从主机状态开始排查。

SECTION 04 远程 Mac CI 的搜索路径为何会放大本地没有暴露的冲突?

远程 Mac CI 并不会自动制造重复模块,但它经常比本地环境拥有更多可见路径。典型差异包括 Homebrew 安装目录不同、系统账户的 Shell 初始化文件不同、工作目录不同、脚本继承了额外环境变量,或者 CI 为了兼容历史项目额外加入了头文件目录。

你应该把本地和远程节点的以下信息做成同一张对照表:

  • xcode-select -pxcrun swift --version
  • xcrun --sdk macosx --show-sdk-path
  • HEADER_SEARCH_PATHSFRAMEWORK_SEARCH_PATHS
  • SWIFT_INCLUDE_PATHSOTHER_SWIFT_FLAGSOTHER_CFLAGS
  • -I-F-fmodule-map-file=
  • Package.resolved、依赖检出目录和构建脚本版本;
  • 运行账号、当前工作目录和 Shell 类型。

xcodebuild 传入的设置拥有较高优先级,因此脚本参数可能覆盖项目文件或 CI 默认设置。Apple 的构建设置文档也建议通过明确的构建设置查看和传递方式管理目标配置。(developer.apple.com)

可以先导出设置,不要立即修改:

xcodebuild \
  -workspace "${WORKSPACE}/<Workspace>.xcworkspace" \
  -scheme "<Scheme>" \
  -configuration Release \
  -showBuildSettings \
  | tee "${ARTIFACT_DIR}/build-settings.txt"

env | sort > "${ARTIFACT_DIR}/environment.txt"

随后在构建日志中搜索 module.modulemap-I-F-fmodule 和依赖目录。若编译器输出不足以显示实际加载路径,再使用与当前工具链匹配的模块诊断参数;不要把某个网上流传的调试参数直接写进生产构建脚本。Swift 官方文档提供了编译器诊断入口,Clang 文档则明确列出模块映射和缓存相关参数。(docs.swift.org)

SECTION 05 缓存应该怎样清理,才不会掩盖真正冲突?

Clang 会把隐式构建的模块保存到模块缓存中;如果头文件或依赖模块发生变化,编译器会根据相关输入重新生成模块。缓存因此可能保存旧状态,但它不是重复 module.modulemap 的根因。(github.com)

正确顺序是:

  1. 用全新工作目录复现一次,确认不是旧检出文件残留。
  2. 为本次任务指定独立的 Module Cache、DerivedData 和依赖缓存目录。
  3. 保存清理前后的路径、时间和构建参数。
  4. 先定位模块来源,再清理与该任务相关的目录。
  5. 修复后分别执行一次冷构建和一次增量构建。
  6. 在同一提交上重复构建,确认错误没有只被缓存暂时隐藏。

如果你使用显式 Clang 参数,可以把缓存路径限定在任务目录:

export MODULE_CACHE_DIR="${CI_WORK_DIR}/module-cache/<JOB_ID>"
export DERIVED_DATA_DIR="${CI_WORK_DIR}/derived-data/<JOB_ID>"

mkdir -p "${MODULE_CACHE_DIR}" "${DERIVED_DATA_DIR}"

xcodebuild \
  -workspace "${WORKSPACE}/<Workspace>.xcworkspace" \
  -scheme "<Scheme>" \
  -derivedDataPath "${DERIVED_DATA_DIR}" \
  OTHER_CFLAGS="-fmodules-cache-path=${MODULE_CACHE_DIR}" \
  clean build

是否能通过 OTHER_CFLAGS 传递该参数,取决于你的工程和脚本封装方式;如果构建系统已经自行管理模块缓存,不要重复覆盖。Clang 文档还给出了默认模块缓存清理周期:未使用模块的修剪间隔默认可达 604,800 秒,保留期限默认可达 2,678,400 秒。这说明缓存不会因为每一次构建自动立即消失,但这些默认值也不应被当作 Xcode 工程的固定行为。(clang.llvm.org)

独立 FAQ:升级后最容易误判的四个问题

DerivedData 清理后,模块重名报错是否就算修好了?

只能在旧模块缓存参与复现时改变结果,不能从根本上解决真实的声明冲突。清理前必须保留日志并使用独立缓存验证;修复后要做冷构建、增量构建和重复构建,否则你可能只是把旧错误暂时移开。

上游 SDK 暂时没有兼容版本,生产 CI 该采用哪种版本策略?

生产线应优先保持稳定工具链,Swift 6.4 节点则使用隔离目录和固定依赖持续验证。等上游发布兼容修复后,再用同一提交和锁文件进行重复构建;如果 SDK 不可控,双轨状态比在生产线打无维护补丁更容易恢复。

为什么旧版工具链能构建,升级 Swift 6.4 后才出现 duplicate module name

新版扫描器改变了重复名称的容忍边界,旧版可能没有在同一阶段报错。先比较稳定工具链与 Swift 6.4 的扫描日志、SDK、搜索路径和依赖锁文件,确认是扫描阶段的新诊断,而不是把所有升级后的错误都归为 Swift 语言变化。

怎样找出两个同名 module.modulemap 分别来自哪个依赖?

不要只运行一次全盘搜索。应先从失败构建命令提取 -I-F 和显式 module map 参数,再沿这些路径搜索文件;然后读取每份文件中的顶层 module 声明,记录所属依赖、版本、路径和进入构建的参数。

SECTION 06 双工具链如何决定“修复、暂缓还是回退”?

最后一步不是比较“哪个版本更快”,而是建立可审计的对照实验。稳定工具链与 Swift 6.4 必须使用同一提交、同一 Package.resolved、同一 SDK 目标、同一构建脚本参数,以及尽可能一致的远程 Mac 账户和工作目录。

决策条件可以这样执行:

  • 若满足: 两个工具链看到的模块来源唯一,Swift 6.4 冷构建通过,增量构建通过,并且重复构建结果一致。
    则选择: 先在非生产分支放量,再扩大到少量 CI 任务。

  • 若满足: 冲突来自仓库自有 module.modulemap、重复 Header Search Paths 或源码与二进制重复接入。
    则选择: 立即修复声明或搜索路径,保留稳定节点作为回归基线。

  • 若满足: 冲突来自第三方 SDK,且你可以升级到已确认兼容的版本。
    则选择: 在隔离节点验证依赖升级,不要只在开发者本地替换文件。

  • 若满足: 第三方 SDK 暂时无法修改,Swift 6.4 仍稳定复现模块重名。
    则选择: 生产 CI 保留稳定工具链,Swift 6.4 继续作为兼容性验证线,并为临时补丁设置失效日期和回滚方法。

  • 若满足: 只有清理共享缓存后才通过,换到新缓存或并发任务仍失败。
    则选择: 判定为未修复,不允许放量;继续追踪模块来源和缓存隔离。

如果团队需要同时运行稳定版和测试版 Xcode,远程 Mac 节点的价值不在于“永远不变”,而在于可以把工具链、依赖缓存和构建目录隔离成可重置的验证单元。你可以先通过 VPSNIX 的远程 Mac 方案说明了解远程节点的访问和权限边界,再结合项目自己的构建参数验收节点。

截至本文更新日,Apple 官方资料确认的是 Xcode 27 beta 6 搭载 Swift 6.4,以及新版 dependency scanner 对 Clang module 唯一名称的要求;这不等于 Beta 行为已经成为最终正式版永久规则。(developer.apple.com) 后续应在 Xcode 27 RC 或正式版发布、Swift 6.4 独立正式版出现、Apple 修改扫描器说明,或主要 SDK 上游发布兼容修复时重新核对。

如果你现在的方案是把所有构建任务塞进一台本地 Mac、共享同一份 DerivedData,或者在 Linux 云主机上通过临时虚拟化方式补齐 macOS 工具链,真实缺点通常是工具链难以并行隔离、缓存污染难以追责,以及出问题后缺少可立即重置的 Apple Silicon 构建环境。对于需要同时保留稳定生产线和 Swift 6.4 验证线的团队,租赁 VPSNIX 的远程 Mac 更适合作为按需启用的隔离节点;但长期满负载、必须连接特定物理设备或需要完全控制硬件的场景,仍应评估自购 Mac。若你准备比较周期成本,可查看 VPSNIX 的套餐与计费页面,再用真实项目的构建频率和回滚要求做决定。