diff --git a/hosts/boot.nix b/hosts/boot.nix index da3d376..6cdfb7d 100644 --- a/hosts/boot.nix +++ b/hosts/boot.nix @@ -13,7 +13,22 @@ { options.ugaif = { + forUser = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Convenience option to configure a host for a specific user. + Automatically adds the user to extraUsers and sets wslUser for WSL hosts. + Value should be a username from ugaif.users.accounts. + ''; + }; + host = { + useHostPrefix = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to prepend the host prefix to the hostname (used in inventory)."; + }; filesystem = { device = lib.mkOption { type = lib.types.str; diff --git a/hosts/default.nix b/hosts/default.nix index 772106b..d2159fd 100644 --- a/hosts/default.nix +++ b/hosts/default.nix @@ -8,10 +8,25 @@ # Host Generator # ============================================================================ # This file contains the logic to generate NixOS configurations for all hosts -# defined in inventory.nix. It handles: -# 1. Common module imports (boot, users, software). -# 2. Host-specific overrides (filesystem, enabled users). -# 3. External flake integration for system overrides. +# defined in inventory.nix. It supports both hostname-based and count-based +# configurations with flexible type associations. +# +# Inventory format: +# { +# "my-hostname" = { +# type = "nix-desktop"; # Host type module to use +# system = "x86_64-linux"; # Optional +# # ... any ugaif.* options or device-specific config +# }; +# +# "lab-prefix" = { +# type = "nix-laptop"; +# count = 5; # Generates lab-prefix1, lab-prefix2, ... lab-prefix5 +# devices = { +# "machine-1" = { ... }; # Override for lab-prefix1 +# }; +# }; +# } let nixpkgs = inputs.nixpkgs; @@ -21,11 +36,11 @@ let { hostName, system ? "x86_64-linux", - extraModules ? [ ], + hostType, + configOverrides ? { }, }: let # Load users.nix to find external user flakes - # We use legacyPackages to evaluate the simple data structure of users.nix pkgs = nixpkgs.legacyPackages.${system}; usersData = import ../users.nix { inherit pkgs; }; accounts = usersData.ugaif.users.accounts or { }; @@ -39,111 +54,177 @@ let { } ) accounts; + # Load the host type module + typeFile = ./types + "/${hostType}.nix"; + typeModule = + if builtins.pathExists typeFile then + import typeFile { inherit inputs; } + else + throw "Host type '${hostType}' not found in hosts/types/"; + + # External flake override if specified + externalFlakeModule = + if configOverrides ? flakeUrl then + (builtins.getFlake configOverrides.flakeUrl).nixosModules.default + else + { }; + + # Config override module - translate special keys to ugaif options + overrideModule = + { ... }: + let + cleanConfig = lib.removeAttrs configOverrides [ + "type" + "count" + "devices" + "overrides" + "defaultCount" + "extraUsers" + "flakeUrl" + "hostname" + "buildMethods" + "wslUser" + ]; + specialConfig = lib.mkMerge [ + (lib.optionalAttrs (configOverrides ? extraUsers) { + ugaif.users.enabledUsers = configOverrides.extraUsers; + }) + (lib.optionalAttrs (configOverrides ? buildMethods) { + ugaif.host.buildMethods = configOverrides.buildMethods; + }) + (lib.optionalAttrs (configOverrides ? wslUser) { + ugaif.host.wsl.user = configOverrides.wslUser; + }) + ]; + in + { + config = lib.mkMerge [ + cleanConfig + specialConfig + ]; + }; + allModules = userFlakeModules - ++ extraModules ++ [ + typeModule + overrideModule { networking.hostName = hostName; } - ]; + ] + ++ lib.optional (configOverrides ? flakeUrl) externalFlakeModule; in { system = lib.nixosSystem { inherit system; - specialArgs = { inherit inputs; }; - modules = allModules; }; modules = allModules; }; - # Function to generate a set of hosts based on inventory count and overrides - mkHostGroup = - { - prefix, - count, - system ? "x86_64-linux", - extraModules ? [ ], - deviceOverrides ? { }, - }: - lib.listToAttrs ( - lib.concatMap ( - i: - let - defaultName = "${prefix}${toString i}"; - devConf = deviceOverrides.${toString i} or { }; - hasOverride = builtins.hasAttr (toString i) deviceOverrides; - hostName = - if hasOverride && (builtins.hasAttr "hostname" devConf) then devConf.hostname else defaultName; - - # Extract flakeUrl if it exists - externalFlake = - if hasOverride && (builtins.hasAttr "flakeUrl" devConf) then - builtins.getFlake devConf.flakeUrl - else - null; - - # Module from external flake - externalModule = if externalFlake != null then externalFlake.nixosModules.default else { }; - - # Config override module (filesystem, users) - overrideModule = - { ... }: - let - # Extract device-specific config, removing special keys that need custom handling - baseConfig = lib.removeAttrs devConf [ - "extraUsers" - "flakeUrl" - "hostname" - "buildMethods" - "wslUser" - ]; - # Handle special keys that map to specific ugaif options - specialConfig = lib.mkMerge [ - (lib.optionalAttrs (devConf ? extraUsers) { ugaif.users.enabledUsers = devConf.extraUsers; }) - (lib.optionalAttrs (devConf ? buildMethods) { ugaif.host.buildMethods = devConf.buildMethods; }) - (lib.optionalAttrs (devConf ? wslUser) { ugaif.host.wsl.user = devConf.wslUser; }) - ]; - in - lib.mkIf hasOverride (lib.recursiveUpdate baseConfig specialConfig); - - config = mkHost { - hostName = hostName; - inherit system; - extraModules = - extraModules ++ [ overrideModule ] ++ (lib.optional (externalFlake != null) externalModule); - }; - - aliasNames = lib.optional (hostName != defaultName) hostName; - names = lib.unique ([ defaultName ] ++ aliasNames); - in - lib.map (name: { - inherit name; - value = config; - }) names - ) (lib.range 1 count) - ); - - # Generate host groups based on the input hosts configuration - hostGroups = lib.mapAttrsToList ( - type: config: + # Process inventory entries - top-level keys are always prefixes + processInventory = lib.mapAttrs ( + prefix: config: let - typeFile = ./types + "/${type}.nix"; - modules = - if builtins.pathExists typeFile then - [ (import typeFile { inherit inputs; }) ] + hostType = config.type or prefix; + system = config.system or "x86_64-linux"; + devices = config.devices or { }; + hasCount = config ? count; + + # 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 - throw "Host type '${type}' not found in hosts/types/"; + "${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 + usePrefix = deviceConfig.ugaif.host.useHostPrefix or true; + hostName = mkHostName prefix deviceKey usePrefix; + # Merge: base config -> overrides -> device-specific config + mergedConfig = lib.recursiveUpdate (lib.recursiveUpdate baseConfig overrides) deviceConfig; + in + { + name = hostName; + value = mkHost { + inherit hostName system hostType; + 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 - mkHostGroup { - prefix = type; - inherit (config) count; - extraModules = modules; - deviceOverrides = config.devices or { }; - } + lib.recursiveUpdate deviceHosts countHosts ) hosts; - allHosts = lib.foldl' lib.recursiveUpdate { } hostGroups; + # Flatten the nested structure + allHosts = lib.foldl' lib.recursiveUpdate { } (lib.attrValues processInventory); in { nixosConfigurations = lib.mapAttrs (n: v: v.system) allHosts; diff --git a/hosts/types/nix-wsl.nix b/hosts/types/nix-wsl.nix index 081bae3..fff1004 100644 --- a/hosts/types/nix-wsl.nix +++ b/hosts/types/nix-wsl.nix @@ -19,7 +19,8 @@ config = { wsl.enable = true; - wsl.defaultUser = config.ugaif.host.wsl.user; + wsl.defaultUser = + if config.ugaif.forUser != null then config.ugaif.forUser else config.ugaif.host.wsl.user; # Enable the headless software profile ugaif.sw.enable = lib.mkDefault true; diff --git a/hosts/user-config.nix b/hosts/user-config.nix index dcdb043..197428b 100644 --- a/hosts/user-config.nix +++ b/hosts/user-config.nix @@ -2,6 +2,7 @@ pkgs, config, lib, + inputs, ... }: @@ -100,7 +101,8 @@ in ugaif.users.enabledUsers = [ "root" "engr-ugaif" - ]; + ] + ++ lib.optional (config.ugaif.forUser != null) config.ugaif.forUser; # Generate NixOS users users.users = @@ -131,6 +133,7 @@ in useUserPackages = true; extraSpecialArgs = { osConfig = config; + inherit inputs; }; users = @@ -146,8 +149,9 @@ in isExternal = user.flakeUrl != ""; # Common imports based on flags - commonImports = - lib.optional user.useZshTheme ../sw/theme.nix ++ lib.optional user.useNvimPlugins ../sw/nvim.nix; + commonImports = lib.optional user.useZshTheme ../sw/theme.nix ++ [ + (import ../sw/nvim.nix { inherit user; }) + ]; in if isExternal then { diff --git a/installer/modules.nix b/installer/modules.nix index cb304d1..6331f00 100644 --- a/installer/modules.nix +++ b/installer/modules.nix @@ -1,14 +1,14 @@ # ============================================================================ # NixOS Modules Export # ============================================================================ -# This file exposes host types and software configurations as reusable NixOS +# This file exposes host types and software configurations as reusable NixOS # modules that can be imported by external flakes or configurations. # # Usage in another flake: # # Full host type configurations (includes hardware + software + system config) # inputs.nixos-systems.nixosModules.nix-desktop # inputs.nixos-systems.nixosModules.nix-laptop -# +# # # Software-only configurations (for custom hardware setups) # inputs.nixos-systems.nixosModules.sw-desktop # inputs.nixos-systems.nixosModules.sw-headless @@ -16,15 +16,30 @@ { inputs }: let # Software modules with their dependencies bundled - mkSwModule = swType: { config, lib, pkgs, ... }: { - imports = [ - ../sw/ghostty.nix - ../sw/nvim.nix - ../sw/python.nix - ../sw/theme.nix - (import ../sw/${swType} { inherit config lib pkgs inputs; }) - ]; - }; + mkSwModule = + swType: + { + config, + lib, + pkgs, + ... + }: + { + imports = [ + ../sw/ghostty.nix + ../sw/nvim.nix + ../sw/python.nix + ../sw/theme.nix + (import ../sw/${swType} { + inherit + config + lib + pkgs + inputs + ; + }) + ]; + }; in { # Host type modules (full system configurations) diff --git a/inventory.nix b/inventory.nix index 370f028..003c368 100644 --- a/inventory.nix +++ b/inventory.nix @@ -2,77 +2,105 @@ # ============================================================================ # Fleet Inventory # ============================================================================ - # This file defines the types of hosts and their counts. It is used by - # hosts/default.nix to generate the full set of NixOS configurations. + # Top-level keys are ALWAYS hostname prefixes. Actual hostnames are generated + # from the devices map or count. # - # Structure: - # = { - # count = ; # Number of hosts to generate (e.g., nix-laptop1, nix-laptop2) - # devices = { # Per-device overrides - # "" = { - # extraUsers = [ ... ]; # Users enabled on this specific device - # flakeUrl = "..."; # Optional external system flake for full override - # ... # Other hardware/filesystem overrides + # Hostname generation rules: + # - Numeric suffixes: no dash (e.g., "nix-surface1", "nix-surface2") + # - Non-numeric suffixes: add dash (e.g., "nix-surface-alpha", "nix-surface-beta") + # - Set ugaif.host.useHostPrefix = false to use suffix as full hostname + # + # Format: + # "prefix" = { + # type = "nix-desktop"; # Optional: defaults to prefix name + # system = "x86_64-linux"; # Optional: default is x86_64-linux + # + # # Option 1: Simple count (quick syntax) + # devices = 5; # Creates: prefix1, prefix2, ..., prefix5 + # + # # Option 2: Explicit count + # count = 5; # Creates: prefix1, prefix2, ..., prefix5 + # + # # Option 3: Default count (for groups with mixed devices) + # defaultCount = 3; # Creates default numbered hosts + # + # # Option 4: Named device configurations + # devices = { + # "1" = { ... }; # Creates: prefix1 + # "alpha" = { ... }; # Creates: prefix-alpha + # "custom" = { # Creates: custom (no prefix) + # ugaif.host.useHostPrefix = false; # }; # }; + # + # # Common config for all devices in this group + # overrides = { + # extraUsers = [ "user1" ]; # Applied to all devices in this group + # # ... any other config + # }; # }; - - # Laptop Configuration - # Base specs: NVMe drive, 34G Swap + # + # Convenience options: + # ugaif.forUser = "username"; # Automatically adds user to extraUsers and sets wslUser for WSL + # + # Examples: + # "lab" = { devices = 3; }; # Quick: lab1, lab2, lab3 + # "lab" = { count = 3; }; # Same as above + # "kiosk" = { + # defaultCount = 2; # kiosk1, kiosk2 (default) + # devices."special" = {}; # kiosk-special (custom) + # }; + # "laptop" = { + # devices = 5; + # overrides.extraUsers = [ "student" ]; # All 5 laptops get this user + # }; + # "wsl" = { + # devices."alice".ugaif.forUser = "alice123"; # Sets up for user alice123 + # }; # ========== Lab Laptops ========== + # Creates: nix-laptop1, nix-laptop2 + # Both get hdh20267 user via overrides nix-laptop = { - count = 2; - devices = { - # Override example: - # "2" = { swapSize = "64G"; }; - - # Enable specific users for this device index - "1" = { - extraUsers = [ "hdh20267" ]; - }; - "2" = { - extraUsers = [ "hdh20267" ]; - }; - - # Example of using an external flake for system configuration: - # "2" = { flakeUrl = "github:user/system-flake"; }; - }; + devices = 2; + overrides.extraUsers = [ "hdh20267" ]; }; - # Desktop Configuration - # Base specs: NVMe drive, 16G Swap - nix-desktop.count = 1; + # ========== Desktop ========== + # Creates: nix-desktop1 + nix-desktop = { + devices = 1; + }; - # Surface Tablet Configuration (Kiosk Mode) - # Base specs: eMMC drive, 8G Swap + # ========== Surface Tablets (Kiosk Mode) ========== + # Creates: nix-surface1 (custom), nix-surface2, nix-surface3 (via defaultCount) nix-surface = { - count = 3; + defaultCount = 3; devices = { "1".ugaif.sw.kioskUrl = "https://google.com"; }; + overrides = { + ugaif.sw.kioskUrl = "https://yahoo.com"; + }; }; - # LXC Container Configuration + # ========== LXC Containers ========== + # Creates: nix-builder (without lxc prefix) nix-lxc = { - count = 1; devices = { - "1" = { - hostname = "nix-builder"; + "nix-builder" = { + ugaif.host.useHostPrefix = false; }; }; }; - # WSL Configuration + # ========== WSL Instances ========== + # Creates: nix-wsl-alireza nix-wsl = { - count = 1; devices = { - "1" = { - hostname = "nix-wsl-alireza"; - extraUsers = [ "sv22900" ]; - wslUser = "sv22900"; - }; + "alireza".ugaif.forUser = "sv22900"; }; }; - # Ephemeral Configuration (Live ISO / Netboot) - nix-ephemeral.count = 1; + # ========== Ephemeral/Netboot System ========== + # Creates: nix-ephemeral1 + nix-ephemeral.devices = 1; } diff --git a/sw/default.nix b/sw/default.nix index 179ceec..5e26d65 100644 --- a/sw/default.nix +++ b/sw/default.nix @@ -73,7 +73,7 @@ in zsh git oh-my-posh - inputs.lazyvim-nixvim.packages.${stdenv.hostPlatform.system}.nvim + # inputs.lazyvim-nixvim.packages.${stdenv.hostPlatform.system}.nvim inputs.agenix.packages.${stdenv.hostPlatform.system}.default ]; } diff --git a/sw/nvim.nix b/sw/nvim.nix index b798db2..d0cf7b3 100644 --- a/sw/nvim.nix +++ b/sw/nvim.nix @@ -1,4 +1,19 @@ -{ pkgs, ... }: +{ user }: +{ + pkgs, + lib, + inputs, + ... +}: +let + nvimPackages = + if user.useNvimPlugins then + [ + inputs.lazyvim-nixvim.packages.${pkgs.stdenv.hostPlatform.system}.nvim + ] + else + [ pkgs.neovim ]; +in { # ============================================================================ # Neovim Configuration @@ -6,8 +21,10 @@ # This module configures Neovim, specifically setting up TreeSitter parsers # to ensure syntax highlighting works correctly. + home.packages = nvimPackages; + # https://github.com/nvim-treesitter/nvim-treesitter#i-get-query-error-invalid-node-type-at-position - xdg.configFile."nvim/parser".source = + xdg.configFile."nvim/parser".source = lib.mkIf user.useNvimPlugins ( let parsers = pkgs.symlinkJoin { name = "treesitter-parsers"; @@ -20,5 +37,6 @@ )).dependencies; }; in - "${parsers}/parser"; + "${parsers}/parser" + ); }