乐趣区

关于ios:全面掌握-Swift-包依赖管理工具-命令行Manifest-APIXcode二进制包集合插件

Swift 包管理工具,即 Swift Manager Package,简称 SwiftPM,是 Swift 开源我的项目的一部分,提供了包依赖治理的性能。绝对于 CocoaPods、Carthage 等第三方管理工具,SwiftPM 是苹果本人研发,并和苹果平台和 Xcode 高度集成,能提供一些第三方工具无奈提供的能力。SwiftPM 从 2018 年开始 release,苹果也在一直地为其增加越来越多的性能,目前曾经能够反对 Swift、Objective-C、C++、C 的混编。因为 SwiftPM 的不便易用性,目前有取代其它第三方管理工具之势,越来越多的新我的项目都开始用 SwiftPM 来组织工程机构,治理依赖。

包是什么?

包(Package)是一个代码和资源的汇合,代码能够是源代码,也能够是二进制。包的次要作用就是散发和共享代码和资源,包能够供其它程序调用,有点相似库,包本人自身也能够作为一个可执行程序。绝对于库,包提供了一个比拟高层的形象,能够基于包的概念做很多配置,比方依赖、包类型、编译产物等。

在命令行中应用 SwiftPM

当装置好 Xcode 命令行工具后,SwiftPM 也就能够应用了,能够用 SwiftPM 的命令行工具来对包进行各种操作。

新建包

以下命令能够新建一个包:

mkdir Hello
cd Hello
swift package init

在 swift package init 命令之后如果不指定 –type 参数,默认会创立一个库类型的包。能够通过 –type 来创立其它的包。

swift package init --type executable

以上会创立一个可执行的包,可执行的包就是蕴含 main 函数的包,它能够被操作系统加载并执行。

默认状况下,包名和文件夹的名字一样,如果你心愿指定一个别的包的名称,能够加上 –name 参数:

swift package init --name < 包名 >

创立好的包目录构造如下:

├── Package.swift  
├── README.md  
├── Sources  
│   └── Hello  
│       └── Hello.swift  
└── Tests  
    ├── HelloTests  
    │   └── HelloTests.swift  
    └── LinuxMain.swift 

默认的构造规定 Sources 目录中能够有一个或多个子目录,每个子目录代表一个 target。

编译包

通过 swift build 命令能够将包编译为二进制。

执行包

如果包是一个可执行包,能够通过 swift run 命令来执行包。

执行单元测试

通过 swift test 命令能够执行包中的单元测试。

通过 --parallel 参数能够并行执行单元测试,放慢单元测试的执行速度。

通过 --filter 参数能够指定执行一部分单元测试。

swift test --parallel --filter ByteBufferTest

以上这些命令都是调用 libSwiftPM 这个库,libSwiftPM 是 Swift 开源我的项目的一部分,它提供了 Swift 包的外围能力。

包的 Manifest API(Package.swift)

下面的命令只是做了一些对于包的基本操作,那么如何指定包里具体有什么信息呢,就是通过 Manifest API,其实就是包中的一个 Package.swift 文件。

Package.swift 自身也是一个 Swift 源代码文件,它遵循 Swift 的语法,通过导入 PackageDescription 模块,并通过结构一个 Package 对象的形式,对包做了各种配置,包含生成产物、指标、依赖、环境等。因为其自身也是用 Swift 语言来写,对把握了 Swift 的开发者十分敌对,浏览和编写都十分不便。

上面看一个根本的 Package.swift

// swift-tools-version: 5.7

import PackageDescription

let package = Package(
    name: "BlogDemoPackage",
    products: [
        .library(
            name: "BlogDemoPackage",
            targets: ["BlogDemoPackage"]),
    ],
    dependencies: [ ],
    targets: [
        .target(
            name: "BlogDemoPackage",
            dependencies: []),
        .testTarget(
            name: "BlogDemoPackageTests",
            dependencies: ["BlogDemoPackage"]),
    ]
)

最上方的是一行正文,它指定了能够编译这个包的最低 Swift 版本。

// swift-tools-version: 5.7

