2022-01-02 by Mark A Vitale
Swift Package Manager is a great way to build command line utilities1. It allows for simple scripting to be combined with a strong type system, modularity, and an ecosystem of packages like Swift Argument Parser. Unlike other languages like python, bash, or even standalone Swift scripts, Swift Package Manager executables do require compilation before running. Swift does provide a handy swift run
command which allows for building and execution of a swift package executable all in one command, but produces some extra output from building that may be undesirable. swift run
also requires the working directory to contain the relevant Package.swift
file or for that directory to be explicitly passed in via the --package-path
option.
There are a few ways to approach building and running your executable: manually building and running the executable, compiling the executable once and storing it somewhere in your path, and writing a wrapper script that builds and runs your executable on demand.
Let’s take a look at the various approaches.
This is conceptually the simplest approach. We’ll invoke swift run
to incrementally build and run the command. We can optionally run the release configuration of the tool for performance.
swift run --package-path /path/to/package -c release ExecutableTargetName [arguments]
Executing this works great but the output retains some output from the implicit swift build
happening which may interfere with commands that expect their stdout to be parsed.
[0/0] Build complete!
Hello, world!
Note: You can omit the --package-path
option if your working directory is the root of the package.
Compile the executable once and copy it somewhere in your path (e.g. /usr/local/bin
). Note that we can take this opportunity to rename the executable to something more idiomatic for the command line.
swift build -c release --package-path "/path/to/package"
cp -f /path/to/package/.build/release/ExecutableTargetName /usr/local/bin/executable-target-name
Calling executable-target-name now no longer invokes a swift build command, so our output is clean of any extra printing from swift build
.
Hello, world!
Put a custom wrapper script in your path (e.g. /usr/local/bin
) that builds and runs the swift package. The wrapper script can be named something more idiomatic for the command line.
#! /bin/zsh
# Define the path to the package
PACKAGE_PATH="/path/to/package"
# Execute the build
# Execute in a subshell to avoid success messages from polluting our command's STDOUT
BUILD_STDOUT=$(swift build --configuration release --package-path "$PACKAGE_PATH" --product "ExecutableTargetName")
BUILD_RESULT=$?
if [[ $BUILD_RESULT -eq 0 ]]; then
# The build succeeded. Let's now run the script, skipping the build since we've just built
swift run --configuration release --package-path "$PACKAGE_PATH" --skip-build "ExecutableTargetName" "$@"
# Forward the script run's exit status
RUN_RESULT=$?
exit $RUN_RESULT
else
# If we failed, print out the build failure STDOUT and forward the build's exist status
echo $BUILD_STDOUT
exit $BUILD_RESULT
fi
Because of our use of the subshell, successful builds and runs of the script do not show any output from the swift build
command. Build failures and anything on STDERR coming from the build command will be printed as expected.
Hello, world!
For my purposes, the wrapper script approach has the right balance of pros and cons. Most code I write lives in a single large repo with tools living as local packages in a subdirectory. There is also an init.sh
file that lives in the root of this repo. Sourcing that file sets up some handy conveniences like adding the repository’s scripts folder to the path for easy access to all the command line tools.
With this type of repository structure, we can take things even further and reduce or outright eliminate many cons of the wrapper script approach. Since our wrapper script and swift package are both part of the same repo, we can make changes to the two in lock-step to avoid getting out of sync. And when an executable is obsolete and removed from the repo, the wrapper script can be removed from path simultaneously.
Some minor tweaks to reduce the cons of the wrapper script approach when operating in a monorepo follow. First we add an init file that developers will source in their shell when working in this repo.
init.sh
:
#! /bin/sh
# Get the directory containing this script, from https://unix.stackexchange.com/questions/76505
export MY_REPO_ROOT=$(exec 2>/dev/null;cd -- $(dirname "$0"); unset PWD; /usr/bin/pwd || /bin/pwd || pwd)
# Add the tools/wrappers directory to our path for easy execution
export PATH=$PATH:"$MY_REPO_ROOT/tools/wrappers"
Next we update our wrapper to use generic paths based on the repo root variable defined in init.sh
.
Wrapper script for a monorepo in tools/wrappers/example-executable
:
#! /bin/zsh
# Define the path to the package
PACKAGE_PATH="$MY_REPO_ROOT/tools/swift/ExampleExecutable"
# Execute the build
# Execute in a subshell to avoid success messages from polluting our command's STDOUT
BUILD_STDOUT=$(swift build --configuration release --package-path "$PACKAGE_PATH" --product "ExampleExecutableTarget")
BUILD_RESULT=$?
if [[ $BUILD_RESULT -eq 0 ]]; then
# The build succeeded. Let's now run the script, skipping the build since we've just built
swift run --configuration release --package-path "$PACKAGE_PATH" --skip-build "ExampleExecutableTarget" "$@"
# Forward the script run's exit status
RUN_RESULT=$?
exit $RUN_RESULT
else
# If we failed, print out the build failure STDOUT and forward the build's exist status
echo $BUILD_STDOUT
exit $BUILD_RESULT
fi
If you just have a couple of swift package executables having a few of these individual wrapper scripts isn’t too bad. If you have a larger suite of tools, you may want to invest in a generic runner script to reduce duplication between wrapper scripts.
Generic wrapper for SPM execution in tools/wrappers/spm-runner
#! /bin/zsh
# A simple runner for Swift Package Manager executables in this repo's tools/swift directory.
# Call this with the first argument being the package name and the second argument being the executable product name.
# All further arguments will be forwarded to the executable
# Example: spm-runner ExampleExecutable ExampleExecutableTarget [arguments-for-executable]
PACKAGE_NAME="$1"
EXECUTABLE_PRODUCT_NAME="$2"
# Define the path to the package
PACKAGE_PATH="$MY_REPO_ROOT/tools/swift/$PACKAGE_NAME"
# Execute the build
# Execute in a subshell to avoid success messages from polluting our command's STDOUT
BUILD_STDOUT=$(swift build --configuration release --package-path "$PACKAGE_PATH" --product "$EXECUTABLE_PRODUCT_NAME")
BUILD_RESULT=$?
if [[ $BUILD_RESULT -eq 0 ]]; then
# The build succeeded so run the script, skipping the build since we've just built, and forwarding on arguments meant for the executable
swift run --configuration release --package-path "$PACKAGE_PATH" --skip-build "$EXECUTABLE_PRODUCT_NAME" "${@:3}"
# Forward the script run's exit status
RUN_RESULT=$?
exit $RUN_RESULT
else
# If we failed, print out the build failure STDOUT and forward the build's exist status
echo $BUILD_STDOUT
exit $BUILD_RESULT
fi
Command-specific wrapper in tools/wrappers/executable-example
#! /bin/zsh
"$MY_REPO_ROOT/tools/wrappers/spm-runner" ExampleExecutable ExampleExecutableTarget "$@"
exit $?
With this simple runner and wrapper, we can now build and run Swift Package Manager based executables directly from command line, ensuring you’re always invoking the latest version of the executable. For a sample repo that implements the monorepo-style wrappers, see this example repository. Thanks for reading!
If you like this, follow the RSS feed to be notified of new posts. Looking for an RSS reader? On macOS and iOS, I like NetNewsWire.
Found this useful or want to support more articles in the future? You can buy me a bourbon.