Accessing Swift Package Manager dependency versions at runtime

I was building a debug screen for an iOS/Mac app and wanted to display the version of a third-party dependencies managed by Swift Package Manager.

My first instinct was to use an Xcode run script build phase to extract version information at build time, but I quickly ran into various code signing errors that made this approach unreliable. Especially because building for macOS triggered code signing errors for different configurations than iOS did. I didn't look very hard, but I wasn't able to find a configuration that worked for both platforms.

After some experimentation, I found a solution that works reliably: copying the Package.resolved file at build time and parsing it at runtime. The Package.resolved file contains the exact commit hashes and versions of all your dependencies, so it's perfect for this use case.

Here's how to set it up. First, create a run script build phase that copies the file into your project directory:

cp ${PROJECT_FILE_PATH}/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ${PROJECT_DIR}/Package.resolved

Make sure to add ${PROJECT_DIR}/Package.resolved under Output Files in your run script build phase. This tells Xcode that this file is generated by the script. I don't know since when but at least in Xcode 26 build scripts run in a sandbox. If you forget to add input and output files Xcode will complain.

Next, add the copied file to your Xcode project (you can drag it into the project navigator or right-click and select "Add Files to [project name]"). Ensure the file is included in "Copy Bundle Resources" and that your run script build phase runs before the copy resources phase.

Package.resolved is just JSON, so we can decode it with a simple struct:

// SPM.swift

import Foundation

struct SPM: Codable {
 let pins: [Package]

 struct Package: Codable {
  struct State: Codable {
   let revision: String
  }

  let identity: String
  let state: State
 }

 static func allPackages() -> [Package] {
  guard let packagesPath = Bundle.main.path(forResource: "Package", ofType: "resolved"),
     let data = try? Data(contentsOf: URL(fileURLWithPath: packagesPath)) ,
     let resolved = try? JSONDecoder().decode(SPM.self, from: data)
  else {
   return []
  }

  return resolved.pins
 }
}

The allPackages() method loads the bundled Package.resolved file and decodes it into an array of packages. Each package includes its identity (name) and the exact commit revision. If you need other fields, take a look at your Package.resolved file to see what's available and add it to the SPM hierarchy.

I don't yet know how reliable this is, but it works fine so far. I later realized it's the same technique used by SPM-Acknowledgments to auto-generate acknowledgments screens. At the time of writing SPM-Acknowledgments was last updated in 2020 which suggests that this approach is stable enough. For me it's for debug information so it's okay if it stops working.