Customizing cflags rebuilds too much

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?

Depending on the number of dependencies/libs that you need compiled with special flags, you can either override the flags per package or you can view this as some case of cross-compilation and use nixpkgs’ cross-infrastructure.

That seems pretty onerous though, the dep graph for something like qt is deep.

Won’t that guarantee I have to rebuild everything?

You’re changing the build flags used in stdenv. This will affect almost every package, as they are built using stdenv or a builder that abstracts stdenv. As you noted about qt there is a larger build tree, like many programs, and by changing stdenv you invalidate the cache for the entire tree.

The reliable options as I understand them are to be more selective in your override, or accept that nix is rebuilding everything as you’ve asked it to do.

A less reliable option could be replacing some dependencies, as discussed in this issue, Feature request: Replace package without rebuilding world · Issue #132749 · NixOS/nixpkgs · GitHub