1. Why you need release if debug already works
When a project is small, it seems that debug is the “real program”, and release is something from the world of big corporations and people in suits. But in almost every CLI project there comes a moment when you want to answer honestly: “Does the final build actually start the same way it does on my machine, and is it not 10 times slower?” That is exactly why you need a release configuration.
In SwiftPM, by default most commands build the project in debug configuration. It is convenient for developers: there is usually more debugging information, builds are faster on incremental changes, and traces are easier to inspect. But the release configuration enables compiler optimizations, can change the size of the binary and the speed of execution, and sometimes exposes problems that did not show up in debug (yes, this is the moment when you say “it worked yesterday”, while yesterday you were running something else).
To avoid being the person who “ships without checking”, we introduce discipline: a release build is a separate check, a separate run, and ideally a separate confirmation that you are launching the release binary itself.
2. swift build -c release: what it does and how it differs from swift build
Here is the good news: a release build in SwiftPM is not a separate complicated process, but just another build configuration. You specify it with the -c release flag (the c stands for configuration), and SwiftPM builds the project in release mode.
In most tutorials the command looks like:
swift build -c release
This is the basic official way: SwiftPM has the debug|release configuration concept, and you choose release explicitly. Important: swift run can also work with configurations and was designed as a “build and run” command so you do not have to dig around .build/… by hand.
If you do the usual:
swift build
you are building the debug variant (by default). Swift.org materials also show that swift run prints “Building for debugging…” and launches the debug variant.
A small “debug vs release” table for beginners
| What we compare | Debug | Release |
|---|---|---|
| For whom | for developers | for users |
| Optimizations | usually minimal | usually maximum |
| Runtime speed | may be lower | often higher |
| Build speed | often faster during development | may take longer |
| “Surprises” | fewer optimization surprises | greater chance of exposing edge-case bugs |
Main idea: release is not “a different program”, but it is a different compilation of the same program. So you need to check both.
3. Where the release binary lives and how not to launch “the wrong one”
When you build a release version, a natural question comes up: “Okay, where is it?” SwiftPM puts the results into the .build/… folder. In a typical case you will have at least two versions:
- .build/debug/LibraryCLI
- .build/release/LibraryCLI
From a beginner’s point of view this looks like “two folders, two realities”. And it is easy to make a mistake: you are sure you are testing release, but in fact you are launching debug because you typed the old command in the terminal or the IDE keeps running the debug configuration.
The most honest way to launch release
After building:
swift build -c release
you launch the binary directly from .build/release/:
./.build/release/LibraryCLI help
Yes, it looks a little low-level, but it gives you rock-solid confidence that you are launching exactly that.
Alternative: swift run -c release
SwiftPM can “build and run” in one go (that is what swift run exists for):
swift run -c release LibraryCLI help
Here SwiftPM will build (if needed) and launch the release variant itself. For everyday checking, that is convenient.
4. Clean build: swift package clean
A clean build means removing all artifacts from previous builds and rebuilding the project from scratch, as if you were seeing it for the first time. In SwiftPM that is done with the command:
swift package clean
And then an ordinary build (in our case, a release build):
swift build -c release
And this pattern shows up not only in teaching examples, but in very practical scenarios: for example, in instructions for building Swift projects in a container you may see the combination swift package clean && swift build -c release.
Why a clean build is needed at all
Imagine your desk is cluttered with sticky notes, and you are trying to find the “latest version”. Sometimes it is easier to rebuild everything than to figure out what belongs where. The same goes for builds: compilation artifacts are caches and intermediate files, and sometimes they can hurt reproducibility.
A clean build is useful when you:
- suspect that “it works only on my machine” because of caches;
- changed settings or the package structure, targets, or moved files;
- got a strange linking/compilation error that does not look like an error in your code;
- want to reproduce “how CI will build it” (even if your CI is only in your head for now).
It is important not to turn swift package clean into the reflex “if it does not work — clean it”. That is like restarting the computer instead of reading the error message. But as discipline before the final release build, it is a very sensible step.
5. Release workflow for LibraryCLI
Now let us put together the minimal sequence of actions you can repeat before shipping the binary (or before sending the project to your teacher/in code review/to the friend who will definitely find something wrong).
It looks like this:
swift package clean
swift test
swift build -c release
./.build/release/LibraryCLI help
Why is swift test here? Because tests are your way to make sure you did not “break the meaning” while thinking about the build. Swift.org documentation for the build system presents swift test as a standard part of the development cycle, and in practice this is especially important before a release: release optimization should not be a moment of “well, I hope everything is fine”.
Process flowchart
flowchart TD
A[swift package clean] --> B[swift test]
B -->|tests green| C[swift build -c release]
C --> D[run .build/release/LibraryCLI]
B -->|tests red| E[fix the code and repeat]
The diagram is simple, but it saves you from the classic “yeah, I just wanted to build, and now it is unclear what I checked”.
6. Checking the release run
What to check besides “it started”
It is very easy to fall into the trap: you launched LibraryCLI, it printed something, and you decided the release is ready. But checking the run is a little more specific. It answers the question: “Does the program correctly handle typical scenarios in the release build?”
Because we are building a CLI, at minimum people usually check at least these things:
- the program starts without arguments and shows a clear message (or help);
- the help command works predictably;
- a command that requires arguments correctly reports an error when the argument is missing (without crashing);
- if you work with files/networks/a repository, verify that the behavior in the release build is the same as in debug (not in speed, but in meaning).
Yes, that sounds obvious. But it is precisely the release check that saves you from the situation where you always launched from the IDE with the right arguments in debug mode, while the user will launch it “however they can” (that is, incorrectly, but honestly).
A small trick: temporarily print the build configuration
To avoid lying to yourself, you can temporarily add a diagnostic print in LibraryCLI that shows you really launched the release variant. This is not a feature for the user, but a small beacon for you.
For example, in your Sources/LibraryCLI/main.swift:
import Foundation
#if DEBUG
print("LibraryCLI: DEBUG build") // In debug you will see this
#else
print("LibraryCLI: RELEASE build") // In release you will see this
#endif
Launch debug (swift run) — you will see DEBUG. Launch release (./.build/release/LibraryCLI) — you will see RELEASE.
One request to yourself: before the final commit/release, do not forget to remove such messages if your CLI contract says it must not print “extra” output during normal runs. In release, the user is not required to read your inner thoughts.
Why swift run -c release does not replace launching from .build/release
There is a subtle but useful point. When you do:
swift run -c release
SwiftPM really builds release and runs it. But in the final check, when you want to make sure the binary is “independent and honest”, it is useful to run the file from .build/release itself.
Why? Because swift run is a “smart assistant” that does steps for you. And a direct launch simulates what real usage will look like: there is a binary, there are arguments, there is an environment.
Ideally you do both scenarios: swift run -c release is convenient for a quick check, and direct launching is useful as a “final exam” (and yes, a binary can get stressed before an exam too).
What can go wrong specifically in release
There is a myth: “If it works in debug, then it must work in release.” In most cases that will be true. But a release build can expose problems you did not notice before. Not because Swift is being “difficult”, but because optimizations change the order and shape of code execution.
Beginners most often encounter three classes of problems.
The first is a hidden dependency on time or order (for example, you assumed somewhere that “this code will always run before that one”, although formally you did not guarantee it). The second is bugs that did not show up in debug because you rarely ran “real” scenarios: for example, you never tried running without arguments or with a wrong file. The third is simple confusion about what exactly you are running: you built release, but by habit you launch the debug variant.
The solution here is not magic: the discipline “clean → test → release build → run” plus careful scenario checking.
7. Typical mistakes when building SwiftPM release
Mistake No. 1: you built with -c release, but run swift run without -c release.
This is classic. You honestly made the release build, and then automatically typed swift run LibraryCLI …, and SwiftPM launched debug again (because that is the default). If you are checking release, launch either swift run -c release or .build/release/LibraryCLI.
Mistake No. 2: you do swift package clean for any error and stop understanding the cause.
Clean build is a good tool, but it does not fix logic errors. If the program parses arguments incorrectly or crashes on invalid input, that is fixed in code and tests, not by cleaning the cache. Cleaning should be a deliberate step, usually before the final check or when strange build symptoms appear.
Mistake No. 3: you check release with print inside critical paths and get a “slow release”.
In a CLI it is very easy to “measure performance” by inserting print inside a loop and then wonder why “release is slow”. Console output itself is slow, and you are measuring the speed of printing, not the speed of the algorithm. For a launch check, print as little as possible, and for measurements, separate computation from I/O.
Mistake No. 4: you think that “if the binary started and printed help, then everything is ready”.
Help is good, but the release check should include at least one or two real scenarios. For example, for LibraryCLI this could be a command for reading/updating data (within what your project already knows how to do at the current stage of the course). And definitely include a scenario with invalid input: user errors should be handled calmly, not “blow up the stack”.
Mistake No. 5: you do not run swift test before release.
Sometimes it feels like tests are about “academic purity”, while release is about “just build it”. In practice it is the opposite: right before release, tests give you confidence that you are not shipping a program that “builds perfectly but does nonsense”. SwiftPM explicitly treats swift test as a normal part of the project build cycle.
GO TO FULL VERSION