Auto-generate a version name for a Swift PM project

Previously, we discussed how to use git describe to generate an informative version name of a project. How do we use it in a Swift Package Manager project to automate the version name generation?

Problem: Multiple sources of truth for version info

It’s a common practice to ship binaries of a program with the version info and provide a way to check the version through its interface.

Let’s say we are building a command line program in Swift using Swift Package Manager:

├── Package.resolved
├── Package.swift
├── README.md
└── Sources
└── MyCommandLineTool
└── MyCommandLineTool.swift

In the main source file MyCommandLineTool.swift, the version is defined as a string constant "1.2.3".

import ArgumentParser

@main
struct MyCommandLineTool: ParsableCommand {
/// Version Constant
static let version = "1.2.3"

static let configuration = CommandConfiguration(
commandName: "MyCommandLineTool",
abstract: "This is MyCommandLineTool",
version: version // Make the version available to the CLI
)

mutating func run() throws {
// ...
}
}

Thanks to Swift Argument Parser, making the version available to the command line is very easy. We can check the version number by passing in the --version flag:

$ swift build
Building for debugging...
[4/4] Linking MyCommandLineTool
Build complete! (0.66s)

$ ./.build/debug/MyCommandLineTool --version
1.2.3

Creating a git tag for each release is also a common practice. In this example, we can tag this release with 1.2.3:

$ git tag -a 1.2.3

This works but is not ideal, because there are two places holding the same version that may get out of sync:

  • A string constant in the source code
  • A git tag of the project repository

How can we consolidate the version into one place? We know it’s possible to generate a pretty informative version name using git describe as discussed in the previous post, so why not write an easy script to automate this version generate process?

Solution

Auto-generate GitInfo.swift

Let’s create a script Scripts/gen-git-info.sh:

#!/usr/bin/env bash

# 1. Define the output Swift file
version_file="Sources/MyCommandLineTool/GitInfo.swift"

# 2. Generate "git_version" string
git_version=$(git describe --abbrev=4 --dirty --always --tags)

# 3. Saving the version string to the output Swift file
echo "/// Auto-Generated Git Info.
/// DO NOT EDIT!
enum GitInfo {
static let version = \"$git_version\"
}
" > $version_file

Running this script will generate a new file Sources/MyCommandLineTool/GitInfo.swift:

/// Auto-Generated Git Info.
/// DO NOT EDIT!
enum GitInfo {
static let version = "1.2.3-dirty"
}

ℹ️ NOTE
The version string has a -dirty suffix because we have uncommitted changes.

Finally, we can update the main MyCommandLineTool.swift file to read the new auto-generated version number:

import ArgumentParser

@main
struct MyCommandLineTool: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "MyCommandLineTool",
abstract: "This is MyCommandLineTool",
version: GitInfo.version // Read auto-generated version
)

// ...
}

Now, let’s check the result:

$ swift build
Building for debugging...
[5/5] Linking MyCommandLineTool
Build complete! (0.65s)

$ ./.build/debug/MyCommandLineTool --version
1.2.3-dirty

🙌 auto-generated version string is printed out!

Ignore the GitInfo.swift file

Because the GitInfo.swift file is auto-generated based on the info from the git repo, committing this file into the repo will affect the versing string generated from git describe. It’s better to make sure we don’t commit the file by adding it to .gitignore:

# Other ignored files ...

# Ignore auto-generated git version file
Sources/MyCommandLineTool/GitInfo.swift

Use Makefile to manage build tasks

Everything works so far, but we have to remember to run the gen-git-info.sh script manually every time before building. How can we automate this process?

Turns out the good old make command can help us. We can create a Makefile for the project and manage the build process:

gen-git-info:
./Scripts/gen-git-info.sh

build: gen-git-info
swift build

release: gen-git-info
swift build -c release

install: release
cp ./.build/release/MyCommandLineTool ~/bin

clean:
rm -rf .build

⚠️
Makefiles must be indented using TABs and not spaces or make will fail.

Here, we make the gen-git-info script as the dependency of the build and release, so that the script is guaranteed to run before the swift build command.

Put everything together

Now our project looks like this:

├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
├── Scripts
│   └── gen-git-info.sh
└── Sources
└── MyCommandLineTool
├── GitInfo.swift
└── MyCommandLineTool.swift

To build a debug build, we can run make build:

$ make build
./Scripts/gen-git-info.sh
swift build
Building for debugging...
[3/3] Emitting module MyCommandLineTool
Build complete! (0.34s)

To build a release build and install it to the ~/bin directory we can run make install:

$ make install
./Scripts/gen-git-info.sh
swift build -c release
Building for production...
[2/2] Compiling MyCommandLineTool GitInfo.swift
Build complete! (0.39s)
cp ./.build/release/MyCommandLineTool ~/bin

References