如果在构建编译过程中,引用了不存在的文件, Xcode 会给出如下错误:
Build input file cannot be found: '{path to some file}'. Did you forget to declare this file as an output of a script phase or custom build rule which produces it?
第一次遇到这个问题是在 Xcode 10 升级, Legacy Build System 升级为 Modern Build System 后,因为编译过程并行化,导致在前序的 Build Phase 步骤中下载的文件,无法被后面的步骤检测到。表现出来的错误的情形是:第一次构建时 Xcode 会报错。再按一次 Build 按钮,编译顺利通过。
我们的情况
在编译项目过程中,我们需要用到一些从服务器下载下来的文件。随着项目变大,需要下载的文件也逐渐变多。没有选择直接放在项目仓库里一是因为这些文件比较大,直接存在 GitHub 上浪费空间,二是因为二进制文件不适合版本管理。因此,我们通过运行一个 Run Script 的 Build Phase 来管理、下载这些文件。在项目每次编译时,首先执行这个步骤,从而确保本地的二进制文件是最新的。
因此,这个脚本需要从一个项目文件读取一些信息,然后从服务器下载对应的个数不定的文件,而这些文件后面会作为 On-Demand Resources 打包进 app bundle。
编译系统
WWDC22: Demystify parallelization in Xcode builds | Apple 介绍了新的现代构建系统通过一些巧妙的办法,最大程度并行化编译过程,以节省时间。不得不说,看了视频里展示类似于火焰图的性能图表,所有构建流程都紧凑地分散在不同的物理核心上时,令人相当心满意足。
视频中提到了两个设置:
- Run Build Script Phases in Parallel:设置是否并行执行编译过程
- User Script Sandboxing:是否将每一个脚本步骤的输入输出隔离,以作为计算编译过程依赖的图结构的标准
经过我的测试,无论是否并行执行,我遇到的上述错误依旧存在;而如果运行存在文件系统输入输出的脚本,sandboxing 设置是需要关闭的。
我的问题
因为我们的下载文件的脚本是一个 Run Script Phase ,根据视频中的介绍,如果有文件系统的输入输出,理论上必须定义输入和输出的文件/文件列表。这样,编译系统才能按照定义的文件路径,来检测这个步骤是否完成,从而消除并行编译过程中的 race condition。
然而,根据我们自己的测试,在不指定输入输出文件的时候,有时 Build Phase 也能正确完成而不抛出错误。因为这个问题只出现在某些文件上,最初遇到时我们百思不得其解,尝试了许多方法去解决这个问题,包括:
- 尝试 debug 下载文件的脚本,看是不是因为产生了 race condition 或者线程问题,导致编译失败。结论是脚本没有这方面的问题。
- 尝试把下载文件的步骤移到 Pre build action 里面,即 xcscheme 设置里面。这个功能大致可以理解为在每次编译之前时,运行一个任意的脚本执行一些功能,和编译本身没有依赖。我们讨论了这个选项,但决定每次编译之前运行这个脚本的成本有点高,每个开发者都要在上面浪费零点几秒到几秒不等,日积月累也是不小的开销。所以决定不用这个功能。
- 尝试把所有输出文件的列表加到 Output Files 里面。因为需要下载的文件数量随着项目成长是逐渐变多的,每次新加文件需要手动添加到列表很麻烦,也容易出错,所以决定不是万不得已还是不用这个方法。
- 尝试通过一个脚本生成一个
xcfilelist
文件来列出所有下载的文件。同上,如果有别的解决方法,就想先不用这个方法。
真正的问题
经过很长时间的尝试,我偶然发现,问题并非来自随机的文件,而是固定的几个文件。进而观察到,每次报错都会涉及到几个和 raster 栅格化底图相关的示例程序。顺藤摸瓜,居然发现罪魁祸首是文件类型!
当我删掉这几个下载的 raster 文件夹,其中包括 tif 和 xml 格式的文件时,发现项目可以成功编译了。而把这些有问题的文件放到一个实际文件夹里面,而非引用逻辑文件夹时,也能解决问题。
因此,在这个 Pull Request 里面,我把受影响的文件都放到了文件系统里的文件夹里,问题迎刃而解。
适合你的解法
写这篇笔记的原因是时隔四五年后再次遇到这个问题。
这次我们需要在编译开始之前,下载一些账号密码和密钥文件。再次遇到 “Build input file cannot be found” 报错时有点摸不到头脑,尝试了一圈以后发现还是文件类型的问题。密钥存储在 plist 文件里时就会报错,移到一个 json 文件里就不报错了。
然而,对于这种下载固定个数的文件的情况,其实我更推荐以下两种解法
- Pre Actions,即前文所说的构建前运行脚本。因为密钥文件大小很小,几毫秒就能下载完,对于项目而言也是一个只读的文件,因此用这个功能完全够用。
- 指定输出文件路径。因为密钥文件数量固定,只要把输出路径设置好,后期很少需要调整。
而通过用别的文件类型以避免错误的方法,我在网上查了半天也没找到权威文档对于这个行为的解释,因此可以看作未来 Xcode 有可能悄没声就改了的行为。如果随便猜的话,或许是因为 Xcode 对某些特定类型的文件有特别的处理,而一些 Xcode 无法处理的文件类型则全当作任意文件处理。
写下这篇笔记时,目前这个行为在 Xcode 11 - Xcode 16 之间仍然存在。
且用且珍惜吧。🤷
Top comments (0)