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