5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftAdvent Calendar 2021

Day 14

Swift Package Managerで公開しているライブラリのPackage.swiftを最適化する

Last updated at Posted at 2021-12-13

はじめに

MultipartFormDataParserというテスト用のライブラリを公開しています。
このライブラリのテストでいくつか別のpackageを使用しているのですが、これが理由でちょっとした課題を発見しました。
この記事では、その課題と解決策をご紹介します。

概要

MultipartFormDataParserとAlamofireのみを使ったサンプルプロジェクトを作成してみました。
以下のようなPackage.swiftを作成しています。

Package.swift
import PackageDescription

let package = Package(
    name: "Dependencies",
    products: [
        .library(
            name: "Dependencies",
            targets: ["Dependencies"]),
    ],
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.4"),
        .package(url: "https://github.com/417-72KI/MultipartFormDataParser.git", .exact("1.4.0")),
    ],
    targets: [
        .target(
            name: "Dependencies",
            dependencies: ["Alamofire"]),
        .testTarget(
            name: "DependenciesTests",
            dependencies: ["Dependencies", "MultipartFormDataParser"]),
    ]
)

XcodeでこのPackage.swiftを含むプロジェクトを開くと依存性解決の結果をProject Navigatorに表示してくれます。
image.png
さて、このProject Navigatorに表示された依存パッケージを見ると、Package.swiftに書いた AlamofireMultipartFormDataParser 以外にも意図していないパッケージがfetchされていることが分かります。

また、Issue Navigatorを開くと
image.png
意図しない警告1が出てしまっています。

原因

当たり前といえば当たり前なんですが、使用しようとしているライブラリが別のpackageに依存している場合、それも取ってくる必要があります(というかそのための依存性解決ですw)
その際Package Managerが見に行くのは dependenciesArray であり、 それらが target で使われるか testTarget で使われるかは関係ありません。

そのため、各ライブラリのテストでしか使われないpackageが余計にfetchされ依存性解決の実行時間に影響を及ぼす結果となっています。

当初MultipartFormDataParserのPackage.swiftは以下のようになっていました。

Package.swift
let package = Package(
    name: "MultipartFormDataParser",
    platforms: [
        .macOS(.v10_14),
        .iOS(.v11),
        .tvOS(.v11)
    ],
    products: [
        .library(
            name: "MultipartFormDataParser",
            targets: ["MultipartFormDataParser"]),
    ],
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.4"),
        .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", from: "9.1.0"),
        .package(url: "https://github.com/ishkawa/APIKit.git", from: "5.2.0"),
        .package(url: "https://github.com/Moya/Moya.git", from: "15.0.0")
    ],
    targets: [
        .target(
            name: "MultipartFormDataParser",
            dependencies: []
        ),
        .testTarget(
            name: "MultipartFormDataParserTests",
            dependencies: [
                "MultipartFormDataParser",
                "Alamofire",
                "APIKit",
                "Moya",
                "OHHTTPStubsSwift"
            ]
        ),
    ]
)

AlamofireAPIKitMoyaOHHTTPStubsdependencies に指定されており、いずれもテストでしか使われていないことが分かります。
また、 MoyaRxSwiftReactiveSwift が指定されているようでした2

解決策

Package.swiftもSwiftファイルなので、Swiftの基本的な機能が使えます。
そこで、テストでだけ使用しているdependenciesを変数として切り出し、リリース時にはそれを含めないようなアプローチを考えました。

Package.swift
import PackageDescription

let isRelease = false // リリース時(タグを打つ時)だけ `true` にする

let testDependencies: [Package.Dependency] = isRelease
? []
: [
    .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.4"),
    .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", from: "9.1.0"),
    .package(url: "https://github.com/ishkawa/APIKit.git", from: "5.2.0"),
    .package(url: "https://github.com/Moya/Moya.git", from: "15.0.0"),
]
let testTargetDependencies: [Target.Dependency] = isRelease
? []
: [
    "Alamofire",
    "APIKit",
    "Moya",
    "OHHTTPStubsSwift",
]

let package = Package(
    name: "MultipartFormDataParser",
    platforms: [
        .macOS(.v10_14),
        .iOS(.v11),
        .tvOS(.v11)
    ],
    products: [
        .library(name: "MultipartFormDataParser", targets: ["MultipartFormDataParser"]),
    ],
    dependencies: testDependencies,
    targets: [
        .target(name: "MultipartFormDataParser", dependencies: []),
        .testTarget(
            name: "MultipartFormDataParserTests",
            dependencies: [
                "MultipartFormDataParser",
            ] + testTargetDependencies
        ),
    ]
)

タグを打つときにだけ isRelease = true とする3所がポイントで、この時Package Managerからは testDependenciestestTargetDependencies も空として扱われるようになります。

その証拠に、このバージョンをサンプルプロジェクトに組み込むと、
image.png
サンプルプロジェクトで指定している AlamofireMultipartFormDataParser 以外のパッケージがfetchされなくなっていることが分かります。

また、Issue Navigatorからも警告が消えました🎉
image.png

2024/09/02追記

Packageはclassなので、各propertyを後から弄ることができます。
定義した後のpackageを弄るように変更することで可読性が上がります。

Package.swift
import PackageDescription

let isDevelop = true // リリース時(タグを打つ時)だけ `false` にする
let isApplePlatform: Bool = { // Linuxで扱えないPackageを除外する
    #if canImport(Darwin)
    true
    #else
    false
    #endif
}()

let package = Package(
    name: "MultipartFormDataParser",
    platforms: [
        .macOS(.v10_14),
        .iOS(.v11),
        .tvOS(.v11)
    ],
    products: [
        .library(name: "MultipartFormDataParser", 
                 targets: ["MultipartFormDataParser"]),
    ],
    dependencies: testDependencies,
    targets: [
        .target(name: "MultipartFormDataParser"),
        .testTarget(name: "MultipartFormDataParserTests", 
                    dependencies: ["MultipartFormDataParser"]),
    ]
)

// MARK: - develop
if isDevelop {
    if isApplePlatform {
        package.dependencies.append(contentsOf: [
            .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.7.0"),
            .package(url: "https://github.com/ishkawa/APIKit.git", from: "5.7.0"),
            .package(url: "https://github.com/Moya/Moya.git", from: "15.0.0"),
        ])
        package.targets
            .filter(\.isTest)
            .forEach {
                $0.dependencies.append(contentsOf: [
                    "Alamofire",
                    "APIKit",
                    "Moya",
                ])
            }
    }
}

余計な型宣言や空配列混じりの三項演算子が無くなったことで読みやすくなっているはずです。

まとめ

テストでしか使わないパッケージが残っていると、それも一緒にfetchしてしまうため、利用者側に余計なコストをかけてしまうことがあります。
利用者としては、なるべく依存するライブラリは最小限にとどめておきたいものです。

test-only-dependenciesについてはForums4でも議論されているようですが、2年前で止まっているためしばらくはこういったワークアラウンドが必要だと考えています。

  1. Issue Navigatorの画像を見ての通り、Moyaに起因する警告でした

  2. QuickNimble も指定されていますがなぜかこれらはfetchされておらず、この原因は分かっていません(末尾の // dev を見てたりする?)

  3. https://github.com/417-72KI/MultipartFormDataParser/blob/1.4.1/Package.swift

  4. https://forums.swift.org/t/test-only-dependencies/3888

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?