Xcode 27 移除 ld64,並要求模組名稱唯一
Apple 用一句話讓一個連結器退場:「ld64 連結器已移除,-ld_classic 選項不再受支援。」1 如今消失的這個旗標,正是 Apple 當年叫開發者加上的旗標。Xcode 15 自己的發行說明,就把 -Wl,-ld_classic 當成兩個連結器缺陷的解方。3
重點摘要
- Xcode 27 移除 ld64,也不再接受
-ld_classic。1 這個旗標當初正是 Apple 在 Xcode 15 說明裡開的藥方,用來繞過弱符號當機與 LTO 匯入缺陷;Xcode 16 宣告淘汰,Xcode 26 則隻字未提。3410 - 第二處破壞在 Swift 編譯器。Apple 把相依性掃描整併成單一共用動作,因此「單一 Swift 相依性掃描動作所能觸及的每一個 Clang 模組,都必須具備唯一的模組名稱」。2 Apple 兩度留了餘地:掃描「可能會回報錯誤」(may report an error),而在此之前掃描器「可能容忍了名稱重複」。2
- 兩者都在升級工具鏈時觸發,與部署目標或 SDK 的選擇無關,也都沒有執行期的成分。真正暴露在風險中的族群是:隨附的二進位檔、CocoaPods,或規模較大的 Swift/Objective-C/C++ 混編程式碼庫。
- Apple 的「可能」不是行文上的謹慎。在 Xcode 26.6 下,我把兩個宣告同一名稱的 module map 放上同一條搜尋路徑,跑了 20 次掃描器:9 次以 SIGSEGV 當掉、5 次中止、6 次順利完成。8
- 隨附的 module map 重新宣告 SDK 內的模組——正是 Apple 點名的情況——今天會靜悄悄地編過,並遮蔽掉真正的模組。我那個隨附的
SQLite3shim 型別檢查以 exit 0 通過,而sqlite3_open從此不存在。8 - 七個受稽核專案的結果:
-ld_classic零次出現,OTHER_LDFLAGS一律未設定,391 個 module map 之中沒有任何重複的模組名稱——因為沒有一個儲存庫存放手寫的 module map。9
這兩則說明都出自 Xcode 27 beta 4 的發行說明,與 Swift 6.4 及 27 系列的各 SDK 同批釋出。11 請以 beta 階段的文字看待。
Apple 當年叫你加上的那個旗標
故事要從一次連結器改寫說起。Xcode 15 公布了新的連結器,讓它成為「所有 macOS、iOS、tvOS 與 visionOS 二進位檔」的預設值,並用一個子句訂下了退場條件:「舊版連結器仍可用 -ld64 明確指定,並將在未來版本中移除。」3
新連結器帶著缺陷登場,而 Apple 在同一頁上兩度寫下了退路。第一則已知問題:「使用弱定義符號的二進位檔,在 iOS 14/macOS 12 或更舊的系統上會於執行期當機。由於 C++ 專案大量使用弱符號,受影響的主要是它們。」Apple 給的替代作法是提高部署目標,「或在 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
結束狀態為零。二進位檔連結成功。改用 -Wl,-ld64 會得到一模一樣、而且指名 -ld_classic 的警告,可見 Apple 用過的兩種寫法今天都走同一條程式路徑。7 Apple 的 Xcode 27 說明只點名 -ld_classic,我無從測試 -ld64 是否以相同方式失敗。
有個細節讓「移除」這個詞變得沒那麼乾脆。用 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 page,連結器至今仍把八種架構交給它處理。7 這些架構沒有一個存活在現行的 app SDK 中:iPhoneOS 26.5 只宣告 arm64e 與 arm64,watchOS 的 SDK 再多一個 arm64_32。7 那份清單全是 32 位元 ARM、i386,以及 Cortex-M 這類嵌入式目標。把「它們不在 SDK 之列」讀成「這次移除對 app 開發者是安全的」,是我的推論,不是 Apple 的說法。
不靠 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 包在它裡面會得到 exit 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,並且在相信任何一個「零」之前,先確認這道指令能命中一個你明知存在的東西。
手工維護的 target 會把這個旗標留在 project.pbxproj 或 .xcconfig 裡;CocoaPods 專案還可能從 post_install 掛鉤取得它,而那不會記錄在任何開發者手動編輯過的檔案中。我稽核的這批專案兩者皆無,所以最後那條路徑並未獲得驗證。9
模組名稱規則,以及 Apple 的兩個保留說法
第二處破壞偽裝成效能改進登場,被歸在「新功能」而非「淘汰項目」底下。Apple 的 Swift 編譯器條目全文如下:
Swift 相依性掃描器已最佳化,在單一相依性掃描動作中查找 Clang 模組時會避免重複的初始化工作與標頭搜尋,掃描效能因此大幅提升。作為這項變更的後果,單一 Swift 相依性掃描動作所能觸及的每一個 Clang 模組,都必須具備唯一的模組名稱。若同一次掃描可見的兩個 module map 宣告了同名的 Clang 模組,掃描可能會回報錯誤。在此之前,掃描器可能容忍了名稱重複。最常見的情況,是專案或 SDK 從標頭搜尋路徑上的一個以上位置提供同一個 Clang 模組名稱,以及隨附的第三方原始碼夾帶了重新宣告 SDK 模組的 module.modulemap。2
這段話裡有四件事值得拆開來看。第一,Apple 把這項要求的範圍限定在單一次掃描所能觸及的東西,不是你整顆磁碟。第二,後果被寫成「可能會回報錯誤」,而舊行為也用同樣的方式留了餘地。第三,Apple 點名了兩種觸發形態:同一個模組名稱從標頭搜尋路徑上的兩個位置被提供出來,以及隨附的 module.modulemap 重新宣告了 SDK 模組。第四,觸發者是工具鏈——整段話沒有提到任何部署目標或 SDK 版本。
這條規則比這次最佳化更早存在。Clang 的 module map 語言把話說得很白:「每個模組只能有單一定義。」6 文件從未說明的,是違反規則之後會怎樣;而這份沉默事出有因:工具鏈給的答案不是一種行為,而是好幾種。
名稱衝突實際上會發生什麼事
Apple 的保留說法讓我想親眼看看失敗長什麼樣,於是我做了最小的版本:兩個目錄,各有一個宣告同一模組的 module.modulemap。8
A/module.modulemap B/module.modulemap
module Widget { module Widget {
header "widget.h" header "widget.h"
export * export *
} }
當兩個目錄都在搜尋路徑上時,單純的型別檢查每次都以同樣的方式失敗,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
相依性掃描器的行為則不同,而這個差別正是 Apple 之所以寫下「可能」的全部理由。把同樣這兩個 module map 丟給 swiftc -scan-dependencies 跑 20 次,得到三種結果:9 次以 SIGSEGV 當掉、5 次中止、6 次乾淨地成功。8 當掉那幾次的堆疊追蹤都經過 performParallelClangModuleLookup,這與 Apple 所述要取代掉的平行查找中存在競爭條件相符。把這種不確定性歸因於該競爭條件,是我對追蹤結果的解讀,不是 Apple 的陳述。
Apple 點名的第二種情況才是安靜的那個。我寫了一個隨附的 module map,宣告 SQLite3——iPhoneOS SDK 中真實存在的模組——把它放上搜尋路徑,然後對著它編譯:8
Vendor/module.modulemap
module SQLite3 {
header "shim.h"
export *
}
編譯成功。exit 0,沒有警告、沒有 note,任何診斷訊息都沒有。接著我在同樣的設定下要求 sqlite3_open:8
error: cannot find 'sqlite3_open' in scope
只要把隨附目錄從搜尋路徑上拿掉,同一個檔案就能編譯。那份隨附的 module map 徹底遮蔽了 SDK 的 SQLite3,而工具鏈一聲不吭。所以今天的隨附重新宣告情況並不會大聲失敗;它失敗的樣子,是你以為已經匯入的模組裡少了一個 API。Apple 的說明表示 Xcode 27 的掃描在這裡「可能會回報錯誤」,那會把一次無聲的遮蔽轉成一次建置失敗。
我的建置環境是 Xcode 26.6(build 17F113),因此上述每一則診斷訊息都來自前一代工具鏈。78 我無法提供 Xcode 27 的錯誤文字,也沒有捏造任何一則。那套診斷機制、值得拿去搜尋的字串,以及 Apple 說要收回的那份寬容,今天都已經存在。
稽核重複的模組名稱
列舉已宣告的模組名稱看起來只是一行 grep,但 module map 語言裡有兩種語法結構會給出錯誤答案。
extern module Foo "Foo.modulemap" 是前向參考,不是定義,而 Apple 的 SDK 光在一個 module map 裡就用了 79 個。7 天真的掃描會把這個參考和真正的定義算成 Foo 的兩次宣告。其次,module Darwin.C { ... } 使用帶點的 module-id 去擴充在別處宣告的模組;SDK 裡有 13 個 module map 以 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
手邊最有力的對照組,就是 Apple 自己的 SDK——它必須滿足這條規則,工具鏈才可能運作。對準 iPhoneOS 26.5 的 SDK 執行,這道指令在 usr/include 底下找到 1,099 個頂層宣告,在各 framework 的 module map 中找到 205 個,兩邊都沒有任何重複。7 對準刻意做成衝突的測試素材執行,它會指出重複的名稱與兩個檔案。8
那些排除條件很有份量。-not -path '*/build/*' 並不會排除 .build/,因為這個樣式需要字面上的目錄名稱,而 SwiftPM 會成打地把自動產生的 module map 寫進 .build。把 .build 留著,就製造出七個專案中唯一的一批「重複」:GrappleCore 出現在六個檔案、GrappleRender 出現在三個,全都是 SwiftPM 的輸出,橫跨兩個 Swift target、分屬不同的建置根目錄。9 這九個檔案甚至不是位元組完全相同,而原因很有教育意義:每一個都把同樣的模組名稱包在一條指向自己建置根目錄下所產生標頭的絕對路徑外面,所以它們只差在一個字串,其餘毫無差別。把這些叫作衝突的稽核,還沒碰到任何真東西,可信度就已經耗盡了。
Xcode 製造同一種誤判時更沒有模糊空間。在單一個 DerivedData 樹裡,它會把每個 Swift 套件自動產生的 module map 寫兩次,一次寫進 GeneratedModuleMaps-iphonesimulator/,一次寫進該 target 自己的中繼產物目錄,而且兩次都使用相對的標頭路徑。在某個專案的樹裡,有 15 個名稱各出現兩次,15 對全都位元組完全相同。9 請把稽核範圍限定在原始碼,不要涵蓋建置輸出。
七個專案裡到底有什麼
我在 Xcode 26.6 上對七個 Xcode 專案跑了這兩項稽核。誠實的結果是一個乾淨的零。9
| 專案 | Swift 檔案 | Objective-C/C++/C | 相依套件 | -ld_classic |
原始碼中的 module map |
|---|---|---|---|---|---|
| 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 個鎖定版本 | 0 | 0 |
| Yawara | 143 | 0 | 2 個本地 SPM | 0 | 0 |
| 總計 | 463 | 0 | 僅 SPM | 0 | 0 |
七個專案的 OTHER_LDFLAGS 全都未設定,整批專案裡沒有任何 .xcconfig 檔案,也沒有 CocoaPods、沒有 Carthage、沒有任何隨附的 .framework 或 .xcframework。9 模組名稱稽核檢視了 391 個 module map,其中 204 個位於專案目錄內,187 個位於 Xcode GUI 建置落腳的共用 DerivedData——每一個都是建置輸出。9 沒有任何一個儲存庫存放手寫的 module map。
這兩個零出於同一個原因,而這正是值得帶走的發現:暴露在風險中的是混合語言專案,而這些都不是。463 個 Swift 檔案之中,這批專案沒有任何 Objective-C、C++ 或 C 原始碼,唯一的非 Swift 編譯程式碼是 Water 裡的兩個 Metal 著色器。9 一個從來就不在風險中的程式碼庫所給出的空結果,對於危害本身是薄弱的證據,對於「誰才需要稽核」卻是有力的證據。
有兩個細節比這些零更重要。已解析套件中唯一手寫的 module map 屬於 swift-crypto,它提供四個 C shim,名稱分別是 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++ 專案。Apple 提高了一道下限,而且只點名 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 不再必然回傳第一個相等的元素;Apple 指出這個行為「從未由標準保證」,儘管 libc++ 一向都這麼做,而且這次沒有附帶任何停用選項。5 請把那個巨集當成一張遷移待辦,而不是修正。
常見問題
為什麼我的專案裡會有 -ld_classic?
幾乎可以確定,是因為 Apple 的 Xcode 15 發行說明開了這帖藥。那裡有兩則已知問題建議把 -Wl,-ld_classic 加進 OTHER_LDFLAGS:一則是使用弱定義符號的二進位檔在 iOS 14 與 macOS 12 或更舊系統上於執行期當機,Apple 指出這「主要影響 C++ 專案」;另一則是 LTO 目的檔把弱符號匯入連結成非弱符號。3 Apple 在 Xcode 16 宣告這個選項淘汰,並在 Xcode 27 移除它背後的連結器。14 如果旗標還在,先確認原始的缺陷今天是否仍能重現,再去找替代方案。
我要怎麼找出專案裡重複的 Clang 模組名稱?
把每個 module map 裡的頂層模組宣告列舉出來,找出同時出現在兩個檔案裡的名稱,並把建置目錄從結果中剔除。有三件事讓這項工作不只是一行 grep:extern module Foo "path" 是參考而非定義;module Foo.Bar 是擴充在別處宣告的模組,而不是宣告 Foo;巢狀的 explicit module 子模組不會和頂層名稱衝突。6 請明確排除 .build、build 與 DerivedData,因為 -not -path '*/build/*' 漏掉 .build,而 SwiftPM 與 Xcode 都會例行性地重複產生 module map。9
在 Xcode 27 底下錯誤長什麼樣?
我說不出來,任何在 Xcode 26 上建置的人也一樣。我的機器跑的是 Xcode 26.6(build 17F113),所以本文完全沒有提供 Xcode 27 的輸出。78 對於兩個宣告同一名稱的 module map,Xcode 26.6 吐出的是 error: redefinition of module 'Widget',並附上一則 note: previously defined here,而且單純型別檢查每一次都是如此。8 Apple 對 Xcode 27 的措辭是掃描「可能會回報錯誤」,所以請在記錄中搜尋 redefinition of module,而不是某人猜出來的字串。
模組名稱唯一性的失敗會不會取決於我的部署目標?
不會。Apple 把這項要求界定在單一次 Swift 相依性掃描動作上,整個條目沒有提到任何 OS 版本、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 的命名空間:Apple 點名了這種情況,而它今天是無聲失敗的。我那個隨附的 SQLite3 shim 以 exit 0 編譯通過,並讓 sqlite3_open 消失。8
- 把稽核範圍限定在原始碼,並排除 .build、build 與 DerivedData。391 個 module map 中每一個看似重複的案例,都是單一 Swift target 自動產生的建置輸出。9
給發行管理者: - 把這兩項變更視為由工具鏈觸發,排進第一次 Xcode 27 建置,而不是排進 SDK 遷移。兩者都沒有提到部署目標,也都沒有執行期的成分。12 - 把 Apple 的保留說法一併寫進工單。掃描「可能會回報錯誤」,而同一組衝突跑 20 次給出三種不同結果,所以一次建置通過什麼都證明不了。28
27 這一輪不斷在不同的地方失敗:啟動畫面鍵值擋下送審,場景生命週期的強制要求擋下啟動,@State 巨集則在原始碼層級擋下建置。連結器的移除與模組名稱規則擋在更低的一層,落在沒有人會刻意去設定的那部分工具鏈裡。完整的系列彙整頁面是 Apple 生態系系列。
參考資料
-
Apple,Xcode 27 Release Notes,Linking 章節,Deprecations in Xcode 27 Beta(radar 165165518)。移除說明的出處,完整引述原文:「The ld64 linker has been removed and the
-ld_classicoption is no longer supported.」於 2026年7月26日透過 Apple 的文件 JSON 查證,因為 HTML 頁面是以 JavaScript 算繪其內容。該日期的頁面標題為「Xcode 27 Beta 4 Release Notes」。 ↩↩↩↩↩ -
Apple,Xcode 27 Release Notes,Swift Compiler 章節,New Features in Xcode 27 Beta(radar 136303612)。相依性掃描器條目的出處,本文正文完整逐字引用,包含兩處保留說法(「the scan may report an error」與「Previously, the scanner may have tolerated duplicating names」)以及兩種被點名的情況。於 2026年7月26日透過 Apple 的文件 JSON 查證。並記錄該條目歸在 New Features 底下,而非 Deprecations 或 Known Issues。 ↩↩↩↩↩↩
-
Apple,Xcode 15 Release Notes,Linking 章節。New Features(radar 108915312)是這段話的出處:「A new linker has been written to significantly speed up static linking. It’s the default for all macOS, iOS, tvOS and visionOS binaries and anyone using the ‘Mergeable Libraries’ feature. The classic linker can still be explicitly requested using -ld64, and will be removed in a future release.」Known Issues 則是兩則建議使用該旗標的替代作法的出處:radar 114813650(FB13097713),「Binaries using symbols with a weak definition crash at runtime on iOS 14/macOS 12 or older. This impacts primarily C++ projects due to their extensive use of weak symbols.」,替代作法是提高部署目標「or add
-Wl,-ld_classicto theOTHER_LDFLAGSbuild setting」;以及 radar 115521975(FB13171424),「Weak symbol imports are linked as non-weak imports, when used from LTO object files.」,替代作法為「Add-Wl,-weak_reference_mismatches,weakor-Wl,-ld_classicoptions to theOTHER_LDFLAGSbuild setting」。於 2026年7月26日透過 Apple 的文件 JSON 查證。 ↩↩↩↩↩↩ -
Apple,Xcode 16 Release Notes,Linking 章節,Deprecations(radar 128502299):「
-ld_classiclinker option is deprecated and will be removed in a future release.」於 2026年7月26日透過 Apple 的文件 JSON 查證。 ↩↩↩ -
Apple,Xcode 27 Release Notes,C++ Standard Library 章節,Deprecations in Xcode 27 Beta。Apple 為整個區塊只列出單一個 radar 178191050,而且是附在最後一項之後,並非逐項標註。此處是這段文字的出處:「The minimum supported deployment target on macOS for the C++ standard library has been increased to 11.0.」;也是
multi{map,set}::find變更的出處(「code relying on the first element being returned fromfindwill be broken, andlower_boundorequal_rangeshould be used instead」);以及退路與其到期時間的出處:「Since this may be tricky to work around in some cases, an escape hatch is provided in this release: defining_LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUNDwill revert to the historical implementation of these operations. That escape hatch will be removed in an upcoming release (likely in the next release).」於 2026年7月26日透過 Apple 的文件 JSON 查證。 ↩↩↩ -
Clang 團隊,Clang Modules documentation,Module Map Language。此處是「Each module shall have a single definition.」的出處,也是 module-id 規則與
extern module宣告、「Theexplicitqualifier can only be applied to a submodule, i.e., a module that is nested within another module.」,以及以檔名module.modulemap進行 module map 探索、並為相容性一併搜尋module.map的出處。引用時視為工具鏈本身對 module map 語言的參考文件,而非 Apple 的開發者文件;Apple 的 clang 衍生自這份實作。 ↩↩ -
作者於 2026年7月26日在 macOS 26.5.2(build 25F84)、Xcode 26.6(build 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 page。SDK 架構取自SDKSettings.plist:iPhoneOS 26.5 宣告arm64e與arm64,WatchOS 26.5 宣告arm64、arm64e與arm64_32。SDK 的 module map 統計數字(usr/include底下 1,099 個頂層宣告、System/Library/Frameworks底下 205 個、兩邊皆無重複)來自對 iPhoneOS 26.5 SDK 執行上文公開的指令;79 個extern module宣告位於usr/include/module.modulemap,同一目錄中有 13 個 module map 以帶點的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。 ↩↩↩↩↩↩↩↩↩↩ -
作者於 2026年7月26日在同一台機器、同一套工具鏈上的重現。兩個目錄各含一個宣告
module Widget的module.modulemap,以swiftc在兩個目錄都位於搜尋路徑的情況下編譯。-typecheck在 20 次執行中全部產生error: redefinition of module 'Widget'與note: previously defined here,結束狀態為 1;拿掉其中一條-I路徑後,同一個檔案就以 exit 0 編譯成功。同樣輸入下的-scan-dependencies執行 20 次,其中 9 次以訊號 11(SIGSEGV)結束、5 次以訊號 6(SIGABRT)結束、6 次以狀態 0 結束,當掉那幾次的堆疊追蹤都經過swift::ModuleDependencyScanner::performParallelClangModuleLookup。另外,一個宣告SQLite3(iPhoneOS 26.5 SDK 中的模組)的隨附 module map 放上搜尋路徑後,型別檢查以 exit 0 通過且無任何診斷訊息,而在同樣設定下呼叫 SDK 的sqlite3_open的檔案則以「error: cannot find ‘sqlite3_open’ in scope」失敗,把隨附目錄從搜尋路徑移除後即可乾淨編譯。測試素材包含一條含有空白的路徑,用來驗證上文公開的指令。該指令比較的是檔案之間的名稱,因此同一個 module map 內部重複宣告的名稱,依設計不在它的範圍內。本文任何地方都未提供 Xcode 27 的輸出。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
作者於 2026年7月26日在 macOS 26.5.2 與 Xcode 26.6(build 17F113)上,對七個 Xcode 專案(Reps、Return、Banana List、Ace Citizenship、Water、ResumeGeni 與 Yawara)所做的稽核。
-ld_classic與ld64在任何工作樹中出現零次,OTHER_LDFLAGS在每個專案中都未設定,整批專案裡沒有.xcconfig檔案、沒有 Podfile、沒有 Carthage,也沒有任何隨附的.framework或.xcframework;搜尋範圍僅涵蓋目前的工作樹,不含 git 歷史或封存的建置記錄,因此結論是「現在不存在」,而不是「從未使用過」。Reps 所引用的SUPPORTED_PLATFORMS那一行來自xcodebuild -showBuildSettings -configuration Release -sdk iphoneos。module map 總計:檢視 391 個,其中 204 個位於七個專案目錄底下,187 個位於這七個專案在共用~/Library/Developer/Xcode/DerivedData中的樹(整個共用目錄裡的數量遠不止於此,其餘屬於其他專案),全部都是建置輸出,七個儲存庫中沒有任何手寫或隨附的 module map。看似重複的項目以內容雜湊逐一檢查,兩個產生器的行為並不相同。Xcode 在受稽核的 ResumeGeni 樹(專案內的build/DerivedData)中產生的 15 對名稱,分別寫在GeneratedModuleMaps-iphonesimulator/與該 target 的中繼產物目錄底下,15 對全都位元組完全相同,因為 Xcode 寫的是相對的標頭路徑。這個數字是按樹計算而非按專案計算:同一個 app 在共用~/Library/Developer/Xcode/DerivedData底下的樹有 16 個,其Index.noindex變體則有 22 個。能一般化的是機制,不是數字。SwiftPM 的副本則不同:Yawara 各個.build目錄底下的六個GrappleCore與三個GrappleRender檔案,分別帶有六個與三個相異的雜湊,大小介於 171 到 185 位元組之間,因為每一個都嵌入了指向自己建置根目錄內所產生-Swift.h的絕對路徑,其餘部分完全相同。把公開的指令對準整個 iPhoneOS 26.5 SDK 時,共找到 1,318 個相異的頂層 Clang 模組名稱,其中 1,099 個位於usr/include、205 個位於System/Library/Frameworks;這批專案所宣告的名稱沒有任何一個出現其中,交集為空。同一次掃描在整個 SDK 中回報零重複,而這正是這條規則所要求的對照組。CryptoKit不在該清單中,因為它以純 Swift framework 形式提供,沒有 Clang module map,所以「沒有Crypto衝突」是建立在 SDK 未以該名稱宣告任何 Clang 模組之上,而不是建立在CryptoKit佔用了該名稱之上。swift-crypto 4.4.0 提供已解析相依關係圖中唯一手寫的 module map(CCryptoBoringSSL、CCryptoBoringSSLShims、CXKCP、CXKCPShims,各一次宣告)。SymbolKit的 host-tool 與 target 變體位於共用的本地套件 941Kit 中,而不在這七個 app 裡。原始碼計數排除了 Python 虛擬環境,而這項修正很關鍵:一次天真的計數曾把 C 與標頭檔算到 Reps 頭上,後來發現它們全都位於.venv與site-packages之中,沒有任何 Xcode target 會編譯它們。Water 沒有本地建置樹,所以它的 module map 計數為零,反映的是一個未經建置的專案,而非一份已驗證乾淨的建置圖。三個「假乾淨」陷阱各自都直接確認過:原生 macOS 沒有timeout,會以 127 結束;zsh 會展開未加引號的--include=*.pbxproj並以「no matches found」中止;xcrun --sdk iphoneos --show-sdk-path回傳的是符號連結,對它執行不帶-H的find回報零個 module map,而find -H回報 266 個。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple,Xcode 26 Release Notes。於 2026年7月26日搜尋
ld_classic、ld64以及任何描述舊版連結器的條目;均無所獲,因此從 Xcode 16 的淘汰通知到 Xcode 27 的移除之間,Apple 沒有任何再次重申。 ↩↩ -
Apple,Xcode 27 Release Notes,Overview:「Xcode 27 beta 4 includes Swift 6.4 and SDKs for iOS 27, iPadOS 27, tvOS 27, watchOS 27, macOS 27, and visionOS 27.」同一份說明在 Intel Deprecation(radar 162138432)底下記載:「Xcode 27 will only install and run on Apple silicon Macs.」於 2026年7月26日查核。 ↩