An issue with pkg-config, libudev, and pkgs.stdenv.isLinux

I have a flake here that is showing a strange behavior with nativeBuildInputs. Basically, if I put the udev dependency into a pkgs.lib.optional block for Linux, pkg-config is somehow unable to find it. But if I move it out, suddenly it becomes available.

For instance, this flake doesn’t work:

{
  description = "testing pkg-config and libudev";
  inputs = { nixpkgs.url = "nixpkgs/nixos-22.05"; };
  outputs = { self, nixpkgs }:
  let
    pkgs = import nixpkgs { system = "x86_64-linux"; };
  in {
    devShell."x86_64-linux" = pkgs.mkShell {
      name = "flake-test";
      nativeBuildInputs = pkgs.lib.optional pkgs.stdenv.isLinux [ pkgs.pkg-config pkgs.udev ];
    };
  };
}
~/src/nix via ❄️  impure (flake-test) 
❯ pkg-config --libs --cflags libudev
Package libudev was not found in the pkg-config search path.
Perhaps you should add the directory containing `libudev.pc'
to the PKG_CONFIG_PATH environment variable
No package 'libudev' found

But when I move pkgs.udev out of the optional block, pkg-config has no trouble finding it.

{
  description = "testing pkg-config and libudev";
  inputs = { nixpkgs.url = "nixpkgs/nixos-22.05"; };
  outputs = { self, nixpkgs }:
  let
    pkgs = import nixpkgs { system = "x86_64-linux"; };
  in {
    devShell."x86_64-linux" = pkgs.mkShell {
      name = "flake-test";
      nativeBuildInputs = [ pkgs.udev ] ++ pkgs.lib.optional pkgs.stdenv.isLinux [ pkgs.pkg-config ];
    };
  };
}

❯ pkg-config --libs --cflags libudev
-I/nix/store/yx6l4ch95n1g7xr0ryp176zflvvaq8ph-systemd-250.4-dev/include -L/nix/store/m6qj9brj0xmigvsadsq5n86kp36cxqb5-systemd-250.4/lib -ludev

To be clear… if I remove pkg-config from the dependency list, pkg-config stops being in my environment. So it really appears that pkgs.stdenv.isLinux is working correctly, but there is something strange about how the flake works with udev.

The goal is to only include dependencies like pkg-config and udev on Linux platforms, where the flake is actually going to be shared amongst Mac and Linux developers.

Can any of you pinpoint what I’m doing wrong here?

You want lib.optionals, not lib.optional. lib.optional wraps the returned value in a single-element list, so:

nativeBuildInputs = pkgs.lib.optional pkgs.stdenv.isLinux [ pkgs.pkg-config pkgs.udev ];

ends up evaluating to:

nativeBuildInputs = [ [ pkgs.pkg-config pkgs.udev ] ];

Somewhere some flattening is happening, possibly accidentally when this gets converted to bash variables, possibly earlier. But since nativeBuildInputs wasn’t a list of derivations but a list of lists, some processing on the inputs was skipped, causing the mismatch in behavior.

Really this should likely fail and/or warn, I suggest filing an issue on nixpkgs to see if people have objections :slight_smile:

1 Like

If you want pkg-config to find libudev, it needs to be in buildInputs not nativeBuildInputs. The latter is for tools ran during the build, the former for libraries linked into the result.

Oh! That would explain part of it, though not why pkgconfig seems to work just fine. Still, switching from lib.optional to lib.optionals worked flawlessly!

I’ve never been clear on the difference between buildInputs and nativeBuildInputs. Does it matter when I’m making a development shell?

1 Like

I’m actually really surprised that nativeBuildInputs and buildInputs don’t just fail on a type check. Doesn’t seem like they would accept a nested list.

For the purposes of a dev shell, it might not but it’s cleaner to make the difference clear.

This is mainly important when the targetPlatform != buildPlatform (i.e. cross-compilation).

The reason why pkg-config worked and not udev is because at some point the nativeBuildInputs are processed to use their dev output if it exists, and while this doesn’t make a difference for pkg-config (AFAIK it only has one output), udev has a dev output which didn’t end up being depended on with the nested list.

(As far as I understand, at least, I didn’t dig very deep.)