Cross Compilation Adventures with Nim lang
This post is part of a series.
TLDR; I want to build cross-platform CLI utility tools that can be compiled on my laptop and run seamlessly on other platforms.
I like working inside the Terminal app. So obviously I am using a lot of Terminal CLI (Command Line Interface) tools π§°. To work with these CLI tools, for every usecase there is a small set of shell (bash/zsh) aliases/functions that I have created to work with for specific use cases and to simplify my life. These do differ based on the operating system I am working with (I mostly work with *nix systems i.e macOS and Linux). Remembering these aliases is, let’s just say, not a good developer user experience or DevUX.
For that matter, I have wanted to build my own CLI tools that I could use instead of my shell dependent aliases. Many CLI tools themselves are cross-platform, so my idea is to build cross platform CLI tools for myself.
In order to build such CLI tools, I have been in search of suitable languages to help me build them. For my use case, an ideal language of choice must-have:
- Ability to generate binaries that are
- cross-platform: Works on Linux, macOS, Windows (and more if possible).
- small footprint in size
- performant (No one wants slow tools π. Terminal is all about speed π)
- Can cross-compile
- Binaries for multiple target platform can be built from one platform itself (i.e from macOS compile binaries that run on macOS as well as Linux and Windows platform).
- Easy to maintain
- The language is easy and simple enough to pick up. Thus enabling maintaining my project in the long run.
The first langauage that I stumbled upon is Nim Lang.
From the official website:
<ul> <li>Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula.</li> <li>Nim generates native dependency-free executables, not dependent on a virtual machine, which are small and allow easy redistribution.</li> <li>The Nim compiler and the generated executables support all major platforms like Windows, Linux, BSD and macOS.</li> </ul>
Sounds good! Let’s dive into building a very basic CLI tool.
A good example to showcase would be to build a CLI tool that can convert from Β°C to F and vice versa. Our tool will take an input for value and the unit to be converted to, then output would be converted temprature value.
<p><strong>NOTE</strong>: I am using macOS (M2 Pro, Apple Silicon), so the instructions follow through using that only. However the steps should work on all platform with little tweaks.</p>
First we need to install nim-lang. Open your Terminal app and execute the command
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
<p><a href="https://nim-lang.org/install.html" target="_blank" rel="noopener">Read here</a> to learn how to install on other platforms</p>
Once installed, you should have access to nim
compiler in your Terminal. If not restart your session or open a new Terminal window so it is loaded in the PATH. Follow through next steps
Create a file named
run.nim
.touch run.nim
Add the below code to the
run.nim
file and save the file.import strutils, std/os proc celsiusToFahrenheit(c: float): float = result = c * 9 / 5 + 32 proc fahrenheitToCelsius(f: float): float = result = (f - 32) * 5 / 9 when isMainModule: var value: string var unit: string if paramCount() == 2: value = $(paramStr(1).parseFloat()) unit = paramStr(2).toUpper() else: echo "Usage: ./run <value> <unit_to_convert_to>" quit(1) var convertedTemperature: float if unit == "C": convertedTemperature = celsiusToFahrenheit(value.parseFloat()) elif unit == "F": convertedTemperature = fahrenheitToCelsius(value.parseFloat()) else: echo "Invalid unit. Please use C or F." quit(1) echo "Converted temperature: ", convertedTemperature, "Β°", unit
<p>I am not going to explain this code as it is simple and self explanatory.</p> <p>To understand and learn the language I used <a href="https://learnxinyminutes.com/docs/nim/" target="_blank" rel="noopener">Learn X in Y minutes: Nim Lang</a> π</p>
Now to compile, execute the
nim
compiler withcompile
argument and therun.nim
file:nim compile run.nim
<p><strong>NOTE</strong>: You can also replace <code>compile</code> with its shorthand version <code>c</code> in above command. i.e <code>nim c run.nim</code>.</p>
You should now have a binary generated in the same directory with the same name as the nim file i.e run.
<p><strong>NOTE</strong>: I use <a href="https://github.com/bootandy/dust" target="_blank" rel="noopener"><code>dust</code></a> CLI tool to list files in directory with their sizes. <strong>TIP</strong>: You can generate an optimized binary by passing <code>-d:release --opt:size</code> flags at the time of compilation. i.e <code>nim -d:release --opt:size c run.nim</code>. Result is just a smaller binary.</p>
Time to execute our generated
run
binary file:β― ./run Usage: ./run <value> <unit_to_convert_to>
Didn’t work π, but we have a helpful message stating how to use the CLI tool π
β― ./run 49 C Converted temperature: 120.2Β°C
Done! That was a super quick intro to working with Nim Language in less than 5 mins π
<p><strong>TIP</strong>: You can also compile and run the <code>.nim</code> file in one command by using the <code>-r</code> flag along with <code>c</code> flag. i.e <code>nim c -r run.nim</code></p>
But we aren’t done yet. This generated binary would work on *nix systems. I mentioned earlier that I would like to have cross-(platform + compilation).
nim lang allows to do that easily about which you can read here. Since we already have *nix compatible binary i.e Linux and macOS are sorted for us. We need to cross compile to a format that Windows understands i.e exe
/executable
. Let’s do that next.
You can read about Windows cross compilation using nim lang here.
First install the
mingw-w64
toolchain using homebrew for macOS:brew install mingw-w64
Install MacPorts
Start a new Terminal session and install
x86_64-w64-mingw32-gcc
sudo port install x86_64-w64-mingw32-gcc
<p><strong>NOTE</strong>: Due to a <a href="https://github.com/nim-lang/Nim/issues/10717#issuecomment-480867457" target="_blank" rel="noopener">path issue</a>, you will have to tell nim where this <code>x86_64-w64-mingw32-gcc</code> is, by editing the <code>nim.cfg</code> file.</p>
To know the path where
x86_64-w64-mingw32-gcc
was installed by Macports, runport content x86_64-w64-mingw32-gcc | grep bin/x86_64-w64-mingw32-gcc
β― port content x86_64-w64-mingw32-gcc | grep bin/x86_64-w64-mingw32-gcc /opt/local/bin/x86_64-w64-mingw32-gcc
from above
x86_64-w64-mingw32-gcc
is installed at path/opt/local/bin
. You need to let nim know about this path.Open
nim.cfg
at path~/.choosenim/toolchains/nim-2.0.0/config/nim.cfg
using a code editor i.e VSCode.code ~/.choosenim/toolchains/nim-2.0.0/config/nim.cfg
Find and edit the path value for key
amd64.windows.gcc.path
undermacosx
, from/user/bin
@if macosx: amd64.windows.gcc.path = "/user/bin" ..
to
/opt/local/bin
@if macosx: amd64.windows.gcc.path = "/opt/local/bin" ...
Save the file and open a new Terminal session.
Compile the
run.nim
file with-d:mingw
flag:nim c -d:mingw run.nim
You should now have a
.exe
binary generated in the same directory with the same name as the nim file i.e run.exe<p><strong>TIP</strong>: You can generate an optimized binary by passing <code>-d:release --opt:size</code> flags at the time of compilation. i.e <code>nim -d:release --opt:size c -d:mingw run.nim</code>. Result is just a smaller binary.</p>
<p><strong>NOTE</strong>: In order to run this .exe file, you need to either execute this on Windows directly or if on a *nix system then make use of <a href="https://www.winehq.org/" target="_blank" rel="noopener">Wine</a>.</p>
<p><strong>TIP</strong>: Further reading about reduceing the binary size, read <a href="https://hookrace.net/blog/nim-binary-size/" target="_blank" rel="noopener">here</a></p>
Thats it. I think Nim Language pretty much does what I wanted to get out of it:
β Generate cross-platform binaries β Can cross-compile to platforms β Easy syntax, so maintainable code
Generate cross-platform binaries | Can cross-compile to platforms | Easy syntax, so maintainable code |
---|---|---|
β | β | β |
All check boxes ticked is good π
<p><strong>BONUS</strong>: While my requirement isn’t about compiling to other platforms, but Nim is quite capable such as compiling for <a href="https://nim-lang.github.io/Nim/nimc.html#crossminuscompilation-for-android" target="_blank" rel="noopener">Android</a>, <a href="https://nim-lang.github.io/Nim/nimc.html#crossminuscompilation-for-ios" target="_blank" rel="noopener">iOS</a> and <a href="https://nim-lang.github.io/Nim/nimc.html#crossminuscompilation-for-nintendo-switch" target="_blank" rel="noopener">Nintendo-Switch</a></p>
I’ll be trying this approach of evaluating more languages in the future. You can find the code for this post here.