Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

uv2nix takes a uv workspace and generates Nix derivations dynamically using pure Nix code. It's designed to be used both as a development environment manager, and to build production packages for projects.

It is heavily based on pyproject.nix and its build infrastructure.

Installation

Classic Nix

Documentation examples in uv2nix are using Flakes for the convenience of grouping multiple concepts into a single file.

You can just as easily import uv2nix without using Flakes:

let
  pkgs = import <nixpkgs> { };
  inherit (pkgs) lib;

  pyproject-nix = import (builtins.fetchGit {
    url = "https://github.com/pyproject-nix/pyproject.nix.git";
  }) {
    inherit lib;
  };

  uv2nix = import (builtins.fetchGit {
    url = "https://github.com/pyproject-nix/uv2nix.git";
  }) {
    inherit pyproject-nix lib;
  };

  pyproject-build-systems = import (builtins.fetchGit {
    url = "https://github.com/pyproject-nix/build-system-pkgs.git";
  }) {
    inherit pyproject-nix uv2nix lib;
  };

in
  ...

Flakes

See usage/hello-world.

Getting started with uv2nix

Before going further and adopting uv2nix, first consider if you really want/need it. You don't need uv2nix to develop uv projects with Nix.

If you are not deploying your application with Nix it's likely that you don't want uv2nix and might be fine using the impure template from pyproject.nix:

nix flake init --template github:pyproject-nix/pyproject.nix#impure

It's also possible that you might want to develop your package(s) in an impure shell (as opposed to using editables with uv2nix, see development shell below), but deploy using uv2nix.

Any combination of approaches is possible, with different trade-offs for each.

Reading along

This getting started guide is intended to be read alongside the hello-world uv2nix template:

nix flake init --template github:pyproject-nix/uv2nix#hello-world

which contains much of the same code, but without elaboration or explanation of all the involved concepts.

Creating a pyproject.toml & uv.lock

Before anything can be done enter a development shell with required bootstrapping dependencies:

nix-shell -p python3 uv

And then use uv to create a boilerplate pyproject.toml & a uv.lock:

uv init --app --package
uv lock

You can now start adding dependencies & more.

Using pyproject.nix/uv2nix

Constructing a base Python set

Uv2nix uses pyproject.nix Python builders which needs to be instantiated with a nixpkgs instance:

pythonBase = pkgs.callPackage pyproject-nix.build.packages {
  inherit python;
};

Creates the necessary structure & build hooks, but it doesn't contain any Python packages itself. In uv2nix all Python packages are generated from uv.lock & explicitly added through overlays.

Loading a uv workspace

The top-level abstraction in uv2nix is the workspace, which needs to be loaded.

workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };

Will recursively discover, load & parse all necessary member projects in a uv workspace.

Uv2nix treats every project as a workspace project, even if it only contains a single pyproject.toml with a single project.

Creating a uv2nix generated overlay

Takes uv.lock & creates an overlay for use with pyproject.nix builders.

overlay = workspace.mkPyprojectOverlay {
  sourcePreference = "wheel";
};

With sourcePreference you have a choice to make:

  • wheel

Prefer downloading packages as binary wheels.

  • sdist

Prefer building packages from source.

Binary wheels are much more likely to "just work" while sdists require manual overrides more often. Wheel/sdist selection can also be done on a per-package basis.

If you are experiencing uv2nix using an sdist where you expect it to use a wheel on MacOS you might need to set the appropriate platform quirks.

Notes on build systems

uv doesn't lock build systems which are required when building packages from source. uv2nix doesn't try to hide this definiency, but instead has an overlay with the most common ones provided.

The build system overlay has the same sdist/wheel distinction as mkPyprojectOverlay:

  • pyproject-build-systems.overlays.wheel

Prefer build systems from binary wheels

  • pyproject-build-systems.overlays.sdist

Prefer build systems packages from source

Gluing everything together into a package set

Compose the Python base set + build systems + uv.lock generated packages into a concrete Python set:

pythonSet = pythonBase.overrideScope (
  lib.composeManyExtensions [
    pyproject-build-systems.overlays.wheel
    overlay
  ]
);

This set contains all Python packages as individual attributes.

Building a virtual environment

Uv2nix builds packages individually, but they aren't really useful until they're aggregated into a virtual environment.

The most convienent way to build a virtualenv is to use one of the dependency presets:

pythonSet.mkVirtualEnv "hello-world-env" workspace.deps.default

But it's also possible to specify which dependencies to install explicitly:

pythonSet.mkVirtualEnv "hello-world-env" {
  # Install hello-world with no enabled extras
  hello-world = [ ];
}

To ship package where the virtualenv is hidden see shipping applications.

Setting up a development environment (optional)

When developing Python packages local packages are normally installed in editable mode. Editable packages make entry points like scripts available in the virtual environment, but instead of installed Python files the virtualenv contains pointers to the source tree. This means that changes to the sources are immeditately activated and doesn't require a rebuild.

