Use Swift Package Manager in your own Swift Package

Use Swift Package Manager in your own Swift Package

Programmatically access Swift package information (Last updated: Nov 2022)

Β·

3 min read

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.

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!