Creating a Nix Package for picosnitch

Hi, I’m the author of picosnitch and could use some help packaging it for Nix.

I saw someone else attempted this last year which helped me get started, and @pxc saw my comment on HN and suggested I post here.

Here is my .nix file so far, I’ve never actually used Nix before so I apologize in advance for any glaring mistakes.

{ lib
, pkgs
, makeWrapper
, python3
, bcc
}:

python3.pkgs.buildPythonApplication rec {
  pname = "picosnitch";
  version = "0.12.0";

  src = python3.pkgs.fetchPypi {
    inherit pname version;
    sha256 = "b87654b4b92e28cf5418388ba1d3165b9fa9b17ba91af2a1a942f059128f68bc";
  };

  nativeBuildInputs = [ makeWrapper ];

  propagatedBuildInputs = with python3.pkgs; [
    bcc
    psutil
    dbus-python
    requests
    pandas
    plotly
    dash
  ];

  serviceFile = ''
    [Unit]
    Description=picoSnitch

    [Service]
    Type=simple
    Restart=always
    RestartSec=5
    ExecStart=$out/bin/picosnitch start-no-daemon
    PIDFile=/run/picosnitch.pid

    [Install]
    WantedBy=multi-user.target
  '';

  postInstall = ''
    wrapProgram $out/bin/picosnitch --prefix PYTHONPATH : ${lib.makeSearchPathOutput "lib" "lib/python3.${python3.sourceVersion.minor}/site-packages" propagatedBuildInputs}
    ln -s ${bcc}/lib/python3.${python3.sourceVersion.minor}/site-packages/bcc $out/lib/python3.${python3.sourceVersion.minor}/site-packages/bcc
    ln -s ${pkgs.python3Packages.psutil}/lib/python3.${python3.sourceVersion.minor}/site-packages/psutil $out/lib/python3.${python3.sourceVersion.minor}/site-packages/psutil
    ln -s ${pkgs.python3Packages.dbus-python}/lib/python3.${python3.sourceVersion.minor}/site-packages/dbus $out/lib/python3.${python3.sourceVersion.minor}/site-packages/dbus
    ln -s ${pkgs.python3Packages.requests}/lib/python3.${python3.sourceVersion.minor}/site-packages/requests $out/lib/python3.${python3.sourceVersion.minor}/site-packages/requests
    mkdir -p ./cmd/picosnitch
    echo "${serviceFile}" > ./cmd/picosnitch/picosnitch.service
    install -D -m0444 -t $out/lib/systemd/system ./cmd/picosnitch/picosnitch.service
  '';

  pythonImportsCheck = [ "picosnitch" ];

  meta = with lib; {
    description = "Monitor network traffic per executable with hashing";
    homepage = "https://github.com/elesiuta/picosnitch";
    changelog = "https://github.com/elesiuta/picosnitch/releases";
    license = licenses.gpl3Plus;
    maintainers = [ ];
    platforms = platforms.linux;
  };
}

I can build and run it with

nix-build -E 'with import <nixpkgs> {}; callPackage ./picosnitch.nix {}'
sudo -E ./result/bin/picosnitch start-no-daemon

Issues I’m having:

  1. Can’t use systemctl to control it, I looked at other packages and think I installed the service file correctly, so it is probably due to the commands I use not actually installing the package?
  2. I was having an issue with it not finding bcc or psutil when running it without sudo, since it then re-executes itself here with os.execvp using sudo and I tried using wrapProgram to fix this, but it did not and that line in my nix file doesn’t actually solve anything and can be removed. It was the symbolic links that fixed this, but there is probably a better way to solve this?
  3. Related to (2), I can’t run picosnitch dash for the same reason, I could probably add the symbolic links, but dash has a lot more dependencies and I’m pretty sure there is a better way.
1 Like

systemctl is a systemd utility for managing services. Systemd expects a corresponding unit file to be present in either a user or system unit file directory.

nix-build just realizes (builds) nix store paths. installing is usually done through nixos-modules and some corresponding logic (e.g. nixos-rebuild switch).

I have a video on making a nixos module

I was having an issue with it not finding bcc or psutil when running it without sudo, since it then re-executes itself here with os.execvp using sudo

Launching it as a system service should make it root by default. Ideally the service would just enable the required capabilities, but if the logic just checks for UID there’s not much you can do.

Additional context: since the nix store doesn’t have the SETUID bit, programs which do will have a corresponding entry in /run/wrappers/bin.

Related to (2), I can’t run picosnitch dash for the same reason, I could probably add the symbolic links, but dash has a lot more dependencies and I’m pretty sure there is a better way.

Probably related to above. I usually find that python applications usually make a lot of assumptions about your system or python environment which makes them packaging quite difficult.

On NixOS, the actual installation of systemd unit files in a way that systemd can see them is done through the module system.

If you want to use a systemd unit file packaged with something in Nix on some other distro, you have to symlink it into the appropriate place (/usr/lib/systemd/system, I think, though there may be preferable dirs under /etc or elsewhere) yourself, then run systemctl daemon-reload. This actually works pretty well so long as you symlink it in from the appropriate Nix profile (I would install the package as root and then symlink it in from /nix/var/nix/profiles/default/... and not directly from the Nix store.

Installing a Nix package with nix-env or nix profile install doesn’t muck about with the init system present on the base system. (I think Home Manager supports systemd user services, for things that aren’t systemwide and can run on a per-user basis.)

How does picosnitch figure out what Python interpreter to use in re-exec’ing itself?

I’m not sure if it’s Nixpkgs-worthy, but one thing I’ve done with some Python programs in other contexts is use writeScriptBin to manually write a simple wrapper that invokes a Python scripts using a python executable that comes from a python3.withPackages result.

I’d like to experiment with what you’ve got so far to give some real suggestions for improvements, but I’m exhausted tonight. I’ll see if I can figure something out that works better when I get the chance, though!

The daemon is the only part that needs root, and it has some logic to guess the UID if run as a service and not by the user, and the environment variable wasn’t set. The only things it needs the UID for is desktop notifications on dbus, and for getting the executable hash of AppImages without allow_other or allow_root.

Thanks! Right now I’m just using the NixOS demo appliance for VirtualBox to test this, but it’s good to know this sort of setup is possible and may consider it for myself at some point.

It is using the one provided by sys.executable

Thank you! Since the main issue comes from re-executing itself, maybe the simplest solution is just remove support for running it like that on nix and require starting it from systemctl or with sudo? With dash it re-executes itself with nohup and runs a small shell script that opens your web browser to localhost:5100 so I could probably modify it to just not do that with substituteInPlace. Ideally I’d like to get this in good enough shape to submit to Nixpkgs if possible.

I also tried substituteInPlace $out/${python3.sitePackages}/picosnitch.py --replace sys.executable "'${python3.interpreter}'" just to be sure and no difference.

I just created a new .nix file and everything seems to be working now (got rid of symlinks, wrapprogram, replaced cases of os.execvp, and added setuptools as a dependency)

{ lib
, pkgs
, python3
, bcc
}:

python3.pkgs.buildPythonApplication rec {
  pname = "picosnitch";
  version = "0.12.0";

  src = python3.pkgs.fetchPypi {
    inherit pname version;
    sha256 = "b87654b4b92e28cf5418388ba1d3165b9fa9b17ba91af2a1a942f059128f68bc";
  };

  propagatedBuildInputs = with python3.pkgs; [
    setuptools
    bcc
    psutil
    dbus-python
    requests
    pandas
    plotly
    dash
  ];

  serviceFile = ''
    [Unit]
    Description=picoSnitch

    [Service]
    Type=simple
    Restart=always
    RestartSec=5
    ExecStart=$out/bin/picosnitch start-no-daemon
    PIDFile=/run/picosnitch.pid

    [Install]
    WantedBy=multi-user.target
  '';

  postInstall = ''
    substituteInPlace $out/${python3.sitePackages}/picosnitch.py --replace "os.execvp(\"bash\", args)" "return ui_dash()"
    substituteInPlace $out/${python3.sitePackages}/picosnitch.py --replace "os.execvp(\"sudo\", args)" "assert sys.argv[1] not in ['start', 'stop', 'restart', 'start-no-daemon'], 'picosnitch requires root privileges to run'"
    substituteInPlace $out/${python3.sitePackages}/picosnitch.py --replace "capeff & cap_sys_admin" "sys.argv[1] not in ['start', 'stop', 'restart', 'start-no-daemon'] or (capeff & cap_sys_admin)"
    substituteInPlace $out/${python3.sitePackages}/picosnitch.py --replace "picosnitch daemon" "picosnitch daemon, WARNING: built in daemon mode is not supported on Nix, use picosnitch start-no-daemon or systemctl instead"
    mkdir -p ./cmd/picosnitch
    echo "${serviceFile}" > ./cmd/picosnitch/picosnitch.service
    install -D -m0444 -t $out/lib/systemd/system ./cmd/picosnitch/picosnitch.service
  '';

  pythonImportsCheck = [ "picosnitch" ];

  meta = with lib; {
    description = "Monitor network traffic per executable with hashing";
    homepage = "https://github.com/elesiuta/picosnitch";
    changelog = "https://github.com/elesiuta/picosnitch/releases";
    license = licenses.gpl3Plus;
    maintainers = [ ];
    platforms = platforms.linux;
  };
}

I installed it by adding this to my configuration.nix file

environment.systemPackages = [ (with import <nixpkgs> {}; callPackage /home/demo/picosnitch.nix {}) ];
systemd.packages = [ (with import <nixpkgs> {}; callPackage /home/demo/picosnitch.nix {}) ];

Let me know if you came up with anything or there’s still ways to make this better, thanks for looking over this and all your help so far!

Since picosnitch is working I opened a pull request for this yesterday picosnitch: init at 0.12.0 by elesiuta · Pull Request #220761 · NixOS/nixpkgs · GitHub

The only issue now is that dash is currently failing to build on the unstable channel.

1 Like