Uv2nix supports editable packages, but requires you to generate a separate overlay & package set for them:

editableOverlay = workspace.mkEditablePyprojectOverlay {
  # Use environment variable pointing to editable root directory
  root = "$REPO_ROOT";
  # Optional: Only enable editable for these packages
  # members = [ "hello-world" ];
};

editablePythonSet = pythonSet.overrideScope editableOverlay;

virtualenv = editablePythonSet.mkVirtualEnv "hello-world-dev-env" workspace.deps.all;

While not strictly required it's a good idea to apply source filtering to reduce how often editable packages need to be rebuilt.

The virtualenv can then be used with mkShell:

pkgs.mkShell {
  packages = [
    virtualenv
    pkgs.uv
  ];

  env = {
    UV_NO_SYNC = "1";
    UV_PYTHON = editablePythonSet.python.interpreter;
    UV_PYTHON_DOWNLOADS = "never";
  };

  shellHook = ''
    unset PYTHONPATH
    export REPO_ROOT=$(git rev-parse --show-toplevel)
  '';
}

The env attribute contains these settings:

  • UV_NO_SYNC

    Prevent uv from managing a virtual environment, this is managed by uv2nix.

  • UV_PYTHON

    Use interpreter path for all uv operations.

  • UV_PYTHON_DOWNLOADS

    Prevent uv from downloading managed Python interpreters, we use Nix instead.

The shellHook contains two interesting pieces:

Some advanced build systems (like meson-python) are more involved & require additional setup.

Overriding

For more detailed information on overriding, see pyproject.nix.

Overriding sdist's (source builds)

  • overrides-sdist.nix
{ pkgs }:
final: prev: {

  pyzmq = prev.pyzmq.overrideAttrs (old: {

    # Use the zeromq library from nixpkgs.
    #
    # If not provided by the system pyzmq will build a zeromq library
    # as a part of it's package build, taking unnecessary time & effort.
    buildInputs = (old.buildInputs or [ ]) ++ [ pkgs.zeromq ];

    # uv.lock does not contain build-system metadata.
    # Meaning that for source builds, this needs to be provided by overriding.
    #
    # Pyproject.nix's build-system-pkgs contains some of the most
    # important build systems already, so you don't have to add these to your project.
    #
    # For a comprehensive list see
    # https://github.com/pyproject-nix/build-system-pkgs/blob/master/pyproject.toml
    #
    # For build-systems that are not present in this list you can either:
    # - Add it to your `uv` project
    # - Add it manually in an overlay
    # - Submit a PR to build-system-pkgs adding the build system
    nativeBuildInputs = old.nativeBuildInputs ++ [
      (final.resolveBuildSystem {
        cmake = [ ];
        ninja = [ ];
        packaging = [ ];
        pathspec = [ ];
        scikit-build-core = [ ];
        cython = [ ];
      })
    ];

  });

}

The proper solution for this would be for uv to lock build systems.

Overriding wheels (pre-built binaries)

  • overrides-wheels.nix
{ pkgs }:
_final: prev: {

  # Wheels are automatically patched using autoPatchelfHook.
  #
  # For manylinux wheels the appropriate packages are added
  # as described in https://peps.python.org/pep-0599/ and various other PEPs.
  #
  # Some packages provide binary libraries as a part of their binary wheels,
  # others expect libraries to be provided by the system.
  #
  # Numba depends on libtbb, of a more recent version than nixpkgs provides in it's default tbb attribute.
  numba = prev.numba.overrideAttrs (old: {
    buildInputs = (old.buildInputs or [ ]) ++ [ pkgs.tbb_2021_11 ];
  });

}

Long term this situation could be improved by PEP-725.

Resources

Overriding build systems

Overriding many build systems manually can quickly become tiresome with repeated declarations of nativeBuildInputs & calls to resolveBuildSystem for every package.

This overlay shows one strategy to deal with many build system overrides in a declarative fashion.

final: prev:
let
  inherit (final) resolveBuildSystem;
  inherit (builtins) mapAttrs;

  # Build system dependencies specified in the shape expected by resolveBuildSystem
  # The empty lists below are lists of optional dependencies.
  #
  # A package `foo` with specification written as:
  # `setuptools-scm[toml]` in pyproject.toml would be written as
  # `foo.setuptools-scm = [ "toml" ]` in Nix
  buildSystemOverrides = {
    attrs = {
      hatchling = [ ];
      hatch-vcs = [ ];
      hatch-fancy-pypi-readme = [ ];
    };
    hatchling = {
      pathspec = [ ];
      pluggy = [ ];
      packaging = [ ];
      trove-classifiers = [ ];
    };
    pathspec = {
      flit-core = [ ];
    };
    pluggy = {
      setuptools = [ ];
    };
    trove-classifiers = {
      setuptools = [ ];
    };
    tomli.flit-core = [ ];
    coverage.setuptools = [ ];
    blinker.setuptools = [ ];
    certifi.setuptools = [ ];
    charset-normalizer.setuptools = [ ];
    idna.flit-core = [ ];
    urllib3 = {
      hatchling = [ ];
      hatch-vcs = [ ];
    };
    pip = {
      setuptools = [ ];
      wheel = [ ];
    };
    packaging.flit-core = [ ];
    requests.setuptools = [ ];
    pysocks.setuptools = [ ];
    pytest-cov.setuptools = [ ];
    tqdm.setuptools = [ ];
  };

