去年升级到 macOS 26 后,我在本地编译 SciPy 时突然遇到无法通过 Meson 的编译器检测的错误。错误信息看似简单,实则涉及 macOS 的运行时加载器 dyld、链接器行为与 conda-forge 提供的 clang/ld64 工具链之间的细微交互。通过社区协作和一次针对 ld64 的小补丁,我们最终在 conda-forge 上发布了修复版本,使得依赖该工具链的用户恢复了本地构建能力。这里把整个排查过程、原因分析、修复步骤以及对其他开发者的建议整理成一篇实用指南,便于遇到类似问题时能迅速定位与处理。 问题出现的症状很明确:在执行 meson setup 时,日志提示"Executables created by c compiler ... are not runnable",Meson 的简单可执行文件检查失败,日志中 dyld 抛出信息"missing LC_LOAD_DYLIB (must link with at least libSystem.dylib)"。若遇到类似现象,先不要慌,这通常并非你的源代码出了错,而是构建工具链或链接行为与系统运行时的新要求之间产生了不兼容。
我复现问题的第一步是读取 Meson 的完整日志,尤其是 Meson 在进行"sanity check"时写出的可执行文件路径和 dyld 错误。Meson 的检查会生成一个极其简单的 C 程序,例如 int main(void) { int class=0; return class; },编译后运行该可执行文件以确认编译器可用。错误提示表明生成的可执行文件虽然存在,但 dyld 在加载时判断该可执行文件未至少链接到 libSystem.dylib,从而拒绝运行。libSystem.dylib 在 macOS 中承担 C 标准库和底层系统调用接口的角色,是运行时必需的核心库。 进一步排查需要关注两点:编译器在链接阶段传递了哪些选项,以及生成的可执行文件的依赖信息。使用编译器的 verbose 输出或 Meson 的日志可以看到实际传给链接器的 -Wl 参数。
conda-forge 的 clang 在默认配置下会添加 -Wl,-dead_strip_dylibs,这个选项会在链接后去掉被认为"未使用"的动态库依赖。正常情况下,去掉未使用的 dylib 能减少二进制依赖,但在 macOS 26 引入的一项新的运行时检查中,如果 SDK 版本标识为 26 或更高,dyld 要求可执行文件至少显式链接到 libSystem.dylib,否则会拒绝加载并报出缺少 LC_LOAD_DYLIB 的错误。 要确认生成文件的依赖,可以用 otool -L path/to/executable 检查可执行文件的动态库引用。若发现 libSystem.dylib 未被列出,而其他库也被正确处理,那么问题的来源便很可能在链接器把 libSystem.dylib 剔除了。定位到链接器以后,下一步是查看具体使用的 ld 实现与版本。在 conda-forge 提供的 clang 工具链中,ld64 源自 tpoechtrager/cctools-port,这个项目实现了 macOS 风格的链接器行为。
在和 conda-forge 社区沟通后,核心问题被集中到 ld64 的一个"dead strip dylibs"逻辑上。具体来说,ld64 有一段代码用于在启用 -dead_strip_dylibs 时移除那些被认为既未提供导出符号也不依赖其他 dylib 的显式链接 dylib。该逻辑原本是合理的,但在 macOS 26 的新检查面前,移除 libSystem.dylib 会导致可执行文件在加载期被判定为"不满足最低依赖",从而无法运行。 修复思路很直观:不要在 dead_strip_dylibs 的清理逻辑中把 libSystem.dylib 当成普通的可移除对象。实现上只需在判断条件中加入对 dylib 安装路径的排除,让安装路径以 /usr/lib/libSystem. 开头的 dylib 保持不移除。对应的代码修改非常小巧:在判断 aDylib 是否会被移除的条件中额外加入 strncmp(aDylib->installPath(), "/usr/lib/libSystem.", 19) != 0 的判断,确保 libSystem.dylib 与其版本化名称不会被误删。
完成补丁后,下一步是将修改应用到 conda-forge 的生产流程中。conda-forge 的包构建由 feedstock 驱动,feedstock 可以在打包前对上游源代码应用补丁。具体步骤包括在本地生成 git 补丁(git format-patch),把补丁文件和必要的 recipe 更新提交到 cctools-and-ld64-feedstock 的 PR 中,并把包的 build number 提升以触发重建。提交 PR 后,conda-forge 的 CI 会在多架构的构建农场上自动构建新版本的包,并生成可下载的构建产物。 我没有在本地编译完整的 ld64,而是直接从 CI 的构建产物下载了对应架构的 conda 包,然后在本机通过 Pixi 指定本地 artifact 的方式来测试该包是否真的能修复问题。将构建产物作为本地 channel 并在 pixi.toml 中引用之后,我再次运行构建任务来验证 Meson 的 sanity check 是否通过。
验证通过后便可以合并 PR,让修复在更广泛的用户群中生效。很快,conda-forge 的主仓库里就出现了包含该补丁的新版 ld64/cctools,用户通过常规的 conda 更新或 Pixi 拉取最新依赖后即可恢复编译能力。 对于普通开发者和构建维护者,有几条实用建议可以帮助在遇到类似问题时更快定位与缓解风险。首先,遇到"可执行不可运行"之类的错误时,优先查看构建系统生成的完整日志,并找出被编译并运行的可执行文件路径。其次,使用 otool -L 检查可执行文件的动态库依赖,确认是否缺少 libSystem.dylib 或其他核心 dylib。再者,检查传入链接器的参数(如 -Wl,-dead_strip_dylibs)并尝试临时移除这些参数以确认是否与链接器行为相关。
若临时措施可行,则可以考虑提交补丁到相关的链接器实现或在本地制作替代包。 在更广泛的生态层面,这次事件凸显了三个方面的重要性。其一是系统级别变化(例如 Apple 在新版 macOS 中新增运行时检查)可能会以意想不到的方式影响到预先工作的工具链,尤其是当这些工具链来自第三方分发(如 conda-forge)而非 Apple 官方时。其二是开源社区的快速协作能力在面对突发问题时极为关键,从问题上报到补丁合并并在 CI 上发布新包,这个闭环若能迅速运转就能把影响降到最低。其三是构建工具和包管理器应尽量提供方便的替换与回滚路径,让开发者能在短时间内切换到已知良好的工具链版本或临时使用社区构建的修复包。 此外,开发者也应关注几项常用的排查命令:查看可执行文件依赖可用 otool -L,查看二进制中的 load command 信息可用 otool -l,若需要修改可执行文件的依赖可以使用 install_name_tool。
对于 Meson 或 CMake 等构建系统,开启 verbose 或 debug 模式能显示完整的编译和链接命令,便于复现问题。对于 conda 环境,利用本地 channel 或直接下载 CI 产物进行验证可以大幅节省重建工具链的时间成本。 通过这次修复,我深刻体会到社区力量和开源生态的重要性。在遇到系统级别断裂点时,单靠个人往往难以在短时间内完成修复,但借助社区的专家知识与自动化构建平台,问题可以被迅速定位并在短时间内发布修复版本。对于依赖复杂工具链的科学计算库开发者而言,关注 conda-forge 的变更日志、积极参与社区讨论并在 CI 中引入更多的健壮性检查,可以显著提高面对平台升级时的应对能力。 最后,若你正在使用 conda-forge 提供的 clang 在 macOS 上进行构建,遇到与可执行文件加载相关的错误,建议先按上文步骤检查可执行文件依赖,确认是否与 libSystem.dylib 有关。
必要时可临时回退到系统 clang 或通过本地 channel 使用已经包含修复的 ld64 包。同时,把相关日志、复现步骤和环境信息(操作系统版本、SDK 版本、conal-forge clang 和 ld64 的版本)整理好并提交到 conda-forge 的社区渠道,将有助于更快获得支持與修复。 这次经历从意外中收获了许多:对链接器与运行时加载器交互机制有了更清晰的理解,也见证了开源社区在短时间内发现问题、制定补丁并发布修复的高效流程。希望把我的调试思路、关键命令与实践步骤分享给更多遇到相似问题的同仁,让大家在面对平台升级或工具链变更时能少走弯路,尽快恢复生产力。 。