Re-design `location.provider`

Hello everyone,

I recently ran into an infinite recursion issue while trying to share my location.* configuration across multiple machines, and it led me to question the current architectural design of the location.provider module. I wanted to bring this up for discussion to see if we can improve how NixOS handles abstract interfaces and services, or maintain consistency among different modules.

This is my first post on NixOS Discourse. There might be inaccurate contents or violations to existing guidelines. Please point out these issues if i have any! :wink:

Current design

The current design of option location.provider requires the user to explicitly specify the provider, where either "manual" (default) or "geoclue2" is accepted.

nixos/modules/config/locale.nix Line 65~75, indentation truncated

provider = lib.mkOption {
  type = lib.types.enum [
    "manual"
    "geoclue2"
  ];
  default = "manual";
## 4 lines omitted ##
};

If "geoclue2" is specified, services.geoclue2.enable will be automatically set to true. In short, the state of location.provider decides the state of services.geoclue2.enable.

nixos/modules/config/locale.nix Line 84, indentation truncated

services.geoclue2.enable = lib.mkIf (lcfg.provider == "geoclue2") true;

Also, all options under location serves as a global interface or shortcut for other services or programs like redshift or gammastep to access location information.

nixos/modules/services/x11/redshift.nix Line 124~147, indentation truncated

systemd.user.services.redshift =
  let
    providerString =
      if lcfg.provider == "manual" then
        "${toString lcfg.latitude}:${toString lcfg.longitude}"
      else
        lcfg.provider;
  in
  {
## 3 lines omitted ##
    serviceConfig = {
      ExecStart = ''
        ${cfg.package}${cfg.executable} \
          -l ${providerString} \
          -t ${toString cfg.temperature.day}:${toString cfg.temperature.night} \
          -b ${toString cfg.brightness.day}:${toString cfg.brightness.night} \
          ${lib.strings.concatStringsSep " " cfg.extraOptions}
      '';
## 2 lines omitted ##
    };
  };

For users and developers who haven’t run into this, the current “Top-Down” design seems perfectly fine. However, this setup might run into problems in the following senarios:

  • Some users, especially those newcomers to NixOS, enables Geoclue and expect other modules (either a service, program, or a arbitary system module) to automatically adopt Geoclue as their backends. However, location.provider still remains the default value "manual" thus Geoclue will not be exposed to these modules
  • If a user only enables Geoclue on specific laptops but uses a shared global profile for location, trying to conditionally set the provider based on the daemon’s status (location.provider = lib.mkIf config.services.geoclue2.enable "geoclue2";) causes an immediate infinite recursion, because locale.nix is simultaneously checking location.provider to determine geoclue2.enable. This is the case I am dealing with.
  • Desktop environments (like GNOME) or other modules (like automatic-timezoned) often enable services.geoclue2.enable = true directly. Because the global location.provider defaults to "manual", a user can have Geoclue perfectly running on their system, but Redshift/Gammastep will still silently fail to fetch a location unless the user also explicitly overrides location.provider = "geoclue2". The abstract interface completely ignores the active capabilities of the system.

My proposal

Instead of the abstract interface dictating the service state, the abstract interface can be desined to dynamically poll the service state to formulate a smart default, while remaining fully overridable.

Similar pattern can already be found in the definition of networking.firewall.backend.

nixos/modules/services/networking/firewall.nix Line 83~107, indentation truncated

backend = lib.mkOption {
  type = lib.types.enum [
    "iptables"
    "nftables"
    "firewalld"
  ];
  default =
    if config.services.firewalld.enable then
      "firewalld"
    else if config.networking.nftables.enable then
      "nftables"
    else
      "iptables";
## 11 lines omitted ##
};

We can adopt this exact methodology for location.provider in two ways:

Approach A: Dynamic Defaulting in locale.nix

Change the default of location.provider to:

default = if config.services.geoclue2.enable then "geoclue2" else "manual";

Approach B: Injecting the default from geoclue2.nix

Alternatively, we keep location.provider default as "manual", but append this inside the Geoclue2 module implementation:

# In nixos/modules/services/desktops/geoclue2.nix
config.location.provider = lib.mkOverride 200 "geoclue2";

lib.mkDefault is not used as "manual" takes the default priority (1000), and normal priority (100) should be reserved for user specification. Also, different priorities can be specified to create an order of overrides if other location providers like gpsd might be added in the future.

In both approaches, we would then remove

services.geoclue2.enable = lib.mkIf (lcfg.provider == "geoclue2") true;

to break the recursion chain. Both approaches should achieve the same result.

This ensures that whenever Geoclue is turned on (by a user, a DE, or a profile), the system automatically takes advantage of it, but a user can still easily force location.provider = lib.mkForce "manual" if their GPS locks up.

An alternative approach

If the consensus is that the strict “Top-Down” design is structurally required, then we should officially force all other services/programs to adopt location.* as the definitive truth.

However, if we take this path, we must heavily document that users should never touch services.geoclue2.enable directly. Enabling Geoclue manually should almost throw a warning, as doing so bypasses the location.* interface and leaves dependent modules (like Redshift) stranded.

Another possible approach

We could theoretically completely remove the options under location, forcing users to use services.geoclue2.enable natively, as Geoclue itself supports defining static/manual locations in its configuration file.

However, this option is not extensible. If alternative location providers are added to Nixpkgs in the future, hardcoding everything to Geoclue’s module will cause major refactoring headaches. location.* is fundamentally a good abstraction layer; it just has its directional logic backward.

I would love to hear your thoughts on this! Is there any specific historical design reason why location forces the state of geoclue2 that I am missing?

1 Like

Approach A sounds best to me. It means that almost any user wouldn’t ever have to touch location.provider.

It doesn’t make sense to use approach B over approach A in my opinion. For best separation of concern only one of the modules should concern itself with another, and in this case location module is already aware of services.geoclue2, there’s no need for services.geoclue2 module to affect location module.

location.provider description should document that service.geoclue2.enable = true should be preferred. The defaultText in networking/firewall.nix kinda serves that purpose, but maybe it would be easier for some users if we write this explicitly.

We should also check where location.provider is set in nixpkgs and update to services.geoclue2.enable accordingly.

2 Likes