Etiquette around exposing a package in a library flake

A recent PR I made to an OSS library (mopidy-tidal) added a flake.nix for development.

While I was at it I thought I might as well expose an actual package: after all I need to pull this into my own system, and it’s easier just to pull in a package I know builds.

I’ve not done this before (tried to provide a package output for others / nixpkgs to pull) so I thought I’d ask for criticism. Here’s the flake:

{
  description = "Mopidy Extension for Tidal music service integration.";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
  };

  outputs = inputs @ {
    self,
    nixpkgs,
    flake-parts,
    ...
  }:
    flake-parts.lib.mkFlake {inherit inputs;} {
      systems = nixpkgs.lib.systems.flakeExposed;
      perSystem = {
        pkgs,
        system,
        ...
      }: let
        pyProject = builtins.fromTOML (builtins.readFile ./pyproject.toml);
        version = pyProject.project.version;
        python = pkgs.python313;
        buildInputs =
          [
            (python.withPackages (ps: [ps.gst-python ps.pygobject3]))
          ]
          ++ (with pkgs; [
            # dev
            uv
            pre-commit
            ruff
            # deps
            mopidy # for its build inputs, and local testing
            gobject-introspection
            glib-networking
            # integration tests
            mpc
            mopidy-mpd
            # local testing
            mopidy-local
            mopidy-iris
          ])
          ++ (with pkgs.gst_all_1; [
            gst-plugins-bad
            gst-plugins-base
            gst-plugins-good
            gst-plugins-ugly
            gst-plugins-rs
          ]);
      in {
        devShells.default = pkgs.mkShell {
          inherit buildInputs;
          env = {
            UV_PROJECT_ENVIRONMENT = ".direnv/venv";
            # libsoup_3 is broken, and why wouldn't you use curl?
            GST_PLUGIN_FEATURE_RANK = "curlhttpsrc:MAX";
          };
          shellHook = ''
            # pre-commit install
            [ ! -d $UV_PROJECT_ENVIRONMENT ] && uv venv $UV_PROJECT_ENVIRONMENT --python ${python}/bin/python
            source $UV_PROJECT_ENVIRONMENT/bin/activate
          '';
        };
        packages.default = pkgs.python3Packages.buildPythonApplication {
          pname = "mopidy-tidal";
          inherit version;
          pyproject = true;
          src = ./.;
          build-system = [pkgs.python3Packages.uv-build];
          nativeCheckInputs = with pkgs.python3Packages; [
            pytestCheckHook
            pytest-asyncio
            pytest-cov # since default pytest invocation includes --cov
            pytest-mock
            pytest-httpserver
            pytest-cases
            trustme
            httpx
          ];

          dependencies = [
            pkgs.mopidy
            pkgs.python3Packages.tidalapi
          ];
        };
      };
    };
}

(I’m really interested in criticism of the packages output, but feel free to criticise the rest).

Reasoning here was:

  • I could use uv2nix, but I’ve not seen it in nixpkgs (presumably to keep the closure small?)
  • there’s no point naming it anything as far as I can see, this lib will only ever have one package

Is it worthwhile submitting a PR to nixpkgs to consume the flake directly?

Well, it’s nice to have an alias, just in case, and so that people can use inherit and lib.attrValues. It’s a 2 line thing:

perSystem = { self', ... }: {
  packages = {
    default = self'.packages.mopidy-tidal;
    mopidy-tidal = # The actual derivation
  };
}

Use packages = buildInputs. buildInputs doesn’t run the native hooks, which can cause issues.


No. “Consume the flake directly” implies using the flake’s nixpkgs, which results in a mind-bendy recursive dependency on old nixpkgs versions, causing closure sizes to explode and CVEs to be unfixable.

Plus, handing package maintainership to third repos fully is problematic anyway. The reverse isn’t much better either. Whichever place you hand maintainership, moving ahead either in an incompatible way becomes impossible if you have a direct dependency on the nix build files, forcing you to end up with a broken package on one side or the other.


I’d also suggest running deadnix and statix (or my very WIP alternative :p) against your code.

1 Like

Oh interesting. Can you elaborate?

Oh thanks, these are cool. I’d wondered idly about something like clippy for nix but not gone looking.

Probably best to just point you at the nixpkgs doc heading: Nixpkgs Reference Manual

For mkShell packages is an alias for nativeBuildInputs. nativeBuildInputs is the correct place to put stuff that run on the build architecture, which is everything for mkShell. That’s why packages exists, so that it’s easy to remember where to put your packages in mkShell. LLMs and blog posts often get it wrong for historic reasons tho.

That’s mostly statix; it’s pretty limited tbh, it only has like 20 lints, most of which are quite niche. It’s also impossible to get PRs in due to maintainer absence IME. Hence I’m working on something a bit better (already noted pkgs.mkShell with buildInputs as a lint to add thanks to you :wink: ).

1 Like

For code analysis, there’s also nixd/libnixf/README.md at 13a89b59d0711390f0c765e693509f8282a1ff7e · nix-community/nixd · GitHub which includes nixf-tidy for standalone analysis. For editor integration there’s nixd (LSP, same repo).

2 Likes

I will say, if you’re thinking about etiquette, I prefer the projects I vendor to not use library flakes like flake-utils and flake-parts. A simple flake like this isn’t gaining much from using flake-parts, and removing it helps to keep the user’s flake.lock smaller. I also think package definitions should live outside the flake.nix itself, so people who don’t use flakes can vendor the project easier, but that’s a fight I’m less likely to win.

2 Likes

Additionally, arguably things like devshells and checks should not be in the entrypoint flake.nix, but a separate one in a subdirectory, since users don’t care about those and they just add additional dependencies.

Generally, I don’t think flakes are really mature enough to use them to distribute software yet; there are many missing details. Hence I don’t really think “etiquette” matters much yet, it’s kind of the wild west still, and likely will remain so for the foreseeable…

But yeah, try not to pull a crate2nix. That’s taking it a bit far.

1 Like

I’d agree that flakes aren’t “ready”, but I think they have been adopted, regardless of whether it’s a good idea. Last year’s survey backs this up, with 85% having the flakes experimental feature enabled. I may not like the status quo, but I recognize the community is going in this direction, and want to encourage sane practices.