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

Usage

While the nixpkgs Python infrastructure is built mainly for manual packaging, pyproject.nix's builders are mainly targeted at lock file consumers like uv2nix.

This example shows the essence of implementing a lock file converter in pure Nix using pyproject.nix. A real world implementation is more complex. To see a lock file converter built according to pyproject.nix best practices see uv2nix.

example.nix

{
  python,
  callPackage,
  pyproject-nix,
  lib,
}:
let
  # Pyproject.nix packages quite a few, but not all build-system dependencies.
  #
  # We only package PyPI packages because lock file generators often miss this metadata, so it's required to help kickstart a Python set.
  # This set is incomplete, and much smaller in both package count and scope than nixpkgs is.
  baseSet = callPackage pyproject-nix.build.packages {
    inherit python;
  };

  # A hypothetical over-simplified "lock file" format to demonstrate what typical usage would look like.
  # This format is absent of important data such as PEP-508 markers and more.
  #
  # Also note that all sources are the same. In a real-world file these would of course all be different.
  lock =
    let
      src = {
        type = "pypi";
        url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz";
        hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760";
      };
    in
    {
      package-a = {
        name = "a";
        version = "1.0.0";
        dependencies = { };
        optional-dependencies = { };
        inherit src;
        build-system = {
          flit-core = [ ];
        };
      };

      package-b = {
        name = "b";
        version = "1.0.0";
        # Depend on a with no optionals
        dependencies = {
          a = [ ];
        };
        inherit src;
        build-system = {
          flit-core = [ ];
        };
      };

      package-c = {
        name = "c";
        version = "1.0.0";
        dependencies = {
          a = [ ];
        };
        # Has an optional dependency on b when cool_feature is activated
        optional-dependencies = {
          cool_feature = {
            b = [ ];
          };
        };
        inherit src;
        build-system = {
          flit-core = [ ];
        };
      };

      package-d = {
        name = "d";
        version = "1.0.0";
        dependencies = {
          c = [ "cool_feature" ];
        };
        # A local package dependend on by it's path
        src = {
          type = "path";
          path = ./packages/d;
        };
        build-system = {
          flit-core = [ ];
        };
      };
    };

  # Create a PEP-508 marker environment for marker evaluation
  environ = pyproject-nix.lib.pep508.mkEnviron python;

  # Transform lock into a Pyproject.nix build overlay.
  # This will create packages from the lock.
  overlay =
    pyfinal: _pyprev:
    lib.mapAttrs (
      name: lockpkg:
      # If package is a local package use a project loader from pyproject-nix.lib.project
      if lockpkg.src.type == "path" then
        (
          let
            project = pyproject-nix.project.loadPyprojectDynamic {
              projectRoot = lockpkg.src.path;
            };
          in
          pyfinal.callPackage (
            # Function called with callPackage
            {
              stdenv,
              pyprojectHook,
              resolveBuildSystem,
            }:
            # Call stdenv.mkDerivation with project
            stdenv.mkDerivation (
              # Render stdenv.mkDerivation arguments from project
              pyproject-nix.build.lib.renderers.mkDerivation
                {
                  inherit project environ;
                }
                {
                  inherit pyprojectHook resolveBuildSystem;
                }
            )
          ) { }
        )
      # If a package is a remote (pypi) package there is no ready made renderers to use.
      # You need to apply your own transformations.
      else if lockpkg.src.type == "pypi" then
        pyfinal.callPackage (
          {
            stdenv,
            fetchurl,
            pyprojectHook,
            resolveBuildSystem,
          }:
          stdenv.mkDerivation {
            pname = lockpkg.name;
            inherit (lockpkg) version;
            src = fetchurl lockpkg.src;

            nativeBuildInputs = [
              # Add hook responsible for configuring, building & installing.
              pyprojectHook
            ]
            # Build systems needs to be resolved since we don't propagate dependencies.
            # Otherwise dependencies of our build-system will be missing.
            ++ resolveBuildSystem lockpkg.build-system;

            # Dependencies go in passthru to avoid polluting runtime package.
            passthru = {
              inherit (lockpkg) dependencies optional-dependencies;
            };
          }
        ) { }
      else
        throw "Unhandled src type: ${lockpkg.src.type}" null
    ) lock;

  # Override set
  pythonSet = baseSet.overrideScope (
    _final: _prev: {
      # Override build platform dependencies
      #
      # Use this when overriding build-systems that need to run on the build platform.
      pythonPkgsBuildHost = overlay;

      # Override target platform packages.
      #
      # Use this to override packages for the target platform.
      pythonPkgsHostHost = overlay;
    }
  );

in
# Create a virtual environment containing our dependency specification
pythonSet.pythonPkgsHostHost.mkVirtualEnv "example-venv" {
  # Depend on package
  build = [ ];
}