{ inputs, lib, config, self ? null, users ? { }, ... }: # ============================================================================ # Fleet Generator # ============================================================================ # This file contains the logic to generate NixOS configurations for all hosts # defined in inventory.nix. It supports both hostname-based and count-based # configurations with flexible type associations. let # Evaluate inventory to get fleet data # Import fleet-option.nix (defines athenix.fleet) and inventory.nix (sets values) # We use a minimal module here to avoid circular dependencies from common.nix's imports hostTypes = config.athenix.hwTypes; # Helper to create a single NixOS system configuration mkHost = { hostName, system ? "x86_64-linux", hostType, configOverrides ? { }, externalModuleThunk ? null, }: let # Lazy evaluation: only fetch external module when building this host externalModulePath = if externalModuleThunk != null then let # Force evaluation of the thunk (fetchGit, fetchTarball, etc.) fetchedPath = externalModuleThunk; # Extract outPath from fetchGit/fetchTarball results extractedPath = if builtins.isAttrs fetchedPath && fetchedPath ? outPath then fetchedPath.outPath else fetchedPath; in if builtins.isPath extractedPath then extractedPath + "/default.nix" else if lib.isDerivation extractedPath then extractedPath + "/default.nix" else extractedPath + "/default.nix" else null; # Load users.nix to find external user modules accounts = config.athenix.users or { }; # Build a map of user names to their nixos module paths (if they exist) # We'll use this to conditionally import modules based on user.enable userNixosModulePaths = lib.filterAttrs (_: v: v != null) ( lib.mapAttrs ( name: user: if (user ? external && user.external != null) then let externalPath = if builtins.isAttrs user.external && user.external ? outPath then user.external.outPath else user.external; nixosModulePath = externalPath + "/nixos.nix"; in if (builtins.isPath externalPath || (builtins.isString externalPath && lib.hasPrefix "/" externalPath)) && builtins.pathExists nixosModulePath then nixosModulePath else null else null ) accounts ); # Create conditional wrapper modules for each user's nixos.nix # Each wrapper checks if the user is enabled before applying the module content userNixosModules = lib.mapAttrsToList ( name: modulePath: { config, lib, pkgs, ... }@args: let # Import the user's nixos module - it returns a function or attrset importedModuleFunc = import modulePath { inherit inputs; }; # If it's a function, call it with the module args; otherwise use as-is importedModule = if lib.isFunction importedModuleFunc then importedModuleFunc args else importedModuleFunc; in { config = lib.mkIf (config.athenix.users.${name}.enable or false) importedModule; } ) userNixosModulePaths; # Get the host type module from the hostTypes attribute set typeModule = hostTypes.${hostType} or (throw "Host type '${hostType}' not found. Available types: ${lib.concatStringsSep ", " (lib.attrNames hostTypes)}"); # External module from fetchGit/fetchurl externalPathModule = if externalModulePath != null then import externalModulePath { inherit inputs; } else { }; # Config override module - translate special keys to athenix options overrideModule = { ... }: let cleanConfig = lib.removeAttrs configOverrides [ "type" "count" "devices" "overrides" "defaultCount" "buildMethods" ]; specialConfig = lib.optionalAttrs (configOverrides ? buildMethods) { athenix.host.buildMethods = configOverrides.buildMethods; }; in { config = lib.mkMerge [ cleanConfig specialConfig ]; }; allModules = userNixosModules ++ [ ./common.nix typeModule overrideModule { networking.hostName = hostName; } { # Inject user definitions from flake-parts level config.athenix.users = lib.mapAttrs (_: user: lib.mapAttrs (_: lib.mkDefault) user) users; } ] ++ lib.optional (externalModulePath != null) externalPathModule; in { system = lib.nixosSystem { inherit system; specialArgs = { inputs = if self != null then inputs // { inherit self; } else inputs; }; modules = allModules; }; modules = allModules; }; # Process inventory entries - top-level keys are always prefixes processInventory = lib.mapAttrs ( prefix: config: let hostType = config.type or prefix; system = config.system or "x86_64-linux"; # Helper to generate hostname from prefix and suffix # Numbers get no dash: "nix-surface1", "nix-surface2" # Letters get dash: "nix-surface-alpha", "nix-surface-beta" mkHostName = prefix: suffix: usePrefix: if !usePrefix then suffix else if lib.match "[0-9]+" suffix != null then "${prefix}${suffix}" # numeric: no dash else "${prefix}-${suffix}"; # non-numeric: add dash # Extract common overrides and default count overrides = config.overrides or { }; defaultCount = config.defaultCount or 0; # If devices is a number, treat it as count devicesValue = config.devices or { }; actualDevices = if lib.isInt devicesValue then { } else devicesValue; actualCount = if lib.isInt devicesValue then devicesValue else (config.count or 0); # Clean base config - remove inventory management keys baseConfig = lib.removeAttrs config [ "type" "system" "count" "devices" "overrides" "defaultCount" ]; # Generate hosts from explicit device definitions deviceHosts = lib.listToAttrs ( lib.mapAttrsToList ( deviceKey: deviceConfig: let # Check if deviceConfig has an 'external' field for lazy evaluation hasExternalField = builtins.isAttrs deviceConfig && deviceConfig ? external; # Extract external module thunk if present (don't evaluate yet!) externalModuleThunk = if hasExternalField then deviceConfig.external else null; # Remove 'external' from config to avoid conflicts cleanDeviceConfig = if hasExternalField then lib.removeAttrs deviceConfig [ "external" ] else deviceConfig; # Merge: base config -> overrides -> device-specific config mergedConfig = lib.recursiveUpdate (lib.recursiveUpdate baseConfig overrides) cleanDeviceConfig; # Check useHostPrefix from the merged config usePrefix = mergedConfig.athenix.host.useHostPrefix or true; hostName = mkHostName prefix deviceKey usePrefix; in { name = hostName; value = mkHost { inherit hostName system hostType externalModuleThunk ; configOverrides = mergedConfig; }; } ) actualDevices ); # Generate numbered hosts from count or defaultCount # If devices are specified, defaultCount fills in the gaps countToUse = if actualCount > 0 then actualCount else defaultCount; # Get which numbered keys are already defined in devices existingNumbers = lib.filter (k: lib.match "[0-9]+" k != null) (lib.attrNames actualDevices); countHosts = if countToUse > 0 then lib.listToAttrs ( lib.filter (x: x != null) ( map ( i: let deviceKey = toString i; # Skip if this number is already in explicit devices alreadyDefined = lib.elem deviceKey existingNumbers; in if alreadyDefined then null else let hostName = mkHostName prefix deviceKey true; mergedConfig = lib.recursiveUpdate baseConfig overrides; in { name = hostName; value = mkHost { inherit hostName system hostType; configOverrides = mergedConfig; }; } ) (lib.range 1 countToUse) ) ) else { }; in lib.recursiveUpdate deviceHosts countHosts ); fleetData = config.athenix.fleet; # Flatten the nested structure allHosts = lib.foldl' lib.recursiveUpdate { } (lib.attrValues (processInventory fleetData)); in { nixosConfigurations = lib.mapAttrs (n: v: v.system) allHosts; modules = lib.mapAttrs (n: v: v.modules) allHosts; }