When should one use Targets instead of Packages in Swift Package Manager?

I am trying to understand the best practices for organizing dependencies in a Swift project using Swift Package Manager. I have come across the concepts of Targets and Packages, but I am unsure of when it is more appropriate to use one over the other.

  1. Targets: I understand that targets in Swift Package Manager are used to group related source files together and define build settings for them. When is it more beneficial to create multiple targets within a package rather than creating separate packages for each set of related functionality?

  2. Packages: On the other hand, packages in Swift Package Manager allow you to define a collection of targets and their dependencies. In what scenarios would it be more advantageous to create separate packages for different sets of functionality rather than grouping them under a single package with multiple targets?

I would appreciate some guidance on how to make an informed decision between using Targets and Packages in Swift Package Manager based on factors such as modularity, code organization, reusability, build time and maintainability. Thank you!

2 Likes

In this question one important concept is omitted: products.

Let's put it this way: in SwiftPM target = module. I'd go as far as to say that target should be renamed to module, because the current naming is confusing, "target" has too many meanings in other contexts (there are also "target triple" and "build system target"), while "module" is quite unambiguous.

A product is a collection of modules that can be consumed by other packages (or Xcode projects if you're developing that way).

Then it's up to you to find the best position on this spectrum: everything is in a single module ↔ everything is split into packages.

Keeping everything in a single module seems like a good default, but unfortunately the only way to control how symbols are consumed in the same module are private and fileprivate modifiers. You can't easily come up with an acyclic graph of dependencies between symbols in the same module. With big modules it's easy to end up with a spaghetti of dependencies that will get in the way if you need to modularize it.

Separate packages bring enough overhead: you have to version them, and then if you version them you better be following SemVer. Then every time a new API is added, you have to bump the version both in the APIs package and potentially in all packages depending on it.

Thus if you don't need APIs consumed externally and don't have very tight performance constraints, splitting into modules, but not exposing them as products is a good middle ground in a lot of cases. Also, if you don't have any products in your package, your default access modifier of choice is package instead of public, allowing you to iterate on and refactor APIs more easily without concerns about breaking other packages.

When you have hard constraints on binary size and need to apply certain performance optimizations, then you'll need to start thinking about CMO, LTO, which APIs are generic/inlinable etc. That will have an impact on how you split code into modules.

13 Likes

Separate packages bring enough overhead: you have to version them, and then if you version them you better be following SemVer

There another step before moving to "fully separate" packages (i.e. packages in different git repos) which is local packages (i.e. packages in the same repo that you refer to by path). This allows you to avoid needing to deal with versioning/coordination but still have the benefits of separating functionality into its own package (for example, being able to open that package by itself and having dependencies scoped to just the targets in a package).

4 Likes

keep in mind though there are some limitations with “local” packages. notably, i don’t believe it’s possible to use such a package as a dependency of another package. so this usually only makes sense for things like benchmarks, tests, etc. that no client would realistically depend on.

there is also some tooling complexity with multiple SwiftPM packages in the same repo. for example, it might be more difficult to generate documentation for the packages.