nix

Evaluating `writeShellApplication` results in what? How is closure determined between packages defined in attributes returned by the module system?


Main questions:

I am following this tutorial from nix.dev.

I am stuck at the end of section 2.11, just before beginning section 2.12.

Basically, I have everything working as expected. But I don't understand how everything is working (lol).

I have a default.nix:

{

    pkgs,
    lib,
    config,
    ...

}: {

    options = {
        scripts = {
            output = lib.mkOption {
                type = lib.types.package;
            };
            geocode = lib.mkOption {
                type = lib.types.package;
            };
        };
        requestParams = lib.mkOption {
            type = lib.types.listOf lib.types.str;
        };
        map = {
            zoom = lib.mkOption {
                type = lib.types.nullOr lib.types.int;  
                default = 10;
            };
            center = lib.mkOption {
                type = lib.types.nullOr lib.types.str;
                default = "brazil";
            };
        };
    };

    config = {
        scripts.output = pkgs.writeShellApplication {
            name = "map";
            runtimeInputs = with pkgs; [
                curl
                feh
            ];
            text = ''
                ${./map.sh} ${lib.concatStringsSep " " config.requestParams} | feh -
            '';
        };
        scripts.geocode = pkgs.writeShellApplication {
            name = "geocode";
            runtimeInputs = with pkgs; [
                curl
                jq
            ];
            text = ''
                exec ${./geocode.sh} "$@"
            '';
        };
        requestParams = [
            "size=640x640"
            "scale=2"
            (
                lib.mkIf (config.map.zoom != null) 
                "zoom=${toString config.map.zoom}"
            )
            (
                lib.mkIf (config.map.center != null)
                "center=\"$(${config.scripts.geocode}/bin/geocode ${lib.escapeShellArg config.map.center})\""
            )
        ];

        map.zoom = 3;
        map.center = "brazil";
    };

}

and I also have an eval.nix:

let

    nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
    pkgs = import nixpkgs {};

in
    
    pkgs.lib.evalModules {
        modules = [

            (
                { 
                    config, 
                    ... 
                }: { 
                    config._module.args = {
                        inherit pkgs;
                    };
                }
            )

            ./default.nix
        ];
    }

[user@hostname:~/path/to/project/folder]$ ls -1 gives me

default.nix
eval.nix
geocode.sh
map.sh
shell.nix

Running [user@hostname:~/path/to/project/folder]$ nix-build eval.nix -A config.scripts.output prints a Nix Store path to stdout, and then running [user@hostname:~/path/to/project/folder]$ ls -1 again gives me

default.nix
eval.nix
geocode.sh
map.sh
result
shell.nix

And result/bin/map works flawlessly.

  1. I am inferring from my understanding of the behavior of nix-build that writeShellApplication evaluates to a derivation. Is this inference correct?

My understanding of nix-build, for reference, is a follows:

nix-build is a wrapper on nix-instantiate and nix-store --realize.

nix-instantiate takes in a path to a package (i.e., a path to a .nix file with a function that evaluates to a derivation), evaluates that package (i.e., evaluates the function defined inside the .nix file that is located in the path given to it), stores that package in some path in the Nix Store (i.e., stores the value obtained from the evaluation of function defined inside the .nix file that is located in the path given to it in some path in the Nix store), and then prints that path to stdout.

nix-store --realize takes in a path to some derivation stored in the Nix Store, goes through with it (i.e. derives files according to the instructions of the derivation itself), stores the build result in the some path in the Nix Store (i.e., stores the derived files in some path in the Nix Store), and symlinks between that Nix Store path and the current directory (i.e., creates the result symlink, linking between the path in which the derived files were stored in the Nix Store and the current directory).

All nix-build does is take a path to a .nix file containing a Nix Expression that evaluates to a derivation, and runs nix-instantiate followed by nix-store --realize on it: it "instantiates" the derivation that is obtained by evaluating the Nix Expression defined in the .nix file that is located in the path given it, storing that derivation in some path in the Nix Store, and then it disposes of that path to "realize" that derivation, symlinking to the current directory the build result.

Now, you can use nix-build with a -A flag. Here's my understanding of that:

man nix-build says that the options passed to the -A flag are passed to nix-instantiate. With this flag, you can specify a value from an attribute within an attribute set that is obtained by evaluating the Nix Expression defined in the ./nix file located in the path passed to the command to be used instead of the Nix Expression defined in the ./nix file located in the path passed to the command. All you have to do is specify the name of that attribute.

Now, assuming that my inference at 1. is correct, and that my understanding of the -A flag in nix-build is also sound, here's my second question:

  1. If I ran [user@hostname:~/path/to/project/folder]$ nix-build eval.nix -A config.scripts.output, how come Nix knew to also "build" config.scripts.geocode? I never pointed nix-build to the expression located at config.scripts.geocode. Does Nix implictly create a dependency between the derivation that is obtained by evaluating config.scripts.output and the one that is obtained by evaluating config.script.geocode? Perhaps some functionality of a package's closure? How would that work?

Also, the reason I assert that "Nix knew how to 'build' config.scripts.geocode" is because, well, ./result/bin/map works. Perhaps I am also wrong about this.


Solution

  • After some quick discussion in Vimjoyer's Discord server:

    1. Nix evaluates config.scripts.output because I pointed nix-build to it with the -A flag.

    2. To evaluate config.scripts.output, Nix must know the value of config.requestParams.

    3. Evaluating config.requestParams requires knowing the value of ${config.scripts.geocode}

    ${config.script.geocode} is a string interpolation. Evaluating the value of config.script.geocode eventually boils down to the return value of mkDerivation. Per this, mkDerivation outputs a special attribute set that can be used in string interpolation,

    and in that case evaluates to the Nix store path of its build result.

    So this is why config.script.geocode gets built.