Building Swift Executables

Published: July 25, 2024
Updated: April 23, 2025
Written by:
Natan Rolnik
Natan Rolnik

You finished the first version of your new shiny Swift tool, and now you want to make it available to your colleagues, or maybe share it with the world. And if you have ever wondered what’s the best way to distribute your tool to other people or environments, well, the answer is: it depends.

Each case by its own
Each case by its own

There are multiple questions to consider when you want others to use your tool:

  • Which operating systems need to be supported? Is it only going to run on macOS, or possibly also on Linux?
  • On macOS, are both Intel and Apple Silicon CPUs be supported, or could you assume every team member (and CI) are running M chip Macs?
  • Is the source available, so others could compile it, or is it closed source?
  • Are there many dependencies to be resolved? How long would build times be?
  • How frequently do you plan to make changes to your tool?

This article will compare the different options and when each one might be more suitable, depending on the answers for the questions above.

Building and Running from Source

There are some cases where building from the source is an option. For example, when there are no dependencies (or few lightweight ones), or the source is already available (such as in your team’s monorepo), without the need to clone a new repository. As long as the Swift compiler is available, one single command is enough, no matter what is the OS or architecture:

swift run [<build-options>] <executable-name> [<arguments>...]

The run command will look for a Package.swift file in the current directory. Then, it will fetch the dependencies, if any, build the tool, and run the executable. There are a few things worth worth mentioning:

  1. Build options come before the executable name. For example, --package-path defaults to the current directory, but allows you to instruct SPM to look for another directory. Run swift run --help to check them all.
  2. Another important option is the build configuration. By default, SPM uses debug, which causes the resulting binary to be slower and bigger. To make use of optimizations, use the -c release option. Specific SDKs or toolchains are also passed as build options.
  3. The executable name is not required if it’s the only executable product in the package and if no arguments are needed. The presence of other executable or passing arguments requires being explicit about the product name.
  4. The arguments for your tool come last in the command, not to be mixed with the build options that are first.

Building a Binary from Source

Similar to the command above, SPM allows to build a binary and use it without additional compilation:

swift build -c release [--product <product-name>]

Again, the product name is only required when there are multiple executable products in the same package, and you can omit it if there’s only one.

To find where SPM places the resulting binary, you can use the --show-bin-path flag. Using this flag will not cause a build, but rather only print the path of the binary. The structure of the path is in the following format:

<package-path>/.build/<architecture>-<OS>/<build-configuration>

Which means it can be, among other combinations:

  • .build/arm64-apple-macosx/debug on an Apple Silicon Mac, in the debug configuration
  • .build/x86_64-apple-macosx/release on an Intel Mac, in the release configuration
  • .build/aarch64-unknown-linux/debug on an ARM64 Linux, in the debug configuration

It might be easier to understand with a screenshot of Finder:

The locations where SPM places binaries
The locations where SPM places binaries

For a tool named content-manager, you can notice that SPM creates, inside .build and then arm64-apple-macosx, both debug and release directories. And there, the final binary file, along with all the other build products.

A Script to Build and Store the Binary

With the commands and flags from the previous section, it is possible to write a short script that will build the binary and place it under /usr/bin, so it can be accessed from any directory:

#!/bin/sh

swift build -c release <product-name>
BINARY_PATH=$(swift build -c release --show-bin-path)
cp "${BINARY_PATH}/<product-name>" /usr/local/bin

After replacing <product-name> with your actual product name, you can use this script to:

  1. Build the product from the package at the current directory, under the release configuration
  2. Ask SPM for the path where it saved the binary
  3. Copy the executable to the /usr/local/bin to make it possible to use it from any directory.

As this last operation requires admin privileges, make sure to prepend running the script with sudo. You can save this script as create-binary.sh, and run it with sudo ./create-binary.sh.

Building for Different OSes and Architectures

Allowing building from source makes the process longer and harder for your users. Offering a batteries included tool, already compiled, definitely helps them, while adds complexity for the maintainers.

In many cases, you will want to ship your tool ready to be used, and to do so, you’ll need to compile it for both Intel and Apple Macs, and also support other OSes, such as Linux or Windows.

Intel and Apple Silicon Support

When building on an Apple Silicon machine, the resulting executable will by default no be able to be ran on Intel Macs. To build for it, you can use the --triple build option. It allows describing the target architecture, vendor, and operating system - therefore, a triple.

swift build \
    --product <product-name> \
    --triple x86_64-apple-macosx

When the situation is inverse - if you’re on an Intel Mac and want to build for an Apple Silicon Mac - use the arm64-apple-macosx triple.

Finally, you can merge both binaries into a universal binary, supporting both architectures. To do so, use the lipo tool, which manipulates and creates universal binary files for multiple architectures:

lipo -create \
    --output <universal-output-path> \
    <path-to-arm-binary> \
    <path-to-x86-binary>

Replace <universal-output-path> with the path you want the universal binary to be saved at, and the paths for each different binary.

Linux Support

Building for Linux will require either a Linux machine or Docker.

Considering you have Docker installed in your machine, this can be done with two commands.

First, run the swift build command in a Docker container, using the official Swift image for Linux. Here we use 5.10, but you can use latest instead:

docker run \
    --name my-container-name \
    -v "$PWD:/src" \
    -w /src \
    swift:5.10 \
    swift build -c release <product-name>

If this is the first time you use the Swift 5.10 Linux image, it will take a few moments to download it first. After that, Docker will create a new container (name my-container-name) using the Swift image. It will mount the contents of the current directory (the package directory) under the src directory, set it as the working directory, and run the build command.

Once the build has succeeded, you can use another Docker command, cp, to copy the binary from the container to the host machine:

docker cp \
    my-container-name:/src/.build/aarch64-unknown-linux-gnu/release/<product-name> \
    <linux-product-name>

Replace <linux-product-name> with the file name you want, making it clear it’s the Linux build.

Note: The reader Christian Tietze correctly pointed out in this post that the cp command above is not necessary in this case. The binary is already in the host machine, in the .build/aarch64-unknown-linux-gnu/release/<product-name> directory, because when using the -v flag, Docker uses a volume, and persists data outside the container’s filesystem.

We left the docker cp example command here for cases where you might not use volumes.

To confirm that this is indeed the correct binary, you can use the file tool, which determines a file type:

file <linux-product-name>

Which results in ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, with debug_info, not stripped.

Alternatively, you can also use GitHub Actions to build the executable on a Linux environment, upload the artifacts to the Action run, and download it from there. Stay tuned for a tutorial about it soon!

Final Remarks

Distributing binaries is a whole domain by itself, so it is normal to be confused between all the different options and paths (pun intended) you can take.

You might consider different approaches for building and distributing your tools:

  • GitHub Actions for automating the builds, and GitHub Releases to store them at
  • Homebrew formulae and bottles
  • Self hosted page with links to the tools separated by OSes

Explore Further

Although this wasn’t a tutorial-styled article, still it had plenty of information. So if you reached until the end, congrats 🎉 Let us know if you have any questions or comments, at X or Mastodon.

You can read more about building and distributing executables in the following pages:

See you at the next post. Have a good one!
Swift, Xcode, the Swift Package Manager, iOS, macOS, watchOS and Mac are trademarks of Apple Inc., registered in the U.S. and other countries.

© Swift Toolkit, 2024-2025