21 August 2024 4min read

Swift Package Manager Dependency Owners

In my team, one of the things we always make sure of is keeping our dependencies up to date. We rarely add a dependency without a really good reason, and when we do, we assign an owner who is responsible for keeping it up to date.

These owners always keep an eye on new updates and make it a task to go through them as soon as they can. But sometimes, life happens, and one is not able to keep up with it. More often than not, though, the reason why updates are not kept on top of is simply because people forget that it’s part of their responsibilities. And I don’t blame them. Right now, we just keep the list of owners in our documentation site, which is a very passive way of doing things.

Instead, we’ve now worked on upgrading our nightly notification system! This existing system not only runs a set of unit tests and sends a message with test results and code coverage to our Slack channel, but it also shows a list of outdated dependencies thanks to swift-outdated and some custom code on top that is part of our development tools.

So before, the message just included a table of dependencies to be updated:

| Package | swift-composable-architecture | 1.12.0 | 1.13.0 | ❗️

But now, we’ve added an @mention to the Slack user responsible for each outdated dependency.

In my experience, the reason updates often get neglected is simply because it can be easy to forget or overlook tasks that aren’t constantly in front of you. That’s why I think having a system for notes is so crucial - it helps keep important responsibilities top-of-mind. By providing a daily reminder of the pending updates they are responsible for, our new notification system has helped streamline things for the whole team.

Doing this is very simple since we already have the infrastructure in place. It was just a matter of having a mapping between dependencies and Slack users in our CLI. Something very low-tech that does the work 👌

But there was an extra requirement of mine: if there is a dependency we don’t have an owner for, we send an error on the nightly message so we can assign one immediately. This is something not trivial with swift-outdated because that reads from the .resolved file, which has no information about what dependencies are transitive and which ones are directly yours. Since in our opinion, we don’t want to be bothered with transitive dependencies, we don’t assign owners to those.

To solve this, we read the Package.swift file in advance so we know what the direct dependencies are. This is quite simple, as SPM makes it very easy to get a JSON representation of a package: swift package dump-package.

So work done.

But you know me, I can’t avoid getting excited with interesting ideas. So when discussing how to approach this with my team, I wondered… what if we could attach the owner to the dependency itself in the Package.swift file? Well, hold my glass of water.

Something I love about SPM is that the package definition file is actually a Swift file, which means you can run Swift code in it. Thanks to that, we can add an API to the PackageDescription that does what we want.

.package(url: "...", exact: "...", owner: "...")

To do this, we can extend the SPM types with our own method. Of course, we need to return what SPM expects.

extension PackageDescription.Package.Dependency {
    static func package(
        url: String,
        exact version: Version,
        owner: String
    ) -> PackageDescription.Package.Dependency {
        .package(url: url, exact: version)
    }
}

So what’s the point of having the new parameter if it’s not used? Well, that we have it in the same source, and we can extract it from it!

But if you’re thinking SPM dump will include this data, well, of course, it won’t, as SPM doesn’t know anything about it. That doesn’t mean we can’t get it; it just means it’s our work. For things like this, I always rely on swift-parsing, and it makes it quite easy to extract the dependency from the file.

This is the output of running a quick proof of concept command line:

▶ ./spm-owners Package.swift
┏━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃   ┃ Package                       ┃ Version  ┃ Owner    ┃
┃   ┃ <String>                      ┃ <String> ┃ <String> ┃
┡━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ 0 │ swift-parsing                 │ 0.13.0   │ Martin   │
│ 1 │ swift-argument-parser         │ 1.5.0    │ Martin   │
│ 2 │ swift-composable-architecture │ 1.13.0   │ Alex     │
└───┴───────────────────────────────┴──────────┴──────────┘

I love this! Having the ownership data as part of the Package.swift makes it so things are always in sync and up to date.

Of course, I’m not using this in our team’s system because it’s not production-ready; it’s just a proof of concept. The big disadvantage is that although Package.swift can run Swift code, you can’t just import a separate package, which means one would have to copy-paste the code above manually. Not a deal-breaker, but not nice enough for me to want to maintain 😂 If one could import in the package file, it would be another story.

So there you go, an interesting proof of concept that has kept me entertained for a couple of hours. I wish SPM would officially support a way to accomplish this, somehow to be able to add metadata and that it would be kept for other tools. And maybe there is, and I’m not aware?

You can check out the proof of concept on the spm-owners repo, if you are curious.

If you enjoyed this post

Continue reading