in
mapAttrs (
  name: spec:
  prev.${name}.overrideAttrs (old: {
    nativeBuildInputs = old.nativeBuildInputs ++ resolveBuildSystem spec;
  })
) buildSystemOverrides

Testing

uv2nix uses the pyproject.nix build infrastructure.

Unlike the nixpkgs, runtime & test dependencies are not available at build time. Tests should instead be implemented as separate derivations.

This usage pattern shows how to:

  • Overriding a package adding tests to passthru.tests
  • Using passthru.tests in Flake checks

flake.nix

{
  description = "Pytest flake using uv2nix";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

    pyproject-nix = {
      url = "github:pyproject-nix/pyproject.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    uv2nix = {
      url = "github:pyproject-nix/uv2nix";
      inputs.pyproject-nix.follows = "pyproject-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    pyproject-build-systems = {
      url = "github:pyproject-nix/build-system-pkgs";
      inputs.pyproject-nix.follows = "pyproject-nix";
      inputs.uv2nix.follows = "uv2nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    {
      nixpkgs,
      uv2nix,
      pyproject-nix,
      pyproject-build-systems,
      ...
    }:
    let
      inherit (nixpkgs) lib;

      forAllSystems = lib.genAttrs lib.systems.flakeExposed;

      workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };

      overlay = workspace.mkPyprojectOverlay {
        sourcePreference = "wheel";
      };

      # Python sets grouped per system
      pythonSets = forAllSystems (
        system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
          inherit (pkgs) stdenv;

          baseSet = pkgs.callPackage pyproject-nix.build.packages {
            python = pkgs.python312;
          };

          # An overlay of build fixups & test additions.
          pyprojectOverrides = final: prev: {

            # testing is the name of our example package
            testing = prev.testing.overrideAttrs (old: {

              passthru = old.passthru // {
                # Put all tests in the passthru.tests attribute set.
                # Nixpkgs also uses the passthru.tests mechanism for ofborg test discovery.
                #
                # For usage with Flakes we will refer to the passthru.tests attributes to construct the flake checks attribute set.
                tests =
                  let
                    # Construct a virtual environment with only the test dependency-group enabled for testing.
                    virtualenv = final.mkVirtualEnv "testing-pytest-env" {
                      testing = [ "test" ];
                    };

                  in
                  (old.tests or { })
                  // {
                    pytest = stdenv.mkDerivation {
                      name = "${final.testing.name}-pytest";
                      inherit (final.testing) src;
                      nativeBuildInputs = [
                        virtualenv
                      ];
                      dontConfigure = true;

                      # Because this package is running tests, and not actually building the main package
                      # the build phase is running the tests.
                      #
                      # In this particular example we also output a HTML coverage report, which is used as the build output.
                      buildPhase = ''
                        runHook preBuild
                        pytest --cov tests --cov-report html
                        runHook postBuild
                      '';

                      # Install the HTML coverage report into the build output.
                      #
                      # If you wanted to install multiple test output formats such as TAP outputs
                      # you could make this derivation a multiple-output derivation.
                      #
                      # See https://nixos.org/manual/nixpkgs/stable/#chap-multiple-output for more information on multiple outputs.
                      installPhase = ''
                        runHook preInstall
                        mv htmlcov $out
                        runHook postInstall
                      '';
                    };

                  };
              };
            });
          };

        in
        baseSet.overrideScope (
          lib.composeManyExtensions [
            pyproject-build-systems.overlays.default
            overlay
            pyprojectOverrides
          ]
        )
      );

    in
    {
      # Construct flake checks from Python set
      checks = forAllSystems (
        system:
        let
          pythonSet = pythonSets.${system};
        in
        {
          inherit (pythonSet.testing.passthru.tests) pytest;
        }
      );
    };
}

pyproject.toml

[project]
name = "testing"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.12"

[project.scripts]
hello = "testing:hello"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[dependency-groups]
dev = [
  "ruff>=0.7.2",
  {include-group = "test"}
]
test = [
  "pytest-cov>=6.0.0",
  "pytest>=8.3.3",
]

Development scripts

It's common to have development scripts that you don't want to publish in a Python package, but that you might want to run ergonomically from Nix.

