Swift 包管理器介绍

5,742 阅读21分钟

SwiftPM 是什么

Swift Package Manager 是一个苹果官方出的管理源代码分发的工具,目的是更简单的使用别人共享的代码。它会直接处理包之间的依赖管理、版本控制、编译和链接。从总体功能上来说,和 iOS 平台上的 Cocoapods、Carthage 一样。

一开始 SwiftPM 只支持在 MacOS 和 Linux 平台使用,大多是一些 Vapor、Perfect 之类的后端服务或者脚本在用,所以对于 iOS 开发者来说会比较陌生。Xcode 11 开始自集成了 libSwiftPM,这样一来,iOS、watchOS、tvOS 等平台也都可以使用了,不过由于是集成的依赖库,所以使用方法有些不太一样,下面会另说。文中不说哪个平台的使用默认是指 MacOS 和 Linux 平台。

SwiftPM 的创建和使用

SwiftPM 管理的每个 Package相当于 Xcode.Project,并且有具体的代码定义,包的目录下必须含有 Package.swiftSources代码文件夹(链接系统的包除外)。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 或者直接点击运行,从而启动一个进程
  • 系统包:这种包是专门为了链接系统库(例如 libgitjpeglibmysql 这种系统库)准备的,本身不需要任何代码,所以也没有 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 进去:

package.swift

编辑 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 类里有很多属性,像 pkgConfigproviders 这些都是上文说到的 Swift 4.2 之前 System package所用到的。Swift 5.0 后基本也不会用到了,下面只介绍些平常可能会用到的属性。

Package.SupportedPlatform

这个 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 配置如下

// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Paper",
    products: [
        // Products define the executables and libraries produced by a package, and make them visible to other packages.
        .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: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.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: "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 {

    /// The requirement is specified by an exact version.
  	/// example: .exact("1.2.3")
    public static func exact(_ version: PackageDescription.Version) -> PackageDescription.Package.Dependency.Requirement

    /// The requirement is specified by a source control revision.
  	/// example: .revision("e74b07278b926c9ec6f9643455ea00d1ce04a021")
    public static func revision(_ ref: String) -> PackageDescription.Package.Dependency.Requirement

    /// The requirement is specified by a source control branch.
  	/// example: .branch("develop")
    public static func branch(_ name: String) -> PackageDescription.Package.Dependency.Requirement

    /// Creates a specified for a range starting at the given lower bound
    /// and going upto next major version.
  	/// example: .upToNextMajor(from: "1.2.3")
    public static func upToNextMajor(from version: PackageDescription.Version) -> PackageDescription.Package.Dependency.Requirement

    /// Creates a specified for a range starting at the given lower bound
    /// and going upto next minor version.
   /// example: .upToNextMinor(from: "1.2.3")
    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 分为三种类型:常规型、测试类型、系统库类型。分别对应下面几个快捷创建方式:

/// The type of this 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.swiftmain.cmain.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 版本:

  • Package.swift (tool-version: 4.2)
  • Package@swift-3.swift (tool-version: 3.0)
  • Package@swift-4.swift (tool-version: 4.0)

这样当 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
}

具体的实现方式还需要很多步骤,感兴趣的可以看下这个 提案

其他待优化项

  • 支持创建包时可以加入自定义模板 SR-7837

  • 一句命令行直接打 tag 并发布到 git 源上。

  • 性能测试:SR-1354

  • 一个 git 源上放多个 Package:SR-3951

  • 等等

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 语言编写的,对于看设计原理也比较方便点。

引用