Xcode 27 移除 ld64,并要求模块名称唯一

苹果用一句话让一个链接器退场:“ld64 链接器已被移除,-ld_classic 选项不再受支持。”1 而这个如今消失的标志,正是苹果当年要求开发者添加的。Xcode 15 自己的发行说明就把 -Wl,-ld_classic 作为两个链接器缺陷的绕行方案。3

摘要

  • Xcode 27 移除了 ld64。在正式版工具链上,-ld_classic 会被忽略并给出一条警告,链接以退出状态 0 结束;而苹果曾经写进文档的另一种写法 -ld64 则直接让链接失败。112 苹果在 Xcode 15 的说明中曾为弱符号崩溃和 LTO 导入缺陷开出这个标志,Xcode 16 将其标记为弃用,Xcode 26 则只字未提。3410
  • 第二处断裂位于 Swift 编译器。苹果把依赖扫描合并成一次共享操作,因此“单次 Swift 依赖扫描操作可触及的每个 Clang 模块都必须拥有唯一的模块名称”。2 苹果两次留了余地:扫描“可能会报错”,而此前扫描器“可能容忍了重名”。2
  • 两者都在升级工具链时触发,与部署目标或 SDK 的选择无关,也都不涉及运行时。真正暴露在风险中的是这样一批项目:随源码附带第三方二进制、使用 CocoaPods,或拥有规模较大的 Swift/Objective-C/C++ 混合代码库。
  • 苹果的“可能”并不是行文上的谨慎。在 Xcode 26.6 下,我把两份声明了同一名称的模块映射放在同一条搜索路径上,运行扫描器 20 次:9 次以 SIGSEGV 崩溃,5 次中止,6 次顺利完成。8在正式版工具链上,对同一输入的 20 次运行每次都打印重定义错误,每次都以退出码 0 结束,而下一条要点里那份随源码附带的模块映射则什么也不打印;详情见第二条 9月15日 更新。13
  • 随源码附带的模块映射重复声明 SDK 模块——正是苹果点名的那种情形——在今天会静默通过编译,并把真正的模块遮蔽掉。我那份随源码附带的 SQLite3 垫片以退出码 0 通过了类型检查,而 sqlite3_open 就此不复存在。8
  • 七个受审计项目的结果:-ld_classic 出现 0 次,OTHER_LDFLAGS 处处未设置,391 份模块映射中重名 0 例——因为没有任何一个仓库存放手写的模块映射。9

两条说明最初都出自 Xcode 27 beta 4 的发行说明,与 Swift 6.4 及 27 系列 SDK 一同发布,并且在 9月14日 的正式版说明中原样保留。1112

更新,9月15日:正式版工具链如何处理这两个标志

Xcode 27(27A266a)于 9月14日 发布,其中的链接器给出了本文 7 月时无法测试的那个问题的答案。xcrun ld -v 现在的输出如下:12

