跳转到内容

便利的代价

便利的代价 {#the-cost-of-convenience}

Section titled “便利的代价 {#the-cost-of-convenience}”

设计一款从小型到大型项目都可以使用的代码编辑器是一项具有挑战性的任务。许多工具通过分层提供解决方案和可扩展性来解决这个问题。最底层非常低级,接近底层构建系统,最高层是高级抽象,使用方便但灵活性较差。通过这种方式,它们使简单的事情变得容易,其他一切成为可能。

然而,苹果决定在 Xcode 上采取不同的方法。原因未知,但优化大型项目的挑战可能从来不是他们的目标。他们在小项目的便利性上投入过多,提供的灵活性很少,并将工具与底层构建系统紧密耦合。为了实现便利性,他们提供了合理的默认值,你可以轻松替换,并添加了许多隐式的构建时解析行为,这些行为是规模化时许多问题的根源。

显式与规模化 {#explicitness-and-scale}

Section titled “显式与规模化 {#explicitness-and-scale}”

在规模化工作时,显式是关键。它允许构建系统提前分析和理解项目结构及依赖关系,并执行原本不可能的优化。同样的显式性对于确保编辑器功能(如 SwiftUI 预览Swift 宏)可靠且可预测地工作也至关重要。由于 Xcode 和 Xcode 项目将隐式性作为实现便利性的有效设计选择(Swift Package Manager 也继承了这一原则),使用 Xcode 的困难同样存在于 Swift Package Manager 中。

::: info TUIST 的角色

我们可以将 Tuist 的角色总结为一个防止隐式定义项目并利用显式性提供更好的开发者体验(例如验证、优化)的工具。Bazel 等工具则更进一步,将其带入构建系统层面。

:::

这是一个在社区中几乎未被讨论的问题,但却是非常重要的一个问题。在开发 Tuist 的过程中,我们注意到许多组织和开发者认为他们当前面临的挑战将通过 Swift Package Manager 得到解决,但他们没有意识到的是,因为它建立在相同的原则之上,尽管它缓解了众所周知的 Git 冲突,却在其他方面降低了开发者体验,并继续使项目无法优化。

在接下来的章节中,我们将讨论一些隐式性如何影响开发者体验和项目健康的真实例子。这个列表并不详尽,但它应该能让你很好地了解在使用 Xcode 项目或 Swift 包时可能面临的挑战。

便利性阻碍你 {#convenience-getting-in-your-way}

Section titled “便利性阻碍你 {#convenience-getting-in-your-way}”

共享构建产品目录 {#shared-built-products-directory}

Section titled “共享构建产品目录 {#shared-built-products-directory}”

Xcode 为每个产品使用 derived data 目录内的目录。在其中存储构建产物,如编译的二进制文件、dSYM 文件和日志。因为项目的所有产品都进入同一目录,默认情况下其他目标可以看到并链接,你可能最终得到隐式相互依赖的目标。虽然只有少数目标时可能不是问题,但当项目增长时,这可能表现为难以调试的构建失败。

这个设计决策的后果是,许多项目意外地编译出一个定义不良好的图。

::: tip TUIST 隐式依赖检测

Tuist 提供了一个命令来检测隐式依赖。你可以使用该命令在 CI 中验证所有依赖都是显式的。

:::

在方案中查找隐式依赖 {#find-implicit-dependencies-in-schemes}

Section titled “在方案中查找隐式依赖 {#find-implicit-dependencies-in-schemes}”

在 Xcode 中定义和维护依赖图随着项目的增长变得越来越困难。困难是因为它们在 .pbxproj 文件中编码为构建阶段和构建设置,没有工具来可视化和处理图中的更改(例如添加新的动态预编译框架),可能需要上游的配置更改(例如添加将框架复制到 bundle 的新构建阶段)。

苹果在某个时候决定,与其将图模型演进为更容易管理的东西,不如添加一个在构建时解析隐式依赖的选项。这又是一个值得商榷的设计选择,因为你可能最终得到更慢的构建时间或不可预测的构建。例如,由于 derived data 中的某些状态(作为单例),构建可能在本地通过,但在 CI 上失败,因为状态不同。

::: tip

我们建议在你的项目方案中禁用此选项,并使用像 Tuist 这样简化依赖图管理的工具。

:::

SwiftUI 预览和静态库/框架 {#swiftui-previews-and-static-librariesframeworks}

Section titled “SwiftUI 预览和静态库/框架 {#swiftui-previews-and-static-librariesframeworks}”

某些编辑器功能(如 SwiftUI 预览或 Swift 宏)需要从正在编辑的文件编译依赖图。编辑器之间的这种集成要求构建系统解析任何隐式性并输出这些功能正常工作所需的产物。正如你所想象的,图的隐式性越高,对构建系统来说任务就越艰巨,因此这些功能不能可靠地工作也就不足为奇了。我们经常听到开发者说他们很久以前就停止使用 SwiftUI 预览了,因为它们太不可靠了。相反,他们要么使用示例应用,要么避免某些东西,比如使用静态库或脚本构建阶段,因为这会导致功能失效。

动态框架虽然更灵活、更易于使用,但对应用的启动时间有负面影响。另一方面,静态库启动更快,但影响编译时间,而且在复杂的图场景中处理起来有点困难。*如果你能根据配置在两者之间切换,那岂不很好?*这一定是苹果决定开发可合并库时的想法。但是,他们又一次将更多的构建时推断推到了构建时。如果要推理依赖图,想象一下当目标的静态或动态性质将在构建时基于某些目标中的构建设置来解决时,你必须这样做。在确保 SwiftUI 预览等功能不中断的情况下可靠地使其工作,祝你好运。

许多用户来找 Tuist 希望使用可合并库,我们的答案始终是一样的。你不需要。 你可以在生成时控制目标的静态或动态性质,从而生成一个在编译前就知道图的项目。不需要在构建时解析任何变量。

Terminal window
# TUIST_DYNAMIC 的值可以从项目中读取
# 根据值将产品设置为静态或动态
TUIST_DYNAMIC=1 tuist generate

显式、显式,还是显式 {#explicit-explicit-and-explicit}

Section titled “显式、显式,还是显式 {#explicit-explicit-and-explicit}”

如果有一个重要的非成文原则,我们建议每个希望与 Xcode 一起扩展开发的开发者或组织遵循,那就是他们应该拥抱显式性。如果使用原始 Xcode 项目难以管理显式性,他们应该考虑其他方案,要么是 Tuist 要么是 Bazel只有这样,可预测性、可靠性和优化才成为可能。

苹果是否会做些什么来防止上述所有问题是未知的。他们在 Xcode 和 Swift Package Manager 中的持续决策并不表明他们愿意这样做。一旦你允许隐式配置作为一种有效状态,**很难在不引入破坏性更改的情况下从中脱身。**回到第一性原则并重新思考工具的设计可能会破坏多年来意外编译的许多 Xcode 项目。想象一下如果发生这种情况社区会有多大反应。

苹果发现自己有点陷入鸡生蛋蛋生鸡的问题。便利性帮助开发者快速入门并为他们的生态系统构建更多应用。但他们为实现这种规模的便利性所做的决策,使他们在确保某些 Xcode 功能可靠工作方面困难重重。

因为未来是未知的,我们尝试尽可能接近行业标准和 Xcode 项目。我们防止上述问题,并利用我们的知识提供更好的开发者体验。理想情况下,我们不需要诉诸项目生成来实现这一点,但 Xcode 和 Swift Package Manager 缺乏可扩展性使其成为唯一可行的选择。这也是一个安全的选择,因为他们必须破坏 Xcode 项目才能破坏 Tuist 项目。

理想情况下,构建系统更具可扩展性,但拥有与隐式世界签约的插件/扩展难道不是坏主意吗?这似乎不是好主意。所以看起来我们需要 Tuist 或 Bazel 这样的外部工具来提供更好的开发者体验。或者苹果可能会让我们惊喜,让 Xcode 更加可扩展和显式……

在此之前,你必须选择是拥抱 Xcode 的便利并承担随之而来的债务,还是在这段旅程中信任我们为你提供更好的开发者体验。我们不会让你失望的。