Posts by Abhinav Sarkar

Subscribe
Archives
October 5, 2022

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.

#include 

int main() {
    printf("Hello World");
    return 0;
}
hello.c

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:

build-clang:
    clang -o hello-clang hello.c
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:

with (import  { });
mkShell {
  buildInputs = [ just gcc ];
}
shell.nix

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")
justfile

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 ]);
}
shell.nix

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
justfile

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
justfile

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")
justfile

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.


  1. 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
    ↩︎
  2. This justfile runs fine on a Linux machine as well, except the build-clang command.↩︎

  3. 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.

Don't miss what's next. Subscribe to Posts by Abhinav Sarkar:
GitHub
This email brought to you by Buttondown, the easiest way to start and grow your newsletter.