@(#)PROGRAM:ld PROJECT:ld-27037.1
BUILD 21:17:51 Aug 25 2026
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em armv8m.main armv8.1m.main
LTO support using: LLVM version 21.0.0 (static support for 30, runtime is 30)

Xcode 26.6 打印的那行「will use ld-classic for:」不见了,ld-classic 这个二进制文件也不见了:工具链的 usr/bin 里只剩一个链接器 ld。7 月时走向同一条代码路径的两种写法,如今分道扬镳。用同一个单行 hello.c:12

xcrun clang hello.c -o hello -Wl,-ld_classic
ld: warning: -ld_classic is no longer supported and will be ignored

退出状态为 0,二进制文件链接成功。接着:

xcrun clang hello.c -o hello -Wl,-ld64
ld: library 'd64' not found
clang: error: linker command failed with exit code 1 (use -v to see invocation)

退出状态为 1,没有生成二进制文件。新链接器不再把 -ld64 当作选择链接器的选项,于是将其解析为 -l d64,也就是请求一个名为 d64 的库,找不到便失败。所以 7 月那句「不再接受 -ld_classic」只对了一半:苹果点名的那个标志会带着警告被容忍,而苹果在 Xcode 15 中点名的 -ld64 才是真正弄坏构建的那个。OTHER_LDFLAGS 里带着 -Wl,-ld64 的项目升级后会失败,而且报错信息从头到尾不提链接器;带着 -Wl,-ld_classic 的项目照常构建,只是每次链接多一条警告。Xcode 27 发布文章把这两者与其他在升级时触发的变化放在了一起。

更新,9月15日:正式版扫描器面对同样两处重名冲突

下文的模块映射实验是 7 月在 Xcode 26.6 上做的。在 27A266a 上,同样两个声明 module Widget 的目录放在同一条搜索路径上,swiftc -typecheck 仍然在 20 次运行中 20 次以 error: redefinition of module 'Widget' 和那条 previously defined here 提示失败,所以编译照样会停下。变的是扫描器。对同一输入运行 swiftc -scan-dependencies,20 次全部以退出码 0 结束,20 次全部把一模一样的 redefinition of module 'Widget' 错误和提示打印到 stderr,20 次全部写出一份完整的依赖图,把 Widget 解析到第一个 -I 目录的模块映射。Xcode 26.6 崩溃 9 次、中止 5 次的地方,正式发布的扫描器每次都报告这处冲突,然后照样跑完。7 月时把苹果那个“可能”解读为给一个不稳定的扫描器打掩护,这种读法在正式版上已站不住脚。813 在正式版上,扫描器打印出错误,然后以退出码 0 结束,这意味着以该状态为依据的构建步骤看到的是一次成功的扫描,失败要晚一步才到来,落在编译阶段。至于 Xcode 自己的构建系统如何对待一次退出码为 0 的扫描所打印的错误,我没有测试。13

随源码附带的那种情形依旧安静。一份重复声明 SQLite3 的 Vendor/module.modulemap 位于搜索路径上时,扫描以退出码 0 结束且 stderr 上没有任何输出,20 次运行 20 次如此,而依赖图把 SQLite3 解析到这份随源码附带的模块映射,而不是 SDK 的 usr/include/SQLite3.modulemap,也就是 usr/include/module.modulemap 第 7 行以 extern module SQLite3 "SQLite3.modulemap" 引用的那个文件;一个导入该模块的文件类型检查干净通过,而一个调用 sqlite3_open 的文件仍然以 error: cannot find 'sqlite3_open' in scope 失败,直到那个附带目录离开搜索路径为止。在正式发布的工具链上,苹果点名的第二种情形不会产生任何指出这处冲突的诊断信息。13

同样两个声明module Widget的模块映射,扫描20次的结果:Xcode 26.6崩溃9次、中止5次、成功6次;Xcode 27在全部20次中都打印了重定义错误却以0退出;两条工具链的类型检查在全部20次中都失败。

苹果当年让你加上的那个标志

故事要从一次链接器重写说起。Xcode 15 宣布了新链接器,让它成为“所有 macOS、iOS、tvOS 和 visionOS 二进制文件”的默认选择,并用一个从句写定了退役条件:“仍可使用 -ld64 显式请求经典链接器,该选项将在未来版本中移除。”3

新链接器带着缺陷登场,苹果在同一个页面上两次写下了应急出口。第一条已知问题:“使用弱定义符号的二进制文件在 iOS 14/macOS 12 及更早版本上运行时崩溃。由于 C++ 项目大量使用弱符号,受影响最大的是它们。”苹果给出的绕行方案是提高部署目标,“或在 OTHER_LDFLAGS 构建设置中加入 -Wl,-ld_classic”。3 第二条针对 LTO 目标文件把弱符号导入按非弱方式链接的问题,在同一项设置中给出了“-Wl,-weak_reference_mismatches,weak 或 -Wl,-ld_classic”。3

两个缺陷都对 C++ 项目打击最重,这也解释了今天还带着这个标志的是谁。没有人会回头去动一项已经止住崩溃的设置。

Xcode 16 用一句话发出了通知:“-ld_classic 链接器选项已弃用,将在未来版本中移除。”4 此后便是沉默。我在 Xcode 26 的发行说明里检索 ld_classic、ld64 和经典链接器,一无所获。10

当前的工具链仍然接受这个标志,也仍然会抱怨。在 Xcode 26.6 上,我用它链接了一个极简的 C 程序:7

xcrun clang hello.c -o hello -Wl,-ld_classic
ld: warning: -ld_classic is deprecated and will be removed in a future release

退出状态为 0。二进制文件链接成功。换成 -Wl,-ld64 会产生完全相同的、同样点名 -ld_classic 的警告,可见苹果用过的两种写法今天都走向同一条代码路径。7 苹果在 Xcode 27 的说明中只提到了 -ld_classic。在正式版工具链上,两种写法就此分开,上面的更新已经展示:-ld_classic 被忽略并给出警告,-ld64 则让链接失败。12

有一个细节让“移除”这个词变得没那么简单。用 xcrun ld -v 让 Xcode 26.6 的链接器自报家门时,它会说明自己把哪些架构转交了出去:7

@(#)PROGRAM:ld PROJECT:ld-1267
BUILD 16:38:58 Jun  8 2026
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em armv8m.main armv8.1m.main
will use ld-classic for: armv6 armv7 armv7s i386 armv6m armv7k armv7m armv7em

ld-classic 作为一个真实的二进制文件存在于工具链中,还带着自己的 man 手册页,链接器至今仍把八种架构路由给它。7 这些架构没有一个还活在当前的应用 SDK 里:iPhoneOS 26.5 只声明了 arm64e 和 arm64,watchOS SDK 另加一个 arm64_32。7 那份清单是 32 位 ARM、i386 和 Cortex-M 嵌入式目标。把它们在 SDK 中的缺席解读为“这次移除对应用开发者是安全的”的理由,是我的推断,而非苹果的说法。

不轻信 grep,也能找出这个标志

-ld_classic 存在于 OTHER_LDFLAGS 中,所以与其在文件里搜索文本,不如直接问构建系统它最终解析成了什么:

xcodebuild -showBuildSettings \
  -project YourApp.xcodeproj \
  -configuration Release -sdk iphoneos 2>/dev/null \
  | grep -E "OTHER_LDFLAGS|SUPPORTED_PLATFORMS"

对 Reps 项目执行,只回来一行:9

    SUPPORTED_PLATFORMS = appletvos appletvsimulator iphoneos iphonesimulator macosx

OTHER_LDFLAGS 根本没有出现,这本身就是答案:该设置未被赋值,因此不会有任何链接器标志由它进入链接步骤。在已解析的构建设置中缺席,所携带的信息量是文本搜索的缺席所没有的。判断平台同样要看 SUPPORTED_PLATFORMS,绝不要看 *_DEPLOYMENT_TARGET——无论目标平台是否真实存在,Xcode 都会把它写进去。

审计过程中有三个陷阱制造出了虚假的“干净”结果,三者都是什么也不打印、并以成功状态退出。原生 macOS 上并没有 timeout,用它包住 xcodebuild 会得到退出码 127 和空输出,看上去就像一个没有任何链接器标志的项目。在 zsh 下,未加引号的 --include=*.pbxproj 会在 grep 看到它之前被通配符展开,于是命令以“no matches found”告终,而不是报告零命中。还有,find 不会跟随作为起点给出的符号链接,这一点很关键,因为 xcrun --sdk iphoneos --show-sdk-path 返回的正是一个符号链接:find "$SDK" -name '*.modulemap' 什么也找不到,而 find -H "$SDK" 能找到 266 个文件。9 给模式加上引号,传入 -H,并且在相信任何一个零之前,先确认命令能匹配到你明知存在的东西。

手工维护的目标会把这个标志放在 project.pbxproj 或某个 .xcconfig 里;CocoaPods 项目还可能从 post_install 钩子里拿到它,而那条路径不会体现在任何开发者手改过的文件中。我审计的这批项目两者都没有,因此最后这条路径未经验证。9

模块名称规则,以及苹果的两处含糊措辞

第二处断裂伪装成一项性能改进登场,被归入“新特性”而不是“弃用”。苹果关于 Swift 编译器的这条说明,全文如下:

Swift 依赖扫描器已经过优化,避免在单次依赖扫描操作中查找 Clang 模块时重复进行准备工作和头文件搜索,扫描性能因此大幅提升。作为这一改动的后果,单次 Swift 依赖扫描操作可触及的每个 Clang 模块都必须拥有唯一的模块名称。如果同一次扫描可见的两份模块映射声明了同名的 Clang 模块,扫描可能会报错。此前,扫描器可能容忍了重名。最常见的情形是:项目或 SDK 在头文件搜索路径的多个位置提供同一个 Clang 模块名称,以及随源码附带的第三方代码中带有重复声明 SDK 模块的 module.modulemap。2

这里有四件事值得分开来看。苹果把这项要求限定在一次扫描所能触及的范围内,而不是整块磁盘。苹果把后果表述为“可能会报错”,并用同样的措辞给旧行为留了余地。苹果点名了两种触发形态:同一个模块名称从头文件搜索路径的两个位置被提供;以及随源码附带的 module.modulemap 重复声明了 SDK 模块。还有,触发条件是工具链本身,因为整段文字没有提到任何部署目标或 SDK 版本。

这条规则比这次优化更早。Clang 的模块映射语言把它讲得很直白:“每个模块应当只有一个定义。”6 文档从未说明的,是违反这条规则的后果,而这份沉默事出有因:工具链给出的答案不是一种行为,而是好几种。

一次重名冲突究竟会发生什么

苹果的含糊措辞让我想亲眼看看这个故障,于是我搭出了最小的版本:两个目录,各有一份声明同一模块的 module.modulemap。8

A/module.modulemap    B/module.modulemap
module Widget {       module Widget {
    header "widget.h"     header "widget.h"
    export *              export *
}                     }

当两个目录都在搜索路径上时,一次普通的类型检查每次都以相同方式失败,20 次运行 20 次如此:8

B/module.modulemap:1:8: error: redefinition of module 'Widget'
1 | module Widget {
  |        `- error: redefinition of module 'Widget'
A/module.modulemap:1:8: note: previously defined here

redefinition of module 就是应当在构建日志里检索的字符串,而 Xcode 26.6 的 clang 已经会输出它。去掉其中一条搜索路径,同样的编译就能成功,这证实触发条件是对同一次编译可见,而不是文件存在于磁盘上。8

依赖扫描器的表现则不同,而在 7 月时,这个差别看上去正是苹果写下“可能”的全部原因;第二条 9月15日 更新重新审视了这一点。把同样两份模块映射交给 swiftc -scan-dependencies 跑 20 次,出现了三种结果:9 次以 SIGSEGV 崩溃,5 次中止,6 次干净地成功。8 崩溃那几次的调用栈都经过 performParallelClangModuleLookup,这与苹果所说被替换掉的并行查找中存在竞态相吻合。把这种不确定性归因于该竞态,是我对调用栈的解读,不是苹果的表述。

苹果点名的第二种情形是安静的那一种。我写了一份随源码附带的模块映射,声明 SQLite3——iPhoneOS SDK 中真实存在的模块——把它放到搜索路径上,然后据此编译:8

Vendor/module.modulemap
module SQLite3 {
    header "shim.h"
    export *
}

编译成功了。退出码 0,没有警告,没有提示,没有任何形式的诊断信息。接着我在同一配置下请求 sqlite3_open:8

error: cannot find 'sqlite3_open' in scope

只要搜索路径上没有那个附带目录,同一个文件就能编译通过。随源码附带的模块映射把 SDK 的 SQLite3 完全遮蔽掉了,而工具链一言不发。所以在今天,“重复声明”这种情形不会大声失败;它表现为你以为已经导入的模块里缺了一个 API。苹果的说明称,Xcode 27 的扫描在这里“可能会报错”,那将把一次静默的遮蔽变成一次构建失败。

我的构建环境是 Xcode 26.6(版本号 17F113),因此上面每一条诊断信息都来自上一代工具链。78 我无法给出 Xcode 27 的报错文本,也没有编造任何一条。诊断机制、值得检索的字符串,以及苹果所说即将取消的那份容忍,今天都已存在。(9月15日:本段以及上一段结尾处的那句预测,描述的都是 7 月的情况;在正式版工具链上,随源码附带的那种情形仍然静默失败,两种情形的输出见本文顶部的第二条更新。13)

审计重复的模块名称

枚举已声明的模块名称看起来只需一行 grep,但模块映射语言中有两种结构会给出错误答案。

extern module Foo "Foo.modulemap" 是前向引用,不是定义,而苹果的 SDK 在一份模块映射里就用了 79 处。7 粗糙的扫描会把这个引用和真正的定义当成 Foo 的两次声明。其次,module Darwin.C { ... } 用带点的 module-id 扩展在别处声明的模块;SDK 中有 13 份模块映射以 module Darwin.something 开头,若把第一段读成顶层声明,就会把这 13 份连同真正的 Darwin 声明凑成一次涉及 14 个文件的“冲突”。7 这两种误报都出现在我的初稿里。

最终留下来的版本会处理注释、跟踪花括号深度以排除嵌套的 explicit module 子模块,并能应对含空格的路径:

find -H "${1:-.}" \( -name '*.modulemap' -o -name 'module.map' \) \
  -not -path '*/build/*' -not -path '*/.build/*' \
  -not -path '*/DerivedData*/*' -print0 |
while IFS= read -r -d '' map; do
  awk -v f="$map" '
    { line = $0; sub(/\/\/.*/, "", line)
      if (depth == 0 && line !~ /extern[[:space:]]+module/ &&
          match(line, /(^|[[:space:]])module[[:space:]]+[A-Za-z_][A-Za-z0-9_.]*/)) {
        n = substr(line, RSTART, RLENGTH); sub(/.*module[[:space:]]+/, "", n)
        if (n !~ /\./) print n "\t" f
      }
      for (i = 1; i <= length(line); i++) {
        c = substr(line, i, 1)
        if (c == "{") depth++; else if (c == "}") depth--
      }
    }' "$map"
done | sort -u > /tmp/modnames.txt

cut -f1 /tmp/modnames.txt | uniq -d | while read -r n; do
  echo "duplicate: $n"
  awk -F'\t' -v n="$n" '$1==n {print "  " $2}' /tmp/modnames.txt
done

能拿到的最强阴性对照是苹果自己的 SDK——它必须满足这条规则,工具链才能正常工作。把命令指向 iPhoneOS 26.5 SDK,可以在 usr/include 下找到 1,099 处顶层声明,在各框架的模块映射中找到 205 处,两边的重名数都是 0。7 指向刻意构造出冲突的测试样例时,它会指出重名以及涉及的两个文件。8

那些排除项分量不轻。-not -path '*/build/*' 并不能排除 .build/,因为该模式需要字面上的目录名,而 SwiftPM 会成打地把生成的模块映射写进 .build。把 .build 留在结果里,制造了七个项目中唯一的一批“重名”:GrappleCore 出现在 6 个文件中,GrappleRender 出现在 3 个文件中,全部是 SwiftPM 的产物,横跨两个 Swift 目标、位于不同的构建根目录下。9 这 9 个文件甚至不是逐字节相同的,原因很有启发性:每一份都把同一个模块名称包在指向各自构建根目录下生成头文件的绝对路径外面,因此它们只在一个字符串上不同,其余毫无差别。一份把它们称作冲突的审计,还没碰到真正的问题,就已经耗尽了自己的可信度。

Xcode 制造的是同一种误报,只是更没有歧义。在单个 DerivedData 目录树内,它会把每个 Swift 包生成的模块映射写两遍,一次写进 GeneratedModuleMaps-iphonesimulator/,一次写进目标自身的中间产物目录,两次都使用相对头文件路径。在某个项目的目录树中,有 15 个名称各出现两次,15 对全部逐字节相同。9 把审计范围限定在源码,而不是构建产物。

七个项目里到底有什么

我在 Xcode 26.6 上对七个 Xcode 项目跑了这两项审计。诚实的结果是一个干干净净的零。9

项目 Swift 文件 Objective-C / C++ / C 依赖 -ld_classic 源码模块映射
Reps 77 0 2 个本地 SPM 0 0
Return 57 0 无 0 0
Banana List 55 0 无 0 0
Ace Citizenship 26 0 无外部依赖 0 0
Water 34 0(2 个 .metal) 无 0 0
ResumeGeni 71 0 3 个远程 SPM,9 条 pin 0 0
Yawara 143 0 2 个本地 SPM 0 0
合计 463 0 仅 SPM 0 0

七个项目中 OTHER_LDFLAGS 全部未设置,整批项目里找不到任何 .xcconfig 文件,没有 CocoaPods,没有 Carthage,也没有随源码附带的 .framework 或 .xcframework。9 模块名称审计检查了 391 份模块映射,其中 204 份位于项目目录内,187 份位于 Xcode 图形界面构建产物所在的共享 DerivedData 中,而它们无一例外都是构建产物。9 没有任何一个仓库存放着手写的模块映射。

两个零出自同一个原因,而这正是值得带走的结论:真正暴露的是混合语言项目,而这些项目不是。在 463 个 Swift 文件之外,这批项目里 Objective-C 为零、C++ 为零、C 源文件为零,唯一的非 Swift 编译代码是 Water 中的两个 Metal 着色器。9 一个从未处于风险中的代码库给出的零结果,对于危险本身是弱证据,对于“谁才需要审计”却是强证据。

有两个细节比这些零更重要。已解析的依赖包中唯一手写的模块映射属于 swift-crypto,它提供了四个 C 垫片,名为 CCryptoBoringSSL、CCryptoBoringSSLShims、CXKCP 和 CXKCPShims,彼此各不相同。9 另外,这批项目声明的名称,没有一个出现在 SDK 的 Clang 模块命名空间里——对照的是命令在 iPhoneOS 26.5 SDK 中找到的全部 1,318 个顶层名称。9 连通过 Supabase 引入的那些通用名称也没有撞上:SDK 声明了名为 Foundation、UIKit 和 SQLite3 的 Clang 模块,却没有任何名为 Crypto、Storage 或 Auth 的模块。

脚下正在抬高的 C++ 部署下限

有一项相邻的改动,打中的正是那些还带着 -ld_classic 的 C++ 项目。苹果抬高了一条下限,并且只点了 macOS,没有提到其他平台:“C++ 标准库在 macOS 上支持的最低部署目标已提高到 11.0。”5

同一条说明给出了一个应急出口,又在下一句里给它定下了移除的时间。对于不构成严格弱序的比较器,libc++ 改变了 std::map 和 std::set 上 lower_bound 与 upper_bound 的结果,而定义 _LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND “将恢复这些操作的历史实现”。紧接着:“该应急出口将在后续版本中移除(很可能就是下一个版本)。”5 一项相关改动让 multimap::find 和 multiset::find 不再必然返回第一个相等的元素——苹果指出这种行为“从未被标准所保证”,尽管 libc++ 一直提供它——而且这项改动没有提供任何退出选项。5 请把那个宏当作一张迁移工单,而不是一个修复。

常见问题

我的项目里为什么会有 -ld_classic?

几乎可以肯定是因为苹果的 Xcode 15 发行说明开出了这个方子。那里有两条已知问题建议把 -Wl,-ld_classic 加入 OTHER_LDFLAGS:一条是使用弱定义符号的二进制文件在 iOS 14 和 macOS 12 及更早版本上运行时崩溃,苹果指出它“受影响最大的是 C++ 项目”;另一条是 LTO 目标文件把弱符号导入按非弱方式链接。3 苹果在 Xcode 16 中弃用了该选项,并在 Xcode 27 中移除了它背后的链接器。14 如果这个标志还在,先确认当初那个缺陷是否仍能复现,再去寻找替代方案。

如何在项目中找出重复的 Clang 模块名称?

把每一份模块映射中的顶层模块声明枚举出来,找出出现在两个文件里的名称,并把构建目录从结果中剔除。有三件事让这项工作不止于一次 grep:extern module Foo "path" 是引用而不是定义;module Foo.Bar 扩展的是在别处声明的模块,而不是在声明 Foo;嵌套的 explicit module 子模块不会与顶层名称发生冲突。6 要显式排除 .build、build 和 DerivedData,因为 -not -path '*/build/*' 漏掉了 .build,而 SwiftPM 和 Xcode 都会例行地重复生成模块映射。9

在 Xcode 27 下报错是什么样子?

在 Xcode 27.0(27A266a)上,报错文本就是 Xcode 26.6 已经会输出的那一条:error: redefinition of module 'Widget',外加一条 note: previously defined here。普通的 -typecheck 打印它并以退出码 1 结束;-scan-dependencies 打印它并以退出码 0 结束。13 7 月时我的机器运行的是 Xcode 26.6(版本号 17F113),因此 7 月的正文完全没有报告任何 Xcode 27 的输出;78 顶部的第二条 9月15日 更新补上了这部分内容。13 苹果对 Xcode 27 的措辞是扫描“可能会报错”,因此请在日志里检索 redefinition of module,而不是某个人猜出来的字符串。

模块名称唯一性导致的失败与部署目标有关吗?

无关。苹果把这项要求限定在单次 Swift 依赖扫描操作上,整条说明没有提到任何操作系统版本、SDK 或部署目标。2 链接器的改动读起来也一样,是从工具链中移除。1 两者都会在 Xcode 27 的第一次构建时落地,与 @State 宏同类,而不是与本轮由 SDK 触发的那些要求同类。

关键要点

给 iOS 开发者: - 用 xcodebuild -showBuildSettings -configuration Release -sdk iphoneos 查询 OTHER_LDFLAGS,而不是用 grep 搜索 -ld_classic。在已解析的设置中缺席意味着未设置;在文本搜索中缺席什么也说明不了。9 - 在构建日志里检索 redefinition of module——这是 Xcode 26.6 已经会输出的诊断信息——而不是凭空编造的 Xcode 27 报错文本。8

给带有随源码附带的 C 或 C++ 依赖的团队: - 先把随源码附带的 module.modulemap 文件与 SDK 的命名空间比对审计:苹果点名的正是这种情形,而它今天是静默失败的。我那份随源码附带的 SQLite3 垫片以退出码 0 通过编译,并让 sqlite3_open 凭空消失。8 - 把审计范围限定在源码,并排除 .build、build 和 DerivedData。391 份模块映射中每一处看似重复的名称,都是同一个 Swift 目标的构建产物。9

给发布管理者: - 把这两项改动都视为由工具链触发,并安排在 Xcode 27 的第一次构建时处理,而不是放进 SDK 迁移。两者都不涉及部署目标,也都没有运行时成分。12 - 把苹果的含糊措辞写进工单。扫描“可能会报错”,而同一处冲突在 Xcode 26.6 上跑 20 次给出了三种不同结果,所以构建通过一次并不能证明什么。28在正式版上,扫描会打印错误却以退出码 0 结束,所以一个绿色的扫描步骤同样证明不了什么。13


27 这一轮的故障不断出现在不同的位置:启动屏幕键会拦下一次提交,场景生命周期强制要求会拦下一次启动,@State 宏则在源码层面拦下一次构建。链接器的移除和模块名称规则把它拦在更低的一层——在那些没人会刻意去配置的工具链部件里。整个系列的入口是苹果生态系列。

参考资料


  1. 苹果,Xcode 27 Release Notes,Linking 一节,Deprecations in Xcode 27 Beta(radar 165165518)。此次移除的出处,全文引用:“ld64 链接器已被移除,-ld_classic 选项不再受支持。”由于该 HTML 页面通过 JavaScript 渲染内容,于 2026 年 7 月 26 日对照苹果的文档 JSON 进行了核实。该日期下的页面标题为“Xcode 27 Beta 4 Release Notes”。 ↩↩↩↩↩

  2. 苹果,Xcode 27 Release Notes,Swift Compiler 一节,New Features in Xcode 27 Beta(radar 136303612)。依赖扫描器那条说明的出处,本文正文已逐字全文引用,包括两处留有余地的措辞(“扫描可能会报错”与“此前,扫描器可能容忍了重名”)以及被点名的两种情形。于 2026 年 7 月 26 日对照苹果的文档 JSON 核实。需要注意的是,它被归入 New Features,而不是 Deprecations 或 Known Issues。 ↩↩↩↩↩↩

  3. 苹果,Xcode 15 Release Notes,Linking 一节。New Features(radar 108915312)是这段话的出处:“我们编写了一个新的链接器,以显著加快静态链接。它是所有 macOS、iOS、tvOS 和 visionOS 二进制文件以及使用‘Mergeable Libraries’特性者的默认选择。仍可使用 -ld64 显式请求经典链接器,该选项将在未来版本中移除。”Known Issues 则是两条推荐使用该标志的绕行方案的出处:radar 114813650(FB13097713),“使用弱定义符号的二进制文件在 iOS 14/macOS 12 及更早版本上运行时崩溃。由于 C++ 项目大量使用弱符号,受影响最大的是它们”,绕行方案是提高部署目标,“或在 OTHER_LDFLAGS 构建设置中加入 -Wl,-ld_classic”;以及 radar 115521975(FB13171424),“弱符号导入在来自 LTO 目标文件时被按非弱导入链接”,绕行方案是“在 OTHER_LDFLAGS 构建设置中加入 -Wl,-weak_reference_mismatches,weak 或 -Wl,-ld_classic 选项”。于 2026 年 7 月 26 日对照苹果的文档 JSON 核实。 ↩↩↩↩↩↩

  4. 苹果,Xcode 16 Release Notes,Linking 一节,Deprecations(radar 128502299):“-ld_classic 链接器选项已弃用,将在未来版本中移除。”于 2026 年 7 月 26 日对照苹果的文档 JSON 核实。 ↩↩↩

  5. 苹果,Xcode 27 Release Notes,C++ Standard Library 一节,Deprecations in Xcode 27 Beta。苹果为整段内容只标了一个 radar 编号 178191050,而且是跟在最后一项后面,而不是逐条标注。它是以下内容的出处:“C++ 标准库在 macOS 上支持的最低部署目标已提高到 11.0”;multi{map,set}::find 的改动(“依赖 find 返回第一个元素的代码将会失效,应改用 lower_bound 或 equal_range”);以及那个应急出口及其失效时间:“由于在某些情况下这可能不易绕开,本版本提供了一个应急出口:定义 _LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND 将恢复这些操作的历史实现。该应急出口将在后续版本中移除(很可能就是下一个版本)。”于 2026 年 7 月 26 日对照苹果的文档 JSON 核实。 ↩↩↩

  6. Clang 团队,Clang Modules documentation,Module Map Language。它是以下内容的出处:“每个模块应当只有一个定义”、module-id 规则与 extern module 声明、“explicit 限定符只能用于子模块,即嵌套在另一个模块内部的模块”,以及模块映射按文件名 module.modulemap 发现、并为兼容性同时搜索 module.map 的规则。此处引用它,是作为工具链自身对模块映射语言的参考文档,而不是作为苹果的开发者文档;苹果的 clang 派生自这一实现。 ↩↩

  7. 作者于 2026 年 7 月 26 日在 macOS 26.5.2(版本号 25F84)、Xcode 26.6(版本号 17F113)、Apple clang 21.0.0 环境下的测试。命令输出为逐字复制。xcrun clang hello.c -o hello -Wl,-ld_classic 打印出“ld: warning: -ld_classic is deprecated and will be removed in a future release”并以 0 退出;换成 -Wl,-ld64 会打印完全相同、同样点名 -ld_classic 的警告。xcrun ld -v 报告 PROJECT:ld-1267 以及上文引用的架构清单。ld-classic 位于 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld-classic,旁边还有一份 man 手册页。SDK 架构来自 SDKSettings.plist:iPhoneOS 26.5 声明了 arm64e 和 arm64,WatchOS 26.5 声明了 arm64、arm64e 和 arm64_32。SDK 模块映射的统计(usr/include 下 1,099 处顶层声明,System/Library/Frameworks 下 205 处,两边重名均为 0)来自对 iPhoneOS 26.5 SDK 运行上文发布的命令;79 处 extern module 声明位于 usr/include/module.modulemap,同一目录下有 13 份模块映射以带点的 module Darwin.* 开头(bank、Darwin_C、Darwin_Mach、Darwin_Mach_machine、Darwin_machine、Darwin_POSIX、Darwin_sys、device、mach_debug、net、netinet、netinet6 和 uuid),若按第一段读取,就会把它们与 Darwin.modulemap 中真正的 Darwin 声明归成一次涉及 14 个文件的冲突。Xcode 27 下的行为未经测试,因为所用机器上并未安装 Xcode 27。 ↩↩↩↩↩↩↩↩↩↩

  8. 作者于 2026 年 7 月 26 日在同一台机器与同一工具链上的复现。两个目录各含一份声明 module Widget 的 module.modulemap,在搜索路径同时包含两个目录的情况下用 swiftc 编译。-typecheck 在 20 次运行中 20 次产生 error: redefinition of module 'Widget' 及 note: previously defined here,退出码 1;去掉其中一条 -I 路径后,同一文件以退出码 0 编译通过。对同一输入运行 -scan-dependencies 20 次,产生 9 次以信号 11(SIGSEGV)退出、5 次以信号 6(SIGABRT)退出、6 次以状态 0 退出,崩溃那几次的调用栈都经过 swift::ModuleDependencyScanner::performParallelClangModuleLookup。另外,一份声明 SQLite3(iPhoneOS 26.5 SDK 中的一个模块)的随源码附带模块映射被放到搜索路径上后,以退出码 0 通过类型检查且没有任何诊断信息,而在同一配置下调用 SDK 的 sqlite3_open 的文件则以“error: cannot find ‘sqlite3_open’ in scope”失败,并在把该附带目录从搜索路径中移除后编译通过。测试样例中包含了一个带空格的路径,用以验证已发布的命令。该命令比较的是文件之间的名称,因此同一份模块映射内部声明两次的名称按设计不在其覆盖范围内。本文任何地方都没有报告 Xcode 27 的输出。7 月的正文没有报告任何 Xcode 27 的输出;第二条 9月15日 更新补上了这部分内容。13 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  9. 作者于 2026 年 7 月 26 日在 macOS 26.5.2 与 Xcode 26.6(版本号 17F113)下对七个 Xcode 项目(Reps、Return、Banana List、Ace Citizenship、Water、ResumeGeni 和 Yawara)的审计。-ld_classic 与 ld64 在任何工作树中都出现 0 次,OTHER_LDFLAGS 在每个项目中都未设置,整批项目中没有 .xcconfig 文件、没有 Podfile、没有 Carthage,也没有随源码附带的 .framework 或 .xcframework;此次搜索只覆盖当前的工作树,不含 git 历史或归档的构建日志,因此结论是“现在没有”,而不是“从未用过”。为 Reps 引用的 SUPPORTED_PLATFORMS 一行来自 xcodebuild -showBuildSettings -configuration Release -sdk iphoneos。模块映射总数:检查了 391 份,其中 204 份在七个项目目录下,187 份在这七个项目位于共享 ~/Library/Developer/Xcode/DerivedData 中的自有目录树下(整个共享目录中的数量远不止于此,其余属于其他项目),全部都是构建产物,七个仓库中没有任何手写或随源码附带的模块映射。看似重复的名称通过内容哈希进行了核对,而两种生成器的行为并不相同。在受审计的 ResumeGeni 目录树(项目内的 build/DerivedData)中,Xcode 产生的 15 对名称——一次写在 GeneratedModuleMaps-iphonesimulator/ 下,一次写在目标的中间产物目录下——15 对全部逐字节相同,因为 Xcode 输出的是相对头文件路径。该计数是按目录树而非按项目统计的:同一应用在共享 ~/Library/Developer/Xcode/DerivedData 下的目录树中有 16 对,其 Index.noindex 变体中有 22 对。可以推广的是机制,而不是这些数字。SwiftPM 的副本则不同:Yawara 各 .build 目录下的 6 份 GrappleCore 和 3 份 GrappleRender 文件分别带有 6 个和 3 个互不相同的哈希值,大小在 171 到 185 字节之间,因为每一份都嵌入了指向自身构建根目录中生成的 -Swift.h 的绝对路径,除此之外完全相同。把已发布的命令指向整个 iPhoneOS 26.5 SDK 时,它找到 1,318 个互不相同的顶层 Clang 模块名称,其中 1,099 个位于 usr/include 下、205 个位于 System/Library/Frameworks 下;这批项目声明的名称没有一个出现在其中,两者的交集为空。同一次扫描在整个 SDK 中报告了零重名,这正是该规则所要求的阴性对照。CryptoKit 不在这份清单里,因为它作为纯 Swift 框架发布,没有 Clang 模块映射;因此“没有 Crypto 冲突”这一点,依据的是 SDK 中没有声明同名的 Clang 模块,而不是 CryptoKit 占用了这个名称。swift-crypto 4.4.0 提供了已解析依赖图中仅有的手写模块映射(CCryptoBoringSSL、CCryptoBoringSSLShims、CXKCP、CXKCPShims,各声明一次)。SymbolKit 的宿主工具变体与目标变体位于共享的本地包 941Kit 中,而不在这七个应用里。源文件计数排除了 Python 虚拟环境,这项修正很重要:一次粗糙的统计曾把一批 C 与头文件算到 Reps 名下,而它们最后都被证明位于 .venv 和 site-packages 内,没有任何 Xcode 目标会编译它们。Water 没有本地构建树,因此它的模块映射计数为零,反映的是一个未构建的项目,而不是一份经过验证的干净构建图。三个造成虚假“干净”结果的陷阱都逐一得到确认:原生 macOS 上没有 timeout,会以 127 退出;zsh 会展开未加引号的 --include=*.pbxproj 并以“no matches found”中止;xcrun --sdk iphoneos --show-sdk-path 返回的是一个符号链接,对它使用不带 -H 的 find 会报告零份模块映射,而 find -H 报告 266 份。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  10. 苹果,Xcode 26 Release Notes。于 2026 年 7 月 26 日检索了 ld_classic、ld64 以及任何描述经典链接器的条目;一无所获,因此苹果在 Xcode 16 中的弃用通知与 Xcode 27 中的移除之间,没有任何中间的重申。 ↩↩

  11. 苹果,Xcode 27 Release Notes,Overview:“Xcode 27 beta 4 包含 Swift 6.4,以及 iOS 27、iPadOS 27、tvOS 27、watchOS 27、macOS 27 和 visionOS 27 的 SDK。”同一份说明在 Intel Deprecation(radar 162138432)下记录:“Xcode 27 将只能在 Apple 芯片的 Mac 上安装和运行。”核实日期为 2026 年 7 月 26 日。 ↩

  12. 作者的测试。macOS 26.6.2(版本 25G83)、Xcode 27.0(版本 27A266a)、Apple clang 21.0.0(clang-2100.3.34.2),2026年9月15日,DEVELOPER_DIR 指向 Xcode 27 的应用包。xcrun ld -v 的输出已在上文原样转录;对工具链 usr/bin 执行 ls,只列出 ld,没有 ld-classic。hello.c 的内容为 int main(void){return 0;},xcrun clang hello.c -o hello -Wl,-ld_classic 打印「ld: warning: -ld_classic is no longer supported and will be ignored」并以状态 0 退出;xcrun clang hello.c -o hello -Wl,-ld64 打印「ld: library ‘d64’ not found」和「clang: error: linker command failed with exit code 1 (use -v to see invocation)」,以状态 1 退出且没有输出文件。同一天通过苹果文档 JSON 获取的正式版 Xcode 27 Release Notes 中,Linking 的弃用条目(165165518)和 Swift Compiler 的依赖扫描器条目(136303612)仍是本文引用的措辞,Overview 现在写作「Xcode 27 includes Swift 6.4 and SDKs for iOS 27, iPadOS 27, tvOS 27, watchOS 27, macOS 27, and visionOS 27.」。 ↩↩↩↩↩

  13. 作者的测试。macOS 26.6.2(版本 25G83)、Xcode 27.0(版本 27A266a)、Apple Swift version 6.4(swiftlang-6.4.0.34.1 clang-2100.3.34.1),2026年9月15日,针对 iPhoneSimulator 27.0 SDK,使用 -target arm64-apple-ios27.0-simulator。测试样例:A/module.modulemap 和 B/module.modulemap 各声明 module Widget { header "widget.h" export * },一个导入 Widget 的 main.swift,以及声明 module SQLite3 { header "shim.h" export * } 的 Vendor/module.modulemap。使用 -I A -I B 时:swiftc -typecheck main.swift 在 20 次运行中 20 次以退出码 1 结束,并给出“error: redefinition of module ‘Widget’”和“note: previously defined here”;swiftc -scan-dependencies main.swift 在 20 次运行中 20 次以退出码 0 结束,20 次中 20 次把同样的错误和提示打印到 stderr,并且 20 次中 20 次写出了一份依赖图,其 Widget 条目带有指向 A/module.modulemap 的 moduleMapPath。只使用 -I A 时,两条命令都以退出码 0 结束。使用 -I Vendor 时:对一个导入 SQLite3 的文件运行 swiftc -scan-dependencies,20 次运行中 20 次以退出码 0 结束且 stderr 为空,并把 SQLite3 解析到 Vendor/module.modulemap;对该导入运行 -typecheck 以退出码 0 结束;对一个调用 sqlite3_open(":memory:", &db) 的文件运行 -typecheck 以退出码 1 结束并报“error: cannot find ‘sqlite3_open’ in scope”,去掉 -I Vendor 参数后则以退出码 0 结束。SDK 在 usr/include/module.modulemap 第 7 行以 extern module SQLite3 "SQLite3.modulemap" 引用该模块,并在 usr/include/SQLite3.modulemap 中以 module SQLite3 [system] 定义它;不带 -I Vendor 时,扫描以退出码 0 结束且 stderr 为空,并把 SQLite3 解析到该文件。把两个搜索路径参数对调为 -I B -I A 后,扫描仍以退出码 0 结束,错误转到了 A/module.modulemap,提示转到了 B/module.modulemap,依赖图则把 Widget 解析到 B/module.modulemap。 ↩↩↩↩↩↩↩↩↩

相关文章

@State 宏:Xcode 27 不再编译的代码

Xcode 27 把 SwiftUI 的 @State 重新实现为 Swift 宏。触发点是工具链升级,而不是部署目标,有两种写法从此无法通过编译。

14 分钟阅读

Xcode 27 弃用 Intel:什么停止了,什么照常发布

Xcode 27 只能运行在 Apple silicon 上,仍可将 Universal 应用向下部署到 macOS 12,却在部署目标达到 27.0 时悄然把 x86_64 移出 ARCHS_STANDARD。

16 分钟阅读

像素艺术的光:昼与夜、天气与阴影

从源码实测《宝可梦 水晶》和《绿宝石》如何保持一天、雨和阴影,并为 Kiradex 的像素小镇写出一份关于昼夜、天气与阴影的简报。

120 分钟阅读