Swift包管理器使用教程

Viewed 0

Swift 包管理器

开发包

简言之,一个包即一个有着语义版本 tag 的 git 仓库,其中包含 Swift 源代码,以及一个放在根目录的 Package.swift 清单文件。

转换库模块(Library Module)为外部包(External Package)

如果你在构建一个有着几个模块的 app,在某个时间点,你可能决定将这些模块放到一个外部包中。这样做可以让代码变成一个可靠的库让其他人使用。

有了包管理器,要做到这一点比较简单:

  1. 在 GitHub 上创建一个新的代码仓库。
  2. 在终端里,进入模块所在目录。
  3. 执行 git init
  4. 执行 git remote add origin [github-URL]
  5. 执行 git tag 1.0.0
  6. 执行 git push origin master --tags

现在删除子目录,并修改你的 Package.swift,让其 package 声明包含如下信息:

let package = Package(
    dependencies: [
        .Package(url: "…", versions: "1.0.0"),
    ]
)

现在输入命令 swift build

同时开发 app 和包

如果你在开发的 app 使用了某个包,而你也需要同时改进这个包,那你有如下几个选择:

  1. 编辑包管理器克隆的代码:克隆的代码位于 ./Packages 中。
  2. 修改你的 Package.swift,让其指向一个本地的包克隆:这可能很乏味,因为你每次做了改变后,都要强制做一个更新,当然包括更新版本 tag。

目前,这两个选择都不是很理想,因为新提交的代码可能很容易破坏同组人的使用。例如,如果你修改了 Foo 的代码并让你的 app 使用这些改动,却并没有提交这些改动到 Foo,那么你就可能将你的同事置于代码依赖的地狱。我们会努力改进工具以避免类似问题,但目前,希望你知悉这些问题。

Package.swift — 清单文件

指示如何构建一个包的清单文件,被称为 Package.swift。你可以定制此文件以声明构建 target 或依赖,引用或排除源文件,以及为模块或单个文件指定构建配置。

如下是一个 Package.swift 文件的例子:

import PackageDescription
let package = Package(
    name: "Hello",
    dependencies: [
        .Package(url: "ssh://git@example.com/Greeter.git", versions: "1.0.0"),
    ]
)

明显, Package.swift 文件是一个 Swift 文件,它用定义在 PackageDescription 模块的类型来声明一个包的配置。这个清单声明了一个对外部包 Greeter 的依赖。

如果你的包包含多个互相依赖的 target,那么你需要指明它们的相互依赖关系。如下例所示:

import PackageDescription
let package = Package(
    name: "Example",
    targets: [
        Target(
            name: "top",
            dependencies: [.Target(name: "bottom")]),
        Target(
            name: "bottom")
    ]
)

target 的名字就是你子目录的名字。

自定义构建

清单文件本质为 Swift 源码,可带来极强的自定义性,例如:

import PackageDescription
var package = Package()
#if os(Linux)
let target = Target(name: "LinuxSources/foo")
package.targets.append(target)
#endif

对于这样的特性,标准配置文件格式,如 JSON,将导致字典结构对每一个特性都增加很多复杂性。

依赖 Apple 模块(如 Foundation)

当前,还没有明确支持依赖于 Foundation、AppKit 等,尽管这些模块在合适的系统位置时应该正常工作。我们将为系统依赖添加明确的支持。注意,目前包管理器还没有支持 iOS、watchOS 或 tvOS 平台。

源码布局

swift build 创建的模块取决于源文件在文件系统里的布局。

例如,如果你创建一个目录包含如下布局:

example/example/Sources/bar.swift
example/Sources/baz.swift

example 目录中运行 swift build 会产生单个库目标文件: example/.build/debug/example.a

要创建多个模块,就创建多个子目录:

example/Sources/foo/foo.swift
example/Sources/bar/bar.swift

此时运行 swift build 将产生两个库目标文件:

  • example/.build/debug/foo.a
  • example/.build/debug/bar.a

要生成一个可执行模块(而不是一个库模块),添加一个 main.swift 到那个模块的子目录即可:

example/Sources/foo/main.swift
example/Sources/bar/bar.swift

此时运行 swift build 将产生:

  • example/.build/debug/foo
  • example/.build/debug/bar.a

这里的 foo 就是一个可执行文件,而 bar.a 是一个静态库。

其它规则

  • 命名为 Tests 的目录会被忽略。
  • 若目录的子目录命名为 SourcesSourcesrcssrc,它们将成为模块。
  • 没有 Sources 目录是可接受的,在这种情况下,根目录就被当做单个模块(将你的源代码放在这里),或者根目录的子目录会被认为是模块。对于简单项目,这种布局比较方便。

系统模块

你可以使用包管理器链接系统库。要做到这一点,指定的包必须被发布,并包含一个模块地图(module map)。

以 IJG’s JPEG 库为例。如下是我们要编译的代码:

import CJPEG
let jpegData = jpeg_common_struct()
print(jpegData)

将代码放入一个叫做 example 目录里。要 import CJPEG,包管理器要求这个 JPEG 库已被某个系统包管理器(如 aptbrewyum 等)安装。

为成为系统库而提供有模块地图的 Swift 包被处理的方式和常规 Swift 包不同。创建一个叫做 CJPEG 的目录,与 example 目录在同一层级,然后创建一个叫做 module.modulemap 的文件,编辑它包含如下信息:

