Xcode 10 新构建系统的一个小问题

swift 发布于 2018年10月16日

新 iPhone 和 Xcode 10 发布已经有一段时间了。 相信大家也都把自己的开发环境切换到了 Xcode 10。 而这次 Xcode 10 同时带来了一个全新的构建系统。 在使用中我们可能会遇到一些新问题。 比如新构建系统的依赖循环检测

如果我们的旧工程,之前对项目之间的依赖设置的不够明确,就可能会在打包或者构建的时候遇到类似这样的错误:

error: Multiple commands produce '/Library/Developer/Xcode/DerivedData/.../GracefulImagePicker.bundle':
1) Target 'GracefulFeedback-GracefulImagePicker' has create directory command with output '/Users/marspro/Library/Developer/Xcode/DerivedData/.../GracefulImagePicker.bundle'
2) Target 'GracefulImagePicker-GracefulImagePicker' has create directory command with output '/Users/marspro/Library/Developer/Xcode/DerivedData/.../GracefulImagePicker.bundle'

上面是我开发中遇到的一个实例, 在使用 Xcode 9 的时候,这个工程编译正常。 切换到 Xcode 10 之后,就遇到了这个错误。

经过一番查询,了解到造成这个问题的原因就是 Xcode 10 的新构建系统对于包之间依赖以及名称冲突的检测更加严格了。 详情在 Xcode 的官方文档中有介绍: https://help.apple.com/xcode/mac/current/#/dev621201fb0

这里面的大致意思给大家总结一下, 就是 Xcode 构建系统中有两种指定包依赖的方式。

第一种方式是显示指定,我们在 Target Dependencies 中指定的包,例如:

上图指定 UIColor_Hex_Swift 这个包为依赖项, 这意味着什么呢? 当我们当期项目开始编译的时候, 构建系统首先会编译 UIColor_Hex_Swift 这个项目, 然后才会编译主工程。

另外一种方式可以称为间接指定。 这种依赖关系相对更加隐蔽, 简单来说就是只要在主工程中引用的任何第三方库,包括 Framwork,子项目等等,都会被构建系统认为主工程依赖于他们。 这样在构建系统开始运作的时候,这些间接依赖依然会被首先编译。

对于咱们上面说的包之间依赖的规则,新的构建系统默认是严格处理,保证不会造成循环依赖,或者名称冲突

循环依赖是什么呢, 假设我们有 A,B,C 三个工程, 用箭头表示他们之间的依赖:

A -> B -> C -> A

按照上面的描述,A 依赖 B, B 依赖 C, 而 C 又反过来依赖了 A。 这就造成了循环依赖。 这样构建系统就无法确认应该优先编译哪个工程。

那么如何解决这类问题呢, 目前有两种解决方案。

退回旧的构建系统

因为旧版的构建系统对包之间这种验证相对宽松,有可能我们旧工程中存在类似的问题,但也能正常构建通过,并没有发生明显的运行问题。这时候如果切换到新的构建系统上,就会造成项目马上不可编译。

如果你这时候有时间的压力,没有足够的精力去修正之前项目中的这类问题的话,Xcode 10 允许开发者回退到旧的构建系统。具体操作方法是这样, 打开 File -> Workspace Settings... , 在随后的窗口中选择旧的构建系统 - Legacy Build System, 如下图:

这样修改后, 基本上你的旧项目就可以直接编译通过了。 当然,如果时间允许的话,还是建议花些精力把自己项目中真正的依赖问题直接处理, 随着时间的推移,可能以后不一定能那么方便的直接切换回旧构建系统了。

修改项目中的依赖冲突问题

就像刚才我说的,如果精力允许的情况下,最好还是从根本上把自己项目中的依赖问题处理掉。 下面就和大家分享下自己项目中的处理方法。 我把刚开始的那段编译错误再贴出来:

error: Multiple commands produce '/Library/Developer/Xcode/DerivedData/.../GracefulImagePicker.bundle':
1) Target 'GracefulFeedback-GracefulImagePicker' has create directory command with output '/Users/marspro/Library/Developer/Xcode/DerivedData/.../GracefulImagePicker.bundle'
2) Target 'GracefulImagePicker-GracefulImagePicker' has create directory command with output '/Users/marspro/Library/Developer/Xcode/DerivedData/.../GracefulImagePicker.bundle'

从错误信息不难看出, 这是一个名称冲突,有两个库挂载了两个重名的 bundle, GracefulImagePicker.bundle

这两个库在上面的信息中也指出来了,是 GracefulFeedbackGracefulImagePicker。 他们是我工程中用到的两个自行开发的内部子模块。

这两个库都各自挂载了一个 bundle 资源包, 从这个错误中可以看出,新的构建系统不允许两个库挂载的 bundle 的名称一样。但在旧的构建系统中这里是不会报错的。

两个库的 bundle 挂载关系如下:

GracefulFeedback -> GracefulImagePicker.bundle GracefulImagePicker -> GracefulImagePicker.bundle

我的理解是,旧的构建系统应该是有一些机制自动处理了这种不同库中,同名的 bundle 如何区分的问题,又或者是根本无视了这种冲突,直接覆盖掉一个。 当然具体机制我没有深入研究。不做过多展开。

而新的构建系统,检测到这种名称冲突后, 把它当做了一个编译错误,让开发者来自行处理。

本来之前对这两个库的定义是,资源文件的名称和库的名称是一样的才对,并不应该出现名称冲突,从报错信息上看,问题明显发生在 GracefulFeedback 这个库上面,它的资源 bundle 本该命名为 GracefulFeedback.bundle, 但从编译错误信息中来看,它似乎命名成了 GracefulImagePicker.bundle

于是我回到 GracefulFeedback 这个工程中查看, 果然发现了问题根源所在,我们的子模块都通过 CocoaPods 来实现,查看它的 .podspec 文件时发现这样一段内容:

s.resource_bundles = {
'GracefulImagePicker' => ['GracefulFeedback/Assets/*.png']
}

果然,它的资源包被命名成了 GracefulImagePicker, 而不是预期的 GracefulFeedback。 这就导致了它的资源包和同样另外一个模块的重名了。

新的构建系统,通过更严格的机制,帮助我发现了这个问题。 将 .podspec 中这段内容修改正确后,这个工程顺利的在新构建系统中编译通过了。 当然,哪种方式更好,也并不是绝对的。

不过我们得出了一个经验,新的构建系统采用更加严格的验证方式。 反过来推动我们投入更多的精力来把之前项目结构中不严谨的地方改正过来。

关于 CocoaPods.podspec 的详细介绍,我们之前的文章有过讨论,大家也可以去了解一下。这里就不过多讨论了。

总结

Xcode 10 这次采用了全新的构建系统,使用更加严格的验证机制。总体来说是一件好事,但是对于开发者来说,需要一个转换的过程。 比如我第一次用 Xcode 10 编译同样的项目一直报错的时候,心里就感觉很疑惑,经过一番了解后,才知道其中的原因。 不过整个过程下来后,回顾起来也有一些收获的。 在这里和大家分享出来,也是希望能帮助大家节省时间,遇到和我类似的问题时候有一个参考。

对于新的构建系统的各种细节,如果大家有更多其他的了解,也欢迎一起交流。


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe