5.2 包集合

原文链接: https://docs.swift.org/latest/documentation/packagemanagerdocs/packagecollections/

5.2 包集合

学习创建、发布和使用 Swift 包集合。

概述

包集合由 SE-0291 引入,它是经过整理的包列表及配套元数据,你可以导入它们,让发现已有包变得更容易。

教育工作者和社区有影响力的人可以发布包集合,配合课程材料或博客文章一起使用,让读者更容易第一次使用这些包,或者为某个特定任务挑选合适的包。企业也可以用集合来提供一组受信任的包,或者一组团队一贯使用的包。你可以把包集合编写成一个静态 JSON 文档,发布到网上,或者在本地文件系统上分发。

使用 package-collection 命令行工具

借助 swift package-collection 命令行接口,SwiftPM 用户可以订阅包集合。已导入包集合的内容对 libSwiftPM 的任何客户端都可访问。

swift package-collection 有下列子命令:

  • add:添加一个新的集合
  • describe:获取某个集合、或者已导入集合中某个包的元数据
  • list:列出已配置的集合
  • refresh:刷新已配置的集合
  • remove:移除已配置的集合
  • search:在已导入的集合中按关键字或模块名搜索包

创建包集合

包集合是一个 JSON 文档,其中包含一个包列表以及每个包的元数据。

任何人都可以创建和发布包集合。swift-package-collection-generator 项目提供了面向包集合发布者的工具:

所有包集合都必须遵循集合数据格式,SwiftPM 才能使用它们。创建包集合的推荐做法是使用 package-collection-generate。如果要自行实现,可以通过 PackageCollectionsModel 模块使用这些数据模型。

输入格式

首先,定义集合的顶层元数据:

  • name:包集合的名称,仅用于展示。
  • overview:包集合的描述。可选。
  • keywords:与该集合关联的关键字数组。可选。
  • formatVersion:该集合遵循的格式版本。目前唯一允许的值是 1.0。
  • revision:该包集合的修订号。可选。
  • generatedAt:该包集合生成时的 ISO 8601 格式日期时间字符串。
  • generatedBy:该包集合的作者。可选。
    • name:作者姓名。
  • packages:包的对象的非空数组。

把包添加到集合中

packages 数组中的每一项都是一个包对象,具有下列属性:

  • url:包的 URL。目前只支持 Git 仓库 URL。URL 应当使用 HTTPS,可以带 .git 后缀。
  • identity:如果该包已发布到注册表,则是它的标识。可选。
  • summary:包的描述。可选。
  • keywords:与该包关联的关键字数组。可选。
  • readmeURL:该包 README 的 URL。可选。
  • license:该包当前的许可证信息。可选。
    • url:许可证文件的 URL。
    • name:许可证名称。推荐使用 SPDX 标识符(例如 Apache-2.0、MIT 等)。未知时可省略。可选。
  • versions:版本对象的数组,代表该包最近和/或相关的若干次发布。

为包添加版本

版本对象包含从 Package.swift 中提取的元数据,以及可选的、来自其他来源的额外元数据:

  • version:语义化版本字符串。
  • summary:该包版本的描述。可选。
  • manifests:按 Swift 工具版本组织的清单文件映射,非空。键是(语义化)工具版本(详见下文),值是:
    • toolsVersion:清单文件中指定的 Swift 工具版本。
    • packageName:包的名称。
    • targets:该包版本的目标数组。
      • name:目标名。
      • moduleName:如果该目标可以作为模块导入,则是模块名。可选。
    • products:该包版本的产品数组。
      • name:产品名。
      • type:产品类型。它的 JSON 表示必须与 SwiftPM 的 PackageModel.ProductType 一致。
      • target:该产品的目标数组。
    • minimumPlatformVersions:该包版本在 Package.swift 中声明支持的平台数组。可选。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "5.2": {
    "toolsVersion": "5.2",
    "packageName": "MyPackage",
    "targets": [
      {
        "name": "MyTarget",
        "moduleName": "MyTarget"
      }
    ],
    "products": [
      {
        "name": "MyProduct",
        "type": {
          "library": ["automatic"]
        },
        "targets": ["MyTarget"]
      }
    ],
    "minimumPlatformVersions": [
      {
        "name": "macOS",
        "version": "10.15"
      }
    ]
  }
}
  • defaultToolsVersion:默认清单文件的 Swift 工具版本。manifests 映射的键中必须包含它。
  • verifiedCompatibility:已验证并测试过的兼容平台与 Swift 版本数组。有效的平台名包括 macOS、iOS、tvOS、watchOS、Linux、Android 和 Windows。Swift 版本应当是语义化版本字符串,并且尽可能具体。可选。
1
2
3
4
5
6
{
  "platform": {
    "name": "macOS"
  },
  "swiftVersion": "5.3.2"
}
  • license:该包版本的许可证。可选。
    • url:许可证文件的 URL。
    • name:许可证名称。推荐使用 SPDX 标识符(例如 Apache-2.0、MIT 等)。未知时可省略。可选。
  • author:该包版本的作者。可选。
    • name:该包版本的作者。
  • signer:该包版本的签名者。可选。 细节参见关于包签名的文档。
    • type:签名者类型。目前唯一有效值是 ADP(Apple Developer Program)。
    • commonName:签名证书主体的通用名。
    • organizationalUnitName:签名证书主体的组织单位名。
    • organizationName:签名证书主体的组织名。
  • createdAt:该包版本创建时的 ISO 8601 格式日期时间字符串。可选。

带版本的清单文件

包集合生成工具应当同时包含「默认」清单文件 Package.swift 中的数据,以及带版本的清单文件中的数据

manifests 映射的键是 Swift 工具(语义化)版本:

  • 对于 Package.swift,应当使用 Package.swift 中指定的工具版本。
  • 对于带版本的清单文件,应当使用文件名中指定的工具版本。例如,对于 Package@swift-4.2.swift 就是 4.2。清单文件中的工具版本必须与文件名中的一致。

带版本的标签

包集合不支持带版本的标签。

配置文件

与包集合相关的配置保存在文件 ~/.swiftpm/config/collections.json 中。它记录用户已配置的集合列表,以及诸如 package-collection add 命令中 --trust-unsigned 和 --skip-signature-check 标志所设置的偏好。

注意:这个文件通过 Swift Package Manager 命令管理,用户不应手动编辑它。

示例

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
{
  "name": "Sample Package Collection",
  "overview": "This is a sample package collection listing made-up packages.",
  "keywords": ["sample package collection"],
  "formatVersion": "1.0",
  "revision": 3,
  "generatedAt": "2020-10-22T06:03:52Z",
  "packages": [
    {
      "url": "https://www.example.com/repos/RepoOne.git",
      "summary": "Package One",
      "readmeURL": "https://www.example.com/repos/RepoOne/README",
      "license": {
        "name": "Apache-2.0",
        "url": "https://www.example.com/repos/RepoOne/LICENSE"
      },
      "versions": [
        {
          "version": "0.1.0",
          "summary": "Fixed a few bugs",
          "manifests": {
            "5.1": {
              "toolsVersion": "5.1",
              "packageName": "PackageOne",
              "targets": [
                {
                  "name": "Foo",
                  "moduleName": "Foo"
                }
              ],
              "products": [
                {
                  "name": "Foo",
                  "type": {
                    "library": ["automatic"]
                  },
                  "targets": ["Foo"]
                }
              ]
            }
          },
          "defaultToolsVersion": "5.1",
          "verifiedCompatibility": [
            {
              "platform": { "name": "macOS" },
              "swiftVersion": "5.1"
            },
            {
              "platform": { "name": "iOS" },
              "swiftVersion": "5.1"
            },
            {
              "platform": { "name": "Linux" },
              "swiftVersion": "5.1"
            }
          ],
          "license": {
            "name": "Apache-2.0",
            "url": "https://www.example.com/repos/RepoOne/LICENSE"
          },
          "createdAt": "2020-10-21T09:25:36Z"
        }
      ]
    },
    {
      "url": "https://www.example.com/repos/RepoTwo.git",
      "summary": "Package Two",
      "readmeURL": "https://www.example.com/repos/RepoTwo/README",
      "versions": [
        {
          "version": "2.1.0",
          "manifests": {
            "5.2": {
              "toolsVersion": "5.2",
              "packageName": "PackageTwo",
              "targets": [
                {
                  "name": "Bar",
                  "moduleName": "Bar"
                }
              ],
              "products": [
                {
                  "name": "Bar",
                  "type": {
                    "library": ["automatic"]
                  },
                  "targets": ["Bar"]
                }
              ]
            }
          },
          "defaultToolsVersion": "5.2"
        },
        {
          "version": "1.8.3",
          "manifests": {
            "5.0": {
              "toolsVersion": "5.0",
              "packageName": "PackageTwo",
              "targets": [
                {
                  "name": "Bar",
                  "moduleName": "Bar"
                }
              ],
              "products": [
                {
                  "name": "Bar",
                  "type": {
                    "library": ["automatic"]
                  },
                  "targets": ["Bar"]
                }
              ]
            }
          },
          "defaultToolsVersion": "5.0"
        }
      ]
    }
  ]
}

保护包集合

对集合签名

对包集合签名可以确立其真实性并保护其完整性。这一步是可选的。在用户添加未签名的集合之前,系统会提示用户确认。你用来给包集合签名的证书必须满足一系列要求。如果不满足这些要求,包管理器会返回错误。

关于包管理器实现的安全特性的更多细节,参见包的安全性。

安全风险

虽然签名能为包集合提供一定程度的保护,降低其内容被恶意行为者篡改的风险,但它无法防止下列攻击手法:

  • 剥离签名:攻击者从已签名的集合中移除签名,使其作为未签名的集合被下载,从而绕过签名检查。这种情况下,发布者应当明确声明该集合是已签名的,而 SwiftPM 用户在「未签名」警告出现在一个本应已签名的集合上时,应当中止 add 操作。
  • 替换签名:攻击者可能篡改集合后用另一个证书重新签名,伪装成同一个实体或另一个实体,只要签名有效,SwiftPM 就会接受它。

为了防御这些攻击,包管理器提供了证书固定(certificate-pinning)配置,让集合发布者可以:

  • 要求对自己的集合进行签名检查——这可以防御「剥离签名」。
  • 限制可以使用哪个证书签名——这可以防御「替换签名」。

集合发布者定义证书固定配置的流程如下:

  1. 编辑 PackageCollectionSourceCertificatePolicy,在 defaultSourceCertPolicies 字典中添加一个条目:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private static let defaultSourceCertPolicies: [String: CertificatePolicyConfig] = [
    // 键应当是包集合 URL 中的 "host" 部分。
    // 这会要求托管在该域名下的所有包集合都必须签名。
    "www.example.com": CertificatePolicyConfig(
        // 签名证书必须具有这个主体用户 ID
        certPolicyKey: CertificatePolicyKey.default(subjectUserID: "exampleUserID"),
        /*
         计算证书的 base64 编码字符串:
         let certificateURL = URL(fileURLWithPath: <path to DER-encoded root certificate file>)
         let certificateData = try Data(contentsOf: certificateURL)
         let base64EncoodedCertificate = certificateData.base64EncodedString()
         */
        base64EncodedRootCerts: ["<base64-encoded root certificate>"]
    )
]
  1. 发起一个拉取请求以供审核。请求者必须能够提供自己的身份以及对域名所有权的证明:
    • 请求者必须提供实际的证书文件(DER 编码)。SwiftPM 团队会验证证书链有效,并且拉取请求中提供的取值正确。
    • 请求者必须添加一条引用该拉取请求的 TXT 记录。SwiftPM 团队会运行 dig -t txt <DOMAIN> 来验证。这可以作为域名所有权的证明。
  2. 变更被接受之后,会在下一个 SwiftPM 版本中生效。

由于证书固定配置与 Web 域名相关联,它只能应用于托管在 Web 上的已签名集合(即 URL 以 https:// 开头),不覆盖本地文件系统中的集合(即 URL 以 file:// 开头)。