Flake packageing program with configuration module

I am trying to package a small tool of mine in a separate flake. The trouble i am currently having is referring to the package from my configuration module.

This is my current setup.

flake.nix

{
  description = "description";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
  };

  outputs = {
    self,
    nixpkgs,
  }: let
    systems = ["x86_64-linux" "aarch64-linux"];
    forAllSystem = f: nixpkgs.lib.genAttrs systems (system: f system);
  in {
    packages = forAllSystem (system: {
      default = nixpkgs.legacyPackages.${system}.callPackage ./soppps.nix {};
    });
    nixosModules = {
      soppps = import ./soppps-service.nix;
      default = self.nixosModules.soppps;
    };
  };
}

soppps-service.nix

{
  lib, config, ...
}: let
  cfg = config.soppps;
in {
  options = {
    soppps = {
      files = lib.mkOption {
        type = lib.types.listOf lib.types.path;
        description = ''
          List of files to be processed. can use unix globs.
        '';
      };
      package = lib.mkOption {
        description = "soppps package to use";
        type = lib.types.package;
      };
    };
  };
  config.systemd.services.soppps = {
    wantedBy = ["sysinit.target"];

    serviceConfig = {
      Type = "oneshot";
      ExecStart = ["${cfg.package}/bin/soppps"];
      RemainAfterExit = true;
    };
  };
}

soppps.nix is just a simple buildRustPackage statement, so i don’t think it’s important for this since it does get packaged just fine.

The pattern with binding config.<package> to cfg and using cfg.package is something i saw on all packages i have looked at (ssh from nixpkgs or sops-nix for example).

Using this in my system however (by adding inputs.soppps-nix.nixosModules.soppps to the imports of my configuration.nix) throws an error because no config argument was passed, which makes sense to me since it was never supplied it. I have gotten it to work by directly passing self.packages.x86_64-linux.default but i feel this isn’t the proper way to do this. Where exactly would the config parameter be created and passed, so that using this module only requires importing it after adding the flake?

When modules are evaluated for a nixos system, argument config is passed to all modules by default – it’s not something that one would need to pass explicitly (outside of certain edge cases and playing with lib.evalModules but I digress).

Fundamentally to use a custom package in a flake’s module you need the package to be available when that module will be evaluated. Two patterns for this:

  1. Overlay – declare an overlay with your package and use the overlay in the target system’s nixpkgs. Then you can just reference pkgs.mypackage in the module and pkgs attrset will have mypackage attribute

  2. Binding at import site in flake, which I believe is what you tried:

    I have gotten it to work by directly passing self.packages.x86_64-linux.default

    I personally prefer this pattern as opposed to carrying around an overlay and inevitably forgetting it at some point.

An example with your flake (replacing your package with pkgs.hello):

# flake.nix
{
  description = "description";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
  };

  outputs = {
    self,
    nixpkgs,
  }: let
    systems = ["x86_64-linux" "aarch64-linux"];
    forAllSystem = f: nixpkgs.lib.genAttrs systems (system: f system);
  in {
    packages = forAllSystem (system: {
      default = nixpkgs.legacyPackages.${system}.hello;
    });
    # This is just to demo how the module works; not a real check
    checks = forAllSystem (system:{
       demo = nixpkgs.legacyPackages.${system}.testers.runNixOSTest {
        name = "demo";
        nodes.machine1 =
          { config, pkgs, ... }:
          {
            services.getty.autologinUser = "root";
            imports = [
              self.nixosModules.default
            ];
          };
        testScript = "start_all()";
      };
    });
    nixosModules = {
      soppps = import ./soppps-service.nix self; # note passing 'self' here
      default = self.nixosModules.soppps;
    };
  };
}
# soppps-service.nix
self: # Add this
{
  lib, config, pkgs, ...
}: let
  cfg = config.soppps;
in {
  options = {
    soppps = {
      files = lib.mkOption {
        type = lib.types.listOf lib.types.path;
        description = ''
          List of files to be processed. can use unix globs.
        '';
      };
      package = lib.mkOption {
        description = "soppps package to use";
        default = self.packages.${pkgs.system}.default; # Add this
        type = lib.types.package;
      };
    };
  };
  config.systemd.services.soppps = {
    wantedBy = ["sysinit.target"];

    serviceConfig = {
      Type = "oneshot";
      ExecStart = ["${cfg.package}/bin/hello"]; # For demo purposes
      RemainAfterExit = true;
    };
  };
}

Then in interactive shell of the check’s driver (nix run -L .#checks.x86_64-linux.demo.driverInteractive):

$ systemctl status soppps
● soppps.service
     Loaded: loaded (/etc/systemd/system/soppps.service; enabled; preset: enabled)
     Active: active (exited) since Mon 2024-03-18 20:48:14 UTC; 26s ago
    Process: 657 ExecStart=/nix/store/7bl684y3qpxrv01ird085rpf5kl6rk6f-hello-2.12.1/bin/hello (code=exited, status=0/SUCCESS)
   Main PID: 657 (code=exited, status=0/SUCCESS)
         IP: 0B in, 0B out
        CPU: 7ms

Mar 18 20:48:14 machine1 systemd[1]: Starting soppps.service...
Mar 18 20:48:14 machine1 hello[657]: Hello, world!
Mar 18 20:48:14 machine1 systemd[1]: Finished soppps.service.