Review flake.nix for my project?

Greetings all. I’m pretty new to Nix/NixOS and was wondering if I could get some feedback on adding a flake.nix to a few small CLI projects of mine. One is actually packaged already in nixpkgs (networkmanager_dmenu…not by me!).

  1. Is it worth it to add a flake.nix to the project, mostly just to show Nix support?
  2. If it is worth it, can I get some feedback on the following flake? Anything that could be done better or different?
{
  description = "Manage NetworkManager connections with dmenu/rofi/wofi instead of nm-applet";

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

  outputs = {
    self,
    nixpkgs,
  }: let
    systems = ["x86_64-linux"];
    forAllSystems = f:
      nixpkgs.lib.genAttrs systems (system:
        f {
          pkgs = import nixpkgs {inherit system;};
        });
  in {
    devShells = forAllSystems ({pkgs}: {
      default = pkgs.mkShell {
        packages = builtins.attrValues {
          inherit
            (pkgs)
            glib
            gobject-introspection
            networkmanager
            ;
          inherit
            (pkgs.python3Packages)
            python
            pygobject3
            ;
        };
      };
    });
    packages = forAllSystems ({pkgs}: {
      default = pkgs.stdenv.mkDerivation {
        name = "networkmanager_dmenu";
        pname = "networkmanager_dmenu";
        dontBuild = true;
        src = ./.;
        buildInputs = builtins.attrValues {
          inherit
            (pkgs)
            glib
            gobject-introspection
            networkmanager
            ;
          inherit
            (pkgs.python3Packages)
            python
            pygobject3
            wrapPython
            ;
        };
        installPhase = ''
          mkdir -p $out/bin $out/share/applications $out/share/doc/$pname
          cp networkmanager_dmenu $out/bin/
          cp networkmanager_dmenu.desktop $out/share/applications
          cp README.md $out/share/doc/$pname/
          cp config.ini.example $out/share/doc/$pname/
        '';
        postFixup = let
          inherit (pkgs.python3Packages) pygobject3;
        in ''
           makeWrapperArgs="\
          --prefix GI_TYPELIB_PATH : $GI_TYPELIB_PATH \
          --prefix PYTHONPATH : \"$(toPythonPath $out):$(toPythonPath ${pygobject3})\""
           wrapPythonPrograms
        '';
        meta = {
          description = "Manage NetworkManager connections with dmenu/rofi/wofi instead of nm-applet";
          homepage = "https://github.com/firecat53/networkmanager-dmenu";
          license = pkgs.lib.licenses.mit;
          maintainers = ["firecat53"];
          platforms = pkgs.lib.platforms.all;
        };
      };
    });
  };
}

Thanks so much to the package maintainer @jensbin who originally submitted the package to nixpkgs ! I would not have been able to even start this without the work they did!

Thank you!

1 Like

Awesome! Yes, having a flake.nix in your project can be very useful. Not just for showing nix support, but also for contributors to ensure they have the same versions of all dependencies as you while trying out changes, and for making it easy to compile more complex projects. I guess for small projects like yours it doesn’t make that much of a difference though.

It makes it a lot easier to add projects to nixpkgs if they already have a flake in their repo, though!

In general, I think your flake is very good! Clear to read, focused and avoids most common pitfalls.

systems = ["x86_64-linux"];`

As you only have a .desktop file and a python script, "aarch64-linux" and "i686-linux" should be compatible as well, right?

pkgs = import nixpkgs {inherit system;};

This should be avoided in flakes. Instead, you should write

pkgs = nixpkgs.legacyPackages.${system}

See 1000 instances of nixpkgs - #14 by zimbatm for more information.

packages = builtins.attrValues {
         inherit
           (pkgs)
           glib

Very nice. This avoids common problems that can happen in with pkgs; [ ].

buildInputs = builtins.attrValues {

You define a lot of dependencies twice. You could put something like commonDependencies into the let part of your flake and inherit from that here and in the mkShell call to reduce duplication, but it’s a matter of taste.

postFixup = let

Hm, maybe you know something I don’t, but why are you using postFixup instead of fixupPhase?

platforms = pkgs.lib.platforms.all;

This seems incorrect.

Thanks for the feedback!

My biggest challenge is trying to get a commonPackages setup to use for both the shell and building the package. I’ve fought with this for many hours :frowning_face: Here’s the flake pared down just to the shell that I can’t seem to get working by passing the commonPackages to devShells.

{
  description = "Manage NetworkManager connections with dmenu/rofi/wofi instead of nm-applet";

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

  outputs = {
    self,
    nixpkgs,
  }: let
    systems = ["x86_64-linux" "i686-linux" "aarch64-linux"];
    forAllSystems = f:
      nixpkgs.lib.genAttrs systems (system:
        f rec {
          pkgs = nixpkgs.legacyPackages.${system};
		  commonPackages = builtins.attrValues {
			inherit
			  (pkgs)
			  glib
			  gobject-introspection
			  networkmanager
			  ;
			inherit
			  (pkgs.python3Packages)
			  python
			  pygobject3
			  ;
			};
        });
  in {
    devShells = forAllSystems ({pkgs}: {
      default = pkgs.mkShell {
		packages = commonPackages;
	  };
    });
  };
}

This gets the following error:

$ nix develop
error: undefined variable 'commonPackages'

       at /nix/store/7pmx8jiaq3dwyzmz94mk9vn6s253wia2-source/flake.nix:34:14:

           33|       default = pkgs.mkShell {
           34|          packages = commonPackages;
             |              ^
           35|    };

I’ve tried adding inherit commonPackages at various levels after the in statement, but keep getting the same error. Where am I going wrong? How will I add in the wrapPython package to commonPackages when the package is built?

Thanks!

You really got the hang of this quickly! There’s just a tiny little mistake:

    devShells = forAllSystems ({pkgs}: {  # In this line
      default = pkgs.mkShell {
		packages = commonPackages;
	  };
    });

the function you’re defining is missing the commonPackages argument. It should be

    devShells = forAllSystems ({pkgs, commonPackages}: {  # Fixed!

And now:

nix flake show
path:/Users/felix/repos/experiments/fireflake?lastModified=1687726855&narHash=sha256-y99JCYGqJ/5/KH/AkoQDJAWwAqHmhz%2BNzZVQpfBb5n8%3D
└───devShells
    ├───aarch64-linux
    │   └───default omitted (use '--all-systems' to show)
    ├───i686-linux
    │   └───default omitted (use '--all-systems' to show)
    └───x86_64-linux
        └───default omitted (use '--all-systems' to show)
1 Like

Hah, I was so close at one point!! Thanks for the tip!

I replaced postFixup with fixupPhase and it didn’t seem to have any ill effects. The original maintainer of this package in nixpkgs wrote the derivation using postFixup. Maybe because postFixup is a hook that happens after the default fixupPhase actions, setting that instead won’t interfere with any default fixupPhase actions? (That’s totally a guess).

For future reference, here’s the final flake:

{
  description = "Manage NetworkManager connections with dmenu/rofi/wofi instead of nm-applet";

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

  outputs = {
    self,
    nixpkgs,
  }: let
    systems = ["x86_64-linux" "i686-linux" "aarch64-linux"];
    forAllSystems = f:
      nixpkgs.lib.genAttrs systems (system:
        f rec {
          pkgs = nixpkgs.legacyPackages.${system};
          commonPackages = builtins.attrValues {
            inherit
              (pkgs)
              glib
              gobject-introspection
              networkmanager
              ;
            inherit
              (pkgs.python3Packages)
              python
              pygobject3
              ;
          };
        });
  in {
    devShells = forAllSystems ({
      pkgs,
      commonPackages,
    }: {
      default = pkgs.mkShell {
        packages = commonPackages;
      };
    });
    packages = forAllSystems ({
      pkgs,
      commonPackages,
    }: {
      default = pkgs.stdenv.mkDerivation {
        name = "networkmanager_dmenu";
        pname = "networkmanager_dmenu";
        dontBuild = true;
        src = ./.;
        buildInputs = commonPackages ++ [pkgs.python3Packages.wrapPython];
        installPhase = ''

          mkdir -p $out/bin $out/share/applications $out/share/doc/$pname
          cp networkmanager_dmenu $out/bin/
          cp networkmanager_dmenu.desktop $out/share/applications
          cp README.md $out/share/doc/$pname/
          cp config.ini.example $out/share/doc/$pname/
        '';
        postFixup = let
          inherit (pkgs.python3Packages) pygobject3;
        in ''
           makeWrapperArgs="\
          --prefix GI_TYPELIB_PATH : $GI_TYPELIB_PATH \
          --prefix PYTHONPATH : \"$(toPythonPath $out):$(toPythonPath ${pygobject3})\""
           wrapPythonPrograms
        '';
        meta = {
          description = "Manage NetworkManager connections with dmenu/rofi/wofi instead of nm-applet";
          homepage = "https://github.com/firecat53/networkmanager-dmenu";
          license = pkgs.lib.licenses.mit;
          maintainers = ["firecat53"];
          platforms = systems;
        };
      };
    });
  };
}
1 Like

Looks great!

And also 100% correct. Generally, it’s quite common to replace something like the installPhase, but for unpackPhase or fixupPhase, using the post-hook is preferred.

1 Like

Not really, the default fixupPhase actually calls the hook internally, so if you override the phase without invoking the hook yourself, the hook will not get called:

And in addition to that, it does much more stuff so you should not ever override it, unless you are absolutely certain you will not need any existing or future functionality of fixupPhase.

nixpkgs-hammering will warn you about this.

When overriding other phases, you can be a bit less careful but it still should be avoided when possible. And you should still take care to run the hooks explicitly.

1 Like

Thanks for the clarification! In hindsight, it was discussed in the nixpkgs manual, but was not so clear when I was first trying to read it!


However, typically one only wants to add some commands to a phase, e.g. by defining postInstall > or preFixup, as skipping some of the default actions may have unexpected consequences.

I will leave it as postFixup !