Kevin De Baerdemaeker

Building Handmade Hero with Nix Flakes

Lately, I'm following along Handmade Hero on NixOS using SDL2 (thank you Handmade Penguin), and wanted to setup Nix Flakes to build the derivation. I made something work for C/C++ with external dependencies such as SDL2.

If you are unfamiliar with the nix ecosystem, I recommend reading my How-To Nix guide, but it's not a requirement. I'm still learning about these concepts myself, so don't worry.

My requirements:

Show the flake.nix already!

For my own system configuration I depend on nixos-unstable, but for projects I like to depend on more stable version nixos-23.05. I need the flake-utils, since it makes working with flakes easier when building derivations for multiple platforms.

{
  description = "Handmade Hero on NixOS";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system};
      in {
        # TODO(Kevin): What goes in here?
      });
}

I use clangd, not sure why, but it's the one I chose. If you want to use gcc, go for it. I also use gf to provide me with a simple debugger as RemedyBG is not available on Linux yet.

The executable depends on SDL2 being available at runtime, which is why we add it to the buildInputs. Usually you would read mkDerivation, but this environment provides gcc by default, not clang++. We have to call the derivation helper from a clangStenv.

packages = {
  default = let inherit (pkgs) clangStdenv;
  in clangStdenv.mkDerivation {
    name = "my-handmade-hero";
    src = ./.;
    buildInputs = with pkgs; [ SDL2 ];

    buildPhase = with pkgs;
      "clang++ ./src/sdl_handmade.cpp -o handmade_hero -lSDL2";

    installPhase = ''
      mkdir -p $out/bin
      cp handmade_hero $out/bin/my-handmade-hero
    '';
  };
};

The isolated development environment loads the same buildInputs and nativeBuildInputs, from the earlier mention package declaration via inputsFrom. The shell reports the installed clang++ version as a sanity check.

devShells = {
  default = pkgs.mkShell.override { stdenv = pkgs.clangStdenv; } {
    packages = with pkgs; [ clang-tools gf SDL2.dev ];

    inputsFrom = [ self.packages.${system}.default ];

    shellHook = with pkgs; ''
      echo "`clang++ --version`"
    '';
  };
};

Now, by bringing all these puzzles pieces together, you end up with the following flake.nix.

{
  description = "Handmade Hero on NixOS";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        name = "my-handmade-hero";
        src = ./.;
        pkgs = nixpkgs.legacyPackages.${system};
        buildInputs = with pkgs; [ SDL2 SDL2.dev ];
        nativeBuildInputs = with pkgs; [ clang-tools gf ];
      in {
        <<package>>
        <<dev-shell>>
      });
}
(ansi-color-apply text)

Generate the lock file, and check if the flake is has been setup correctly.

nix flake lock
nix flake show
git+file:///home/venikx/code/venikx.com?dir=src/content/blog/handmade-hero-nixos
├───devShells
│   ├───aarch64-darwin
│   │   └───default omitted (use '--all-systems' to show)
│   ├───aarch64-linux
│   │   └───default omitted (use '--all-systems' to show)
│   ├───x86_64-darwin
│   │   └───default omitted (use '--all-systems' to show)
│   └───x86_64-linux
│       └───default: development environment 'nix-shell'
└───packages
    ├───aarch64-darwin
    │   └───default omitted (use '--all-systems' to show)
    ├───aarch64-linux
    │   └───default omitted (use '--all-systems' to show)
    ├───x86_64-darwin
    │   └───default omitted (use '--all-systems' to show)
    └───x86_64-linux
        └───default: package 'my-handmade-hero'

Verify the SDL2 Headers with a Message Box

The simplest way to check if C/C++ code properly links with the SDL2 library is by showing a message box. Your editor (I use Emacs btw 😎) should now also be able to complete SDL functions when you type SDL_.

#include <SDL2/SDL.h>
#include <stdio.h>

int main(int arc, char **argv) {
  SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Handmade Hero",
                           "This is Handmade Hero", 0);
  return 0;
}
Finally! Some C++ code!

The hardest parts are behind us now. Running nix build creates an executable file inside result. After running ./result/bin/handmade_hero you should see a message box. Or equivalently run nix run.

nix run