Skip to content

Disallow declaring options named config and options (and others) #162398

@infinisil

Description

@infinisil

Issue description

Options being allowed to be named config and options causes problems related to module syntax. The canonical module syntax has a number of sections, mainly config, options, imports, disabledModules and freeformType, which are the only allowed top-level attributes. This might look like

{
  imports = [ ... ];
  options = ...;
  config = ...;

  # Not allowed
  foo = ...;
}

But another syntax, sometimes called shorthand syntax, is also supported, which triggers when neither options nor config is set. In this alternate syntax, setting arbitrary attributes is valid, with the semantics that they get put into the config section. This might look like this:

{
  imports = [ ... ];
  # options, config <- needs to not be defined
  
  foo = ...;
}

This is then equivalent to

{
  imports = [ ... ];
  config.foo = ...;
}

If you don't need any option definitions, this syntax is more convenient.


The problem arises when you have options with names matching the section names, such as config, options, imports, or others. Assuming we have an option named options, then we can't use the convenient shorthand syntax:

{
  # Since `options` is declared, the canonical syntax is used, not the shorthand one,
  # making this be an option declaration, not a definition!
  options = ...;
}

To actually declare this option you need to use

{
  config.options = ...;
}

Similarly, if you have an option named config, you can't do

{
  config = ...;
}

but instead need to write

{
  config.config = ...;
}

If options have a name matching another section name, like imports or disallowedModules, shorthand syntax also can't be used, because these attributes are interpreted as sections, not options definitions. E.g. with an option named imports, the following doesn't work:

{
  imports = ...;
}

Instead this is needed:

{
  config.imports = ...;
}

These same problems are also caused by submodules, except that there it's even more nuanced, because submodules declared with types.submodule historically have always forced the shorthand syntax by wrapping its definitions into a config value. This was done to prevent surprises when trying to set a config option. E.g. if you have an option foo of type types.submodule, which itself has a nested config option, then you can do this:

{
  foo.config = ...;
}

Instead of having to do this:

{
  foo.config.config = ...;
}

But the flip side is that there's no way to then declare non-config sections. The only known hack around this is to declare values as a function, because only attribute values are wrapped under config (since anything else wouldn't be valid as av config value):

{
  foo = { ... }: {
    options = ...;
  };
}

With the introduction of types.submoduleWith, a parameter shorthandOnlyDefinesConfig was introduced, which controls whether this config wrapping is done, defaulting to it not being done (in contrast with types.submodule)

Suggested solution

By disallowing options to be named according to a section name, we can always know whether an attribute is a section or an option name, allowing us to avoid all these problems:

  • No more weird edge cases regarding shorthand syntax: Every option can be defined using shorthand syntax.
  • The whole config wrapping for types.submodule and types.submoduleWith can be removed, cleaning up code and removing the need for function hacks.
  • We should also change the rule of "options or config being set implies canonical syntax" to just "config being set implies canonical syntax". If options can't be named options, then there's no reason options couldn't be declared using an options attribute while using shorthand syntax for config.

The hardest part is backwards compatibility and transition, because in NixOS there are a whole bunch of options named config and some named options (and one named imports). Another section name that could be problematic is key.

We should also consider whether it's possible to rename config and options to something else. I've heard complaints in the past that these have been confusing.

Ping @roberth @zimbatm

Metadata

Metadata

Assignees

No one assigned

    Labels

    2.status: stalehttps://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md6.topic: module systemAbout "NixOS" module system internals

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions