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! ![]()
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.nixLine 65~75, indentation truncatedprovider = 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.nixLine 84, indentation truncatedservices.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.nixLine 124~147, indentation truncatedsystemd.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.providerstill 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, becauselocale.nixis simultaneously checkinglocation.providerto determinegeoclue2.enable. This is the case I am dealing with. - Desktop environments (like GNOME) or other modules (like
automatic-timezoned) often enableservices.geoclue2.enable = truedirectly. Because the globallocation.providerdefaults 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 overrideslocation.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.nixLine 83~107, indentation truncatedbackend = 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?