Swift Package Manager Logo

Implementing Swift Package Manager –The Manifest File

This entry is part 3 of 14 in the series Swift Package Manager

The Package Manifest Is An Executable Swift File

Unlike manifests for other build systems, which tend to be declarative (non-executable) files, the SPM manifest is an actual Swift source code file that is consumed and executed by the Swift build system, and used to control the build of the package. It is provided by the package creator, and used during the package consumption.

The principal goal of Package.swift is to create an instance of the Package class, and assign it to a module-scope variable called “package.”

That is the principal artifact of the Package Manifest. Since it is executable, there are actions that can happen, in order to develop this artifact.

Here is the source listing for the Package class.

As you can see, it is declared public final class. That means that it is visible to be used and instantiated by anyone that imports the PackageDescription module, but cannot be subclassed.

It can, however, be extended, and it is possible to add executable statements in the code; much like in the main.swift file for a command-line utility. We can’t override any of the built-in methods or properties, and there are no default-implementation protocol elements (meaning that we can only add to it with extension).

The package execution happens with the Foundation module being included, but no other standard libraries, and that should be kept in mind, when writing executable code.

Swift Tools Version

An important consideration, is that the first non-blank line MUST be a “two-slash” comment line, in the following format:

// swift-tools-version:XXX.XXX

with “XXX.XXX” being a semantic version, indicating the Swift build tools version required as a minimum baseline to build the package.

It is important to note that NO OTHER TEXT can precede this line in the file, so it has to come before any standard licensing header.

Instantiating the Package Class

The most important job of the Package.swift file is to create an instance of the Package class, and assign it to a module-scope immutable variable, called package.

import PackageDescription

let package = Package(< ARGUMENTS >)

The initializer arguments (“< ARGUMENTS >“) is where most of the action happens. The class init() is set up, so that most of the arguments are optional, but, if provided, can be quite extensive.

The only required argument is name, but the instance is pretty much worthless, without some of the other arguments.

Several of these arguments are collections of other classes/structs/enums, which I’ll get to, on a case-by-case basis.

The two that should definitely be provided, are products, and targets.

The Products Argument

This defines the result of the Swift build. There can be multiple products.

It is an Array, with each element, an instance of the Product class.

The Product class is instantiated with a name argument, which will be used as the product name, but not the module name (important. I’ll get to that). It will also have an optional type argument, which will indicate what subtype the product will be (I’ll get to that), and also a targets argument, which is an Array of String, which will indicate the targets used to build the product. These strings will act as “keys” for the main targets argument.

Currently, there are two types of product: .library, and .executable.

Library Product Type

The result of this is a library (either static or dynamic), that can be linked (and maybe embedded) into the package consumer deliverable.

The type of library is determined by the optional type argument. If it is .dynamic, then the resulting library will be a Swift dylib, and will need to be embedded. If it is left out, or specified as .static, then the result will be a static library that will be “hard-linked” into the deliverable.

Executable Product Type

This is a simple type that ignores the type argument. The deliverable is considered to be some form of executable.

AN IMPORTANT NOTE ABOUT THE NAME

The name argument in the products Array defines the deliverable wrapper name, not the link name. This becomes important when you are including dynamic libraries in an app that is to be submitted to the App Store.

The App Store requires a particular naming convention (bundle ID) that disallows underscores (_). This name is the one used to build that deliverable’s bundle ID, so it cannot have underscores. The library module can have underscores (that is defined by the main name argument), but this name cannot use them. I use this name argument to replace the underscores with dashes.

It should also be noted that the library/module name (what we import) is defined by the main name argument. This means that we don’t declare separate library names for different destinations, if the code is the same (the usual LibraryName_iOS, LibraryName_macOS, etc.).

The Targets Argument

This is an Array of the Target class, that defines the targets that are used to create the builds. The name argument for each target is used as a “key” for the strings in the targets argument of each product. They have the same name, but are different types.

Regular targets are created using the .target static factory function.

