{ 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 # 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 fetchedPath = if builtins.isAttrs externalModuleThunk && externalModuleThunk ? _type && externalModuleThunk._type == "lazy-fetchGit" then # New format: lazy fetchGit - only execute when needed (builtins.fetchGit { inherit (externalModuleThunk) url rev submodules; }).outPath else # Legacy: pre-fetched derivation or path 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 # Resolve external path (lazy fetchGit if needed) externalPath = if builtins.isAttrs user.external && user.external ? url && user.external ? rev then # New format: lazy fetchGit (builtins.fetchGit { inherit (user.external) url rev; submodules = user.external.submodules or false; }).outPath else if builtins.isAttrs user.external && user.external ? outPath then # Legacy: pre-fetched user.external.outPath else # Direct path 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; # 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 ]; }; # Hardware-specific external modules hwSpecificModules = lib.optional (hostType == "nix-lxc") "${inputs.nixpkgs.legacyPackages.${system}.path}/nixos/modules/virtualisation/proxmox-lxc.nix"; allModules = userNixosModules ++ [ ./common.nix overrideModule { networking.hostName = hostName; } { # Inject user definitions from flake-parts level config.athenix.users = lib.mapAttrs (_: user: lib.mapAttrs (_: lib.mkDefault) user) users; } # Enable the appropriate hardware module based on hostType { config.athenix.hw.${hostType}.enable = lib.mkDefault true; } ] ++ hwSpecificModules ++ 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 spec (don't evaluate fetchGit yet!) externalModuleThunk = if hasExternalField then let ext = deviceConfig.external; in # New format: { url, rev, submodules? } - create lazy fetchGit thunk if builtins.isAttrs ext && ext ? url && ext ? rev then { _type = "lazy-fetchGit"; inherit (ext) url rev; submodules = ext.submodules or false; } # Legacy: pre-fetched or path else ext 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; }