而后是一个导入语句,导入了这个 Manifest API 依赖的模块,能够看到这些 API 是通过 PackageDescription 这个模块提供的。

import PackageDescription

接下来是一个赋值语句:

let package = Package(
    name: "BlogDemoPackage",
    ...
)

这条语句创立了一个 Package 类型的对象赋值给 package 常量,name 参数指定了包的名称。这是 Manifest 的规范写法,只有这一条语句就能够实现包的所有配置。接下来咱们一个一个的看一下别的参数,比拟重要的包含 products、dependencies 和 targets。

products

products: [
    // 包提供了一个库产品
    .library(
        name: "BlogDemoPackage", // 指定库的名字
        type: .dynamic, // 指定库是动态库还是动静库,默认是动态库
        targets: ["BlogDemoPackage"]), // 指定库中蕴含了哪些 targets 的产物
],

products 参数定义了包的产物,它要求传入一个元素为 PackageDescription.Product 类型的数组。能够作为 Product 的有库、可执行程序和插件。Product 提供了若干个类办法来便捷地创立这些 Product 对象。

如下面的例子所示,应用 .library 这个 Product 类中的类办法来定义一个库产物,库蕴含了能够被其它代码导入的模块。name 指定了库的名称,type 是可选的,默认为 static,示意动态库,也可指定为 dynamic 动静库。targets 指定了要把哪些 target 的产物打包到库里。

一个包能够有多个 product,比方输入两个库产品,一个输入 Swift 库另一个输入纯 C 库。或者既能产生库也能产生可执行程序。

dependencies

dependencies: [.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.1"))
],

dependencies 指定了这个包依赖哪些其它包。每一个依赖包通过 .package 来执行,须要传入一个 url 参数,代表 git 地址,和一个 Range<Version> 类型的要求版本参数。

如果想应用一个第三方库,能够去 GitHub 上把库的 git 地址复制粘贴过去,再通过第二个参数制订版本就能够了,十分不便。

对于版本号,Swift 包严格遵守语义版本标准(Semantic Versioning)。.package 办法的第二个参数能够有以下几种取值形式:

  • upToNextMajor 代表大版本号不变,能够尽可能更新到最新版本。例如 .upToNextMajor(from: “5.6.1”) 示意应用 5.6.1 开始到 5.xx.xx 任何最大数字版本,然而不蕴含 6.0.0。通常这是首选选项。
  • upToNextMinor 代表大版本号和两头版本号放弃不变,小版本号能够尽可能更新。例如 .upToNextMinor(from: “5.6.1”) 示意应用 5.6.1 开始到 5.6.x 的任何最大数字版本,然而不蕴含 5.7.0。如果是对这个库的应用比拟激进,能够应用此项。
  • from 则示意从指定版本开始当前的任何版本都能够应用。
  • exact 则制订某个确定的版本,不会更新,在一些非凡的状况下会用到。

另外也能够通过 branch 来指定依赖某个分支,或通过 revision 来指定依赖某个具体的 commit,这两种只能在开发时应用,无奈蕴含在公开公布的包中。

targets

targets: [
    .target(
        name: "BlogDemoPackage",
        dependencies: []),
    .testTarget(
        name: "BlogDemoPackageTests",
        dependencies: ["BlogDemoPackage"]),
]

target 是包中最根本的编译单元,每个 target 都会生成一个独立的编译产物。每个 target 都是一个模块或者单元测试套件。target 能够依赖所在包中的其它 target,也能够依赖所在包依赖的其它包中的 target。

有以下几种类型的 target:

  • .target 一般 target
  • .testTarget 单元测试
  • .executableTarget 可执行程序
  • .binaryTarget 二进制 target
  • .plugin 包插件

二进制包

头三个都曾经很分明了,.binaryTarget 代表这个 target 的产物是一个二进制,从 Xcode 11 开始苹果引入了 XCFramework,让 framework 能够已编译好的二进制模式提供,并反对多种架构。.binaryTarget 的用法如下:

targets: [
    .binaryTarget(
        name: "Emoji",
        url: "https://example.com/Emoji/Emoji-1.0.0.xcframework.zip",
        checksum: "6d988a1a27418674b4d7c31732f6d60e60734ceb11a0ce9b54d1871918d9c194"
    )
]

