Use Swift Package Manager in your own Swift Package
Programmatically access Swift package information (Last updated: Nov 2022)
As a Swift developer, you are probably aware of Apple's Swift Package Manager (SPM) as a command-line interface to create and work with Swift packages π¦.
But did you know that you can also use Swift Package Manager as a package dependency in your own Swift Package?
Yes, you can π because SPM is also a package that exposes the library SwiftPM
under the same product name. The community generally refers to it as libSwiftPM
and I will continue to use that term in this blog post.
For example, it is possible to create an executable Swift package in which you can programmatically access manifest information from other Swift packages on your local machine and print that information in your terminal.
The GitHub repository of Swift Package Manager contains such an example: package-info
However, the package dependency to libSwiftPM
is declared a local, relative package. We want to use libSwiftPM
as a remote package dependency, so let's re-build the example.
First, we create a folder and name it swift-package-info
. Then we use Swift Package Manager to create our executable package.
swift package init --type executable
IMPORTANT: The description below was written using Swift Package Manager for Swift 5.3.
In November 2022 I updated my swift-package-info to use and be compatible with Swift 5.7
Now let's add Swift Package Manager as a dependency to our package. There is a caveat for Swift 5.3 as libSwiftPM does not have a version tagged for 5.3 (status: March 2021)
Thankfully there is a community fork that has a version tagged and otherwise no modifications.
.package(name: "SwiftPM", url: "https://github.com/SDGGiesbrecht/swift-package-manager.git", .exact("0.50302.0")),
The complete manifest of our package
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "swift-package-info",
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(name: "SwiftPM", url: "https://github.com/SDGGiesbrecht/swift-package-manager.git", .exact("0.50302.0")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "swift-package-info",
dependencies: ["SwiftPM"]),
.testTarget(
name: "swift-package-infoTests",
dependencies: ["swift-package-info"]),
]
)
Now we can make use of modules in libSwiftPM
to access several levels of information. In this blog post I'll focus on the manifest itself.
The ManifestLoader
of module PackageLoading
is a utility class for reading the manifest data and produce a properly formed PackageModel.Manifest
object. Hence, we import the necessary module in our main.swift
file
import PackageLoading
The API requires knowing where the Swift compiler is. The following code snippet(s) are straight from the original example and some are slightly simplified.
// PREREQUISITES
// ============
// We will need to know where the Swift compiler is.
let swiftCompiler: AbsolutePath = {
let string: String
#if os(macOS)
string = try! Process.checkNonZeroExit(args: "xcrun", "--sdk", "macosx", "-f", "swiftc").spm_chomp()
#else
string = try! Process.checkNonZeroExit(args: "which", "swiftc").spm_chomp()
#endif
return AbsolutePath(string)
}()
For sake of simplification let's assume that our executable Swift Package will run in a directory in which a root package is available.
// We need a package to work with.
// This assumes there is one in the current working directory:
let packagePath = localFileSystem.currentWorkingDirectory!
With the path to the package in question as well as the Swift compiler we can finally load the manifest.
let diagnostics = DiagnosticsEngine()
let manifest = try ManifestLoader.loadManifest(packagePath: packagePath, swiftCompiler: swiftCompiler, packageKind: .root)
Now we can programmatically access information like products or targets.
// Manifest
let products = manifest.products.map({ $0.name }).joined(separator: ", ")
print("Products:", products)
let targets = manifest.targets.map({ $0.name }).joined(separator: ", ")
print("Targets:", targets)
The full source code, including other APIs, is available here.