module CJPEG [system] {
    header "/usr/include/jpeglib.h"
    link "jpeg"
    export *
}

我们期望社区会遵循的惯例是在这样的模块名前冠以字母 C,并驼峰命名模块,一如每个 Swift 模块命名的惯例。然后社区就可以自由命名另外的模块为 JPEG,它将包含更多原始 C API 的 更 Swifty 的函数包装。

包就是 Git 仓库,以语义标签指明版本号,并在根目录包含一个 Package.swift 文件。因此,我们必须创建 Package.swift 并初始化一个至少有一个版本 tag 的 Git 仓库。

现在要使用 JPEG,我们必须在 example app 的 Package.swift 里声明我们的依赖:

import PackageDescription
let package = Package(
    dependencies: [
        .Package(url: "../CJPEG", majorVersion: 1)
    ]
)

这里我们使用了一个相对路径的 URL 以加快初始开发。如果(我们希望)你将你的模块地图包 push 到公共仓库里,你必须修改上面的 URL 引用,改为一个完整合格的 git URL。现在,如果我们在 example app 目录里输入 swift build,我们将创建一个可执行文件。

有依赖的模块地图

让我们扩展 example 以包含 JasPer,它是一个 JPEG-2000 库,且依赖于 JPEG 库。

首先,创建一个叫做 CJasPer 的目录,与 CJPEG 和我们的 example 同级。JasPer 依赖 JPEG,因此任何使用 CJasPer 的包都必须知晓要 import CJPEG。我们通过在 CJasPer 的 Package.swift 里指定依赖来达成这一点。

import PackageDescription
let package = Package(
    dependencies: [
        .Package(url: "../CJPEG", majorVersion: 1)
    ]
)

CJasPer 的模块地图类似 CJPEG 的:

module CJasPer [system] {
    header "/usr/local/include/jasper/jasper.h"
    link "jasper"
    export *
}

当心,模块地图必须指明此系统包使用的所有头文件,但是,你一定不能指定你的头文件已经指定过的头文件。例如,使用 JasPer 的 example 会包含许多头文件,但所有被 jasper.h 包含的都要避免。如果你引用不对,那么你可能不时会遇到编译问题,很难调试。

一个包就是有着语义标签指明版本的 Git 仓库,并包含一个 Package.swift 文件,所以我们必须创建一个 Git 仓库。包管理器克隆标签。如果你编辑了 module.modulemap 但没有 git tag -f 1.0.0,那么你将不能构建本地的修改。

回到我们的 example app 的 Package.swift,我们改变依赖到 CJasPer

import PackageDescription
let package = Package(
    dependencies: [
        .Package(url: "../CJasPer", majorVersion: 1)
    ]
)

JasPer 依赖 CJPEG,所以我们不再需要在 example app 的 Package.swift 里指明我们依赖 CJPEG。修改 example 的 main.swift 以测试 JasPer 支持,然后运行即可。

请注意在 Ubuntu 15.10 上,上面步骤可能会失败,因为 jpeglib.h 不是一个正确绑定到 Ubuntu 的模块。在 jpeglib.h 顶部添加 #include <stdio.h> 可修正此问题。JPEG lib 自身需要打补丁,但由于这个情况比较普遍,我们打算添加一个解决方案到模块包里。

提供多个库的包

一些系统包提供多个库。在这种情况下,你需要将所有的库信息添加到 Swift 模块地图包的 .modulemap 文件里:

module CFoo [system] {
    header "/usr/local/include/foo/foo.h"
    link "foo"
    export *
}
module CFooBar [system] {
    header "/usr/include/foo/bar.h"
    link "foobar"
    export *
}
module CFooBaz [system] {
    header "/usr/include/foo/baz.h"
    link "foobaz"
    export *
}

foobarfoobaz 链接到 foo;我们不需要模块地图里指定这个信息,因为头文件 foo/bar.hfoo/baz.h 都包含依赖的头文件,此外,当模块被引入 Swift,依赖模块不会被自动引入,将引起链接错误。如果链接错误在包的用户处发生,将导致你的包很难调试。

跨平台的模块地图

模块地图必须包含绝对路径,因此它们不是跨平台的。我们打算在包管理器里提供一个解决方案。长期来看,我们希望系统库和系统包都提供模块地图,那时,包管理器的这个组建就会变得多余。值得注意的是,上述步骤在使用 Homebrew 安装 JPEG 和 JasPer 时将不会工作,因为目前文件会被安装到 /usr/local,但如提到的,我们打算支持基本重定位。

模块地图的版本

语义化模块地图的版本。语义版本的意思不太清晰,所以使用你最佳的判断。不要跟随模块所标示的系统库的版本,应该单独对待模块地图的版本。遵循系统包的惯例;例如 python3 的 debian 包叫做 python3,如果为 python3 做一个模块地图,你应该称其为 CPython3

可选依赖的系统库

目前,你需要制作另外一个模块地图包来表示有着可选依赖的系统包。例如, libarchive 可选依赖于 xz,这意味着它可以在 xz 支持下编译,但这不一定是必要的。要提供一个有着 xz 的 libarchive 包,你必须做一个 CArchive+CXz 包,其依赖于 CXz,同时提供 CArchive

0 Answers