SwiftPackageManagerで依存グラフを書く

2024/12/01に公開

記事を書くのが久しぶりすぎて、記事の書き方を忘れましたTOSHです。
iOS開発におけるライブラリ管理ツールはCocoaPods, Carthage, SPMといったようにいろんな歴史と共に移り変わってきました。
自分もCocoaPods信者ではありましたが、CocoaPodsがメンテナンスモードに入ったこと、Xcode16以降でファーストパーティであるSPMもだいぶ安定してきたこともりありSPMに徐々に移行していきました。
本記事ではその中で、SPMにおけるライブラリの依存関係を可視化する方法を紹介しようと思います。

前提条件

Package.swiftが必要です。
XcodeのGUI上でぽちぽち追加をしていると、Package.swiftは生成されず、ライブラリの依存情報は.pbxprojファイルに保存されていきます。この場合だと今回紹介する方法では、依存関係のグラフを作成することができないので、こちらの記事を参考にPackage.swiftを導入してみましょう。

TL;DR

Package.swiftが一番上の階層であるディレクトリまで移動して下記のコマンドを実行してあげてください。

swift package show-dependencies --format dot | dot -Tpdf -o graph.pdf

やり方

Package.swiftを作成する

こちらの記事を参考にPackage.swiftを作成しました。。
ディレクトリ構成に追加は下記のようになっています。

.
└── SampleProject/
    ├── Dependencies/
    │   ├── Sources
    │   ├── Package.swift
    │   └── Package.resolved
    ├── SampleProject
    ├── SampleProject.xcodeproj
    └── SampleProject.xcworkspace

Package.swift

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

import PackageDescription

let package = Package(
    name: "Dependencies",
    platforms: [.iOS(.v16)],
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "Dependencies",
            targets: ["Dependencies"]
        ),
    ],
    dependencies: [
        .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.29.0"),
        .package(url: "https://github.com/googleanalytics/google-tag-manager-ios-sdk", from: "7.4.0"),
        .package(url: "https://github.com/google/GoogleDataTransport", from: "9.4.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "Dependencies",
            dependencies: [
                .product(name: "FirebaseAnalytics", package: "firebase-ios-sdk"),
                .product(name: "GoogleTagManager", package: "google-tag-manager-ios-sdk"),
                .product(name: "GoogleDataTransport", package: "GoogleDataTransport"),
            ]
        ),
    ]
)

Package.resolved

上記のPackage.swiftをresolveすると、下記のようにresolveされます。

{
  "originHash" : "e4c7f43051438a4d0246859880214c62630b38cba31d95dbe6dffe6a108d3748",
  "pins" : [
    {
      "identity" : "abseil-cpp-binary",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/abseil-cpp-binary.git",
      "state" : {
        "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27",
        "version" : "1.2024011602.0"
      }
    },
    {
      "identity" : "app-check",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/app-check.git",
      "state" : {
        "revision" : "3b62f154d00019ae29a71e9738800bb6f18b236d",
        "version" : "10.19.2"
      }
    },
    {
      "identity" : "firebase-ios-sdk",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/firebase-ios-sdk",
      "state" : {
        "revision" : "eca84fd638116dd6adb633b5a3f31cc7befcbb7d",
        "version" : "10.29.0"
      }
    },
    {
      "identity" : "google-tag-manager-ios-sdk",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/googleanalytics/google-tag-manager-ios-sdk",
      "state" : {
        "revision" : "6883594bd2db065aac641851abd1f24f0f8910fb",
        "version" : "7.4.6"
      }
    },
    {
      "identity" : "googleappmeasurement",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleAppMeasurement.git",
      "state" : {
        "revision" : "fe727587518729046fc1465625b9afd80b5ab361",
        "version" : "10.28.0"
      }
    },
    {
      "identity" : "googledatatransport",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleDataTransport",
      "state" : {
        "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565",
        "version" : "9.4.0"
      }
    },
    {
      "identity" : "googleutilities",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleUtilities.git",
      "state" : {
        "revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6",
        "version" : "7.13.3"
      }
    },
    {
      "identity" : "grpc-binary",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/grpc-binary.git",
      "state" : {
        "revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359",
        "version" : "1.62.2"
      }
    },
    {
      "identity" : "gtm-session-fetcher",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/gtm-session-fetcher.git",
      "state" : {
        "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
        "version" : "3.5.0"
      }
    },
    {
      "identity" : "interop-ios-for-google-sdks",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
      "state" : {
        "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648",
        "version" : "100.0.0"
      }
    },
    {
      "identity" : "leveldb",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/leveldb.git",
      "state" : {
        "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
        "version" : "1.22.5"
      }
    },
    {
      "identity" : "nanopb",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/nanopb.git",
      "state" : {
        "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
        "version" : "2.30910.0"
      }
    },
    {
      "identity" : "promises",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/promises.git",
      "state" : {
        "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
        "version" : "2.4.0"
      }
    },
    {
      "identity" : "swift-protobuf",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-protobuf.git",
      "state" : {
        "revision" : "ebc7251dd5b37f627c93698e4374084d98409633",
        "version" : "1.28.2"
      }
    }
  ],
  "version" : 3
}

これは余談ですが、Package.swiftの// swift-tools-version6.0にするとresolvedのpinsのバージョンは3が適用され、それ以下でセットすると2が適用される。

SPMのPackage.resolvedはCocoapodsのPodfile.lockとは違い、それぞれのライブラリのdependencyの依存関係までは読み解くことができず、最終的なresolveした結果しか載っていません。
これだと何のバージョンが何に依存しているかがわかりませんよね。
ではこの依存関係を読み解いていきましょう。