name 指定了 target 的名字,url 指定了 xcframework 文件的门路,checksum 是一个校验和,SwiftPM 会在下载 xcframework 后通过校验和来校验文件是否被篡改。

.plugin 代表这个 target 的产物是一个插件能够给 Xcode 工程或者其它包应用,对于包插件上面会有大节独自阐明。

多平台反对

在 Package.swift 中能够通过 platforms 参数来配置以后的包反对哪些平台和平台版本。

platforms: [.macOS(.v10_15), iOS(.v13),
],

如果以后的工具链不反对某个版本的 API,也能够应用基于字符串的 API

.macOS("10.15"), iOS("13")

在代码中如果须要解决一些平台特定工作,能够利用 Swift 的条件编译:

#if os(Linux)
// 这里的代码只会在 Linux 上运行
#endif

#if canImport(Network)
// 这里的代码只会在 Network 模块可用时运行
#endif

在 Xcode 中应用包

从 Xcode 11 开始,Xcode 集成了一系列和包无关的 GUI 工具,能够不便的应用 Xcode 对包进行各种操作,这些 GUI 工具也是通过调用 libSwiftPM 来实现性能。

应用 SwiftPM 来导入第三方库

在一个现有的 App 我的项目中,能够通过点击 File -> Add Packages... 来增加依赖包。随后会关上一个对话框,将包的 git 地址粘贴到右上角的搜寻框中,对话框会显示出包的信息,能够在左边配置要求的版本,和增加到哪个工程,最初点右下角的 Add Package 按钮即可实现增加。

增加包后,Xcode 会进行包的解析,解析胜利后右边的导航栏目录树下方会呈现一个 Swift Dependencies 区域,外面列出了所有依赖的包。

创立本地包

点击 Xcode 的 File -> New -> Package… 能够创立一个包,而后用 Xcode 编辑、编译、执行这个包或执行单元测试。

本地磁盘上的一个包,能够双击 Package.swift 文件,Xcode 会辨认到这是一个包,而后关上整个包目录构造,十分不便。

如果想要公布包到 GitHub,能够在 Xcode 上登录 GitHub 账号,通过 Xcode 间接主动在 GitHub 下仓库并把包的内容提交下来。

通过本地包来治理工程构造

得益于和 Xcode 的高度集成和不便的配置,当初越来越多的工程会抉择用本地包来治理我的项目构造。

通过本地包来将工程分成多个模块,因为能够通过 Package.swift 进行丰盛的配置,比以前应用 target 或者应用多工程要好用的多。

比方须要开发一个 UI 无关的业务模块,在一个关上的工程中,点击 File -> New -> Package… 新建一个新的包增加到工程中。用本地包来分模块成果如下图:

编辑和调试近程包

咱们会把本人的包放到 GitHub 上供大家分享,而后通过包依赖导入到咱们的我的项目中。因为 Xcode 会主动治理依赖包,并锁定了这些包的文件,这些包无奈被间接编辑。然而咱们须要去开发和调试这些包。或者是某个第三方库可能有问题,须要批改代码并调试一下,这时能够通过本地包笼罩的形式来解决。

在工程中如果存在同名的本地包和近程包,Xcode 会优先应用本地包,这时近程包就被忽略了。

Xcode 怎么治理和更新依赖包的版本

当工程配置了依赖包后,Xcode 会对依赖图进行解析,下载并配置所需依赖包,并决定一个适合的版本。如果通过给定的依赖包的要求版本信息,找不到一个能满足所有依赖需要的版本,Xcode 在解析依赖包过程会报告一个谬误,能够通过这个谬误来查看时哪些包的版本产生了抵触,从而解决。

当依赖包解析胜利后,Xcode 会在 project.xcworkspace/xcshareddata/swiftpm/Package.resolved 这个地位生成一个 Package.resolved 文件,文件中蕴含了以后应用的所有依赖包和应用的依赖包版本的信息,这个文件能够提交到 git 仓库中共团队成员共享,这样所有团队成员都会有统一的依赖包版本。