Each element is instantiated with the following arguments:

  • name (String)
    This is the only required argument. It should be a name that is used by the targets Array in the products argument. It is not used for anything else.
  • dependencies (Array of Target.Dependency) –NOTE: NOT Package.Dependency)
    This outlines any dependencies required to build this target. It is a simple type that contains a name, and a package, which is a “key” to the global dependency list (more on that in a bit).
  • path (String)
    This is a path, relative to the root for the module (the location of the Package file), to use as the search base for the source files to be used to build this target.
    Source files are searched recursively, so if this is a directory, or elements of the sources or exclude Array are directories, they will be recursively searched.
  • exclude (Array of String)
    This tells the target to ignore specific filenames. These should be explicit filenames, relative to the root, as defined by path.
  • sources (Array of String)
    This tells the target to explicitly consider the files to be valid sources for the build. These are relative to the root, as defined by path. Additionally, these are subservient to the exclude Array. If the same filename is listed in exclude, then it will not be considered.
  • resources (Array of String)
    This tells the target to include the indicated files as resources in the build. These are relative to the root, as defined by path. Additionally, these are subservient to the exclude Array. If the same filename is listed in exclude, then it will not be considered.
  • publicHeadersPath (String)
    This points to a directory, containing C-style header files, to be embedded as public headers.
  • There are four more “settings” arguments that allow a more granular control of the build, but I won’t cover them here, as I’m trying to keep this as simple as possible.

It should be mentioned that the presence of an Xcode project or workspace file means nothing. The build is done via the swift build command.

This can also be used to aggregate test (XCTest) targets, by using the .testTarget static factory function, which uses the same argument list as the .target function, minus the last six arguments (the resources and publicHeadersPath arguments, and the four “settings” arguments).

The Dependencies Argument

This is an optional argument, but is required, if any of the targets in this project are dependent upon another Swift Package. It is an Array of the Package.Dependency class.

Each Array element in the dependencies argument contains a name, a URI, and version range.

This is different from the dependencies listed in the targets Array. It’s confusing, and something that hung me up for quite a while.

The contents of this Array are what is used to actually fetch the dependency from the linked package. The target is a reference to the package defined in this Array.

YOU MUST HAVE BOTH for dependencies to work. The main Array tells you where to get the dependency, and the required version, and the target dependency links that to the target build.

It should also be noted that this only covers dependencies that are required for the deliverable target builds. It does not apply to dependencies that may be used internally, for targets such as tests or test harnesses. We only need to declare dependencies that the consumer will need, in order to use this package’s deliverable target.

The Platforms Argument

This is optional, but should be provided. It is an Array of Platform. It indicates which platforms, and which versions of those platforms, are eligible for the products. These apply to every product in the products argument.

The platforms can be macOS, iOS, watchOS, tvOS, and linux, with the arguments specifying the version. This argument is a String, with a semantic version, or it can be a predefined constant.

There’s a heck of a lot more to this file, but this covers the basics of what we need to use it for apps on the MacOS, iOS, iPadOS, WatchOS, or TVOS platforms.

Here is an example of a fairly simple shipping Package.swift file (From the RVS_BlueThoth project):

// swift-tools-version:5.2
import PackageDescription

let package = Package(
    name: "RVS_BlueThoth",
    platforms: [
        .iOS(.v11),
        .tvOS(.v11),
        .macOS(.v10_14),
        .watchOS(.v5)
    ],
    products: [
        .library(name: "RVS-BlueThoth", type: .dynamic, targets: ["RVS_BlueThoth"])
    ],
    dependencies: [
        .package(name: "RVS_Generic_Swift_Toolbox", url: "git@github.com:RiftValleySoftware/RVS_Generic_Swift_Toolbox.git", from: "1.2.1")
    ],
    targets: [
        .target(
            name: "RVS_BlueThoth",
            dependencies: [
                .product(name: "RVS-Generic-Swift-Toolbox", package: "RVS_Generic_Swift_Toolbox")
            ],
            path: "./src/Source"
        )
    ]
)

It is a simple library that can be compiled for the four major Apple environments, and has a single dependency on the RVS_Generic_Swift_Toolbox project.

Note the two places where dashes (-) are used, instead of underscores (_). This is only required for dynamic libraries, as they are embedded in the deliverable.