Build

The pyproject.nix build infrastructure is mainly targeted at python2nix authors, and is being used in uv2nix.

Pyproject.nix can be used with nixpkgs buildPythonPackage/packageOverrides/withPackages, but also implements it's own build infrastructure that fixes many structural problems with the nixpkgs implementation.

Problems with nixpkgs Python builders

Nixpkgs Python infrastucture relies on dependency propagation. The propagation mechanism works through making dependencies available to the builder at build-time, and recording their Nix store paths in $out/nix-support/propagated-build-inputs. Setup hooks are then used to add these packages to $PYTHONPATH for discovery by the Python interpreter which adds everything from $PYTHONPATH to sys.path at startup.

Propagation causes several issues downstream.

$PYTHONPATH leaking into unrelated builds

Consider the following development shell using nixpkgs Python builders:

let
  pkgs = import <nixpkgs> { };
  pythonEnv = pkgs.python3.withPackages(ps: [ ps.requests ]);
in pkgs.mkShell {
  packages = [
    pkgs.remarshal
    pythonEnv
  ];
}

Any Python package, such as remarshal, will have their dependencies leaking into $PYTHONPATH, making undeclared dependencies available to the Python interpreter. Making matters even worse: Any dependency on $PYTHONPATH takes precedence over virtualenv installed dependencies!

Infinite recursions

Nix dependency graphs are required to be a DAG, but Python dependencies can be cyclic. Dependency propagation is inherently incompatible with cyclic dependencies. In nixpkgs propagation isssues are commonly worked around by patching packages in various ways.

Binary wrapping

Nixpkgs Python builders uses wrappers for Python executables in bin/, these set environment variables NIX_PYTHONPATH & friends These environment variables are picked up by the interpreter using a sitecustomize.py in the system site-packages directory.

Any Python programs executing another child Python interpreter using sys.executable will have it's modules lost on import, as sys.executable isn't pointing to the environment created by withPackages.

Extraneous rebuilds

Because buildPythonPackage uses propagation runtime dependencies of a package are required to be present at build time.

In Python builds runtime dependencies are not actually required to be present. Making runtime dependencies available at build-time results in derivation hashes changing much more frequently than they have to.

Solution presented by pyproject.nix's builders

The solution is to decouple the runtime dependency graph from the build time one, by putting runtime dependencies in passthru:

stdenv.mkDerivation {
  pname = "setuptools-scm";
  version = "8.1.0";
  src = fetchurl {
    url = "https://files.pythonhosted.org/packages/4f/a4/00a9ac1b555294710d4a68d2ce8dfdf39d72aa4d769a7395d05218d88a42/setuptools_scm-8.1.0.tar.gz";
    hash = "";
  };

  passthru = {
    dependencies = {
      packaging = [ ];
      setuptools = [ ];
    };

    optional-dependencies = {
      toml = { toml = [ ]; };
      rich = { rich = [ ]; };
    };
  };

  nativeBuildInputs = [
    pyprojectHook
  ] ++ resolveBuildSystem (
    {
      setuptools = [ ];
    }
  );
}

Resolving

Because runtime dependencies are not propagated every package needs to resolve the runtime dependencies of their build-system's.

Additionally packages can't simply be consumed, but must be aggregated into a virtual environment to be useful:

{ pyproject-nix, pkgs }:

let
  python = pkgs.python312;

  # Inject your own packages on top with overrideScope
  pythonSet = pkgs.callPackage pyproject-nix.build.packages {
    inherit python;
  };

in pythonSet.pythonPkgsHostHost.mkVirtualEnv "test-venv" {
  build = [ ];
}

Cyclic dependencies

Cyclic dependencies are supported thanks to the resolver returning a flat list of required Python packages. For performance reasons two solvers are implemented:

  • One that does not support cyclic dependencies A much more performant resolver used by resolveBuildSystem and has all known build-systems memoized.

  • One that does support cyclic dependencies Used to resolve virtual environments

It's possible to override the resolver used entirely, so even though cyclic build-system's are not supported by default, it can be done with overrides.

Less rebuilds

As the runtime dependency graph is decoupled from the build time one derivation hashes change far less frequently.

An additional benefit is improved build scheduling: Because the dependency graph is much flatter than a buildPythonPackage based one derivations can be more efficiently scheduled in parallel.

Use virtualenvs

Instead of binary wrappers & environment variables pyproject.nix's builders use standard Python virtual environments.