Package.resolved 由 Xcode 主动治理,不倡议手动批改。

这个机制和 CocoaPods 的 Podfile.lock,和 Carthage 的 Cartfile.resolved 理念是一样的。

包汇合

从 Swift 5.5 开始,SwiftPM 反对了包汇合,就是能够将多个包打包成一个汇合公布。通过汇合能够有机会让包有更多的曝光机会,不便开发者找到适合的包,也能够不便教学者或布道者进行展现。

在 Xcode 中点击菜单 File -> Add Package…,在关上的对话框左侧,就是汇合的列表,外面默认蕴含了一个苹果提供的汇合名叫 Apple Swift Package,这个汇合中蕴含了 swift-algorithms,swift-nio 等一系列包。能够点击左下角的加号按钮,抉择 Add Package Collection 而后将某个汇合的 json 文件地址粘贴进来,就能够增加汇合。

苹果提供了一个工具 Swift Package Collection Generator 能够把多个包打包成一个汇合,只有输出一个以下格局的 JSON 文件:

{
  "name": "WWDC21 Demo Collection",
  "overview": "Packages to be used in our demo app",
  "keywords": ["wwdc21"],
  "author": {"name": "Boris Buegling"}
  "packages": [{ "url": "https://github.com/apple/swift-format"},
    {"url": "https://github.com/Alamofire/Alamofire"}
  ],
}

而后通过 package-collection-generate 工具,以及签名工具,能够生成一个 输入文件,Xcode 能够读取这个文件来加载汇合和汇合中的包。命令如下:

// 生成输入文件
package-collection-generate input.json collection.json
// 对输入文件进行签名
package-collection-sign collection.json collection-signed.json developer-key.pem developer-cert.cer

流程如下图:

Swift 包插件

从 Xcode 14 和 Swift 5.7 开始,Swift 包反对了一个新的产品:插件。插件是一个 Swift 脚本,能够运行在 Swift 包或 Xcode 工程上,插件能够用来简化和改良开发流程,做一些例如代码查看,生成源代码,自动化 release 工作等工作。

插件也是以 Swift 包的模式实现的,一个包能够蕴含库和可执行的同时蕴含插件,也能够只蕴含插件。插件的代码不会被带入到生产环境,只能在开发时执行。

插件能够扩大 Swift 包管理器的性能。和 Manifest API 相似,苹果也提供了一个 PackagePlugin 模块,蕴含可供插件调用的 API。

SwiftPM 目前反对以下两种类型的插件:

  • 构建工具插件(BuildToolPlugin),提供了在编译之前或者在编译中执行的命令。构建工具插件能够接管一些输出文件并产出一个或多个输入文件。
  • 命令插件(CommandPlugin),能够通过 Xcode 来执行,也能够通过 swift package 命令来执行。命令插件能够执行一些自定义的 Swift 脚本,也能够应用 Process 类来执行别的脚本和系统命令。

这两种插件的次要区别就是构建工具插件是在构建过程中主动执行了的,命令插件须要人为地被动触发。

每个插件都作为一个独立的过程运行,它被封装在一个沙盒中,阻止了插件进行网络申请或者写入文件系统的任意地位。

WWDC22 上通过一下两个视频介绍了插件和应用。

  • Meet Swift Package Plugins
  • Create Swift Package Plugins

构建插件

构建插件是在构建过程中主动执行的插件,蕴含构建前执行和构建中执行两种,构建前执行的插件会在构建开始时执行,能够生成任意数量的名字不可在预测输入文件。

构建中插件就会被并入到构建零碎的依赖图中,构建零碎会依据预约义的输入输出以及工夫戳来抉择在适合的机会执行插件。

两种构建插件的次要区别在于他们能够产生的输入不同:

  • 构建前插件必须被用在输入是不可预测的状况。
  • 构建中插件须要指定一组输入文件。

构建插件以包的模式提供,包的 Package.swift 如下:

// swift-tools-version: 5.7

import PackageDescription