This pattern shows how to:

  • Take a directory of development scripts (examples/)
  • Wrap the scripts in a virtualenv
  • Make scripts runnable using nix run

flake.nix

{
  description = "Using Nix Flake apps to run scripts with uv2nix";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

    pyproject-nix = {
      url = "github:pyproject-nix/pyproject.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    uv2nix = {
      url = "github:pyproject-nix/uv2nix";
      inputs.pyproject-nix.follows = "pyproject-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    pyproject-build-systems = {
      url = "github:pyproject-nix/build-system-pkgs";
      inputs.pyproject-nix.follows = "pyproject-nix";
      inputs.uv2nix.follows = "uv2nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    {
      nixpkgs,
      uv2nix,
      pyproject-nix,
      pyproject-build-systems,
      ...
    }:
    let
      inherit (nixpkgs) lib;
      inherit (lib) filterAttrs hasSuffix;

      workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };

      overlay = workspace.mkPyprojectOverlay {
        sourcePreference = "wheel"; # or sourcePreference = "sdist";
      };

      pkgs = nixpkgs.legacyPackages.x86_64-linux;

      python = pkgs.python312;

      pythonSet =
        (pkgs.callPackage pyproject-nix.build.packages {
          inherit python;
        }).overrideScope
          (
            lib.composeManyExtensions [
              pyproject-build-systems.overlays.default
              overlay
            ]
          );

      venv = pythonSet.mkVirtualEnv "development-scripts-default-env" workspace.deps.default;
    in
    {

      apps.x86_64-linux =
        let
          # Example base directory
          basedir = ./examples;

          # Get a list of regular Python files in example directory
          files = filterAttrs (name: type: type == "regular" && hasSuffix ".py" name) (
            builtins.readDir basedir
          );

        in
        # Map over files to:
        # - Rewrite script shebangs as shebangs pointing to the virtualenv
        # - Strip .py suffixes from attribute names
        #   Making a script "greet.py" runnable as "nix run .#greet"
        lib.mapAttrs' (
          name: _:
          lib.nameValuePair (lib.removeSuffix ".py" name) (
            let
              script = basedir + "/${name}";

              # Patch script shebang
              program = pkgs.runCommand name { buildInputs = [ venv ]; } ''
                cp ${script} $out
                chmod +x $out
                patchShebangs $out
              '';
            in
            {
              type = "app";
              program = "${program}";
            }
          )
        ) files;

    };
}

Using meta

uv2nix primarily builds virtual environments, not individual applications.

Virtual environment derivations have no concept of what their "main" binary is, meaning that a call like lib.getExe or a command like nix run won't know what is considered the main program.

To supplement meta fields in virtualenv derivations add an override:

{
    # Expose Python virtual environments as packages.
    packages = forAllSystems (
      system:
      let
        pythonSet = pythonSets.${system};

        # Add metadata attributes to the virtual environment.
        # This is useful to inject meta and other attributes onto the virtual environment derivation.
        #
        # See
        # - https://nixos.org/manual/nixpkgs/unstable/#chap-passthru
        # - https://nixos.org/manual/nixpkgs/unstable/#chap-meta
        addMeta =
          drv:
          drv.overrideAttrs (old: {
            # Pass through tests from our package into the virtualenv so they can be discovered externally.
            passthru = lib.recursiveUpdate (old.passthru or { }) {
              inherit (pythonSet.testing.passthru) tests;
            };

            # Set meta.mainProgram for commands like `nix run`.
            # https://nixos.org/manual/nixpkgs/stable/#var-meta-mainProgram
            meta = (old.meta or { }) // {
              mainProgram = "hello";
            };
          });

      in
      {
        default = addMeta (pythonSet.mkVirtualEnv "testing-env" workspace.deps.default);
        full = addMeta (pythonSet.mkVirtualEnv "testing-env-full" workspace.deps.all);
      }
}

Shipping applications

uv2nix primarily builds virtual environments, not individual applications.

Sometimes the fact that an application is written in Python & using a virtualenv is an implementation detail that you don't want to expose in your Nix package.

For such cases pyproject.nix provides a utility function mkApplication:

