Getting started with Swift package manager
Mastering the command line is important, especially when trying to build and deploy Swift on a production Linux machine or in the Cloud. Since Xcode will not be available on those hosts, Apple has provided us with an easy-to-use command-line tool to help create, build, and distribute our Swift code. This tool is called the Swift package manager and it is useful for managing the distribution of Swift code while integrating with the Swift build system to automate the process of downloading, compiling, and linking dependencies. The following are some of the useful commands provided by the package manager to quickly get you started:
swift package init
: This will create a Swift package or module that is an easy portable way to share code. It will create a package using the name of the folder you are currently in. Passing a --type executable
option will make an executable package where the product of the build will be an executable program such as a web server or a command-line program. Think of this as gems for Ruby or node modules for Node.js.swift build
: This builds the Swift package you currently are in by compiling Swift code in your Sources
folder. If your package is an executable, then it will generate a binary in the .build/debug
folder. If you pass a release configuration using the --configuration
release option, then it will build a highly optimized binary and place it in .build/release
. The same output is generated for non-executable binary but generate Swift modules instead to be imported by whoever wants to use this module.swift run
: A quick way to run a Swift executable package from the command line. This command builds the Swift code if it is not built already and runs the binary. You can pass the -c
release option to build and run the optimized version of the binary.swift test
: To run tests written in the Test
folder of your package.swift package generate-xcodeproj
: This command generates an Xcode project file so that you can work on the package in Xcode instead of a plain text editor.
These are some of the more important commands that will come in handy when trying to build and test your web server in Swift and also when deploying and running your web application in production. There are a lot more commands and you can learn about them by running swift package
in the Terminal:
Right now, we will go through an exercise to build a simple Swift package and learn about the important files and folders. We will also publish this package and consume it in another Swift package to show how we can publish packages and import them as dependencies. For our exercise, we will create a simple cat
command-line tool which will concatenate and print the contents of the files specify relative to the current directory.
In order for us to do so we will first build a package called FileReader
which will read and return the contents of the file. To build this Swift package, we need to do the following:
- Create a folder called
FileReader
(mkdir FileReader
) and change directory (cd
) into that folder - Run
Swift package init
and it will generate files and folders for the package
Let's inspect the contents of the package. The following is the file and folder structure inside of FileReader
:
~/W/FileReader $ tree .
.
├── Package.swift
├── README.md
├── Sources
│ └── FileReader
│ └── FileReader.swift
├── Tests
├── FileReaderTests
│ └── FileReaderTests.swift
└── LinuxMain.swift
4 directories, 5 files
Package.swift
: This file is where you describe meta-information about the package, including dependencies of the package.Sources
: This is where you place your Swift code that will get built by the Swift package manager when you run the swift build
command. It can contain multiple folders if you want to build multiple products or targets in your package.Tests
: This is where you place your test files and that get run when swift test
is run from the command line.
Now that we know the basic file and folder structure, we can start writing our Swift code to read files from disk inside of the FileReader.swift
file. By default, it will contain boilerplate code which we can remove and replace with this:
import Foundation
class FileReader {
static func read(fileName: String) -> String? {
let fileManager = FileManager.default
let currentDirectoryURL = URL(fileURLWithPath:
fileManager.currentDirectoryPath)
let fileURL = currentDirectoryURL.appendingPathComponent(fileName)
return try? String(contentsOf: fileURL, encoding: .utf8)
}
}
In this file, we import Foundation
, which is a standard library available in macOS and Linux and it provides us with the standard library to read from a file path using the FileManager
. After that, we define the FileReader
class and create one static function in it, called read
, that takes a filename and this function will return the contents of the file if the file exists. The code inside the function does the following:
- Gets a singleton
FileManager
object:
let fileManager = FileManager.default
- Creates a URL pointing to the current directory. The current directory is set to the directory from which the OS Process using this library was called from:
let currentDirectoryURL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
- Appends the filename passed to this function to the current directory:
let fileURL = currentDirectoryURL.appendingPathComponent(fileName)
- Tries to read contents of the file if it exists and return it:
return try? String(contentsOf: fileURL, encoding: .utf8)
Now that we have the code, we can build it using Swift build. To test that our code is working, we need to write a test for it and we can do so by taking the following steps:
- Editing the
FileReaderTests.swift
file and replacing the body of testExample
function block with the following:
XCTAssertEqual(FileReader.read(fileName: "hello.txt"), "Hello World")
- Running the following command to create a
hello.txt
file in the root directory of the package with the contents Hello World
:
printf "Hello World" > hello.txt
- Run the test for your package using the
swift test
command. You should see the test pass and print as such:
~/W/FileReader $ swift test
Compile Swift Module 'FileReaderTests' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/FileReaderPackageTests.xctest/Contents/MacOS/FileReaderPackageTests
Test Suite 'All tests' started at 2017-09-29 12:14:57.278
Test Suite 'FileReaderPackageTests.xctest' started at 2017-09-29 12:14:57.278
Test Suite 'FileReaderTests' started at 2017-09-29 12:14:57.278
Test Case '-[FileReaderTests.FileReaderTests testExample]' started.
Test Case '-[FileReaderTests.FileReaderTests testExample]' passed (0.094 seconds).
Test Suite 'FileReaderTests' passed at 2017-09-29 12:14:57.372.
Executed 1 test, with 0 failures (0 unexpected) in 0.094 (0.094) seconds
Test Suite 'FileReaderPackageTests.xctest' passed at 2017-09-29 12:14:57.372.
Executed 1 test, with 0 failures (0 unexpected) in 0.094 (0.094) seconds
Test Suite 'All tests' passed at 2017-09-29 12:14:57.372.
Executed 1 test, with 0 failures (0 unexpected) in 0.094 (0.094) seconds
Now that we have a working Swift package, we can publish it.
Publishing a Swift package
Publishing a Swift package is as simple as committing code, tagging it, and pushing it up to a git repository. To publish the package, perform the following steps:
- Create a public git repository on github.com.
- Open the Terminal and change your directory to your package's path,
cd /path/to/your/swift/package
. Then initialize the git repository by running the git init
command. - Add a remote origin to the local git repo by running this command:
git remote add origin git@github.com:<repoaccount>/<reponame>.git
- Make sure to replace the repo account and repo name with the one you created in Step 1.
- Add all files to this repo using
git add .
and commit them using git commit -m "Initial Commit"
. - Tag it with a version. Since it is our first package we will tag it 1.0.0,
git tag 1.0.0
. - Publish it by pushing it up to the repo along with the tag:
git push origin master --tags
It is that easy to make a Swift package and publish it. All you need is a git repository to push your code to and tag your code appropriately so that whoever uses your package as a dependency can point to a specific version.
Consuming a Swift package
Next, we will try to use this package to create an executable package called cat
that concatenates and prints the contents of the files passed in as arguments to the command. This executable will work like the built-in-system cat
command found in most Unix based operating systems. To do so, we need to perform the following steps:
- Open the Terminal and create a directory called
cat
(mkdir cat
) and change the directory into it (cd cat
). - Initialize the package by running
swift package init --type executable
. This will generate a main.swift
, which is the entry point for the executable and the code will start executing line by line starting from that file. - Add the URL to your GitHub repo that contains the
FileReader
package and add the following line in your Package.swift
under dependencies:
.package(url: "https://github.com/<repoaccount>/<reponame>", from: "1.0.0"),
- Add your
FileReader
package to the dependencies under the targets section in Package.swift
:
import PackageDescription
let package = Package(
name: "cat",
dependencies: [
.package(url: "https://github.com/ankurp/FileReader", from: "1.0.0"),
],
targets: [
.target(
name: "cat",
dependencies: ["FileReader"]),
]
)
- Add the following code to
main.swift
:
import FileReader
for argument in CommandLine.arguments {
guard argument != "arg1" else { continue }
if let fileContents = FileReader.read(fileName: argument) {
print(fileContents)
}
}
Let's try to understand what we have done in the preceding code:
- Import the
FileReader
package:
import FileReader
- Iterate over the command-line arguments:
for argument in CommandLine.arguments {
- We ignore the first argument using the
guard
clause in Swift because it is the command name cat
:
guard argument != "arg1" else { continue }
- Print the contents of the file by printing it in the console:
if let fileContents = FileReader.read(fileName: argument) {
print(fileContents)
}
Now that we have understood the code, let's build and run it to see whether it works. To build and run, just type the following command in the Terminal:
$ swift run cat Package.swift Sources/cat/main.swift
You should see the contents of both the files, Package.swift
and Sources/cat/main.swift
, printed in the console. Great job! We have a working command line tool written in Swift using one of our published Swift packages:
Installing the package's executable
How do we install the command line tool we just created? Don't worry, it's simple too. All you need to do is build it with the release configuration, so that it builds a highly optimized binary and also add flags to statically link the Swift standard library. This means that the executable can work even when Swift versions change on your operating system, or if you plan on distributing it on another platform, such as Linux. The following is the command to build the executable command with the release configuration:
$ swift build -c release -Xswiftc -static-stdlib
Once you have the binary built, you need to copy it to one of the directories where binaries are stored in your user path. One such place is /usr/local/bin
. To copy it, just run the following command and call your binary file whatever you want. In my case, I chose to rename my command to swiftycat
:
$ cp -f .build/release/cat /usr/local/bin/swiftycat
Now, try it out in the Terminal by running the following command:
$ swiftycat Package.swift Sources/cat/main.swift