Compilation, Build Modes & Deployment
Build Modes
The default invocation nim c app.nim produces a debug build: runtime checks, assertions and stack traces stay in, and the C compiler is not asked to optimize. That is the build you develop and test against, because it is the one that tells you where a bug happened.
Debug, Release and Danger
Nim's own config/nim.cfg defines what the two optimized modes mean. Reading the definitions is more reliable than any blog post about them:
-d:releasesetsstacktrace:off, excessiveStackTrace:off, linetrace:off, debugger:off, line_dir:off, opt:speedanddefine:release— fast code, no helpful traces.-d:dangeradds "all runtime checks off, assertions off" on top of the release settings — the fastest and smallest build, and the one allowed to skip index checks, overflow checks andnilchecks.
nim c app.nim # debug: checks on, traces on, optimizations off
nim c -d:release app.nim # ship this: speed on, traces off
nim c -d:release --panics:on app.nim
nim c -d:danger app.nim # measurements only: no checks at all
Treat -d:danger as a benchmark tool, not a shipping mode: with checks disabled, an out-of-range index reads or writes whatever memory follows the array instead of terminating with a message. --panics:on ("turn panics into process terminations", off by default) is the safer middle ground — it removes the exception unwinding machinery for defects while leaving ordinary exceptions intact, which shrinks binaries and speeds up error paths.
Memory Management at Build Time
The strategy is a compile-time choice. The compiler option list describes --mm as selecting "which memory management to use; default is 'orc'", so a stock 2.x build already has deterministic destruction plus cycle collection. Change it only with a measurement in hand:
nim c -d:release app.nim # --mm:orc is already the default
nim c -d:release --mm:arc app.nim # no cycle collector: fewer instructions,
# leaks any reference cycle you create
nim c -d:release --mm:none app.nim # manual memory only: for freestanding targets
Never mix strategies inside one program, and re-run the test suite for every strategy you ship — =destroy hooks and move transfers behave differently once the tracing collector is gone.
Optimisation, Size and Checks
Three dials are independent of the build mode and are worth setting explicitly in a release recipe: the optimisation level, how much code the binary carries, and whether a defect terminates the process.
nim c -d:release --opt:speed app.nim # default for release: optimize for speed
nim c -d:release --opt:size app.nim # small targets, embedded systems
nim c -d:release --opt:none app.nim # no C optimizations: fastest to build
nim c -d:release --panics:on app.nim # defects terminate; less unwinding code
nim c -d:release --passC:"-O3" --passL:"-s" app.nim # extra C flags, stripped
--passC and --passL forward flags verbatim to the C compiler and linker, which is how you reach backend-specific tuning without touching Nim code. Two warnings are worth remembering: stripping (-s) makes stack traces useless, and aggressive C optimisation can expose undefined behaviour a debug build tolerated — so always run the suite against the exact flags you ship.
Packaging
A Nim project is a source tree plus a manifest. The manifest is what turns "clone this and guess the commands" into nimble build.
Executables, GUI Apps and Libraries
--app: | Produces | When to use |
|---|---|---|
console | A command-line executable (default) | Tools, servers, test binaries |
gui | An executable with no console window on Windows | Windowed desktop programs |
lib | A shared library (.dll / .so) | Plugins and components loaded at run time |
staticLib | A static archive (.a) | Components embedded in a C or C++ program |
nim c -d:release -o:build/app app.nim # name the output
nim c -d:release --app:gui src/desk.nim # no console window
nim c -d:release --app:lib --header:api.h --out:libapi.so api.nim
Libraries are the case that needs one extra step: an exported symbol is only usable if the host can declare it, so --header writes the C header matching your {.exportc.} declarations, and the host must call NimMain() once before using them.
Nimble Packages
The manifest is a .nimble file written in NimScript — real Nim syntax, executed by the Nimble tool. It declares the package, its dependencies and its tasks.
# app.nimble — the package manifest read by the Nimble tool.
version = "0.1.0"
author = "Your Name"
description = "A command line tool written in Nim"
license = "MIT"
srcDir = "src" # sources live in src/, not at the root
bin = @["app"] # 'nimble build' produces this executable
requires "nim >= 2.0.0" # the compiler floor for this package
requires "cligen >= 1.7.0" # third-party dependencies, with versions
task test, "Run the test suite":
# Tasks are NimScript: ordinary statements run by Nimble, not a shell string.
exec "nim c -r tests/tall.nim"
exec "nim c -r -d:release tests/tall.nim"
nimble build # compile everything listed in 'bin'
nimble test # run the 'test' task in the manifest
nimble install # build and install into the local package store
nimble develop # register this checkout so other projects pick it up live
nimble dump # print the resolved package: version, deps, source paths
nimble dump is the debugging tool for dependency problems: it shows the version Nimble resolved, the paths it will compile from, and whether a checkout or an installed copy is in play — the answer to "why is my local edit not being used?".
A Release Workflow
Two properties separate a build you can trust from one you cannot: the same input produces the same output, and the flags live in the repository rather than in somebody's shell history. Nim supports both without any extra tooling.
Flags in the Repository
config.nims is a NimScript file the compiler loads automatically, so build switches that the project always wants can be committed next to the code.
# config.nims — read automatically by the compiler from the project root.
switch("path", "src") # every build can 'import app' without prefixes
switch("define", "release") # a release binary is the default here
switch("opt", "speed") # explicit, instead of relying on the default
when defined(windows):
switch("passL", "-static") # platform-specific linking, in one place
# A per-target file wins over config.nims: tests keep the debug settings.
# tests/config.nims
switch("define", "debug") # no traces lost while testing
switch("define", "release") # (remove the line above to re-enable release)
Because the file is executed rather than parsed, conditions read as normal Nim: when defined(windows), when hostOS == "linux". That is also why it must be reviewed like code — a switch here silently applies to every build in the directory tree.
Cross-Compilation
The C backend turns the target into a pair of switches: --os and --cpu select what the generated C is compiled for, and --cc selects which C compiler does it.
nim c -d:release --os:linux --cpu:arm64 --cc:clang -o:app-linux-arm64 app.nim
nim c -d:release --os:windows --cpu:amd64 -o:app.exe app.nim
nim c --os:any --cpu:wasm32 --cc:clang -o:app.wasm app.nim # WebAssembly
Cross-compilation needs a C toolchain that targets the destination platform — the switches select it, the compiler does not supply it. Cross-compiled code must also be tested on the destination, or in an emulator: a runtime check that passes on the build machine proves nothing about a platform with a different word size or a different libc.
The compiler user guide lists the complete switch reference — --opt, --panics, --mm, --passC, --passL, --os, --cpu, --cc — and the Nimble documentation covers the manifest fields and tasks used above.