跳转到内容

依赖

随着项目规模增长,将其拆分为多个 target 以共享代码、定义边界并缩短构建时间是很常见的做法。多个 target 意味着需要在它们之间定义依赖关系,形成一个依赖图,其中也可能包含外部依赖。

XcodeProj 定义的依赖图 {#xcodeprojcodified-graphs}

Section titled “XcodeProj 定义的依赖图 {#xcodeprojcodified-graphs}”

由于 Xcode 和 XcodeProj 的设计特点,维护依赖图可能是一项繁琐且容易出错的任务。以下是一些可能遇到的问题示例:

  • 由于 Xcode 的构建系统会将所有项目的产物输出到 derived data 中的同一目录,target 可能会导入本不应该导入的产物。编译可能会在 CI 上失败(CI 上更常见的是全新构建),或者在之后使用不同配置时失败。
  • 一个 target 的传递动态依赖需要复制到 LD_RUNPATH_SEARCH_PATHS 构建设置所在的目录之一。如果没有被复制,target 将无法在运行时找到它们。在依赖图较小时,这很容易理解和设置,但随着依赖图变大,这就成了一个问题。
  • 当一个 target 链接一个静态的 XCFramework 时,target 需要一个额外的构建阶段,让 Xcode 处理该 bundle 并为当前平台和架构提取正确的二进制文件。这个构建阶段不会自动添加,很容易忘记添加。

以上只是几个例子,还有更多我们多年来遇到的问题。想象一下,如果你需要一个工程师团队来维护一个依赖图并确保其有效性。或者更糟糕的是,这些复杂性由一个你无法控制或定制的闭源构建系统在构建时解决。听起来很熟悉?这就是 Apple 对 Xcode 和 XcodeProj 采用的方法,Swift Package Manager 也继承了这一方法。

我们坚信依赖图应该是显式的静态的,因为只有这样它才能被验证优化。使用 Tuist,你只需描述什么依赖什么,剩下的由我们来处理。复杂的实现细节对你来说是透明的。

在以下部分中,你将学习如何在项目中声明依赖。

::: tip 依赖图验证 Tuist 在生成项目时会验证依赖图,确保没有循环依赖且所有依赖都有效。多亏了这一点,任何团队都可以参与优化依赖图,而不必担心破坏它。 :::

Target 可以依赖同一项目和不同项目中的其他 target,以及二进制文件。在实例化 Target 时,你可以传入 dependencies 参数,支持以下选项:

  • Target:声明对同一项目中 target 的依赖。
  • Project:声明对不同项目中 target 的依赖。
  • Framework:声明对二进制框架的依赖。
  • Library:声明对二进制库的依赖。
  • XCFramework:声明对二进制 XCFramework 的依赖。
  • SDK:声明对系统 SDK 的依赖。
  • XCTest:声明对 XCTest 的依赖。

::: info 依赖条件 每种依赖类型都接受一个 condition 选项,可以根据平台有条件地链接依赖。默认情况下,它会为 target 支持的所有平台链接依赖。 :::

Tuist 还允许你在项目中声明外部依赖。

Swift 包是我们推荐的在项目中声明依赖的方式。你可以使用 Xcode 的默认集成机制或使用 Tuist 的 XcodeProj 集成方式来集成它们。

Tuist 的 XcodeProj 集成 {#tuists-xcodeprojbased-integration}

Section titled “Tuist 的 XcodeProj 集成 {#tuists-xcodeprojbased-integration}”

虽然 Xcode 的默认集成方式最方便,但缺乏中大型项目所需的灵活性和控制力。为了解决这个问题,Tuist 提供了一种基于 XcodeProj 的集成方式,允许你使用 XcodeProj 的 target 将 Swift 包集成到项目中。因此,我们不仅可以让你更好地控制集成方式,还可以使其与缓存选择性测试运行等工作流兼容。

XcodeProj 的集成可能需要更长时间来支持新的 Swift 包功能或处理更多包配置。然而,Swift 包和 XcodeProj target 之间的映射逻辑是开源的,社区可以贡献。这与 Xcode 的默认集成不同,后者是闭源的,由 Apple 维护。

要添加外部依赖,你需要在 Tuist/ 下或项目根目录创建一个 Package.swift

::: code-group

5.9
import PackageDescription
#if TUIST
import ProjectDescription
import ProjectDescriptionHelpers
let packageSettings = PackageSettings(
productTypes: [
"Alamofire": .framework, // 默认为 .staticFramework
]
)
#endif
let package = Package(
name: "PackageName",
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"),
],
targets: [
.binaryTarget(
name: "Sentry",
url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.40.1/Sentry.xcframework.zip",
checksum: "db928e6fdc30de1aa97200576d86d467880df710cf5eeb76af23997968d7b2c7"
),
]
)

:::

::: tip 包设置 包装在编译器指令中的 PackageSettings 实例允许你配置包的集成方式。例如,在上面的示例中,它用于覆盖包的默认产品类型。默认情况下,你不需要它。 :::

[!IMPORTANT] 自定义构建配置 如果你的项目使用自定义构建配置(标准 DebugRelease 以外的配置),你必须使用 baseSettingsPackageSettings 中指定它们。外部依赖需要了解你项目的配置才能正确构建。例如:

#if TUIST
import ProjectDescription
let packageSettings = PackageSettings(
productTypes: [:],
baseSettings: .settings(configurations: [
.debug(name: "Base"),
.release(name: "Production")
])
)
#endif

更多信息请参阅 #8345

Package.swift 文件只是一个声明外部依赖的接口,没有其他作用。这就是为什么你不在包中定义任何 target 或产品。定义好依赖后,你可以运行以下命令来解析并拉取依赖到 Tuist/Dependencies 目录:

Terminal window
tuist install
# 正在解析和获取依赖。 {#resolving-and-fetching-dependencies}
# 正在安装 Swift Package Manager 依赖。 {#installing-swift-package-manager-dependencies}

如你所见,我们采用了一种类似于 CocoaPods 的方法,将依赖的解析作为独立的命令。这让用户可以控制何时解析和更新依赖,并允许打开 Xcode 项目并准备好编译。这是一个我们认为 Apple 与 Swift Package Manager 的集成体验随着项目增长而下降的领域。

然后,你可以从项目 target 中使用 TargetDependency.external 依赖类型引用这些依赖:

::: code-group

import ProjectDescription
let project = Project(
name: "App",
organizationName: "tuist.io",
targets: [
.target(
name: "App",
destinations: [.iPhone],
product: .app,
bundleId: "dev.tuist.app",
deploymentTargets: .iOS("13.0"),
infoPlist: .default,
sources: ["Targets/App/Sources/**"],
dependencies: [
.external(name: "Alamofire"), // [!code ++]
]
),
]
)

:::

::: info 不会为外部包生成 schemes 为了保持 schemes 列表整洁,不会自动为 Swift 包项目创建 schemes。你可以通过 Xcode 的 UI 创建它们。 :::

Xcode 的默认集成 {#xcodes-default-integration}

Section titled “Xcode 的默认集成 {#xcodes-default-integration}”

如果你想使用 Xcode 的默认集成机制,在实例化项目时传入 packages 列表:

let project = Project(name: "MyProject", packages: [
.remote(url: "https://github.com/krzyzanowskim/CryptoSwift", requirement: .exact("1.8.0"))
])

然后从你的 target 中引用它们:

let target = .target(name: "MyTarget", dependencies: [
.package(product: "CryptoSwift", type: .runtime)
])

对于 Swift 宏和构建工具插件,你需要分别使用类型 .macro.plugin

::: warning SPM 构建工具插件 SPM 构建工具插件必须使用 Xcode 的默认集成 机制声明,即使你的项目依赖使用 Tuist 的 XcodeProj 集成。 :::

SPM 构建工具插件的实际应用是在 Xcode 的”运行构建工具插件”构建阶段执行代码检查。在包清单中定义如下:

5.9
import PackageDescription
let package = Package(
name: "Framework",
products: [
.library(name: "Framework", targets: ["Framework"]),
],
dependencies: [
.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", .upToNextMajor(from: "0.56.1")),
],
targets: [
.target(
name: "Framework",
plugins: [
.plugin(name: "SwiftLint", package: "SwiftLintPlugin"),
]
),
]
)

要生成一个包含构建工具插件的 Xcode 项目,你必须在项目清单的 packages 数组中声明包,然后在 target 的依赖中包含类型为 .plugin 的包。

import ProjectDescription
let project = Project(
name: "Framework",
packages: [
.remote(url: "https://github.com/SimplyDanny/SwiftLintPlugins", requirement: .upToNextMajor(from: "0.56.1")),
],
targets: [
.target(
name: "Framework",
dependencies: [
.package(product: "SwiftLintBuildToolPlugin", type: .plugin),
]
),
]
)

由于 Carthage 输出的是 frameworksxcframeworks,你可以运行 carthage update 将依赖输出到 Carthage/Build 目录,然后使用 .framework.xcframework target 依赖类型在 target 中声明依赖。你可以将其包装在一个脚本中,在生成项目之前运行。

#!/usr/bin/env bash
carthage update
tuist generate

::: warning 构建和测试 如果你通过 xcodebuild buildtuist test 构建和测试项目,同样需要确保通过运行 carthage update 命令来获取 Carthage 解析的依赖,然后再进行构建或测试。 :::

CocoaPods 需要一个 Xcode 项目来集成依赖。你可以使用 Tuist 生成项目,然后运行 pod install 来创建包含项目和 Pods 依赖的工作区,从而集成依赖。你可以将其包装在一个脚本中,在生成项目之前运行。

#!/usr/bin/env bash
tuist generate
pod install

::: warning CocoaPods 依赖与 buildtest 等工作流不兼容,这些工作流会在生成项目后立即运行 xcodebuild。它们还与二进制缓存和选择性测试不兼容,因为指纹逻辑没有考虑 Pods 依赖。 :::

框架和库可以静态链接或动态链接,这个选择对应用大小和启动时间有重要影响。尽管它很重要,但这个决定通常是在没有太多考虑的情况下做出的。

一般的经验法则是,你希望发布构建中尽可能多的内容静态链接以实现快速启动,而调试构建中尽可能多的内容动态链接以实现快速迭代。

在项目依赖图中切换静态和动态链接的挑战在于,这在 Xcode 中并不简单,因为一个更改会对整个依赖图产生级联影响(例如,库不能包含资源,静态框架不需要嵌入)。Apple 尝试用 Swift Package Manager 的自动静态和动态链接决策或可合并库等编译时解决方案来解决这个问题。然而,这会在编译图中添加新的动态变量,增加非确定性的新来源,并可能导致一些依赖编译图的功能(如 Swift 预览)变得不可靠。

幸运的是,Tuist 在概念上压缩了切换静态和动态链接的复杂性,并生成了标准化的 bundle 访问器,适用于各种链接类型。结合通过环境变量的动态配置,你可以在调用时传入链接类型,并在清单中使用该值来设置 target 的产品类型。

// 使用此函数返回的值来设置 target 的产品类型。
func productType() -> Product {
if case let .string(linking) = Environment.linking {
return linking == "static" ? .staticFramework : .framework
} else {
return .framework
}
}

请注意,Tuist 不会因为隐式配置的代价而默认使用便利设置。这意味着我们依赖你来设置链接类型以及有时需要的任何其他构建设置,比如 -ObjC 链接器标志,以确保生成的二进制文件是正确的。因此,我们采取的立场是为你提供资源,通常是文档形式,来做出正确的决定。

::: tip 示例:可组合架构 许多项目集成的 Swift 包是 The Composable Architecture。在本节中查看更多详细信息。 :::

在某些情况下,将链接完全设置为静态或动态不可行或不是一个好主意。以下是一个非详尽的场景列表,你可能需要混合使用静态和动态链接:

  • 带扩展的应用:由于应用及其扩展需要共享代码,你可能需要让这些 target 动态链接。否则,你最终会在应用和扩展中重复相同的代码,导致二进制文件大小增加。
  • 预编译的外部依赖:有时你会被提供预编译的二进制文件,它们是静态的或动态的。静态二进制文件可以包装在动态框架或库中以进行动态链接。

更改依赖图时,Tuist 会分析它并在检测到”静态副作用”时显示警告。此警告旨在帮助你识别通过动态 target 静态链接依赖传递依赖的 target 可能存在的问题。这些副作用通常表现为二进制文件大小增加,或者在最坏的情况下导致运行时崩溃。

Objective-C 依赖 {#objectivec-dependencies}

Section titled “Objective-C 依赖 {#objectivec-dependencies}”

集成 Objective-C 依赖时,可能需要在消费 target 上包含某些标志以避免运行时崩溃,详见 Apple 技术问答 QA1490

由于构建系统和 Tuist 无法推断该标志是否必要,而且该标志可能带有不良副作用,Tuist 不会自动应用这些标志,而且由于 Swift Package Manager 将 -ObjC 视为 .unsafeFlag 的一部分,大多数包在需要时无法将其包含在默认链接设置中。

Objective-C 依赖(或内部 Objective-C target)的使用者应在消费 target 上设置 OTHER_LDFLAGS 时应用 -ObjC-force_load 标志。

Firebase 和其他 Google 库 {#firebase-other-google-libraries}

Section titled “Firebase 和其他 Google 库 {#firebase-other-google-libraries}”

Google 的开源库虽然功能强大,但集成到 Tuist 中可能很困难,因为它们的构建方式通常使用非标准架构和技术。

以下是集成 Firebase 和 Apple 平台其他 Google 库可能需要遵循的一些提示:

确保将 -ObjC 添加到 OTHER_LDFLAGS {#ensure-objc-is-added-to-other_ldflags}

Section titled “确保将 -ObjC 添加到 OTHER_LDFLAGS {#ensure-objc-is-added-to-other_ldflags}”

许多 Google 库是用 Objective-C 编写的。因此,任何消费 target 都需要在 OTHER_LDFLAGS 构建设置中包含 -ObjC 标志。这可以在 .xcconfig 文件中设置,也可以在 Tuist 清单中 target 的设置中手动指定。示例:

Target.target(
...
settings: .settings(
base: ["OTHER_LDFLAGS": "$(inherited) -ObjC"]
)
...
)

有关更多详细信息,请参阅上面的 Objective-C 依赖 部分。

FBLPromises 的产品类型设置为动态框架 {#set-the-product-type-for-fblpromises-to-dynamic-framework}

Section titled “将 FBLPromises 的产品类型设置为动态框架 {#set-the-product-type-for-fblpromises-to-dynamic-framework}”

某些 Google 库依赖于 FBLPromises,这是 Google 的另一个库。你可能会遇到一个提到 FBLPromises 的崩溃,类似以下内容:

NSInvalidArgumentException. Reason: -[FBLPromise HTTPBody]: unrecognized selector sent to instance 0x600000cb2640.

Package.swift 文件中将 FBLPromises 的产品类型显式设置为 .framework 应该可以修复此问题:

5.10
import PackageDescription
#if TUIST
import ProjectDescription
import ProjectDescriptionHelpers
let packageSettings = PackageSettings(
productTypes: [
"FBLPromises": .framework,
]
)
#endif
let package = Package(
...

可组合架构 {#the-composable-architecture}

Section titled “可组合架构 {#the-composable-architecture}”

此处故障排除部分所述,当静态链接包时(这是 Tuist 的默认链接类型),你需要将 OTHER_LDFLAGS 构建设置设置为 $(inherited) -ObjC。或者,你可以将包的产品类型覆盖为动态的。

静态链接时,测试和应用 target 通常可以正常工作,但 SwiftUI 预览会损坏。这可以通过动态链接所有内容来解决。在下面的示例中,Sharing 也被添加为依赖,因为它通常与可组合架构一起使用,并且有自己的配置陷阱

以下配置将动态链接所有内容——因此应用 + 测试 target 和 SwiftUI 预览都能正常工作。

::: tip 静态还是动态 并非总是推荐动态链接。详见静态还是动态部分。在这个示例中,为简单起见,所有依赖都是有条件地动态链接的。 :::

6.0
import PackageDescription
#if TUIST
import enum ProjectDescription.Environment
import struct ProjectDescription.PackageSettings
let packageSettings = PackageSettings(
productTypes: [
"CasePaths": .framework,
"CasePathsCore": .framework,
"Clocks": .framework,
"CombineSchedulers": .framework,
"ComposableArchitecture": .framework,
"ConcurrencyExtras": .framework,
"CustomDump": .framework,
"Dependencies": .framework,
"DependenciesTestSupport": .framework,
"IdentifiedCollections": .framework,
"InternalCollectionsUtilities": .framework,
"IssueReporting": .framework,
"IssueReportingPackageSupport": .framework,
"IssueReportingTestSupport": .framework,
"OrderedCollections": .framework,
"Perception": .framework,
"PerceptionCore": .framework,
"Sharing": .framework,
"SnapshotTesting": .framework,
"SwiftNavigation": .framework,
"SwiftUINavigation": .framework,
"UIKitNavigation": .framework,
"XCTestDynamicOverlay": .framework
],
targetSettings: [
"ComposableArchitecture": .settings(base: [
"OTHER_SWIFT_FLAGS": ["-module-alias", "Sharing=SwiftSharing"]
]),
"Sharing": .settings(base: [
"PRODUCT_NAME": "SwiftSharing",
"OTHER_SWIFT_FLAGS": ["-module-alias", "Sharing=SwiftSharing"]
])
]
)
#endif

::: warning 你不能使用 import Sharing,而必须改为 import SwiftSharing。 :::

传递静态依赖通过 .swiftmodule 泄露 {#transitive-static-dependencies-leaking-through-swiftmodule}

Section titled “传递静态依赖通过 .swiftmodule 泄露 {#transitive-static-dependencies-leaking-through-swiftmodule}”

当动态框架或库通过 import StaticSwiftModule 依赖静态模块时,符号会被包含在动态框架或库的 .swiftmodule 中,可能导致编译失败。为防止这种情况,你必须使用internal import导入静态依赖:

internal import StaticModule

::: info 导入的访问级别包含在 Swift 6 中。如果你使用的是较旧版本的 Swift,你需要改用@_implementationOnly: :::

@_implementationOnly import StaticModule