How do I package a python script?

Hi!

sorry to say, but I’m new to packaging anything, so explain it like I’m 5.

I have a single python script that requires dependencies and I’ve been told that it would be a good idea to package it all together with nix. (the script is for a custom waybar module on my nixos hyprland setup.)

from what I’ve learned online, this is how far I got:

file named “waybar-media-player.nix”

{ pkgs, python3Packages}:

python3Packages.buildPythonApplication rec {
    pname = "waybar-mediaplayer-script";
    version = "0.0.1";
    pyproject = false;

    propagatedBuildInputs = [
      pkgs.playerctl
      (python3Packages (python-pkgs: with python-pkgs; [
        pandas
        requests
        syncedlyrics
        pillow
        pygobject3
      ]))
    ];
    dontUnpack = true;
    installPhase = ''
        install -Dm755 "${./${pname}}" "$out/bin/${pname}"
    '';
}

I got it from here

any help would be appreciated (and honestly, an explanation on how to make my own in the future would be fantastic.)

Is there a problem with the derivation you have, or do you have it working, but are looking for an explanation how it works?

I don’t have an “A to Z” explainer of Python packaging, so I would recommend first looking into this tutorial on nix.dev. It packages C software instead, but the concepts it introduces are reused for almost anything you package with Nix.

I’d follow that with reading the Python section of the Nixpkgs manual. It mostly deals with packaging Python applications using the pythonPackages module set in nixpkgs. There are alternatives such as poetry2nix if one is instead using poetry for their Python project, or uv2nix if using uv.

Similar to the above comment, I think the best way to learn the specifics of functions like buildPythonApplication is to read the docs, but I will point out why your current derivation is broken.

The part where you’re including python3Packages is wrong:

    propagatedBuildInputs = [
      pkgs.playerctl
      (python3Packages (python-pkgs: with python-pkgs; [
        pandas
        requests
        syncedlyrics
        pillow
        pygobject3
      ]))
    ];

If you try building the derivation as-is, you get the following error:

       error: attempt to call something which is not a function but a set: { APScheduler = «thunk»; BTrees = «thunk»; Babel = «thunk»; BlinkStick = «thunk»; ColanderAlchemy = «thunk»; CommonMark = «thunk»; ConfigArgParse = «thunk»; EasyProcess = «thunk»; Fabric = «thunk»; FormEncode = «thunk»; «8861 attributes elided» }
       at /root/test/waybar-media-player-bad.nix:10:8:
            9|       pkgs.playerctl
           10|       (python3Packages (python-pkgs: with python-pkgs; [
             |        ^
           11|         pandas

This is because in nix, the way you call functions is function arg1 argN, eg

nix-repl> map (x: x * 2) [1 2 3]
[2 4 6]

So you’re trying to call python3Packages as a function, when really you’re just trying to access its attributes. To do so, you can use something like this:

{pkgs}:
pkgs.python3Packages.buildPythonApplication rec {
  pname = "waybar-mediaplayer-script";
  version = "0.0.1";
  pyproject = false;

  propagatedBuildInputs = with pkgs;
  with pkgs.python3Packages; [
    playerctl
    pandas
    requests
    syncedlyrics
    pillow
    pygobject3
  ];

  dontUnpack = true;
  installPhase = ''
    install -Dm755 "${./${pname}}" "$out/bin/${pname}"
  '';
}

Or if you don’t want to conflate the scope of python packages and system packages:

  propagatedBuildInputs = with pkgs; [
    playerctl
    python3Packages.pandas
    python3Packages.requests
    python3Packages.syncedlyrics
    python3Packages.pillow
    python3Packages.pygobject3
  ];

Regardless, this will create a proper list of build inputs and should allow you to build the derivation.

Also, for completeness and in case you didn’t know, you’ll need a default.nix file to pull and pass in nixpkgs:

# cat default.nix
let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
  pkgs = import nixpkgs {};
in {
  waybar-media-player = pkgs.callPackage ./waybar-media-player.nix {};
}

This should let you build your derivation:

$ nix-build
/nix/store/phqb1ij55vympnrznr0k6khky1hkdklm-waybar-mediaplayer-script-0.0.1
$ ./result/bin/waybar-mediaplayer-script
script doing stuff!

Hello,

Here’s a trivial example from a couple of days ago: https://github.com/drupol/markdown-code-runner/blob/392466df5ae6819ab70b7a8e1b2437b5ab75a52d/nix/pkgs/markdown-code-runner/package.nix

But there are hundreds of others examples here: nixpkgs/pkgs/development/python-modules at master · NixOS/nixpkgs · GitHub

Let us know if this is helping already.

thank you so much!!! this is exactly what I needed

but, how would I then add this custom package into my own nixos config?

(I use flakes if that changes anything)

1 Like

No worries! So right now we have a waybar-media-player.nix file and a default.nix file. waybar-media-player.nix provides the actual derivation for your package, so if you’re wanting to include it in your nixos config that’s the thing you should include.

In default.nix, i showed how you can include the waybar-media-player derivation and provide it as an attribute set:

$ cat default.nix
let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
  pkgs = import nixpkgs {};
in {
  waybar-media-player = pkgs.callPackage ./waybar-media-player.nix {};
}
$ nix eval --file ./default.nix
{ waybar-media-player = «derivation /nix/store/pkikbkqbfl227rr03xd6z1myzarxlqvm-waybar-mediaplayer-script-0.0.1.drv»; }

Here we’re using callPackage to call buildPythonApplication in the file and pass in any required arguments automagically (in this case just pkgs).

But you could also do this within your nixos configuration, since calling the build function returns the package derivation you can use it in systemPackages:

let
  waybar-media-player = pkgs.callPackage ./path/to/waybar-media-player.nix {};
in {
  environment.systemPackages = [ waybar-media-player ];
}

or instead of using a variable just call it directly within systemPackages:

environment.systemPackages = [ (pkgs.callPackage ./path/to/waybar-media-player.nix {}) ];

Hope that’s helpful!

1 Like

You could use your own flake for that, or alternatively, submit it to nixpkgs so that the package will be maintained by a bigger user base and in the long run.

If you’re still looking for the first option, I made this that might help: GitHub - drupol/my-own-nixpkgs: A template for maintaining your own Nix expressions in your own repository