In the past weeks I was creating more and more small Swift libraries to extract common utilities I use in my day to day work into separate modules. For this purpose I was using the Swift Package Manager (SPM) as it seems it will be the primary dependency manager to be used in Swift based projects in the future.
In this post I want to elaborate how to use SPM to create a standalone library and use GitHub actions to run the unit tests defined by your package.
Create a library
To create a library the first thing to do is to run the spm
command line tool to create one.
mkdir MyAwesomeLibrary
cd MyAwesomeLibrary
swift package init --type library
This command creates the following base structure for a Swift library.
.
├── Package.swift
├── README.md
├── Sources
│ └── MyAwesomeLibrary
│ └── MyAwesomeLibrary.swift
└── Tests
├── LinuxMain.swift
└── MyAwesomeLibraryTests
├── MyAwesomeLibraryTests.swift
└── XCTestManifests.swift
The most important parts of this structure are the Package.swift
file and the Sources
and Tests
directories. As the name already suggests, Sources
and Tests
contain your sources and test files respectively.
Package.swift
For now let us focus on the Package.swift
. This file is a written manifest of you SPM package. It describes what your package contains and what it produces as its output.
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MyAwesomeLibrary",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "MyAwesomeLibrary",
targets: ["MyAwesomeLibrary"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "MyAwesomeLibrary",
dependencies: []),
.testTarget(
name: "MyAwesomeLibraryTests",
dependencies: ["MyAwesomeLibrary"]),
]
)
The first line sets the syntax version in which the manifest file is written in. This is important if you package has to work with older version of the SPM command line tools. The complete content inside the file is written in Swift, which makes it easy for Swift developers to structure and write such a file.
Within the products
array developers can declare which executables and libraries their package is producing and make them visible in order to be consumed by other libraries or executables. In this small introduction we will just have one simple library which includes a single target.
The dependencies
array declares other packages that the package we are creating is depending on. This is a package global description. In other words you can declare your dependencies here and import them throughout every target you define in your package.
targets
declare the different targets that live within your package. They can depend on other targets of your package or on any of the external dependencies declared in the dependencies
array.
If you want to have a look at a more sophisticated Package.swift
file, you can have a look into my libraries/executables I created, e.g:
Please keep in mind that these files can change from time to time since they are subject to change due to undergoing development.
Run SPM Tests
To run the tests defined the manifest file the only thing we need to run is:
swift test --enable-code-coverage
Please note that I have added the --enable-code-coverage
flag which generates a .json
file. This file contains information about the code coverage of the tests. This is useful if you want to use some tool to visualize the code coverage of your tests.
Integrate SPM test into GitHub Actions
After looking at the SPM manifest file and having the knowledge of how to run tests for our small library, we can now create a workflow file for GitHub actions to execute the tests.
A workflow file is a description, written in the .yml
format, of what GitHub Actions should run we certain events happen. The file must be placed under .github/workflows/
, in your projects directory, in order for GitHub to find and parse it.
For our small example the file could look like this:
name: CI
on:
# Trigger the workflow on pull requests on main and develop branch
pull_request:
branches:
- main
- develop
jobs:
tests:
name: Unit-Tests
runs-on: macOS-latest
steps:
- name: Checkout repository
uses: actions/checkout@v1
- name: Run Unit Tests
run: |
swift test --enable-code-coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./.build/debug/codecov/MyAwesomeLibrary.json
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
The first thing to do is to define what should trigger the jobs
. This is done via the on
section. In this example the jobs
section is run every time when a pull request is made to the main
or develop
branch.
The jobs
section consists of one or more job that each defines a name, an environment in which the job runs and several smaller steps that are executed during the job. In this small example we have a single step that executes our unit tests. It has the name "Unit-Tests"
and runs on the latest macOS version. You can also define a specific macOS version or even a linux distribution as the environment. A list of all available environments and their installed software can be found on the documentation page of GitHub.
Next, the different steps that are executed during the job are defined. A job can either reference an already existing step hosted on GitHub or be a shell script that is run when executed. When a external hosted step is referenced, the uses
keyword is used. The run
keyword is used when we want to execute a simple shell script as part of a step. There are many more keywords that can be used here. I would definitely recommend having a look into the syntax definitions for GitHub Actions to see which keywords are available.
In the example file above we can see types of steps:
External hosted:
actions/checkout@v1
to clone our repository to the environmentcodecov/codecov-action@v1
action to upload our code coverage file to codecov- This step utilizes a secret API key which can be entered in your repositories settings: "Secrets -> Actions". The values entered there are encrypted and can be safely used within your steps without leaking them to anyone.
Custom ones:
Run Unit Tests
which simply executes the shell script as we saw earlier when running the unit tests on our local machine
The only thing left to do now is to commit the workflow file to the repository. GitHub will then automatically verify that your file is syntactically right and afterwards run your defined workflow when the on
triggers are matched. You can see an example in some of my libraries, e.g. in LoggingKit.
Conclusion
In this article we saw how we can create a Swift library with the help of the Swift Package Manager. Furthermore we integrated GitHub Actions in order to run the unit tests defined in the Package.swift
file to verify that the code we want to merge to the develop
and main
branch is working correctly.
If you have suggestions or question don't hesitate to reach out to me !
See you around and stay healthy 👋