Package.swift in details

In the last two posts we discovered the theory behind Swift Package Manager and gained knowledge about its core functions: add, edit and update a package. Now the time has come to dig into details of Package.swift file – configuration file for a given package.

Let’s see how it’s built!

Overview

Package.swift as mentioned earlier is the main configuration file for the package. The configuration itself is done via initialization of Package class. The convention is that we provide a single nested init statement for this class and then do not change it.

Example of Package.swift from Cards:

// swift-tools-version:5.1

import PackageDescription

let package = Package(
    name: "Cards",
    platforms: [.iOS(.v13)],
    products: [
        .library(
            name: "Cards",
        targets: ["Cards"]
        ),
],
dependencies: [
        .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.35.8"),
        .package(url: "https://github.com/dziennikp/DesignTheme", from: "0.2.0")
],
    targets: [
        .target(
            name: "Cards",
            dependencies: ["DesignTheme"]
        ),
        .testTarget(
            name: "CardsTests",
            dependencies: ["Cards"]
        ),
    ],
    swiftLanguageVersions: [.v5]
)

Note the first two lines of the file, that are obligatory for each Package.swift.

Swift-tools-version simply declares the minimum version of Swift required to build this package.

PackageDescription, on the other hand, is required for Package class.

Now let’s discuss all of the possible parameters of initialization statement (not all of them are present in the snippet above)

Parameters

Name 

name property is just the name of Swift package

Products

products property is a list of products from our package. As mention in our first post from this series [https://blog.aspiresys.pl/technology/swift-package-manager], a product could be library or executable. Given that we have two initializers:

static func library(name: String, type: Product.Library.LibraryType? = nil, targets: [String]) -> Product 

static func executable(name: String, targets: [String]) -> Product 

We can see that Product.library has optional parameter type which can be either static or dynamic. If nil is passed then the type of library if chosen based on the client’s preference.

Targets

The target contains a set of source files compiled into a module or test suite. We can define it as a basic building block in Swift. They are vented to other packages by including it in products. The target can have dependencies on other targets in the same package or on products listed in dependencies.

There are 3 types of the target we can set: target, testTarget, systemLibrary.

target – contains code for package functionality

testTarget  – contains tests for package other targets

Both types use the same function to define:

targets: [
    .target(name: String,
              dependencies: [Target.Dependency],
              path: String?,
              exclude: [String]?,
              sources: [String]?,,
cSettings: [CSetting]?,
cxxSettings: [CXXSetting]?,
swiftSettings: [SwiftSetting]?,
linkerSettings: [LinkerSetting]?),

.testTarget(name: String ...)
],

  1. Name – the name of the target
  2. Path – relative to the package root, the path to the target. If nil SPM will look at predefined search paths and subdirectory with the target’s name. Predefined paths are: `Tests`, `Sources`, `Source`, `src`, `srcs`.
  3. Exclude – the paths you want to exclude from source inference.
  4. Sources – the source files for the target. If nil, all valid sources from the path will be included.
  5. CSettings, cxxSettings, swiftSettings, linkerSettings – build settings that we can define using  BuildConfiguration:
.target(
name: "MyTool",
dependencies: ["Utility"],
cSettings: [
.headerSearchPath("path/relative/to/my/target"),
.define("DISABLE_SOMETHING", .when(platforms: [.iOS], configuration: .release)),
],
swiftSettings: [
.define("ENABLE_SOMETHING", .when(configuration: .release)),
],
linkerSettings: [
.linkLibrary("openssl", .when(platforms: [.linux])),
]
),

systemLibrary – adapts library on the system (most often written in C) to work with package

public static func systemLibrary(name: String, path: String? = nil, pkgConfig: String? = nil, providers: [PackageDescription.SystemPackageProvider]? = nil) -> PackageDescription.Target
  1. Name – name of the target
  2. Path –  custom path for the target. By default, a targets sources are expected to be located in the predefined search paths,  such as `[PackageRoot]/Sources/[TargetName]`.
  3. PkgConfig – The name of the `pkg-config` file for this system library.
  4. Providers – The providers for this system library.

Supported platforms

This list contains each platform our package will support. Possible values are all of Apple devices along with concrete versions e.g. .iOS(.v13) and Linux platform

products: [
   .executable(name: "tool", targets: ["tool"]),
   .library(name: "Paper", targets: ["Paper"]),
   .library(name: "PaperStatic", type: .static, targets: ["Paper"]),
   .library(name: "PaperDynamic", type: .dynamic, targets: ["Paper"]),
 ],

Supported languages

We can enlist supported Swift versions using swiftLanguageVersions property. There is also a possibility to define C and C++ respectively via cLanguageStandard and cxxLanguageStandard properties.

Dependencies

dependencies property defines… well, dependencies used by our package. A package dependency consists of a Git URL to the source of the package and a requirement for the version of the package. Note that we can also use local URL for dependency:

dependencies: [
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.35.8"),
.package(url: "../DesignTheme", from: "0.0.0"),
],

System providers

We can define a list of system packages using providers property. Depending on the system we can choose between Homebrew and apt-get packages:

static func brew(_ packages: [String]) -> SystemPackageProvider 

static func apt(_ packages: [String]) -> SystemPackageProvider

PKG config

pkgConfig property is the name that will be used for all of C modules. If its present, the Swift Package Manager will search for a <name>.pc file to get the required additional flags for a system target.

Summary

This is the end of our Swift Package Manager. We learned the basics, but also some depths of it. Now we can start using SPM in our applications!

But be aware that not all of the packages we are used to are available for SPM. So before deciding that you will use it for brandy new application, do some analysis if you can go live with SPM as it has some limitations which are blockers for some apps! What they are? Well, that could be next out-of-series post 🙂

Stay tuned! 

Tags: