Templates (Deprecated)

Note: This section is primarily for my own learning purposes and is not necessarily useful for most users, as it targets NixOS rather than Linux.

Here you will find deprecated project templates for setting up a Nix environment.

The current template, nixos-pythonml/, should not be used for Python machine learning project development on Linux, as it will fail. It was originally intended for a full NixOS environment.

Layout

.
└── nixos-pythonml
    ├── .envrc
    ├── .gitignore
    ├── .python-version
    ├── flake.nix
    ├── pyproject.toml
    ├── README.md
    └── src

Template Description

NixOS Python ML

Note on Package Managers

Packages are managed both through nix and uv. As a rule of thumb:

  • Handle your Python packages with uv and everything else through nix. For instance, there may be external system packages required for your Python packages to work correctly—use nix for those cases.

  • Specify your Python version with nix for perfect reproducibility. (This is not done in the Linux environment because Nix Python expects some libraries to be in /nix/store, not in /usr/lib or other proprietary library locations on Linux. This requires extending LD_LIBRARY_PATH—see below—but particularly fails for CUDA drivers.)

  • If a package does not work when installed from uv, you may want to specify it with nix.

    • For example, pytorch might not recognize your GPU, and specifying it with nix might solve the issue. However, its version will be immutable and can only be changed through nix. Additionally, building from source can be prohibitively slow.

    • This is why uv is generally preferred for handling Python dependencies.

How to Add New Packages

If you want to skip the context, jump directly to the Summary Section.

There are four ways to add new packages. While you should be aware of the different options (for debugging), your day-to-day workflow will primarily use the last method (uv).

This Python ML template is intended to work on the following systems: macOS, Linux, NixOS (and supposedly Windows). Let’s clarify the distinction between nix-installed programs and others.

  • In NixOS (and also when using the Nix Package Manager), packages are installed in paths like /nix/store/[hash]-[package-name], and dependencies are managed internally so that packages installed through nix only look for their dependencies in that store. This provides complete isolation between packages and enables multiple versions of the same package, contributing to the reliability and reproducibility of the Nix system.

  • The problem is that most programs expect access to certain runtime libraries, such as a C compiler or GPU toolkit. For programs installed from nix, dependencies are handled through the /nix/store. But for foreign programs, they are unaware of /nix/store/. On Linux, runtime libraries are expected to be listed under the LD_LIBRARY_PATH with .so file extensions. On macOS, the equivalent is DYLD_LIBRARY_PATH and .dylib file extensions (note that DYLD_LIBRARY_PATH is not defined by default, but you can specify your runtime package paths under that variable).

    • What problem does this cause? On pure NixOS systems, the LD_LIBRARY_PATH variable is not defined by default. When installing packages from other sources (like Python packages from uv), they will still expect their runtime dependencies to be present in LD_LIBRARY_PATH. So even if you have the right packages installed in /nix/store/, you have to add them to LD_LIBRARY_PATH.

      • This is what nix-ld is for (but it is only available for NixOS).

      • Alternatively, you can manually extend LD_LIBRARY_PATH, which is the approach chosen in flake.nix for compatibility between systems.

        let
          ...
          # The custom list of nix packages that should be available at runtime
          LD_pkgs = 
              (with pkgs; [
                  # ADD NEEDED PACKAGES HERE
                  stdenv.cc.cc     # useful for compiling C/C++ code
                  glib             # core application building blocks for libraries written in C
                  libGL            # useful for graphical rendering, e.g., required for matplotlib
                  ...
              ]) ++ pkgs.lib.optionals pkgs.stdenv.isLinux 
              (with pkgs; [ # ADD PACKAGES HERE IF ONLY REQUIRED BY LINUX 
                  cudatoolkit      # required for GPU libraries
                  ...
              ])
          ;
          ...
        in
          # CUSTOM SHELL FOR OUR PROJECT
          devShells = {
              default = mkShell {
                  # DEFINE THE CUSTOM LD_LIBRARY_PATH
                  NIX_LD_LIBRARY_PATH = lib.makeLibraryPath LD_pkgs;
                  # Add LD_pkgs to the shell as a rule of thumb
                  packages = ... ++ LD_pkgs; 
                  shellHook = ''
                  # Extend the original LD_LIBRARY_PATH with the `NIX_LD_LIBRARY_PATH` (only for this shell)
                  export LD_LIBRARY_PATH=$NIX_LD_LIBRARY_PATH:$LD_LIBRARY_PATH
                  ...
                  '';
              };
          };
          ...
        
    • Does this apply to us with our Ubuntu machine + Nix as a package manager?

      • No, because on Linux or macOS, those runtime packages are usually already present. The LD_LIBRARY_PATH for Linux is already set and should include all the utilities you need; the same applies for macOS (though DYLD_LIBRARY_PATH is not set by default, runtime dependencies are already in locations like /usr/bin/).

      • Maybe, because bugs can still occur. Packages may not work properly (and it’s not necessarily because you are using Nix as a package manager).

        • If an external runtime package is required (the error might be something like cannot find [name of the package]), extending LD_LIBRARY_PATH should resolve the issue. The code snippet above ensures this is done in an isolated fashion and won’t corrupt your system. Note: This flake assumes you won’t have issues with your macOS setup. It only modifies LD_LIBRARY_PATH, which is ignored on macOS. If you encounter errors, you may want to modify DYLD_LIBRARY_PATH instead.