let package = Package(
    name: "SwiftLintPlugin",
    platforms: [.iOS(.v16), .macOS(.v10_13)],
    products: [
        .plugin(
            name: "SwiftLintPlugin",
            targets: ["SwiftLintPlugin"]
        )
    ],
    targets: [
        .plugin(
            name: "SwiftLintPlugin",
            capability: .buildTool())
    ]
)

targets 和 products 中都有 .plugin 这个配置,示意存在一个 target 产物为一个插件,包的产品蕴含了这个 target 的插件产物。

在 targets 的配置中,通过 .plugin 配置一个插件产物,通过 name 指定插件的名称,通过 capability 指定为 .buildTool() 代表这是一个构建插件。

别的包想应用这个插件能够通过以下 Package.swift 配置:

let package = Package(
    name: "my-plugin-example",
    dependencies: [.package(url: "https://github.com/example/my-plugin-package.git", from: "1.0")
    ],
    targets: [
        .executableTarget(
            name: "MyExample",
            plugins: [.plugin(name: "MyBuildToolPlugin", package: "my-plugin-package")
            ]
        )
    ]
)

首先要依赖这个插件所在的包,而后在 target 中配置 plugins 参数并制订具体包中的插件,这样每次在包构建的时候,这个构建插件就会起作用。

编写插件的实现代码,须要在插件所在包的目录下新建一个 Plugins 目录,在 Plugins 目录中创立一个子目录,目录名字为插件的名称,而后在这个目录中放一个 plugin.swift 文件作为插件的入口点,目录构造如下:

在 plugin.swift 中,须要导入 PackagePlugin 模块,创立一个合乎 BuildToolPlugin 协定的构造体,并标记为 @main

import Foundation
import PackagePlugin

@main
struct MyPlugin: BuildToolPlugin {func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {// 插件开始代码}
}

命令插件

命令插件必须人为触发,能够通过在命令行执行上述命令触发,也能够通过 Xcode GUI 菜单触发,当工程中蕴含了含有命令插件的包,Xcode 胜利解析后,会在工程的鼠标右键上下文菜单中增加一个插件的选项,能够通过点击这个选项来执行插件。

命令插件的 Package.swift 定义如下:

// swift-tools-version: 5.7

import PackageDescription

let package = Package(
    name: "CodeGeneratorPlugin",
    platforms: [.iOS(.v16)],
    products: [
        .plugin(
            name: "CodeGenerator",
            targets: ["CodeGenerator"]
        )
    ],
    dependencies: [],
    targets: [
        .plugin(
            name: "CodeGenerator",
            capability: .command(
                intent: .custom(
                    verb: "generate-code",
                    description: "Generates code"
                ),
                permissions: [.writeToPackageDirectory(reason: "Generate Code")
                ]
            )
        )
    ]
)

和构建插件不同的中央在于 targets 中的 .plugin 的 capability 参数指定了这个插件是一个命令插件,命令插件须要 intent 和 permissions 两个参数,intent 代表插件存在的起因,结构 intent 须要一个 verb 和一个 description,这里的 vert 指定了这个命令插件在命令行中被调用时的名字,description 则是一个人类可读的形容。能够通过以下命令来执行插件:

swift package plugin <verb> [args...]

命令插件的入口点必须是一个合乎了 CommandPlugin 协定的构造体,并实现了协定中的 performCommand 办法,插件的具体执行代码就是从 performCommand 办法开始运行。如下所示:

import Foundation
import PackagePlugin

@main
struct CodeGenerator: CommandPlugin {func performCommand(context: PluginContext, arguments: [String]) async throws {// ...}
}

列出以后可应用的所有插件

swift package plugin --list

总结

本文顺次介绍了包的概念、包管理工具在命令行的应用、Manifest API、包管理工具在 Xcode 中的应用,包汇合、插件等内容。因为 SwiftPM 的个性,他正变得越来越风行,咱们在新我的项目中会优先采纳 SwiftPM 作为包管理工具。如果在应用 SwiftPM 时遇到什么问题,欢送留言探讨。

参考资料

  • https://developer.apple.com/w…
  • https://developer.apple.com/w…
  • https://developer.apple.com/w…
  • https://developer.apple.com/w…
  • https://developer.apple.com/w…
退出移动版