Odin and Me · Chapter 12

Chapter 12: Shipping a console application

This chapter:

  • Build a release version of Muninn, not just our everyday debug loop.
  • Understand -target: for cross-compiling, and its real-world limits.
  • Read build.sh and run.sh line by line, as scripts you can adapt.
  • Consider where a save file really belongs, honestly, not just where I put it.
  • Look back at the whole journey, and see where to go from here.

Where we left off: Muninn works, end to end. Now we turn it into something you can actually hand to someone: a single, self-contained release binary.


12.1 Debug vs release

Every chapter so far has used odin run .: compile a throwaway binary, run it immediately, useful for the write-check-run loop we've been in since Chapter 1. But when you're ready to ship your code to someone else, you'll want a different command:

odin build . -o:speed -out:muninn

odin build compiles and stops, leaving muninn behind rather than running it for you. -o:speed asks for an optimised build rather than our fast-to-compile, slower-to-run debug default. -out:muninn names the resulting binary explicitly. The result is the single file you'd actually copy onto someone else's machine.

12.2 Cross-compiling, and its real limits

Odin can compile for a platform other than the one you're on:

odin build . -o:speed -target:windows_amd64 -out:muninn.exe
odin build . -o:speed -target:linux_amd64 -out:muninn
odin build . -o:speed -target:darwin_arm64 -out:muninn

odin build --help lists every supported -target: (or run it with -target:"?" for the current list). Whether cross-compiling finishes, specifically, whether it can also link a working binary for a platform other than your own, depends on your toolchain and platform combination. Don't assume it works everywhere.

On my own machine (macOS), compiling for windows_amd64 got as far as generating code and then stopped at linking, giving me an honest message rather than a broken binary:

$ odin build . -target:windows_amd64 -out:muninn.exe
Linking for cross compilation for this platform is not yet supported (windows amd64)

Gotcha: don't assume cross-compiling "just works" until you've tried it on your own setup. My pragmatic fallback, and what I actually recommend here, is to build on each target platform itself, or in a CI pipeline running on that platform. -target: is genuinely useful when it works for your host/target combination: try it, but verify the binary actually runs before you trust a clean compile alone.

12.3 The scripts

code/build.sh and code/run.sh are what our ./build.sh/./run.sh shorthand has been calling all along. Read them once, end to end: you'll want to adapt something like them for your own future Odin projects.

#!/usr/bin/env sh
# build.sh, a release build for the host platform: one optimised, self-contained
# native binary. Nothing to ship beside it; the save file is created next to it
# on first run.
#
# On Windows the output is muninn.exe; cross-compile with, e.g.:
#   odin build . -out:muninn.exe -o:speed -target:windows_amd64
set -e

OUT=muninn
[ "$(uname)" = "Windows_NT" ] && OUT=muninn.exe   # (Git Bash etc.)

odin build . -out:"$OUT" -o:speed

# Soft size check: warn, don't fail.
SIZE=$(wc -c < "$OUT")
echo "Built $OUT (${SIZE} bytes)"
[ "$SIZE" -gt 8000000 ] && echo "note: binary is over 8 MB" || true

set -e stops the script at the first failing command, rather than pressing on with a broken build. [ "$(uname)" = "Windows_NT" ] is a Git-Bash-on-Windows detail: native cmd.exe/PowerShell users would invoke odin build directly rather than through a POSIX shell script like this one. The size check is deliberately a warning, never a hard failure: a console application growing past a soft budget is worth noticing, not a reason to refuse to ship.

#!/usr/bin/env sh
# run.sh, the everyday loop: compile and launch Muninn in one step.
# `odin run` builds a throwaway binary and runs it immediately.
set -e
odin run . -out:muninn

run.sh is deliberately the smaller of the two: it's the command you'll run dozens of times an hour while working.

12.4 Where the save file lives

Muninn writes muninn_notes.txt right next to its own binary. That's a deliberate simplification on my part, and I'd rather state it plainly than leave you to discover it the hard way. It means running Muninn from two different directories gives you two separate note collections, and running a Muninn installed in a read-only directory means it simply can't save at all.

