Mastering C: Building a Project with Nix Flakes

5 minute read Published: 2024-06-14

Upgrading Your C Project Build to Use Nix Flakes: A Modern Guide

Hello again, fellow C enthusiasts! In our previous blog post, we explored building a C program with external library dependencies using Nix. Today, we’re taking it a step further by upgrading our Nix setup to use Nix flakes, a more structured and modern way to manage dependencies and ensure reproducibility. This post will guide you through updating your existing Nix build to a Nix flake and explain each component in detail. Let’s dive in!

What Are Nix Flakes?

Nix flakes introduce a standardized way to define Nix projects, providing better dependency management, versioning, and reproducibility. They offer:

For more details, you can check out the Nix Flakes documentation.

Transitioning from a Classic Nix Setup to Nix Flakes

We’ll start with our simple-parse-example C program, which uses the jansson library for JSON parsing. We’ll convert our existing default.nix to a flake.nix file.

Original default.nix

Here’s a quick look at our original default.nix:

{ pkgs ? import <nixpkgs> {} }:

let
  jansson = pkgs.jansson;
in
pkgs.stdenv.mkDerivation {
  pname = "simple-parse-example";
  version = "1.0";

  src = ./.;

  nativeBuildInputs = [ pkgs.clang ];
  buildInputs = [ jansson ];

  buildPhase = ''
    clang -g -Wall -o simple-parse-example simple-parse-example.c -ljansson
  '';

  installPhase = ''
    mkdir -p $out/bin
    cp simple-parse-example $out/bin/
  '';

  meta = with pkgs.lib; {
    description = "A simple program using jansson";
    license = licenses.mit;
    maintainers = [ maintainers.yourname ];
    platforms = platforms.unix;
  };
}

Converting to flake.nix

We’ll now convert this setup to use Nix flakes.

Step 1: Creating flake.nix

Create a file named flake.nix in your project directory with the following content:

{
  description = "A simple C program using jansson built with Nix flakes";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  };

  outputs = { self, nixpkgs }: {
    packages = nixpkgs.lib.genAttrs [ "aarch64-darwin" "x86_64-linux" ] (system:
    let
      pkgs = import nixpkgs { inherit system; };
    in
    rec {
      simple-parse-example = pkgs.stdenv.mkDerivation {
        pname = "simple-parse-example";
        version = "1.0";

        src = ./.;

        nativeBuildInputs = [ pkgs.clang ];
        buildInputs = [ pkgs.jansson ];

        buildPhase = ''
          clang -g -Wall -o simple-parse-example simple-parse-example.c -ljansson
        '';

        installPhase = ''
          mkdir -p $out/bin
          cp simple-parse-example $out/bin/
        '';

        meta = with pkgs.lib; {
          description = "A simple program using jansson";
          license = licenses.mit;
          maintainers = [ maintainers.yourname ];
          platforms = platforms.unix;
        };
      };
    });

    defaultPackage = {
      aarch64-darwin = self.packages.aarch64-darwin.simple-parse-example;
      x86_64-linux = self.packages.x86_64-linux.simple-parse-example;
    };

    defaultApp = {
      forAllSystems = nixpkgs.lib.mapAttrs' (system: pkg: {
        inherit system;
        defaultApp = {
          type = "app";
          program = "${pkg.simple-parse-example}/bin/simple-parse-example";
        };
      }) self.packages;
    };
  };
}

Breaking Down the Flake Configuration

Let’s break down each component of the flake.nix file to understand what it does.

1. description

description = "A simple C program using jansson built with Nix flakes";

This provides a brief description of the flake. It’s useful for documentation and understanding the purpose of the flake.

2. inputs

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};

The inputs section defines dependencies for the flake. Here, we are specifying that we want to use the unstable branch of the Nixpkgs repository. This is where we’ll get our packages like clang and jansson.

3. outputs

outputs = { self, nixpkgs }: {
  packages = nixpkgs.lib.genAttrs [ "aarch64-darwin" "x86_64-linux" ] (system:
  let
    pkgs = import nixpkgs { inherit system; };
  in
  rec {
    simple-parse-example = pkgs.stdenv.mkDerivation {
      pname = "simple-parse-example";
      version = "1.0";

      src = ./.;

      nativeBuildInputs = [ pkgs.clang ];
      buildInputs = [ pkgs.jansson ];

      buildPhase = ''
        clang -g -Wall -o simple-parse-example simple-parse-example.c -ljansson
      '';

      installPhase = ''
        mkdir -p $out/bin
        cp simple-parse-example $out/bin/
      '';

      meta = with pkgs.lib; {
        description = "A simple program using jansson";
        license = licenses.mit;
        maintainers = [ maintainers.yourname ];
        platforms = platforms.unix;
      };
    };
  });

  defaultPackage = {
    aarch64-darwin = self.packages.aarch64-darwin.simple-parse-example;
    x86_64-linux = self.packages.x86_64-linux.simple-parse-example;
  };

  defaultApp = {
    forAllSystems = nixpkgs.lib.mapAttrs' (system: pkg: {
      inherit system;
      defaultApp = {
        type = "app";
        program = "${pkg.simple-parse-example}/bin/simple-parse-example";
      };
    }) self.packages;
  };
};

Building and Running with Nix Flakes

To ensure that everything works as expected, follow these steps:

  1. Navigate to your project directory:

    cd ~/dev/learning-c/parsing-json
    
  2. Enable Flakes: Make sure flakes are enabled in your Nix configuration:

    echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
    
  3. Build the project: Build the project using:

    nix build .#simple-parse-example
    
  4. Run the binary: After building, you can run the binary with:

    ./result/bin/simple-parse-example
    

Conclusion

Congratulations! You've successfully updated your C project to use Nix flakes. This modern approach ensures your builds are reproducible and dependencies are well-managed. Nix flakes provide a powerful way to handle complex projects with ease, making your development process smoother and more efficient.

Exercise Prompt

Try converting another one of your existing projects to use Nix flakes. Start by writing a flake.nix file that includes all your dependencies and build instructions. Share your experience, any challenges you encountered, and how Nix flakes improved your build process.

Happy coding, and may your builds always be reproducible!