https://juejin.im/post/6844904019815579661 2019年12月13日
https://swift.org/package-manager/
SPM是苹果官方推出的包管理工具。从Xcode11开始,有对SPM的插件加成。 这个包管理工具包含在swift3或者更高版本中。
SwiftPM 是什么
Swift Package Manager 是一个苹果官方出的管理源代码分发的工具,目的是更简单的使用别人共享的代码。它会直接处理包之间的依赖管理、版本控制、编译和链接。从总体功能上来说,和 iOS 平台上的 Cocoapods、Carthage 一样。
一开始 SwiftPM 只支持在 MacOS 和 Linux 平台使用,大多是一些 Vapor、Perfect 之类的后端服务或者脚本在用,所以对于 iOS 开发者来说会比较陌生。Xcode 11 开始自集成了 libSwiftPM
,这样一来,iOS、watchOS、tvOS 等平台也都可以使用了,不过由于是集成的依赖库,所以使用方法有些不太一样,下面会另说。文中不说哪个平台的使用默认是指 MacOS 和 Linux 平台。
概念
Modules
Swift将代码组织到模块中。每个模块都指定一个名称空间,并强制执行访问控制,以便可以在模块外部访问使用。
程序可以将所有代码都放在一个模块中,也可以将其他模块作为依赖项导入。除了少数系统提供的模块外,例如macOS上的Darwin或Linux上的Glibc,大多数依赖项都需要下载并构建代码才能使用。
当您为解决特定问题的代码使用单独的模块时,该代码可以在其他情况下复用。使用模块,您可以在其他开发者的代码的基础上构建代码,而不必自己从头开始实现。
Packages
一个Package由Swift源文件和清单(manifest)文件组成。清单(manifest)文件称为Package.swift,它使用PackageDescription
模块定义了包的名称及其内容。
每个Package中有一个或多个targets。每个targets都指定一个product,并可以声明一个或多个依赖项。
import PackageDescription
let package = Package(
name: "dealer",
products: [
.executable(name: "Dealer", targets: ["Dealer"]),
],
dependencies: [
.package(url: "https://github.com/apple/example-package-deckofplayingcards.git", from: "3.0.0"),
],
targets: [
.target(
name: "Dealer",
dependencies: ["DeckOfPlayingCards"]),
]
)
Products
一个target可以构建library或可执行文件作为其Product。一个library包含一个可以被其他Swift代码导入的模块。可执行文件是可以由操作系统运行的程序。
Dependencies
target的依赖项,依赖是一个递归的过程。
SwiftPM 的创建和使用
SwiftPM 管理的每个 Package
相当于 Xcode.Project,并且有具体的代码定义,包的目录下必须含有 Package.swift
和 Sources
代码文件夹(链接系统的包除外)。Package.swift
是整个 Package 的配置项,类似 Cocoapods 中 .podspec
和 .podfile
的集合体。下面介绍下 SwiftPM 的简单创建和使用。
创建一个可执行的包
执行以下命令
➜ mkdir MyPackage
➜ cd MyPackage
➜ swift package init --type executable
➜ swift build
➜ swift run
Hello, World!
这里在创建时使用了 --type
参数,通过执行 help
命令可得知,一共有四个类型
--type empty|library|executable|system-module
分别是 空包、静态包、可执行包、系统包,默认不加参数时创建的是 library
类型,主要区别如下:
- 空包:Source 文件夹下什么都没有,也不能编译
- 静态包:Source 文件夹下有个和包同名 swift 文件,里面有个空结构体
- 可执行包:Source 文件夹下有个
main.swift
文件,在 build 之后会在 .build/debug/ 目录下生成一个可执行文件,可通过 swift run
或者直接点击运行,从而启动一个进程
- 系统包:这种包是专门为了链接系统库(例如
libgit
、jpeglib
、mysql
这种系统库)准备的,本身不需要任何代码,所以也没有 Source 文件夹,但是需要编辑 module.modulemap
文件去查找系统库路径 (Swift 4.2 已经被其他方式取代)
这里的几个类型只是根据标志性文件定义,比如静态包默认是不能编译的,但是加了 main.swift
之后,就变成可执行包了。
添加依赖
如果需要依赖其他的包, 需要在 Package.swift
定义依赖项和版本,像下面这样:
// swift-tools-version:4.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "WYMobileWebsite",
dependencies: [
.package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.0"),
.package(url:"https://github.com/PerfectlySoft/Perfect-MySQL.git", from: "3.0.0"),
.package(url:"https://github.com/PerfectlySoft/Perfect-Session.git", from: "3.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "WYMobileWebsite",
dependencies: ["PerfectHTTPServer","PerfectMySQL","PerfectSession"]),
.testTarget(
name: "WYMobileWebsiteTests",
dependencies: ["WYMobileWebsite"]),
]
)
需要注意的是 最上面那行注释不要删,它是用来查找对应的 swift-tool-version 版本的。
在 Package.swift
配置之后,再执行下 Swift build
之后就会生成 .build 文件夹, Source
文件夹的代码里也就可以 import Perfect
了
添加系统依赖包
编写 MacOS/Linux 程序时经常会遇到依赖本地系统库的情况,在官方文档上有很长一段文字描述如何设置系统库依赖的,需要建立一个专门用来链接系统库的包,但是在 Swift 4.2 之后已经被替代了(但是老的写法暂时也还能用)。比较有趣的是,这个更改已经在 2018 年 9 月 的 提交 就已经更新到 github 了,代码实现已经合并到 master,但是文档没更新到 master.readme,对之前写法感兴趣的可以去看看这个 [链接](warning: system packages are deprecated; use system library targets instead)。下面主要介绍新的写法。
下载依赖的 C 库到本地
如果要添加系统依赖库,首先得本地有系统库,以 Cairo
这个 2D 图形库为例(官方文档是用这个做例子的,我就照抄过来了,而且这个库不需要什么环境变量),先通过 gem 或者 homebrew 下载库到本地,以 homebrew 为例
brew install cairo
下载完成后在 /usr/local/lib/pkgconfig/
文件夹下就能找到 cairo.pc
文件,这个配置文件用于找寻系统库的 lib 和 header 文件。有的库通过 homebrew 下载后并不会自动添加到 /usr/local/lib/pkgconfig/
这个地址(比如 mysql 这个 zz),需要通过 ln- s
命令把具体目录地址的文件复制一份放在这里。
创建一个可执行的包作为主包
➜ mkdir example
➜ cd example
➜ swift package init --type executable
创建系统依赖文件并编辑
cd Sources
mkdir cairo
cd cairo
touch cairo.h
touch module.modulemap
此时文件目录变成这样:
.
├── Package.swift
├── README.md
├── Sources
│ ├── cairo
│ │ ├── cairo.h
│ │ └── module.modulemap
│ └── example
│ └── main.swift
└── Tests
├── LinuxMain.swift
└── exampleTests
├── XCTestManifests.swift
└── exampleTests.swift
在 cairo.h
中添加这行代码:#include <cairo.h>
,在 module.modulemap
中添加下面这行代码
module Foo {
umbrella header "cairo.h"
link "cairo"
}
这里 Foo
这个 moduleName 我们想怎么写都行,等下在 Package.swift
要写的也是这个名字。第一行标明我们头文件名,就是我们刚才创建的 cairo.h
文件,第二行的 link
是系统库的编译链接标记,给 Clang 用的,不能随意更改。
添加系统依赖进可执行包里
编辑 Package.swift
文件,添加 Foo
进去:
编辑 main.swift
:
import Foo
let surface: OpaquePointer = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 120, 120)
print(surface)
然后编译项目(正常情况下不会报错):
➜ example swift build
[2/2] Linking ./.build/x86_64-apple-macosx/debug/example
➜ example swift run
0x00007f8dfbd007c0
➜ example
module.modulemap
文件中可以添加多个 module
,所以可以比较方便的添加多个系统库。还有些系统库需要注意的点例如系统库的版本号之类的可以查看 github 上的 说明。
采用 Xcode 运行
我们编写代码可以通过 VSCode、Atom 加入各种插件甚至 txt 文本编辑器写,但是肯定还是 Xcode 用起来比较方便,默认 swift build 是不会生成 packageName.xcodeproj
这种 Xcode 可以直接打开的工程文件,但是可以通过 swift package generate-xcodeproj
命令行生成一个 .xcodeproj
文件,然后就可以通过 Xcode 运行该项目了,如果需要配置什么环境变量,则需要通过 Build Setting
中的选项配。
需要注意的一点事,通过 swift run 和通过 Xcode 启动的是不同的进程,两种方式生成的可执行文件并不是同一个,所以如果需要把可执行文件更新到其他地方的时候注意别弄错了。
解析 Package
Package
类一般用在 Package.swift
里来表标记当前使用的包。Package
类里有很多属性,像 pkgConfig
、providers
这些都是上文说到的 Swift 4.2 之前 System package
所用到的。Swift 5.0 后基本也不会用到了,下面只介绍些平常可能会用到的属性。
这个 Struct
用于设置包的最小依赖平台版本,具体 API 定义可以进入代码文档中查看,下面给出示例:
let package = Package(
name: "example",
platforms: [.macOS(.v10_10)],
dependencies: [
.package(url:"https://github.com/Alamofire/Alamofire.git", .branch("master"))
],
targets: [
.systemLibrary(name: "Foo", path: "Sources/cairo", pkgConfig: "cairo"),
.target(
name: "example",
dependencies: ["Foo", "Alamofire"]),
.testTarget(
name: "exampleTests",
dependencies: ["example"]),
]
)
需要注意的是虽然这个属性是个数组,但是目的是为了让设置不同平台的最小依赖,如果设置了多个同平台的值进去,就会报错,例如这样:[.macOS(.v10_10), .macOS(.v10_11)],
error: manifest parse error(s):found multiple declaration for the platform: macos
Package.Product
Product 是 Package 编译后对外的产品输出,一般可分为两种类型:
具体 Package.swift
配置如下
import PackageDescription
let package = Package(
name: "Paper",
products: [
.executable(name: "tool", targets: ["tool"]),
.library(name: "Paper", targets: ["Paper"]),
.library(name: "PaperStatic", type: .static, targets: ["Paper"]),
.library(name: "PaperDynamic", type: .dynamic, targets: ["Paper"]),
],
dependencies: [
],
targets: [
.target(name: "Paper", dependencies: []),
.target(name: "tool", dependencies: ["Paper"]),
]
)
当执行完 Swift build
之后,就会在 .build/debug
下生成对应的可执行文件 tool
和静态库 libPaperStatic.a
、动态库 libPaperDynamic.dylib
。
再来看下 Product
这个类的构造(去除了 暂时不关心的 encode
等方法):
/// Defines a product in the package.
public class Product : Encodable {
/// Represents an executable product.
final public class Executable : PackageDescription.Product {
/// The names of the targets in this product.
public let targets: [String]
}
/// Represents a library product.
final public class Library : PackageDescription.Product {
/// The type of library product.
public enum LibraryType : String, Encodable {
case `static`
case dynamic
}
/// The names of the targets in this product.
public let targets: [String]
/// The type of the library.
///
/// If the type is unspecified, package manager will automatically choose a type.
public let type: PackageDescription.Product.Library.LibraryType?
}
/// Create a library product.
public static func library(name: String, type: PackageDescription.Product.Library.LibraryType? = nil, targets: [String]) -> PackageDescription.Product
/// Create an executable product.
public static func executable(name: String, targets: [String]) -> PackageDescription.Product
}
注意下 Library.type
属性,看注释文档上写的是如果没写类型的话,就会自动选一个。苹果推荐如果当前库支持打成静态库或者动态库的任意一种的话,就不要指定类型,让 SwiftPM 在最终编译产品时根据编译设置自动选择静态库或者动态库。
Package.Dependencies
Package.dependencies
用于添加包的依赖,一般是包括指向包源的 git
路径和版本环境,或指向依赖包的本地路径(至于使用 SVN 路径这种方式没接触过,可能需要自定义一些东西)。
在执行 Swift build
时会自动执行一个 swift package resolve
命令,该命令会解析 Package.swift
的依赖,并生成对应的 package.resolved
文件,下面有介绍。
先简单看下 Dependency
的 API
public static func package(url: String, from version: PackageDescription.Version) -> PackageDescription.Package.Dependency
public static func package(url: String, _ requirement: PackageDescription.Package.Dependency.Requirement) -> PackageDescription.Package.Dependency
public static func package(url: String, _ range: Range<PackageDescription.Version>) -> PackageDescription.Package.Dependency
public static func package(url: String, _ range: ClosedRange<PackageDescription.Version>) -> PackageDescription.Package.Dependency
/// Add a dependency to a local package on the filesystem.
public static func package(path: String) -> PackageDescription.Package.Dependency
先不用一个一个方法看,现在主要说下第二个方法
public static func package(url: String, _ requirement: PackageDescription.Package.Dependency.Requirement) -> PackageDescription.Package.Dependency
先说它是因为其他几个方法都是这个方法抽象出来的简便创建方式。
对于 Version
这个结构体,它实现了 ExpressibleByStringLiteral
,所以可以直接用字符串字面量表示,同时也实现了 Comparable
协议,所以可以直接用 <
比较大小,用 ..<
和 ...
表示范围。
看下 Package.Dependency.Requirement
public enum Requirement {
case _exactItem(PackageDescription.Version)
case _rangeItem(Range<PackageDescription.Version>)
case _revisionItem(String)
case _branchItem(String)
case _localPackageItem
}
extension Package.Dependency.Requirement : Encodable {
public static func exact(_ version: PackageDescription.Version) -> PackageDescription.Package.Dependency.Requirement
public static func revision(_ ref: String) -> PackageDescription.Package.Dependency.Requirement
public static func branch(_ name: String) -> PackageDescription.Package.Dependency.Requirement
public static func upToNextMajor(from version: PackageDescription.Version) -> PackageDescription.Package.Dependency.Requirement
public static func upToNextMinor(from version: PackageDescription.Version) -> PackageDescription.Package.Dependency.Requirement
}
直接从枚举的定义中就可以看出 Package.dependencies
支持如下五种方式:
- git 源 + 确定的版本号
- git 源 + 版本区间
- git 源 + Commit 号
- git 源 + 分支名
- 本地路径
对于 extension
里的几个方法也只是一个便捷函数,方法注释里已经有相应的使用示例,upToNextMajor
表示从某个版本到下个主要版本之前,比如 .upToNextMajor(from: "1.2.3")
表示 SwiftPM 选择的版本可能是 1.2.3
或者 1.3.5
,但是不会选择 2.0.0
以及以上版本。upToNextMinor
指的是次要版本,也就是第二位数字,这两个方法也都会转为 _rangeItem(Range<PackageDescription.Version>)
这个枚举值。
再回头看下之前说的先不用管的 Package.Dependency
的几个 API
,它们内部也是转化成这个 Requirement
这个枚举的(不信的自己去看源码),
-
public static func package(url: String, from version: PackageDescription.Version) -> PackageDescription.Package.Dependency
这个方法是通过 upToNextMajor
转换成 Requirement.
_rangeItem,这就意味着这个方法会选择某个版本号到下个主版本号之前的版本
-
public static func package(url: String, _ range: Range<PackageDescription.Version>) -> PackageDescription.Package.Dependency
这个方法是直接转换成 Requirement.
_rangeItem,所以它可是无视主版本号、次版本号控制这种隐形规则的
-
public static func package(path: String) -> PackageDescription.Package.Dependency
这个方法是转换成 Requirement._localPackageItem
,
看完了上面的介绍,平常的使用方式如下所示:
.package(url: "https://github.com/Alamofire/Alamofire.git", .exact("1.2.3")),
.package(url:"https://github.com/Alamofire/Alamofire.git", .branch("master")),
.package(url:"https://github.com/Alamofire/Alamofire.git", from: "1.2.3"),
.package(url: "https://github.com/Alamofire/Alamofire.git", .revision("e74b07278b926c9ec6f9643455ea00d1ce04a021"),
.package(url: "https://github.com/Alamofire/Alamofire.git", "1.2.3"..."4.1.3"),
.package(path: "../Foo"),
Package.Target
target
是 Package 的基本构件,和 xcodeproject
一样,Package 可以有多个 target
。
target
分为三种类型:常规型、测试类型、系统库类型。分别对应下面几个快捷创建方式:
public enum TargetType : String, Encodable {
case regular
case test
case system
}
public static func target(name: String, dependencies: [PackageDescription.Target.Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, publicHeadersPath: String? = nil, cSettings: [PackageDescription.CSetting]? = nil, cxxSettings: [PackageDescription.CXXSetting]? = nil, swiftSettings: [PackageDescription.SwiftSetting]? = nil, linkerSettings: [PackageDescription.LinkerSetting]? = nil) -> PackageDescription.Target
public static func testTarget(name: String, dependencies: [PackageDescription.Target.Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, cSettings: [PackageDescription.CSetting]? = nil, cxxSettings: [PackageDescription.CXXSetting]? = nil, swiftSettings: [PackageDescription.SwiftSetting]? = nil, linkerSettings: [PackageDescription.LinkerSetting]? = nil) -> PackageDescription.Target
public static func systemLibrary(name: String, path: String? = nil, pkgConfig: String? = nil, providers: [PackageDescription.SystemPackageProvider]? = nil) -> PackageDescription.Target=
先介绍下 Target
的几个主要的属性
- name:名字
- dependencies:依赖项,注意不要和上面的
Package.Dependency
搞混了,不是一个东西,这里可以依赖上面 Package.Dependency
的东西或者依赖另一个 target
。所以这里只需要写 Package 或者 Target 的名字字符串(Target.Dependency
这个枚举也实现了 ExpressibleByStringLiteral
)。
- path:
target
的路径,默认的话是 [PackageRoot]/Sources/[TargetName]
。
- source:源文件路径,默认
TargetName
文件夹下都是源代码文件,会递归搜索
- exclude:需要被排除在外的文件 / 文件夹,这些文件不会参与编译。
- publicHeadersPath:C 家族库的公共头文件地址。
- swiftSettings:定义一个用于特定环境(例如 Debug)的宏,需要设置的话可以去
API
上研究下
- linkerSettings:用于链接一些系统库
对于常规型 target
,可以包含 Swift 代码文件或者 C 家族的代码文件,但是不能在一个 target
中混合两种语言。和上面说过的类似,如果 target
的代码文件中含有 main.swift
,main.c
,main.cpp
文件,这个 target
就会被编译成可执行文件,否则就会被编译成静态库或者动态库。
系统库的 target
上面介绍 Package 时已经使用过了,需要注意的是 module.modulemap
文件中的路径配置,主要是用于找寻系统库的 .pc
文件,.pc
文件里有系统库的具体配置。
需要注意的小点
通过预处理命令区分编译环境
上文也说过,Package 可以通过 SwiftPM 执行 swift build
进行编译,也可以通过生成 xcodeproj
从而通过 Xcode 进行编译,两者的编译环境并不相同,生成的可执行文件也不是同一个地址,所以可以通过 SWIFT_PACKAGE
区分编译环境
#if SWIFT_PACKAGE
import Foundation
#endif
选择特定 Swift 版本的 Package
SwiftPM 设计时就支持每个包工作在不同的 Swift 语言版本和 SwiftPM 版本,看了官方的 文档 之后,还是有点不清不楚 (我自己也没试过),觉得下面我的理解有问题的可以自行阅览刚才的链接文档。
先介绍下如何选择特定 Swift 语言版本的包。举个例子,工具包 Foo
在某个迭代中已经适配升级成 Swift 5.0 ,但是有些项目还是在用 Swift 3.0,所以 Foo
还得支持 Swift 3.0。对于这种包功能相同但是 Swift 语言版本号不同的情况,可以才用 tag 后缀的形式,例如 [1.0.0,1.2.0@swift-3, 1.3.0]
,这样一来,SwiftP··M 3.0 在查找 Foo
库的可用版本时,就会直接忽略 1.0.0,1.3.0
,而去直接寻找 1.2.3@swift-3
。
选择特定 SwiftPM 版本的包。举个例子,我写了一个工具包,支持 Swift 3.0 和 Swift 4.0、Swift 4.2,但是 SwiftPM 在 3.0 、4.0、4.2 的 API 又不一样,这就导致被依赖的时候需要不同的 Package.swift
,所以 SwiftPM 提供了一个这样的写法:Package@swift-3.swift
。我们熟知的 Alamofire
就是这样写的,在 Alamofire 4.8
中就提供了如下三个 Package.swift
版本:
这样当 Swift 4.2 的项目依赖 Alamofire
时,会直接找到 Package.swift
,当 Swift 3.0 的项目依赖它时,找到 Package.swift
发现 tool-version
版本不匹配,就会再去找与 Swift 3.0 兼容的 Package@swift-3.swift
。
无论上上面哪两种方式,都是 Swift 5.0 API 稳定之前的中间产物,以后废不废弃这些写法也不一定,苹果的文档里也说除非必要不然希望我们这样做,所以新开的 Swift 5.0 起步的项目也不需要关心这些。
Package.resolved
和大多数包管理工具类似,SwiftPM 也会生成一个 Package.resolved
文件来记录依赖项的解析结果,当执行依赖解析的时候,会优先解析这个文件,不存在时才会解析 Package.swift
。这一点和 Cocoapods 的 podfile.lock
文件类似,有的项目进行工程管理时为了能每个成员自由的执行 Update 操作,都会在上传时把它忽略掉。
swift package resolve
命令会解析项目的依赖,如果之前有 resolve 文件,但是和本地的 Package.swift
的配置有不对应(小组成员提交了更改),该命令会更新包依赖。平常使用的 swift build
等命令中都会含有该命令。
Build Configuration
同大多数苹果的软件设计类似,SwiftPM 编译时也有 Debug/Realease 之分,可以通过 swift build -c debug/realease
进行编译,具体的区别主要是代码的编译优化和调试信息。默认不加参数时下是 debug 环境。
Swift Package VS Swift Module
Swift Module 是一个代码集合体,有自己的访问控制。而 Swift Package 是 SwiftPM 中所使用的,由代码文件和 Package.swift
构成,Swift Package 可以包含多个 Swift Module,而且并不局限于 Swift 语言(比如 C/C++ 语言)。从某种意义上说 Swift Package 类似 Xcode.project,Swift Module 类似于 Xcode.target。
Swift Evolution Ideas
下面是一些 SwiftPM 开发者们在考虑的优化项,顺序不代表优先级,目前除了支持资源文件这一项,其他的暂时还没安排到具体的代码中去。
Mirror and Fork Support
Mirror:我们构建 Package 肯定希望它的依赖项都是稳定的链接,尤其是在发布的时候。所以希望如果在某项依赖由于某种原因(配置文件或者代码等等不完整或者仓库被删了、该库链接的网络访问失败等)下载失败时,可以提供一个备用的链接,这一点在务端的发布中是非常有用的。
Fork:有时候第三方库的公开版有 bug,但是并没发布修复完的版本,很多人就会 fork 一个 bugfix 版本,这时候其实很多人会直接把依赖的 git 链接改成自己的 fork 链接,这样虽然能简单的解决问题,但是对于下面这种情况就会变得复杂:
比如当前我在开发主包 Package A,存在一个依赖链:A --> B --> C --> D,但是此时 D 出现了一个 bug,需要 fork 一个 bugfix 版本,如果按照上面的思路,就需要把依赖链中间的 B、C 都要 fork 一个 bugfix 版本,不然 B、C 还是下载的之前有 bug 的版本。
所以未来可能是在 Root Package (比如上例中的 A)中以下面这种方式实现:
let package = Package(
name: "app",
dependencies: [
// App depend on the libCore dependency.
.package(url: "https://github.com/example/libCore.git", from: "1.0.0"),
// Override the dependency with our fork until an important patch is merged upstream.
.fork(package: "libCore", url: "https://github.com/myName/libCore.git", .branch("CVE-5715")),
],
...
)
这里含义是将 Package 依赖图里所有的 libCore
库换成下面的链接,不过需要需要注意的一点是这个特性只能在 Root Package 中使用,当作为 Package 发布出去被别的 Package 依赖时 SwiftPM 会直接报错,不然会因为设置多个 fork 而导致解析错误。
这两个优化点具体的讨论在 forums 可以看到。到目前为止,应该还没有解决,在 master 的代码中也没看见对应的介绍。
有条件的添加依赖
举个例子,有些 Dependency
只希望在 Linux
环境下被依赖,其他环境下不被依赖。这个特性已经被提到了 这里,希望用如下的这种方式:
.package(url: "https://...", from: "1.0.0", when: .testing),
.package(url: "https://...", from: "2.0.0", when: .os(.linux),
但是 SwiftPM 的开发人员表示该特性需要再考虑下改如何实现,目前还是搁置暂议。
添加资源文件
到目前为止集成到 Swift 语言里的 PackageDescription 里(即 Swift 5.1),还没有对应的机制又来存放资源文件,Source
文件夹里只能读取到代码文件。关于这点,之前有人提出过这个 bug,这个 链接 里也有相应的讨论。
这个问题公开版虽然没有解决,但是在 master 的源码中已经能看到对应的 API,不出意外的话,Swift 5.2 中就会把这个 API 集成进来。
/// The explicit list of resource files in the target.
@available(_PackageDescription, introduced: 5.2)
public var resources: [Resource]? {
get { _resources }
set { _resources = newValue }
}
private var _resources: [Resource]?
在 Package 编译时添加自定义编译脚本等
很多时候我们想在 Swift build
时添加很多自定义的编译脚本,例如添加一些自定义的输入和输出,SwiftPM 的开发人员提了一个未来用于解决这个提案的方式,添加 PackageExtentsion
:
extension Target {
static func packageExtension(
name: String,
dependencies: [Dependency] = []
) -> Target
}
extension Product {
static func packageExtension(
name: String
) -> Product
}
具体的实现方式还需要很多步骤,感兴趣的可以看下这个 提案。
其他待优化项
SwiftPM 在 iOS 平台的使用
其实说 SPM 支持 iOS 等平台,个人觉得是有点问题的,因为这里只是 Xcode11 集成了 libSwiftPM
,适配了 SPM 系统,从 SPM 本身的设计来看,并不能严格的说支持 iOS 等平台。而根据 github 上面的 文档 显示,这个库最近还是会经常变动的。
对于 SwiftPM 在 iOS 平台的使用,并不是像 MacOS 平台这样通过配置文件实现,而是通过 Xcode 的官方插件进行集成。自己试了下,流程还是有点慢。
SwiftPM 对比 Cocoapods 和 Carthage
Cocoapod
最古老的的一种包管理工具,也是使用最广泛的工具,依赖放在各个源(master 或者 自己的源)上的 podspec
文件进行下载代码库,在本地生成一个 workspace 进行统一管理、添加依赖。
-
自动化 / 侵入性高:一键配置和集成依赖 / 自动更改 Xcode.project 的配置
-
中心化:提供各个源管理仓库配置文件,所有更新仓库文件索引可能会很慢。
-
缓存:除了项目根目录的缓存之外,还有较完整的本地缓存体系,所以不同工程下载同一个库时会直接从本地拿。
-
生态环境:比较完善,大多数库都提供了 pod 的集成方式,各种文档和工具也比较多
Carthage
-
去中心化:没有统一管理的中心,所以没有更新中心服务器的文件索引这种耗时步骤
-
成本略高 / 侵入性低:Carthage 只会帮你把各个库下载到本地(默认会编译成静态库),具体的 Project 配置需要自己弄
-
生态环境:很差,很多库都没有提供这种依赖方式
-
缓存:只有项目根目录的缓存,所以不同项目对于同一个库需要重新下载
SwiftPM
- 去中心化:没有统一管理的中心,所以没有更新中心服务器的文件索引这种耗时步骤
- 自动化 / 侵入性高:默认情况下需要有一定的目录格式
- 生态环境:怎么说呢,不能说差,只能说不够成熟,还有很多待优化项,毕竟是官方开发,Xcode 自集成
- 缓存:只有项目根目录的缓存,所以不同项目对于同一个库需要重新下载
上面是这三个东西在 Swift 包管理上的对比,至于如何选择,个人觉得如果所有的库都支持这些管理方式,那当然是选有官方认证的 SwiftPM,而且它也是使用 Swift 语言编写的,对于看设计原理也比较方便点。
引用