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.
Hello world
This example shows you how to set up a Uv workspace using uv2nix
.
It has the following features:
-
Creating package set from
uv.lock
With a virtualenv that can be built using
nix build
-
Development shells
-
One using
nix
to manage virtual environmentsWith dependencies installed in editable mode.
Enter this shell with
nix develop .#uv2nix
-
One using
uv
to manage virtual environmentsEnter this shell with
nix develop .#impure
-
flake.nix
{
description = "Hello world 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 =
{
self,
nixpkgs,
uv2nix,
pyproject-nix,
pyproject-build-systems,
...
}:
let
inherit (nixpkgs) lib;
# Load a uv workspace from a workspace root.
# Uv2nix treats all uv projects as workspace projects.
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
# Create package overlay from workspace.
overlay = workspace.mkPyprojectOverlay {
# Prefer prebuilt binary wheels as a package source.
# Sdists are less likely to "just work" because of the metadata missing from uv.lock.
# Binary wheels are more likely to, but may still require overrides for library dependencies.
sourcePreference = "wheel"; # or sourcePreference = "sdist";
# Optionally customise PEP 508 environment
# environ = {
# platform_release = "5.10.65";
# };
};
# Extend generated overlay with build fixups
#
# Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds.
# This is an additional overlay implementing build fixups.
# See:
# - https://pyproject-nix.github.io/uv2nix/FAQ.html
pyprojectOverrides = _final: _prev: {
# Implement build fixups here.
# Note that uv2nix is _not_ using Nixpkgs buildPythonPackage.
# It's using https://pyproject-nix.github.io/pyproject.nix/build.html
};
# This example is only using x86_64-linux
pkgs = nixpkgs.legacyPackages.x86_64-linux;
# Use Python 3.12 from nixpkgs
python = pkgs.python312;
# Construct package set
pythonSet =
# Use base package set from pyproject.nix builders
(pkgs.callPackage pyproject-nix.build.packages {
inherit python;
}).overrideScope
(
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]
);
in
{
# Package a virtual environment as our main application.
#
# Enable no optional dependencies for production build.
packages.x86_64-linux.default = pythonSet.mkVirtualEnv "hello-world-env" workspace.deps.default;
# Make hello runnable with `nix run`
apps.x86_64-linux = {
default = {
type = "app";
program = "${self.packages.x86_64-linux.default}/bin/hello";
};
};
# This example provides two different modes of development:
# - Impurely using uv to manage virtual environments
# - Pure development using uv2nix to manage virtual environments
devShells.x86_64-linux = {
# It is of course perfectly OK to keep using an impure virtualenv workflow and only use uv2nix to build packages.
# This devShell simply adds Python and undoes the dependency leakage done by Nixpkgs Python infrastructure.
impure = pkgs.mkShell {
packages = [
python
pkgs.uv
];
env =
{
# Prevent uv from managing Python downloads
UV_PYTHON_DOWNLOADS = "never";
# Force uv to use nixpkgs Python interpreter
UV_PYTHON = python.interpreter;
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
# Python libraries often load native shared objects using dlopen(3).
# Setting LD_LIBRARY_PATH makes the dynamic library loader aware of libraries without using RPATH for lookup.
LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1;
};
shellHook = ''
unset PYTHONPATH
'';
};
# This devShell uses uv2nix to construct a virtual environment purely from Nix, using the same dependency specification as the application.
# The notable difference is that we also apply another overlay here enabling editable mode ( https://setuptools.pypa.io/en/latest/userguide/development_mode.html ).
#
# This means that any changes done to your local files do not require a rebuild.
#
# Note: Editable package support is still unstable and subject to change.
uv2nix =
let
# Create an overlay enabling editable mode for all local dependencies.
editableOverlay = workspace.mkEditablePyprojectOverlay {
# Use environment variable
root = "$REPO_ROOT";
# Optional: Only enable editable for these packages
# members = [ "hello-world" ];
};
# Override previous set with our overrideable overlay.
editablePythonSet = pythonSet.overrideScope (
lib.composeManyExtensions [
editableOverlay
# Apply fixups for building an editable package of your workspace packages
(final: prev: {
hello-world = prev.hello-world.overrideAttrs (old: {
# It's a good idea to filter the sources going into an editable build
# so the editable package doesn't have to be rebuilt on every change.
src = lib.fileset.toSource {
root = old.src;
fileset = lib.fileset.unions [
(old.src + "/pyproject.toml")
(old.src + "/README.md")
(old.src + "/src/hello_world/__init__.py")
];
};
# Hatchling (our build system) has a dependency on the `editables` package when building editables.
#
# In normal Python flows this dependency is dynamically handled, and doesn't need to be explicitly declared.
# This behaviour is documented in PEP-660.
#
# With Nix the dependency needs to be explicitly declared.
nativeBuildInputs =
old.nativeBuildInputs
++ final.resolveBuildSystem {
editables = [ ];
};
});
})
]
);
# Build virtual environment, with local packages being editable.
#
# Enable all optional dependencies for development.
virtualenv = editablePythonSet.mkVirtualEnv "hello-world-dev-env" workspace.deps.all;
in
pkgs.mkShell {
packages = [
virtualenv
pkgs.uv
];
env = {
# Don't create venv using uv
UV_NO_SYNC = "1";
# Force uv to use Python interpreter from venv
UV_PYTHON = "${virtualenv}/bin/python";
# Prevent uv from downloading managed Python's
UV_PYTHON_DOWNLOADS = "never";
};
shellHook = ''
# Undo dependency propagation by nixpkgs.
unset PYTHONPATH
# Get repository root using git. This is expanded at runtime by the editable `.pth` machinery.
export REPO_ROOT=$(git rev-parse --show-toplevel)
'';
};
};
};
}
pyproject.toml
[project]
name = "hello-world"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"urllib3>=2.2.3",
]
[project.scripts]
hello = "hello_world:hello"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"ruff>=0.6.7",
]
Notes
In the interest of keeping documentation conceptually simple, no Flakes framework such as flake-utils
or flake-parts
are being used for this example.
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
-
Override utility functions
-
Third party overrides collection
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";
};
};
# This example shows testing with pytest using uv2nix.
# You should first read and understand the hello-world example before this one.
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"
readme = "README.md"
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";
};
pyprojectOverrides = _final: _prev: {
# Implement build fixups here.
};
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
pyprojectOverrides
]
);
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 = util.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
Uv2nix can not just build environments and applications, it can also build redistributable wheels:
pythonSet.hello-world.override {
pyprojectHook = pythonSet.pyprojectDistHook;
}
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 not in PyPI
In some cases a package isn't published on PyPI, but is packaged in nixpkgs.
One such package is the seccomp
package.
Installing a wheel (recommended)
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 = [ ];
})
];
});
}
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;
# Map over all nixpkgs supported systems to create the `packages` set
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
# Nixpkgs package set
pkgs = nixpkgs.legacyPackages.${system};
# Use Python 3.12
python = pkgs.python312;
# Use base package set from pyproject.nix builders
baseSet = pkgs.callPackage pyproject-nix.build.packages {
inherit python;
};
# Implement build fixups here.
pyprojectOverrides = _final: _prev: {
};
in
lib.mapAttrs (
name: script:
let
# Create package overlay from workspace.
overlay = script.mkOverlay {
sourcePreference = "wheel";
};
# Construct package set
pythonSet = baseSet.overrideScope (
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]
);
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}
);
# Use an impure devshell as we're managing many scripts and can't build a single cohesive environment.
devShells = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
python = pkgs.python312;
in
{
default = pkgs.mkShell {
packages = [
python
pkgs.uv
];
env =
{
UV_PYTHON_DOWNLOADS = "never";
UV_PYTHON = python.interpreter;
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1;
};
shellHook = ''
unset PYTHONPATH
'';
};
}
);
};
}
scripts/example.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "tqdm",
# ]
# ///
from tqdm import tqdm
for i in tqdm(range(10000)):
pass
Django
Building on the previous simple testing example we're building a web application using Django.
This example aims to be a showcase of many different capabilities & possibilites of uv2nix
:
-
Packaging a web application using Django
-
Using Django's staticfiles app
-
Constructing Docker containers
Using
dockerTools
from nixpkgs. -
Creating NixOS modules for
uv2nix
appsWith accompanying NixOS tests.
A real-world deployment should use a reverse proxy, for example
nginx
. Use this documentation as inspiration, not as a best practices guide.
flake.nix
{
description = "Django application 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 =
{
self,
nixpkgs,
uv2nix,
pyproject-nix,
pyproject-build-systems,
...
}:
let
inherit (nixpkgs) lib;
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
asgiApp = "django_webapp.asgi:application";
settingsModules = {
prod = "django_webapp.settings";
};
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
overlay = workspace.mkPyprojectOverlay {
sourcePreference = "wheel";
};
editableOverlay = workspace.mkEditablePyprojectOverlay {
root = "$REPO_ROOT";
};
# Python sets grouped per system
pythonSets = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) stdenv;
# Base Python package set from pyproject.nix
baseSet = pkgs.callPackage pyproject-nix.build.packages {
python = pkgs.python312;
};
# An overlay of build fixups & test additions
pyprojectOverrides = final: prev: {
# django-webapp is the name of our example package
django-webapp = prev.django-webapp.overrideAttrs (old: {
# Add tests to passthru.tests
#
# These attribute are used in Flake checks.
passthru = old.passthru // {
tests =
(old.tests or { })
// {
# Run mypy checks
mypy =
let
venv = final.mkVirtualEnv "django-webapp-typing-env" {
django-webapp = [ "typing" ];
};
in
stdenv.mkDerivation {
name = "${final.django-webapp.name}-mypy";
inherit (final.django-webapp) src;
nativeBuildInputs = [
venv
];
dontConfigure = true;
dontInstall = true;
buildPhase = ''
mkdir $out
mypy --strict . --junit-xml $out/junit.xml
'';
};
# Run pytest with coverage reports installed into build output
pytest =
let
venv = final.mkVirtualEnv "django-webapp-pytest-env" {
django-webapp = [ "test" ];
};
in
stdenv.mkDerivation {
name = "${final.django-webapp.name}-pytest";
inherit (final.django-webapp) src;
nativeBuildInputs = [
venv
];
dontConfigure = true;
buildPhase = ''
runHook preBuild
pytest --cov tests --cov-report html tests
runHook postBuild
'';
installPhase = ''
runHook preInstall
mv htmlcov $out
runHook postInstall
'';
};
}
// lib.optionalAttrs stdenv.isLinux {
# NixOS module test
nixos =
let
venv = final.mkVirtualEnv "django-webapp-nixos-test-env" {
django-webapp = [ ];
};
in
pkgs.nixosTest {
name = "django-webapp-nixos-test";
nodes.machine =
{ ... }:
{
imports = [
self.nixosModules.django-webapp
];
services.django-webapp = {
enable = true;
inherit venv;
};
system.stateVersion = "24.11";
};
testScript = ''
machine.wait_for_unit("django-webapp.service")
with subtest("Web interface getting ready"):
machine.wait_until_succeeds("curl -fs localhost:8000")
'';
};
};
};
});
};
in
baseSet.overrideScope (
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]
)
);
# Django static roots grouped per system
staticRoots = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) stdenv;
pythonSet = pythonSets.${system};
venv = pythonSet.mkVirtualEnv "django-webapp-env" workspace.deps.default;
in
stdenv.mkDerivation {
name = "django-webapp-static";
inherit (pythonSet.django-webapp) src;
dontConfigure = true;
dontBuild = true;
nativeBuildInputs = [
venv
];
installPhase = ''
env DJANGO_STATIC_ROOT="$out" python manage.py collectstatic
'';
}
);
in
{
checks = forAllSystems (
system:
let
pythonSet = pythonSets.${system};
in
# Inherit tests from passthru.tests into flake checks
pythonSet.django-webapp.passthru.tests
);
nixosModules = {
django-webapp =
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.django-webapp;
inherit (pkgs) system;
pythonSet = pythonSets.${system};
inherit (lib.options) mkOption;
inherit (lib.modules) mkIf;
in
{
options.services.django-webapp = {
enable = mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable django-webapp
'';
};
settings-module = mkOption {
type = lib.types.string;
default = settingsModules.prod;
description = ''
Django settings module
'';
};
venv = mkOption {
type = lib.types.package;
default = pythonSet.mkVirtualEnv "django-webapp-env" workspace.deps.default;
description = ''
Django-webapp virtual environment package
'';
};
static-root = mkOption {
type = lib.types.package;
default = staticRoots.${system};
description = ''
Django-webapp static root
'';
};
};
config = mkIf cfg.enable {
systemd.services.django-webapp = {
description = "Django Webapp server";
environment.DJANGO_STATIC_ROOT = cfg.static-root;
serviceConfig = {
ExecStart = ''
${cfg.venv}/bin/daphne django_webapp.asgi:application
'';
Restart = "on-failure";
DynamicUser = true;
StateDirectory = "django-webapp";
RuntimeDirectory = "django-webapp";
BindReadOnlyPaths = [
"${
config.environment.etc."ssl/certs/ca-certificates.crt".source
}:/etc/ssl/certs/ca-certificates.crt"
builtins.storeDir
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
];
};
wantedBy = [ "multi-user.target" ];
};
};
};
};
packages = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
pythonSet = pythonSets.${system};
in
lib.optionalAttrs pkgs.stdenv.isLinux {
# Expose Docker container in packages
docker =
let
venv = pythonSet.mkVirtualEnv "django-webapp-env" workspace.deps.default;
in
pkgs.dockerTools.buildLayeredImage {
name = "django-webapp";
contents = [ pkgs.cacert ];
config = {
Cmd = [
"${venv}/bin/daphne"
asgiApp
];
Env = [
"DJANGO_SETTINGS_MODULE=${settingsModules.prod}"
"DJANGO_STATIC_ROOT=${staticRoots.${system}}"
];
};
};
}
);
# Use an editable Python set for development.
devShells = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
editablePythonSet = pythonSets.${system}.overrideScope (
lib.composeManyExtensions [
editableOverlay
(final: prev: {
django-webapp = prev.django-webapp.overrideAttrs (old: {
src = lib.fileset.toSource {
root = old.src;
fileset = lib.fileset.unions [
(old.src + "/pyproject.toml")
(old.src + "/README.md")
(old.src + "/src/django_webapp/__init__.py")
];
};
nativeBuildInputs =
old.nativeBuildInputs
++ final.resolveBuildSystem {
editables = [ ];
};
});
})
]
);
venv = editablePythonSet.mkVirtualEnv "django-webapp-dev-env" {
django-webapp = [ "dev" ];
};
in
{
default = pkgs.mkShell {
packages = [
venv
pkgs.uv
];
env = {
UV_NO_SYNC = "1";
UV_PYTHON = "${venv}/bin/python";
UV_PYTHON_DOWNLOADS = "never";
};
shellHook = ''
unset PYTHONPATH
export REPO_ROOT=$(git rev-parse --show-toplevel)
'';
};
}
);
};
}
pyproject.toml
[project]
name = "django-webapp"
version = "0.1.0"
description = "A django web application developed & deployed using uv2nix"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"django>=5.1.3",
"daphne>=4.1.2",
]
[dependency-groups]
dev = [
{include-group = "test"},
{include-group = "typing"},
{include-group = "lint"},
]
typing = [
"django-stubs[compatible-mypy]>=5.1.1",
"mypy>=1.13.0",
]
test = [
"pytest-cov>=6.0.0",
"pytest-django>=4.9.0",
"pytest>=8.3.3",
]
lint = [
"ruff>=0.7.2",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "django_webapp.settings"
[tool.mypy]
exclude = ["manage.py"]
plugins = ["mypy_django_plugin.main"]
[tool.django-stubs]
django_settings_module = "django_webapp.settings"
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 buildersmkEditablePyprojectOverlay
: Generate an overlay to use with pyproject.nix's build infrastructure to install dependencies in editable mode.config
: Workspace config as loaded byloadConfig
deps
: Pre-defined dependency declarations for top-level workspace packagesdefault
: No optional-dependencies or dependency-groups enabledoptionals
: All optional-dependencies enabledgroups
: All dependency-groups enabledall
: 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.