tweag / nix-ux

Nix UX improvements
36 stars 2 forks source link

Use cases supported by the existing module system and overlays/overrides #20

Open infinisil opened 4 years ago

infinisil commented 4 years ago

Module system features and use-cases for them

Priority-based merging

mkDefault, mkForce, etc. Also works recursively. This allows precise control over how defaults are persisted or overridden. E.g.

{
  value = {
    foo = mkDefault "foo";
    bar = mkDefault {
      baz = 10;
      qux = true;
    };
    baq = {
      foo = 20;
    };
  };
}
{
  value = {
    foo = "foo-changed";
    bar = mkDefault {
      qux = mkForce false;
    };
    baq = mkForce "baq-changed";
  };
}
# turns into
{
  value = {
    foo = "foo-changed";
    bar = {
      baz = 10;
      qux = false;
    };
    baq = "baq-changed";
  };
}

Conditional definitions using mkIf

This allows optionally not having a definition at all, which makes sure to use the default instead. E.g.

{ config, ... }: {
  value = mkIf config.enable {
    foo = mkIf config.enableFoo "foo";
  };
}
# turns into (wich enable = true and enableFoo = false)
{
  value = {};
}

Option and type merging

Options can be defined multiple times with different types, which are then merged together. Useful for submodule modularity and changing submodule defaults. This is used for the fileSystems NixOS option. E.g. this allows splitting modules into backend-specific settings:

{
  options.value = mkOption {
    type = attrsOf (submodule ({ config, ... }: {
      options = {
        result = mkOption {
          type = lines;
        };
        backend = mkOption {
          type = enum [];
        };
        shared = mkOption {
          type = types.str;
        };
      };
      config.result = ''
        shared = ${config.shared};
      '';
    }));
  };
}
# Another module (and can be many more like this)
{
  options.value = mkOption {
    type = attrsOf (submodule ({ config, ... }: {
      options.backend = mkOption {
        type = enum [ "foo" ];
        default = "foo";
      };
      options.foo.fooSpecific = mkOption {
        type = str;
      };
      config.result = mkIf (config.backend == "foo") ''
        foo = ${config.foo.fooSpecific}
      '';
    }));
  };
}

Custom types

lib.types provides a bunch of ready-made types, but users can also define their own, with custom checks and merging strategies. See also https://github.com/NixOS/nixpkgs/pull/75584

{
  options.smolString = mkOption {
    type = types.addCheck (s: builtins.stringLength s < 32) types.str;
  };
  options.funType = mkOption {
    type = types.mkOptionType {
      check = builtins.isFunction;
      # Merge functions by joining the attributes they create
      merge = loc: defs: arg: foldl' (l: r: l // r) {} (map (d: d.value arg) defs);
    };
  };
}

Freeform modules

Recently introduced freeform modules add the ability to not have to declare all options, providing a fallback type for all definitions not matched to an option, which gets merged into the result. Very useful for not having to declare every option of package settings, while still having custom type checking for some of them:

{
  options.settings = mkOption {
    default = {};
    type = with types; submodule {
      # All values that don't have an associated option have to be of this type
      freeformType = attrsOf int;

      options.port = mkOption {
        type = port;
        default = 80;
        description = "The port to use.";
      };
    };
  };

  # Works, even though foo isn't declared as an option
  config.settings.foo = 10;

  # error: The option value `settings.port' in `example.nix' is not of type
  # `16 bit unsigned integer; between 0 and 65535 (both inclusive)'.
  #config.settings.port = "not-a-port";

  ## Overrides default value
  config.settings.port = 8080;
}

Disabling of modules

If a modules is included by default, it can be removed with disabledModules:

{
  disabledModules = [
    # Disable because it slows things down
    "tasks/lvm.nix"
    # Disable because I want to provide my own services.murmur.* set
    "services/networking/murmur.nix"
  ];
}

Read-only options

Aka, only allow 1 definition, which is usually declared by the module that sets read-only directly. This is usually used for exposing module evaluation results.

{
  options.value = mkOption {
    readOnly = true;
    # We set it here, nobody else can set it now
    default = "value";
  };
}

Type coercion

This is really just another custom type, but its implementation allows converting one definition type into another one. Very useful for backwards compatibility

{
  # First version
  options.value = mkOption {
    type = str;
  };

  # Backwards-compatible second version
  options.value = mkOption {
    type = coercedTo str (s: { name = s; }) (submodule {
      options.name = mkOption { type = str; };
      options.other = mkOption { /* ... */ };
    });
  };
}

Inspecting options

Including their type, defaults, definitions, etc., and using them for new options or configs:

{ options, ... }: {
  options.foo = mkOption {
    type = options.qux.type;
    default = options.qux.default;
  };

  config.bar = mkIf options.baz.isDefined {
    defs = options.baz.definitions;
  };
}

Priority-based definition ordering

Using mkBefore, mkAfter, and co. This is mostly useful for specifying element orders in lists. But it could also be used for e.g. overlay order.

{
  # Order this one before others
  value = mkBefore [ "value" ];
}

Overlay/overrides features and use-cases of them

Overriding package sets

Such that package changes propagate to all the dependents of it

self: super: {
  pythonPackages = super.pythonPackages.override (old: {
    overrides = lib.combineExtensions old.overrides (pself: psuper: {
      requests = myCustomRequests;
    });
  });
}

Overriding callPackage arguments per-package

Changing the callPackage arguments for just a single package, without influencing others:

{
  foo = callPackage ./path/to/foo {
    libbar = libbar_1_10;
  };
}

Similarly with .override

Overriding package sets per-package

If a package and all its dependencies need a different version, this could be done with override like this:

pkgs.foo.override {
  libbar = libbar_1_10;
  baz = baz.override {
    libbar = libbar_1_10;
  };
  qux = qux.override {
    baz = baz.override {
      libbar = libbar_1_10;
    }
  };
}

Which is very painful, and incorrect if you missed a dependency. Some package sets (like the haskell one) provide .overrideScope for this purpose:

pkgs.foo.overrideScope (self: super: {
  libbar = libbar_1_10;
})

See also https://github.com/NixOS/nixpkgs/pull/67422. pkgs.appendOverlays and pkgs.extend work like this as well. Also, this can also work on package sets themselves, e.g.

pkgs.haskellPackages.extend (self: super: {
  # More packages, not added via overlays
})

Overriding mkDerivation, derivation, and other function arguments

hello.overrideDerivation {
  # override derivation arguments
}
hello.overrideAttrs (old: {
  # override derivation arguments
})
hello.override (old: {
  # override callPackage arguments
})
hello.overridePythonAttrs (old: {
  # override buildPythonPackage arguments
})

See also https://github.com/NixOS/rfcs/pull/67

Aliased packages and package sets

This should ideally be disallowed, but this is currently a thing:

self: with self; {
  pythonPackages = python.pkgs;
  python2Packages = python2.pkgs;
  python27Packages = pypy27.pkgs;

  linuxPackagesFor = kernel: { ... };
  linuxPackages_5_8 = linuxPackagesFor pkgs.linux_5_8;
  linuxPackages_latest = linuxPackages_5_8;
  linux_latest = linuxPackages_latest.kernel;
}

Overriding function arguments

Possible using __functor:

self: super: {
  # Has .override
  fetchurl = super.fetchurl.override {
    cacert = null;
  };

  # But can also be called
  result = self.fetchurl { ... };
}
carnotweat commented 6 months ago

Aliased packages and package sets as I understand what functor does is object closure ( eg summing some integers ), I am not sure how that helps with overriding args of the function -- noob