Exposing a derivation created in a NixOS module

I created a NixOS module that allows configuring Django sites like so:

django.sites.mysite = {
  package = pkgs.mySite;
  hostname = "www.mysite.com";

This module creates a script and exposes it in environment.systemPackages so you can easily interact with the deployed site.

Now to my problem: for some projects I need to regularly execute tasks that should call this script with some arguments. I know I can do this with a systemd timer, but apparently I need to specify the absolute path to the executable in the service file. But since the executable is created in the NixOS module, I can’t access it from my system configuration file.

So, how can I get the path to a derivation created in my module? Or how can I somehow expose it?

Note the script needs to be generated by the NixOS module because it contains stuff like environment variables and references to other Python libs, so I can’t just add a manageScript option to the module and pass the derivation from the caller.

Make a read only option and set it inside the same module. Other modules can read it through config.

I didn’t know about read-only options (it seems the NixOS manual doesn’t mention them?) but from what I could see in the code, it’s just a matter of passing readOnly = true to mkOption, right? So my option would look like this:

  options.django = {
    sites = mkOption {
      type = types.attrsOf (types.submodule {
        options = {
          package = mkOption { type = types.package; };
          staticFilesPackage = mkOption { type = types.package; };
          manageScript = mkOption {
            type = types.package; 
            readOnly = true;
            visible = false;

But now I need to define the option for each site from within the module, which I don’t know how to do without getting an infinite recursion error:

# This of course doesn’t work
config = mkIf (cfg.sites != [ ]) ({
  # ...
} // lib.attrsets.mapAttrs (site: conf: {
  django.sites.${site}.manageScript = conf.manageScript;
}) siteConfigs);

Move the mapAttrs lower in the attribute path. Nix can’t tell without evaluating it that it doesn’t set some other options (like the inputs to siteConfigs itself), and it can’t evaluate it without having final values for those options, so it creates an infinite recursion.

I think this is the lowest I can get:

config = mkIf (cfg.sites != [ ]) {
  django.sites = lib.attrsets.mapAttrs(site: conf: {
    ${site}.manageScript = conf.manageScript;
  }) siteConfigs;

But this still gives me an infinite recursion error, since siteConfigs is defined like this: siteConfigs = mapAttrs siteToConfig cfg.sites;. :confused:

Then move the logic into the submodule itself? A submodule behaves like an independent invocation of the module system during merge, so you can actually write “modules” that work at its level. Home-manager does this, for example, that’s why you can import normal home-manager modules when using it as a nixos module.

Thanks for the pointers. I managed to get it to work. :slight_smile:

I remembered the php-fpm module uses a similar syntax to what I’m trying to achieve and has a read-only socket option defined by the module itself. It’s done by passing a callable to the submodule option, where you get the name of the attr (ie. the site to configure).

This feature is actually documented in the NixOS manual:

submodule is a very powerful type that defines a set of sub-options that are handled like a separate module.

It takes a parameter o , that should be a set, or a function returning a set with an options key defining the sub-options. Submodule option definitions are type-checked accordingly to the options declarations. Of course, you can nest submodule option definitons for even higher modularity.

Surprisingly there is no information on what parameters are passed to the submodule function. If anyone knows this, I’d be interested to know!

1 Like