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 mainname
argument), but this name cannot use them. I use thisname
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
, that defines the targets that are used to create the builds. The Target
classname
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
ofTarget.Dependency
) –NOTE: NOTPackage.Dependency
)
This outlines any dependencies required to build this target. It is a simple type that contains aname
, and apackage
, 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 thesources
orexclude
Array
are directories, they will be recursively searched.exclude
(Array
ofString
)
This tells the target to ignore specific filenames. These should be explicit filenames, relative to the root, as defined bypath
.sources
(Array
ofString
)
This tells the target to explicitly consider the files to be valid sources for the build. These are relative to the root, as defined bypath
. Additionally, these are subservient to theexclude
Array
. If the same filename is listed inexclude
, then it will not be considered.resources
(Array
ofString
)
This tells the target to include the indicated files as resources in the build. These are relative to the root, as defined bypath
. Additionally, these are subservient to theexclude
Array
. If the same filename is listed inexclude
, 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.