In the code snippet above, you have already seen one way to add packages—those related to LD_LIBRARY_PATH, which may be specific to Linux or any system. These packages are stored in the LD_pkgs variable.

The next three ways to add new packages are detailed in the Summary Section, but in short, they are for:

  • Adding nix packages that are not runtime dependencies (i.e., should not be present in LD_LIBRARY_PATH). This includes uv, your Python version, or the Python ShellHook (the tool that creates .venv). It could also be specific packages your project requires, like duckdb for fast database queries.

  • Adding Python packages with nix. This should not be the default and should only be used if you cannot make it work through uv.

  • Adding Python packages with uv. This will be your standard workflow.

To Summarise

  1. Packages that will be present in LD_LIBRARY_PATH, should be on every OS, and can be found on Nix Packages Search. These are stored in LD_pkgs.

     LD_pkgs = 
         (with pkgs; [
             # ADD NEEDED PACKAGES HERE
             ...
         ]) ++ pkgs.lib.optionals pkgs.stdenv.isLinux 
         (with pkgs; [
             # ADD PACKAGES THAT SHOULD ONLY BE PRESENT FOR LINUX
             ...
         ]);
    
  2. Packages installed for Python through nix, because you cannot make them work with uv.

    python_with_pkgs = pkgs.python311.withPackages (pp: with pp [
        # ADD PYTHON PKGS HERE THAT YOU NEED FROM NIX REPOS
        ...
    ]);
    
  3. Packages that should be installed through nix that are not Python packages and should not be available in LD_LIBRARY_PATH (i.e., not runtime dependencies):

     shell_packages = 
         # ADD NIX PACKAGES NOT REQUIRED IN LD_LIBRARY_PATH
         (with pkgs; [
             python311Packages.venvShellHook
             mpkgs.uv
             ...
             ])
    
  4. The standard way to install packages with uv (this should be the default and is the most important; the other options are mainly for debugging):

uv pip install [name-of-the-package]

How to Change Python Version

Change the Python distribution in nix the flake.nix. You can then update the .python-version file with:

uv python pin

Another Template: FHS-Compliant

FHS stands for Filesystem Hierarchy Standard. It is essentially a map that tells you where to find the tools you need, such as /usr/bin/. On NixOS, the /usr/bin folder doesn’t exist and all tools are located in /nix/store/, as mentioned above. This enables multiple versions of programs and ensures each program only sees what it needs. However, some programs or languages (like R) expect an FHS system, i.e., a /usr/bin folder. In such cases, you need to add tools from /nix/store/ into a project-specific /usr/bin. This is what pkgs.BuildFHSEnv is for.

You may want to read the NixOS Wiki on the subject.

You could therefore have a flake.nix like the one from Ank’s Neusis Setup:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    nixpkgs_master.url = "github:NixOS/nixpkgs/master";
    systems.url = "github:nix-systems/default";
    flake-utils.url = "github:numtide/flake-utils";
    flake-utils.inputs.systems.follows = "systems";
  };

  outputs =
    {
      self,
      nixpkgs,
      flake-utils,
      ...
    }@inputs:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = import nixpkgs {
          inherit system;
          config.allowUnfree = true;
          config.cudaSupport = true;
        };

        mpkgs = import inputs.nixpkgs_master {
          inherit system;
          config.allowUnfree = true;
          config.cudaSupport = true;
        };

        ### FHS ENVIRONMENT DEFINITION
        fhsenv = pkgs.buildFHSEnv {
          name = "fhs-shell";
          targetPkgs =
            pkgs: with pkgs; [
              gcc
              cudatoolkit
              libGL
              libz
              mpkgs.uv
              python311
            ];
          profile = ''
            export VENVDIR=.venv
            if [[ -d "$VENVDIR" ]]; then
              printf "%s\n" "Skipping venv creation, '$VENVDIR' already exists"
              source "$VENVDIR/bin/activate"
            else
              printf "%s\n" "Creating new venv environment in path: '$VENVDIR'"
              python -m venv "$VENVDIR"
              source "$VENVDIR/bin/activate"
            fi
          '';
          runScript = "zsh";
        };
      in
      {
        devShells = {
          default = fhsenv.env;
        };
      }
    );
}
# Things one might need for debugging or adding compatibility
# export CUDA_PATH=${pkgs.cudaPackages.cudatoolkit}
# export LD_LIBRARY_PATH=${pkgs.cudaPackages.cuda_nvrtc}/lib
# export EXTRA_LDFLAGS="-L/lib -L${pkgs.linuxPackages.nvidia_x11}/lib"
# export EXTRA_CCFLAGS="-I/usr/include"

# Data syncthing commands
# syncthing cli show system | jq .myID
# syncthing cli config devices add --device-id $DEVICE_ID_B
# syncthing cli config folders $FOLDER_ID devices add --device-id $DEVICE_ID_B
# syncthing cli config devices $DEVICE_ID_A auto-accept-folders set true

# FHS related help
# https://discourse.nixos.org/t/best-way-to-define-common-fhs-environment/25930
# https://ryantm.github.io/nixpkgs/builders/special/fhs-environments/

This is a slightly more complex environment than the one proposed in the previous template. It is not necessarily recommended, as it can be confusing. It is somewhat equivalent to overriding LD_LIBRARY_PATH for your project, but even stronger. You generally do not want to use this option, as it conflicts with the Nix philosophy of building isolated environments using /nix/store/. Instead, it symlinks packages from /nix/store/ and adds them to /usr/bin (and possibly other directories). See the documentation for BuildFHSEnv.

References