Just, Nix Shell and Podman are a Killer Combo
Let’s say, for some unclear reasons, you need to compile the “Hello World” C program using a variety of C compilers.
Let’s also say that your dev machine is a MacBook, and some of these C compilers run only on Linux.
Thanks to the advancements made in Nix, everything in this post is completely unnecessary now. Just
setup a Linux builder on Macos by
following this tutorial,
and mark the Linux-only packages so in their Nix config using the system
parameter for the nixpkgs config. Rest is taken care by Nix automatically.
Let’s start with Clang. Clang is the default C compiler on macOS, and you probably have it already
installed. Let that be the case, and you compile hello.c
by running:
clang -o hello-clang hello.c
Well and good. But you don’t want to be typing all that every time you need to compile (your unusual
circumstances compell you to compile the file again and again). So you put it in a justfile
:
Great! Now you use Just, a modern command runner, to run Clang, like so:
just build-clang
Just when you think you troubles are over, you remember that you need to compile the file using GCC as well. macOS does not come with GCC installed, and now you need to figure out how to install it and its dependencies. You can Homebrew that stuff, but you know better.
Enter Nix. Nix is a lot of things, but for the purpose of this post, it is a way to easily create
reproducible development environments. So after installing Nix, you quickly put together the shell.nix
file that
gathers your dependencies, and makes them present your shell’s $PATH
:
Next, you expand the justfile
so that it compiles hello.c
with GCC:
in_nix_shell := env_var_or_default("IN_NIX_SHELL", "false")
root_dir := justfile_directory()
_run-in-nix-shell cmd *args:
#!/usr/bin/env -S sh -eu
if [ "{ in_nix_shell" = "false" ]; then
nix-shell "shell.nix" --run "just \"{ root_dir/{ cmd\" { args"
else
just "{ root_dir/{ cmd" { args
fi
_build-gcc:
gcc -o hello-gcc hello.c
build-gcc: (_run-in-nix-shell "_build-gcc")
With this setup, you compile hello.c
with GCC by running:
just build-gcc
The _run-in-nix-shell
Just command takes care of automatically starting the nix-shell
if
required. nix-shell
downloads GCC and its dependencies for you, and sets them up correctly,
so that you don’t have to care about a thing in the world.
Except one thing: now you also need to compile hello.c
with TinyCC, and for some bizzare reasons,
it so happens that TinyCC runs only on Linux, and not on macOS. You can spin up a Docker container,
but again, you know better.
You decide to use Podman.
First you alter shell.nix
to set up Podman et al., and TinyCC:
with (import { });
mkShell {
buildInputs =
# packages available on both linux and macos
[ just gcc ]
# packages available only on linux
++ (lib.optionals stdenv.isLinux [ tinycc ])
# macos tooling to run linux packages
++ (lib.optionals stdenv.isDarwin [ podman qemu ]);
}
Then you write the Just commands to create and operate a Podman container:
container_name := "demo"
_create-vm:
podman machine init --cpus 12 --memory 8192 --disk-size 50 \
--volume $HOME:$HOME || true
_start-vm: _create-vm
podman machine start || true
_stop-vm:
podman machine stop
_create-container: _start-vm
podman container ls -a | grep { container_name > /dev/null || \
podman create -t --name { container_name -w /workdir \
-v { root_dir:/workdir nixos/nix
_start: _create-container
podman start { container_name
_stop: && _stop-vm
podman stop { container_name || true
And the helper commands to run Just commands in the Podman container:
_podman-exec cmd *args: _start && _stop
podman exec -it { container_name nix-shell \
--command "just { cmd { args"
_run-in-podman cmd *args:
#!/usr/bin/env -S sh -eu
if [ "{ os()" = "macos" ]; then
just _podman-exec "{ cmd" { args
else
just "{ cmd" { args
fi
And finally, the commands to run TinyCC on hello.c
:
__build-tcc:
tcc -o hello-tcc hello.c
_build-tcc: (_run-in-podman "__build-tcc")
build-tcc: (_run-in-nix-shell "_build-tcc")
Finally, you compile hello.c
with TinyCC by running1:
just build-tcc
You watch in amazement as Nix downloads Podman and QEMU, Just sets up and runs the container, Nix downloads
TinyCC within the container, and TinyCC finally compiles the file. Everything cleans up afterwards,
and you are left with a hello-tcc
binary file in your directory, which you cannot run because it was
compiled on Linux, and you are on macOS2. But whatever. You job was to compile, not to run. You pack
up your laptop, move to the living room, and open it again to browse Reddit. A day well spent3.
If you have Just installed at the OS level, you can run Just commands from other directories as well, like this:
↩︎just ~/Projects/just-nix-podman-demo/build-tcc
This
justfile
runs fine on a Linux machine as well, except thebuild-clang
command.↩︎The use-case described in this post is a rather trivial and contrived example, but this pattern has served me well in real-world use-cases.↩︎
This post was originally published on abhinavsarkar.net.
If you liked this post, please leave a comment.