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

# はじめに
[MultipartFormDataParser](https://github.com/417-72KI/MultipartFormDataParser)というテスト用のライブラリを公開しています。
このライブラリのテストでいくつか別のpackageを使用しているのですが、これが理由でちょっとした課題を発見しました。
この記事では、その課題と解決策をご紹介します。

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

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135304/d60973fe-3f11-7640-f380-06c00f1984c9.png)
さて、このProject Navigatorに表示された依存パッケージを見ると、Package.swiftに書いた `Alamofire` と `MultipartFormDataParser` 以外にも意図していないパッケージがfetchされていることが分かります。

また、Issue Navigatorを開くと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135304/791ddb65-81e2-1309-a42d-ff6a28650d22.png)
意図しない警告[^1]が出てしまっています。
[^1]: Issue Navigatorの画像を見ての通り、Moyaに起因する警告でした


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

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

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

```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"
            ]
        ),
    ]
)
```

`Alamofire`、 `APIKit`、 `Moya`、 `OHHTTPStubs` が `dependencies` に指定されており、いずれもテストでしか使われていないことが分かります。
また、 `Moya` で `RxSwift` と `ReactiveSwift` が指定されているようでした[^2]。
[^2]: `Quick` と `Nimble` も指定されていますがなぜかこれらはfetchされておらず、この原因は分かっていません(末尾の `// dev` を見てたりする？)

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

```swift: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からは `testDependencies` と `testTargetDependencies` も空として扱われるようになります。
[^3]: https://github.com/417-72KI/MultipartFormDataParser/blob/1.4.1/Package.swift

その証拠に、このバージョンをサンプルプロジェクトに組み込むと、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135304/a1ea7c5b-db52-44d6-c1d4-2e58d972839d.png)
サンプルプロジェクトで指定している `Alamofire` と `MultipartFormDataParser` 以外のパッケージがfetchされなくなっていることが分かります。

また、Issue Navigatorからも警告が消えました🎉
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135304/54bd9e6c-618f-9d7b-07a8-5702b01e09c3.png)

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

```swift: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についてはForums[^4]でも議論されているようですが、２年前で止まっているためしばらくはこういったワークアラウンドが必要だと考えています。
[^4]: https://forums.swift.org/t/test-only-dependencies/3888