{
    packages = forAllSystems (
      system:
      let
        pythonSet = pythonSets.${system};
        pkgs = nixpkgs.legacyPackages.${system};
        inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication;
      in
      {
        # Create a derivation that wraps the venv but that only links package
        # content present in pythonSet.hello-world.
        #
        # This means that files such as:
        # - Python interpreters
        # - Activation scripts
        # - pyvenv.cfg
        #
        # Are excluded but things like binaries, man pages, systemd units etc are included.
        default = mkApplication {
          venv = pythonSet.mkVirtualEnv "application-env" workspace.deps.default;
          package = pythonSet.hello-world;
      };
}

Cross compilation

Overriding build systems

When cross compiling build systems needs to be overriden twice. Once for the build host and once for the target host

let
  pyprojectOverrides = final: prev: {
    hatchling = prev.hatchling.overrideAttrs (old: {
      nativeBuildInputs =
        old.nativeBuildInputs
        ++ final.resolveBuildSystem {
          pathspec = [ ];
        };
    });
  };

in
pythonSet.overrideScope (
  lib.composeExtensions (_final: prev: {
    pythonPkgsBuildHost = prev.pythonPkgsBuildHost.overrideScope pyprojectOverrides;
  }) pyprojectOverrides
)

Adding native build dependencies

final: prev: {

  foobar = prev.foobar.overrideAttrs (old: {
    nativeBuildInputs = old.nativeBuildInputs ++ [
      final.pkgs.buildPackages.cmake
    ];

    buildInputs = (old.buildInputs or [ ]) ++ [ final.pkgs.ncurses ];
  });

}

Building redistributable wheels/sdists

Building wheels

As a part of building a Python package uv2nix builds a wheel which is installed into the Nix store. The intermediate wheel file is normally discarded once the build is complete and the wheel has been installed into it's Nix store prefix.

Using multiple outputs

Using multiple outputs allows us to not only perform our regular install steps, but also to install the wheel files into a separate output. To add a separate dist output:

pythonSet.hello-world.override (old: {
  outputs = [ "out" dist" ];
})

This will install the produced wheel into the build output directory of the dist output, producing the same contents as a uv build would produce in dist/.

Augmenting install behaviour

To augment the install behaviour to install the produced wheel into the Nix store output by overriding our package:

pythonSet.hello-world.override {
  pyprojectHook = pythonSet.pyprojectDistHook;
}

This will install the produced wheel into the build output directory, producing the same contents as a uv build would produce in dist/. Note that this method will not perform the regular pyproject.nix install steps, making the output unsuitable for usage with mkVirtualEnv.

Because of the risk of Nix store path references ending up in the wheel file via references to shared libraries & other Nix/nixpkgs specific behaviour the outputs are scanned for Nix store path references, and the build will fail if any are found.

Building sdists

By default pyproject.nix's builders will produce a wheel.

If you want to distribute an sdist instead override uvBuildType:

(pythonSet.hello-world.override {
  pyprojectHook = pythonSet.pyprojectDistHook;
}).overrideAttrs(old: {
  env.uvBuildType = "sdist";
})

Installing packages from nixpkgs

In some cases a package isn't published on PyPI, but is packaged in nixpkgs, or you might be struggling trying to figure out the overrides to build a particular package. In such cases it's possible to use Python packages from nixpkgs through an interoperability layer.

We are using the seccomp package for demonstration here, which is not available on PyPI.

By using UV_FIND_LINKS and a shellHook we can install the nixpkgs built wheel, making it possible to use both impure (uv managed) workflows & pure uv2nix managed workflows.

let
  python = pkgs.python3;

  uv-links = pkgs.symlinkJoin {
    name = "uv-links";
    paths = [
      # Note: Using the dist output which contains a wheel
      python.pkgs.seccomp.dist
    ];
  };

in
mkShell {
  packages = [ pkgs.uv python ];
  shellHook = ''
    ln -sfn ${uv-links} .uv-links
    export UV_FIND_LINKS=$(realpath -s .uv-links)
  '';
}

To be able to read the sources from a Flake evaluation you will also have to override the seccomp package sources, as .uv-links is not added to Git.

let
  pyprojectOverrides = final: prev: {
    seccomp = prev.seccomp.overrideAttrs(old: {
      buildInputs = (old.buildInputs or []) ++ python.pkgs.seccomp.buildInputs;
      src = python.pkgs.seccomp.dist;
    });
  };
in ...

Using pyproject.nix hacks

It's also possible to skip making uv aware of the package and only add the nixpkgs package from a Nix evaluation.

let
  pyprojectOverrides = final: prev: {
    seccomp = hacks.nixpkgsPrebuilt {
      from = python.pkgs.seccomp;
    };
  };

  pythonSet' = pythonSet.overrideScope pyprojectOverrides;
in
  pythonSet.mkVirtualEnv "seccomp-env" {
    seccomp = [ ];
  }

Using private (authenticated) dependencies

Uv2nix uses pkgs.fetchurl for fetching from PyPI, and inherits authentication support from nixpkgs.

Getting authentication running in the sandbox requires some system setup.

Project setup

[project]
name = "with-private-deps"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]

[[tool.uv.index]]
name = "my-index"
url = "https://pypi-proxy.fly.dev/basic-auth/simple"
explicit = true

[tool.uv.sources]
iniconfig = { index = "my-index" }

[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"

Creating a netrc file

In this documentation we assume that the netrc file is saved as /etc/nix/netrc.

machine pypi-proxy.fly.dev
login public
password heron

To use this netrc file inside our development shell run:

$ export NETRC=/etc/nix/netrc

Overriding source fetching

While pkgs.fetchurl can use a netrc file, it won't do so by default. We'll need to override our authenticated package's src attribute to use our provided file.

let
  pyprojectOverrides = _final: prev: {
    iniconfig = prev.iniconfig.overrideAttrs(old: {
      src = old.src.overrideAttrs(_: {
        # Make curl use our netrc file.
        curlOpts = "--netrc-file /etc/nix/netrc";
        # By default pkgs.fetchurl will fetch _without_ TLS verification for reproducibility.
        # Since we are transferring credentials we want to verify certificates.
        SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
      });
    });
  };
in ...

Building

To build a package we need to provide our netrc file inside the Nix sandbox.

nix build -L -v --option extra-sandbox-paths /etc/nix/netrc

For a persistent setup extra-sandbox-paths should be added to nix.conf.

Patching dependencies

With uv2nix, you can apply patches to your Python dependencies.

Use this superpower judiciously: if you're building a Python library, you probably don't want to apply patches to your dependencies that users of your library would also somehow have to apply.

Applying a patch

This overlay applies a patch to the arpeggio library.

arpeggio.patch:

--- /dev/null
+++ b/arpeggio/patched.py
@@ -0,0 +1 @@
+print("You have patched arpeggio. Congratulations!")

Note how the overlay forces us to build from sdist, which requires specifying the build system. See Overriding build systems for more details.

final: prev: {
  arpeggio =
    (prev.arpeggio.override {
      # We build from sdist (not a wheel) to apply a patch to the source
      # code.
      # Alternatively, if you're using a wheel, you could apply patches to the
      # Python code in `postInstall`/`postFixup`, but YMMV.
      sourcePreference = "sdist";
    }).overrideAttrs
      (old: {
        patches = (old.patches or [ ]) ++ [
          ./arpeggio.patch
        ];

        nativeBuildInputs = old.nativeBuildInputs ++ [
          (final.resolveBuildSystem {
            setuptools = [ ];
          })
        ];
      });
}

Advanced build systems (editables)

When using more advanced build systems, such as cython which builds native dependencies, or meson-python which relies on import hooks to dynamically perform recompilation on import an additional step needs to be taken to bridge the gap between the sandboxed Nix build and the source tree:

pkgs.mkShell {
  packages = [
    virtualenv
    pkgs.uv

    # Add build-editable package from pyproject.nix
    pyproject-nix.packages.${system}.build-editable
  ];

  env = {
    UV_NO_SYNC = "1";
    UV_PYTHON = python.interpreter;
    UV_PYTHON_DOWNLOADS = "never";
  };

  shellHook = ''
    unset PYTHONPATH
    export REPO_ROOT=$(git rev-parse --show-toplevel)

    # Re-run editable package build for side effects
    build-editable
  '';
};

Whenever you want to perform a build invoke build-editable which will in turn invoke your build system and write side effects such as .so's in-place in your source tree. For most (all?) pure-Python build systems this is not relevant.

Source filtering

Nix has functionality to apply filtering to local sources when copying to the store. This allows users to tune how often a package is rebuilt by controlling what sources affect the Nix store path hashing.

Editable packages

Source selection/filtering is extra important for editable packages, which should ideally only be rebuilt when project metadata changes.

Most Python build backends only require enough sources to discover what importable Python packages to provide for an editable build to succeed:

    app = prev.app.overrideAttrs (old: {
      src = lib.fileset.toSource rec {
        root = ./.;
        fileset = lib.fileset.unions [
          (root + "/pyproject.toml")
          (root + "/app/__init__.py")
        ];
      };
    });

This example uses the fileset API to explicitly select sources.

Another way to reduce the amount of rebuilds even further is to construct dummy sources:

  app = prev.app.overrideAttrs(old: {
    src = pkgs.runCommand "app-src" {} ''
      cp ${./pyproject.toml} pyproject.toml
      mkdir app
      touch app/__init__.py
    '';
  });

External resources

Platform quirks

pyproject.nix tries to set reasonable platform defaults when evaluating PEP-508 markers & checking wheel compatibility. There is platform metadata which is important to the Python dependency graph which may have to be overriden or supplemented from the inferred defaults.

For example Nixpkgs doesn't know which version of MacOS you're actually using. It only has information about what minimum SDK versions it supports & there is no way for Nix code to know which MacOS version you intend to target.

Setting MacOS version

To override the MacOS SDK version used for marker evaluation & wheel compatibility checks override darwinSdkVersion in stdenv.targetPlatform in the original package set creation call:

pkgs.callPackage pyproject-nix.build.packages {
  inherit python;
  stdenv = pkgs.stdenv.override {
    targetPlatform = pkgs.stdenv.targetPlatform // {
      # Sets MacOS SDK version to 15.1 which implies Darwin version 24.
      # See https://en.wikipedia.org/wiki/MacOS_version_history#Releases for more background on version numbers.
      darwinSdkVersion = "15.1";
    };
  };
}

Setting Linux kernel version for marker evaluations

Nixpkgs also doesn't know which Linux kernel you're actually targetting, but makes a reasonable guess from the linuxHeaders package. To override the Linux kernel version used for marker evaluation in the call to mkPyprojectOverlay:

let
  overlay = workspace.mkPyprojectOverlay {
    sourcePreference = "wheel";
    environ = {
      platform_release = "5.10.65";
    };
  }
in ...

Inline metadata

Uv supports locking dependencies for inline metadata scripts.

This example shows you how to set up a uv2nix from a directory of locked scripts.

It has the following features:

  • Creating one Python package set per script from ${script}.py.lock

  • Build each script with nix build .#script

  • Run each script with nix run .#script

flake.nix

{
  description = "Use PEP-723 inline metadata scripts with uv2nix";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

    pyproject-nix = {
      url = "github:pyproject-nix/pyproject.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    uv2nix = {
      url = "github:pyproject-nix/uv2nix";
      inputs.pyproject-nix.follows = "pyproject-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    pyproject-build-systems = {
      url = "github:pyproject-nix/build-system-pkgs";
      inputs.pyproject-nix.follows = "pyproject-nix";
      inputs.uv2nix.follows = "uv2nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    {
      self,
      nixpkgs,
      uv2nix,
      pyproject-nix,
      pyproject-build-systems,
      ...
    }:
    let
      inherit (nixpkgs) lib;
      forAllSystems = lib.genAttrs lib.systems.flakeExposed;

      # Load all Python scripts from ./scripts directory
      scripts =
        lib.mapAttrs
          (
            name: _:
            uv2nix.lib.scripts.loadScript {
              script = ./scripts + "/${name}";
            }
          )
          (
            lib.filterAttrs (name: type: type == "regular" && lib.hasSuffix ".py" name) (
              builtins.readDir ./scripts
            )
          );

      packages' = forAllSystems (
        system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
          python = pkgs.python3;
          baseSet = pkgs.callPackage pyproject-nix.build.packages {
            inherit python;
          };
        in
        lib.mapAttrs (
          _name: script:
          let
            # Create package overlay from script
            overlay = script.mkOverlay {
              sourcePreference = "wheel";
            };

            # Construct package set
            pythonSet = baseSet.overrideScope (
              lib.composeManyExtensions [
                pyproject-build-systems.overlays.wheel
                overlay
              ]
            );
          in
          # Write out an executable script with a shebang pointing to the scripts virtualenv
          pkgs.writeScript script.name (
            # Returns script as a string with inserted shebang
            script.renderScript {
              # Construct a virtual environment for script
              venv = script.mkVirtualEnv {
                inherit pythonSet;
              };
            }
          )
        ) scripts
      );

    in
    {
      # Drop .py suffix from scripts, making example.py runnable as example
      packages = forAllSystems (
        system:
        lib.mapAttrs' (name: drv: lib.nameValuePair (lib.removeSuffix ".py" name) drv) packages'.${system}
      );

      # Make each script runnable directly with `nix run`
      apps = forAllSystems (
        system:
        lib.mapAttrs (_name: script: {
          type = "app";
          program = "${script}";
        }) self.packages.${system}
      );
    };
}

scripts/example.py

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "tqdm",
# ]
# ///

from tqdm import tqdm

for i in tqdm(range(10000)):
    pass

Dependency conflicts

Uv exposes internally generated package markers(1), therefore uv2nix support for conflicts is limited and may have incorrect marker(2) evaluation results.

Uv has support for creating mutually exclusive groups of conflicting dependencies.

To use conflicting dependencies with uv2nix you have to tell it which conflict resolution to take when creating the package overlay:

workspace.mkPyprojectOverlay {
  sourcePreference = "wheel";
  dependencies = {
    hello-world = [ "extra1" ];
  };
}
  1. Uv pull request #4339
  2. PEP-508 environment markers

Flake Templates

hello-world

A minimal example using uv2nix for both development & packaging.

nix flake init --template github:pyproject-nix/uv2nix#hello-world

hello-tkinter

Building on on the hello-world template, adding a dependency on the tkinter graphics library.

Tkinter is normally shipped with Python and not published on PyPI. For closure size reasons nixpkgs splits out tkinter to it's own separate package in the nixpkgs Python package set.

nix flake init --template github:pyproject-nix/uv2nix#hello-tkinter

inline-metadata

Use uv2nix with locked inline metadata scripts.

nix flake init --template github:pyproject-nix/uv2nix#inline-metadata

FAQ

My package $foo doesn't build!

uv2nix can only work with what it has, and uv.lock metadata is notably absent of important metadata.

For more details see overriding.

Why doesn't uv2nix come with overrides?

Users coming from poetry2nix may be surprised to find that uv2nix doesn't come with any bundled overrides.

Overrides are required because Python tooling is lacking important metadata, complexity which surfaces when using Nix. Uv2nix focuses on getting the translation from pyproject.toml & uv.lock to Nix right, without trying to taper over deficiencies in metadata.

In poetry2nix much of the requirement of overriding came from it's choice to build sdist's by default. uv2nix doesn't have a default package source preference, instead requiring users to make that choice. Binary wheels are much more likely to "just work", making it feasible to use uv2nix without an overrides collection at all. Maintaining overrides was the biggest source of maintainer burnout for poetry2nix.

Users will either have to maintain their own set of overrides, or use a third-party override collection.

workspace

lib.workspace.loadWorkspace

Load a workspace from a workspace root

Arguments

workspaceRoot: Workspace root as a path

config: Config overrides for settings automatically inferred by loadConfig

Can be passed as either:

  • An attribute set
  • A function taking the generated config as an argument, and returning the augmented config

Workspace attributes

  • mkPyprojectOverlay: Create an overlay for usage with pyproject.nix's builders
  • mkEditablePyprojectOverlay: Generate an overlay to use with pyproject.nix's build infrastructure to install dependencies in editable mode.
  • config: Workspace config as loaded by loadConfig
  • deps: Pre-defined dependency declarations for top-level workspace packages
    • default: No optional-dependencies or dependency-groups enabled
    • optionals: All optional-dependencies enabled
    • groups: All dependency-groups enabled
    • all: All optional-dependencies & dependency-groups enabled

lib.workspace.loadConfig

Load supported configuration from workspace

Supports:

  • tool.uv.no-binary
  • tool.uv.no-build
  • tool.uv.no-binary-package
  • tool.uv.no-build-package

lib.workspace.discoverWorkspace

Discover workspace member directories from a workspace root. Returns a list of strings relative to the workspace root.

structured function argument

: workspaceRoot

: Workspace root directory

pyproject

: Workspace top-level pyproject.toml

lock1

lib.lock1.resolveDependencies

Resolve dependencies from uv.lock .

structured function argument

: lock

: Lock file as parsed by parseLock

environ

: PEP-508 environment as returned by pyproject-nix.lib.pep508.mkEnviron

dependencies

: List of dependency names to start resolution from

lib.lock1.isLocalPackage

Check if a package is a local package. .

package

: Function argument

lib.lock1.getLocalPath

Get relative path for a local package .

package

: Function argument

lib.lock1.filterPackage

Filter dependencies/optional-dependencies/dev-dependencies from a uv.lock package entry .

environ

: Function argument

lib.lock1.filterConflicts

Filter package conflicts from lock according to dependency specification.

This function exists to filter uv.lock before being passed to resolveDependencies, allowing the runtime solver to treat the lock as if no conflicts exists.

structured function argument

: lock

: Function argument

spec

: Function argument

lib.lock1.parseLock

Parse unmarshaled uv.lock .

lib.lock1.parsePackage

Parse a package entry from uv.lock .

overlays

lib.overlays.mkOverlay

Generate an overlay to use with pyproject.nix's build infrastructure.

See https://pyproject-nix.github.io/pyproject.nix/lib/build.html

structured function argument

: sourcePreference

: Whether to prefer sources from either: - wheel - sdist See FAQ for more information.

environ

: PEP-508 environment customisations. Example: { platform_release = "5.10.65"; }

spec

: Dependency specification used for conflict resolution. By default mkPyprojectOverlay resolves the entire workspace, but that will not work for resolutions with conflicts.

localProjects

: Local projects loaded from lock1.loadLocalPackages

config

: Workspace config

workspaceRoot

: Workspace root

lock

: Lock parsed by lock1.parseLock

Hacking

This document outlines hacking on uv2nix itself, and lays out it's project structure.

Project structure & testing

All Nix code lives in lib/. Each file has an implementation and a test suite. The attribute path to a an attribute mkOverlay in lib/lock.nix would be lib.lock.mkOverlay.

A function in lib/test.nix maps over the public interface of the library and the test suite to generate coverage tests, ensuring that every exported symbol has at least one test covering it.

Integration tests meaning tests that perform environment constructions & builds lives in test/ and are exposed through Flake checks.

The manual you are reading right now is built from the doc/ directory. To edit a specific page see the "Edit this page on GitHub" link in the footer for each respective page.

Running tests

  • Run the entire unit test suite $ nix-unit --flake .#libTests

  • Run unit tests for an individual function `$ nix-unit --flake .#libTests.lock.mkOverlay

  • Run integration tests $ nix flake check

Formatter

Before submitting a PR format the code with nix fmt and ensure Flake checks pass with nix flake check.

Support

Commercial support for the pyproject.nix ecosystem is offered by Bladis Limited.