show-dependenciesコマンドを使用する

swift packageコマンドにはshow-dependenciesというコマンドがあります。これを使用すると、下記のようなtree構造で出力されます。

$ cd <プロジェクトpath>/Dependencies
$ swift package show-dependencies
.
├── firebase-ios-sdk<https://github.com/firebase/firebase-ios-sdk@10.29.0>
│   ├── promises<https://github.com/google/promises.git@2.4.0>
│   ├── swift-protobuf<https://github.com/apple/swift-protobuf.git@1.28.2>
│   ├── googleappmeasurement<https://github.com/google/GoogleAppMeasurement.git@10.28.0>
│   │   ├── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│   │   │   └── promises<https://github.com/google/promises.git@2.4.0>
│   │   └── nanopb<https://github.com/firebase/nanopb.git@2.30910.0>
│   ├── googledatatransport<https://github.com/google/GoogleDataTransport@9.4.0>
│   │   ├── nanopb<https://github.com/firebase/nanopb.git@2.30910.0>
│   │   ├── promises<https://github.com/google/promises.git@2.4.0>
│   │   └── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│   │       └── promises<https://github.com/google/promises.git@2.4.0>
│   ├── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│   │   └── promises<https://github.com/google/promises.git@2.4.0>
│   ├── gtm-session-fetcher<https://github.com/google/gtm-session-fetcher.git@3.5.0>
│   ├── nanopb<https://github.com/firebase/nanopb.git@2.30910.0>
│   ├── abseil-cpp-binary<https://github.com/google/abseil-cpp-binary.git@1.2024011602.0>
│   ├── grpc-binary<https://github.com/google/grpc-binary.git@1.62.2>
│   │   └── abseil-cpp-binary<https://github.com/google/abseil-cpp-binary.git@1.2024011602.0>
│   ├── leveldb<https://github.com/firebase/leveldb.git@1.22.5>
│   ├── interop-ios-for-google-sdks<https://github.com/google/interop-ios-for-google-sdks.git@100.0.0>
│   └── app-check<https://github.com/google/app-check.git@10.19.2>
│       ├── promises<https://github.com/google/promises.git@2.4.0>
│       └── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│           └── promises<https://github.com/google/promises.git@2.4.0>
├── google-tag-manager-ios-sdk<https://github.com/googleanalytics/google-tag-manager-ios-sdk@7.4.6>
│   ├── firebase-ios-sdk<https://github.com/firebase/firebase-ios-sdk@10.29.0>
│   │   ├── promises<https://github.com/google/promises.git@2.4.0>
│   │   ├── swift-protobuf<https://github.com/apple/swift-protobuf.git@1.28.2>
│   │   ├── googleappmeasurement<https://github.com/google/GoogleAppMeasurement.git@10.28.0>
│   │   │   ├── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│   │   │   │   └── promises<https://github.com/google/promises.git@2.4.0>
│   │   │   └── nanopb<https://github.com/firebase/nanopb.git@2.30910.0>
│   │   ├── googledatatransport<https://github.com/google/GoogleDataTransport@9.4.0>
│   │   │   ├── nanopb<https://github.com/firebase/nanopb.git@2.30910.0>
│   │   │   ├── promises<https://github.com/google/promises.git@2.4.0>
│   │   │   └── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│   │   │       └── promises<https://github.com/google/promises.git@2.4.0>
│   │   ├── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│   │   │   └── promises<https://github.com/google/promises.git@2.4.0>
│   │   ├── gtm-session-fetcher<https://github.com/google/gtm-session-fetcher.git@3.5.0>
│   │   ├── nanopb<https://github.com/firebase/nanopb.git@2.30910.0>
│   │   ├── abseil-cpp-binary<https://github.com/google/abseil-cpp-binary.git@1.2024011602.0>
│   │   ├── grpc-binary<https://github.com/google/grpc-binary.git@1.62.2>
│   │   │   └── abseil-cpp-binary<https://github.com/google/abseil-cpp-binary.git@1.2024011602.0>
│   │   ├── leveldb<https://github.com/firebase/leveldb.git@1.22.5>
│   │   ├── interop-ios-for-google-sdks<https://github.com/google/interop-ios-for-google-sdks.git@100.0.0>
│   │   └── app-check<https://github.com/google/app-check.git@10.19.2>
│   │       ├── promises<https://github.com/google/promises.git@2.4.0>
│   │       └── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│   │           └── promises<https://github.com/google/promises.git@2.4.0>
│   └── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
│       └── promises<https://github.com/google/promises.git@2.4.0>
└── googledatatransport<https://github.com/google/GoogleDataTransport@9.4.0>
    ├── nanopb<https://github.com/firebase/nanopb.git@2.30910.0>
    ├── promises<https://github.com/google/promises.git@2.4.0>
    └── googleutilities<https://github.com/google/GoogleUtilities.git@7.13.3>
        └── promises<https://github.com/google/promises.git@2.4.0>

これでPodfile.lockのようなdependency情報を出すことができました。

PDFへと変換する

ただ、まだ見にくいですよね、これをPDFにしていきましょう。一度Graphvizのdot言語にするために
--formatをいうoptionを使いましょう。

swift package show-dependencies --format dot

これをさらに、PDFに変換します。コマンドをまとめるとこんな感じです。

swift package show-dependencies --format dot | dot -Tpdf -o graph.pdf

すると下記のように書き出されます。おっと、zennはPDFガゾウガハレナイミタイエデスネ。
仕方がないですね、ではPNGで書き出しましょう。

$ swift package show-dependencies --format dot | dot -Tpng -o graph.png


できました!

Discussion