A shipped tool doing this properly would write to a per-OS user data directory instead:

  • Windows: %APPDATA%
  • macOS: ~/Library/Application Support/
  • Linux: $XDG_DATA_HOME (falling back to ~/.local/share/)

core:os doesn't hand you these paths directly out of the box; finding them correctly per platform is exactly the kind of small, fiddly, genuinely useful project to attempt once you're comfortable with everything we've covered. I've pointed at it in Appendix C as a next step.

Why not just do it properly, if I already know the answer? Because naming the shortcut honestly is far more useful to you than hiding it behind extra code you'd have to untangle. It's the same approach I used in Odin't Know That: state the shortcut, name the real answer, and let you decide whether your project needs to go further.

12.5 A tour back

Here is the path, start to finish, of what got you here:

ChapterWhat it added
1A working toolchain, and your first compiled line of output.
2Variables, control flow, procedures: the vocabulary everything else leans on.
3Reading and parsing input; the (value, ok) pattern.
4Strings as bytes, runes, strings.Builder.
5Nested loops, and turning a pattern into a reusable procedure.
6Structs, enums, a seeded random generator, pointers that mutate in place.
7Raw ANSI escapes: clearing, colour, box-drawing, a menu-loop state machine.
8Whole-file I/O, a simple record format, tolerant loading.
9The Note struct and Store: add/find/delete/list/search over [dynamic]Note.
10Everything wired together: a complete, usable Muninn.
11or_return, one validated-read helper, and honest handling of a corrupt save file.
12A release build, and the loose ends named plainly.

12.6 Where to go next

Here are a few things I deliberately left out, each a great next project once you want more than Muninn currently offers:

  • Multiple screens or menus beyond Muninn's one level: a settings screen, or a notes-by-category browser.
  • Undo: especially for delete, given §10.5's confirmation prompt is our only safety net right now.
  • Tags or categories: a second field per note, and a way to filter by it.
  • Exporting notes: to plain text or Markdown, for use outside Muninn entirely.
  • A proper per-OS data directory: §12.4's shortcut, done properly.

And if you want to push Odin further than I set out to teach in this book:

  • Yo Odin (https://nofuss.co.za/yo_odin/) and Odin't Know That (https://nofuss.co.za/odint_know_that/): my sibling volumes for when you want to add graphics, sound and a real-time game loop. Everything we covered here about procedures, structs, pointers and core: packages transfers directly.
  • Understanding the Odin Programming Language by Karl Zylinski (https://odinbook.com/): the book to reach for next if you want the language treated in full depth, well past our deliberately gentle scope.

Checkpoint

You now have a complete, robust, persistent Muninn, built as a single optimised native binary with ./build.sh, ready to hand to a friend, plus an honest view of where our shortcuts are for the day you decide to go past them.

Exercises

  1. Run ./build.sh yourself, and check the binary's actual size against the 8 MB soft budget in the script. Then run the same build with odin build . -out:muninn_debug (no -o:speed) and compare the two file sizes. Which is bigger, and does that match what you'd expect from an optimised build versus a debug one?
  2. Try -target: for a platform other than the one you're building on, following §12.2. Report honestly what happened on your machine: a working cross-compiled binary, or a linking error like I hit. Either result is a legitimate answer to this exercise.
  3. Implement §12.4's "proper" save location for just your own platform: look up the right environment variable or core:os call for where your OS expects application data to live, and change SAVE_FILE in persist.odin to build a full path there instead of a bare filename. Confirm Muninn still loads and saves correctly from a directory other than its own.
  4. build.sh's size check warns past 8 MB but never fails the build. Change it to exit 1 past a much smaller threshold, say 1 MB, and rebuild to see the script actually refuse. Then decide, and justify in a sentence: should a real project's build script ever hard-fail on size, or is a warning always the right call?
  5. Look back at Chapter 1's "the map of the journey" section, written before any of this existed. Which chapter, in hindsight, surprised you most, either by being harder than you expected, or by teaching you something you didn't know you needed? No code for this one; just an honest answer.

Further reading