I’m learning nix, and investigating how to build the dependencies for my app using a flake. I need all of my direct and indirect dependencies to be built with special flags, so figuring out how to do that properly is my focus. I started with this flake, which does no customization and just pulls in the hello package:
{
description = "Example C++ development environment for Zero to Nix";
inputs = {
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2405.*.tar.gz";
};
outputs = { self, nixpkgs }: let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
hello
];
};
};
}
Then with enough experimenting I figured out that because I want both direct and indirect dependencies of my app to be built with flags that it wasn’t enough to just use overrideAttrs
for my direct dependencies, I was going to need an overlay that overrides stdenv
. This version adds the flag -frecord-gcc-switches
just to demo that this is the right approach. It’s a very easy to verify flag because it causes gcc to embed all the arguments to the compiler into the binary:
{
description = "Example C++ development environment for Zero to Nix";
inputs = {
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2405.*.tar.gz";
};
outputs = { self, nixpkgs }: let
system = "x86_64-linux";
overlay = final: prev: {
stdenv = prev.withCFlags [ "-frecord-gcc-switches" ] prev.stdenv;
};
pkgs = import nixpkgs {
inherit system;
overlays = [ overlay ];
};
in {
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
hello
];
};
};
}
Which I could then verify with readelf:
$ readelf -p .GCC.command.line $(which hello)
String dump of section '.GCC.command.line':
[ 0] GNU C11 14.2.1 20241116 -mtune=generic -march=x86-64 -g -ggdb -O2 -std=gnu11 -fzero-call-used-regs=used-gpr -fno-strict-overflow -fgnu89-inline -fmerge-all-constants -frounding-math -fstack-protector-strong -fno-common -fmath-errno -fPIE -fcf-protection=full -ftls-model=initial-exec -frandom-seed=dny8mwd5y7
However, this solution caused a problem – extremely long build times, because nix was rebuilding everything used to bootstrap nix itself with the flag, so bison, perl, gcc, etc were all getting rebuilt too which is overkill. So with Claude’s help I came up with this:
{
description = "Example C++ development environment with custom compiler flags";
inputs = {
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2405.*.tar.gz";
};
outputs = { self, nixpkgs }: let
system = "x86_64-linux";
# We want to add custom compile flags, but not to the bootstrap
# packages, because that will trigger unnecessary building of
# stuff that we never actually link, like bison.
pkgs = import nixpkgs {
inherit system;
overlays = [(final: prev: let
isBootstrap = name: builtins.match "bootstrap-.*" name != null;
in {
stdenv = if isBootstrap prev.stdenv.name
then prev.stdenv
else prev.withCFlags [ "-frecord-gcc-switches" ] prev.stdenv;
})];
};
in {
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
hello
];
};
};
}
Now everything seemed right, running nix develop
only triggered a rebuild of hello
, and it was fast! Great, but since hello
doesn’t really have any library dependencies I wanted to verify with a more complicated package, so I added ncdu
to the packages list – and compilation became slow again! Again it wanted to rebuild the world, bison, perl, cmake, openssl and more.
Because the purpose of my flake is to just build all the OSS libraries I depend on with my custom flags (hello and ncdu are just temporary standins for my app), I don’t care about binaries like the perl interpreter or bison, because I’m not linking those. So ideally I would exclude them and somehow still get the cached versions, but how could I exclude binary dependencies as a category? Maybe there’s a way to make an overlay that only affects packages that have a *.a or *.so output? Or maybe there’s a way to exclude dependencies that are only used as part of the build process to get rid of things like cmake?