From c05cc888435d70c525f826e2f373473eaf414658 Mon Sep 17 00:00:00 2001 From: oqyude Date: Thu, 1 Oct 2026 15:14:37 +0300 Subject: [PATCH] restructuring --- configurations/default.nix | 4 +- lib/xlib.nix | 262 ----------------------------------- lib/xlib/default.nix | 66 +++++++++ lib/xlib/device.nix | 65 +++++++++ lib/xlib/dirs.nix | 34 +++++ lib/xlib/helpers.nix | 156 +++++++++++++++++++++ modules/containers/3x-ui.nix | 32 +---- modules/default.nix | 2 + modules/options.nix | 41 ++++++ 9 files changed, 369 insertions(+), 293 deletions(-) delete mode 100644 lib/xlib.nix create mode 100644 lib/xlib/default.nix create mode 100644 lib/xlib/device.nix create mode 100644 lib/xlib/dirs.nix create mode 100644 lib/xlib/helpers.nix create mode 100644 modules/options.nix diff --git a/configurations/default.nix b/configurations/default.nix index 258b6f8..525c95b 100644 --- a/configurations/default.nix +++ b/configurations/default.nix @@ -2,13 +2,13 @@ let lib = inputs.nixpkgs.lib; mkSystem = import ../lib/mkSystem.nix flakeContext; - xlibLib = import ../lib/xlib.nix { inherit lib; }; + xlibLib = import ../lib/xlib { inherit lib; }; # One record per host. The attribute name IS the hostname, so it is written # exactly once; `hostname` is only needed where the attribute name is not # the real hostname (the `default` entry). # - # device device type, must be a key of `devices` in lib/xlib.nix + # device device type, must be a key of `devices` in lib/xlib/device.nix # modules module body for this host hosts = { default = { diff --git a/lib/xlib.nix b/lib/xlib.nix deleted file mode 100644 index 9efb020..0000000 --- a/lib/xlib.nix +++ /dev/null @@ -1,262 +0,0 @@ -{ - lib, - ... -}: -# Pure host library: no module system involved. -# -# `mkXlib` derives everything a host needs to know about itself (identity, -# well-known paths, capability flags, shared helpers) from a single record. -# It is built in flake-level code (configurations/default.nix) and handed to -# every module as the `xlib` argument via lib/mkSystem.nix, so modules read -# plain `xlib.*` values instead of `config.xlib.*` and nothing in xlib can be -# overridden per host — the host record is the only place to change it. -let - # Every supported device type and its capabilities. Single source of truth: - # replaces the old `lib.types.enum` in modules/options.nix and the - # hand-written type lists in modules/default.nix and home/home.nix. - devices = { - minimal = { - desktop = false; - headless = false; - }; - primary = { - desktop = true; - headless = false; - }; - secondary = { - desktop = true; - headless = false; - }; - server = { - desktop = false; - headless = true; - }; - vds = { - desktop = false; - headless = true; - }; - wsl = { - desktop = false; - headless = true; - }; - termux = { - desktop = false; - headless = true; - }; - }; - - # tmpfiles rule: "type dir mode user group -" - mkTmpfile = - type: dir: mode: user: group: - "${type} ${dir} ${mode} ${user} ${group} -"; - - # several tmpfiles types for the same dir, e.g. ["d" "z"] or ["d" "Z"] - mkTmpDirs = - { - dir, - mode, - user, - group, - types ? [ - "d" - "z" - ], - }: - map (type: mkTmpfile type dir mode user group) types; - - # fileSystems bind mount - mkBindMount = - { - what, - where, - }: - { - "${where}" = { - device = what; - fsType = "none"; - options = [ - "bind" - "nofail" - ]; - }; - }; - - # systemd.mounts bind mount (automount variant) - mkSystemdBind = - { - what, - where, - }: - { - enable = true; - options = "bind,x-systemd.automount,nofail"; - requires = [ "local-fs.target" ]; - type = "none"; - wantedBy = [ "multi-user.target" ]; - inherit what where; - }; - - # Full "service storage" block: services-mnt source dir + /var/lib target, - # tmpfiles d/z + automount bind. Used as: - # storage = xlib.helpers.mkServiceStorage { name = "x"; user = "x"; group = "x"; }; - # systemd = storage.systemd; - mkServiceStorage = - { - name, - user, - group, - mode ? "0755", - target ? "/var/lib/${name}", - base ? "/mnt/services", - }: - let - sourceDir = "${base}/${name}"; - in - { - inherit sourceDir target; - systemd = { - tmpfiles.rules = mkTmpDirs { - dir = sourceDir; - inherit mode user group; - }; - mounts = [ - (mkSystemdBind { - what = sourceDir; - where = target; - }) - ]; - }; - }; - - # ntfs3 mount, e.g. fileSystems = mkNtfsMount { path = ...; uuid = ...; } - mkNtfsMount = - { - path, - uuid, - mask ? "0007", - enable ? null, - }: - { - "${path}" = { - device = "/dev/disk/by-uuid/${uuid}"; - fsType = "ntfs3"; - options = [ - "defaults" - "uid=1000" - "gid=1000" - "fmask=${mask}" - "dmask=${mask}" - "nofail" - ]; - } - // lib.optionalAttrs (enable != null) { inherit enable; }; - }; - - # exfat mount, e.g. fileSystems = mkExfatMount { path = ...; uuid = ...; } - mkExfatMount = - { - path, - uuid ? null, - label ? null, - }: - { - "${path}" = { - device = if uuid != null then "/dev/disk/by-uuid/${uuid}" else "/dev/disk/by-label/${label}"; - fsType = "exfat"; - options = [ - "nofail" - "uid=1000" - "gid=1000" - ]; - }; - }; - - # home-manager out-of-store symlinks: path = source (target name = attr name) - mkSymlinks = - config: paths: - lib.mapAttrs' (sourcePath: targetPath: { - name = targetPath; - value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}"; - }) paths; - - helpers = { - inherit - mkTmpfile - mkTmpDirs - mkBindMount - mkSystemdBind - mkServiceStorage - mkNtfsMount - mkExfatMount - mkSymlinks - ; - }; - - # Well-known paths. Everything derives from `username`, which is why the - # whole set can be computed outside the module system. - mkDirs = - username: - let - user-home = "/home/${username}"; - wsl-home = "/mnt/c/Users/${username}"; - server-home = "${user-home}/External"; - services-mnt-folder = "/mnt/services"; - in - { - inherit - user-home - wsl-home - server-home - services-mnt-folder - ; - - user-storage = "${user-home}/Storage"; - wsl-storage = "${wsl-home}/Storage"; - server-credentials = "${server-home}/Credentials/server"; - storage = "${server-home}/Storage"; - calibre-library = "${server-home}/Books-Library"; - services-folder = "${server-home}/Services"; - services-nodes-folder = "${services-mnt-folder}/nodes"; - postgresql-folder = "${services-mnt-folder}/postgresql"; - music-library = "${user-home}/Music"; - - archive-drive = "/mnt/archive"; - lamet-drive = "/mnt/lamet"; - mobile-drive = "/mnt/mobile"; - therima-drive = "/mnt/therima"; - vetymae-drive = "/mnt/vetymae"; - soptur-drive = "/mnt/soptur"; - }; - - mkXlib = - { - hostname, - type, - username ? "oqyude", - }: - let - # Unknown device type fails here, at flake level, with the valid list. - capabilities = devices.${type} or (throw "xlib: unknown device type '${type}', expected one of ${lib.concatStringsSep ", " (builtins.attrNames devices)}"); - in - { - device = { - inherit - hostname - type - username - ; - }; - isDesktop = capabilities.desktop; - isHeadless = capabilities.headless; - dirs = mkDirs username; - inherit helpers; - }; -in -{ - inherit - devices - helpers - mkDirs - mkXlib - ; -} diff --git a/lib/xlib/default.nix b/lib/xlib/default.nix new file mode 100644 index 0000000..e7bf951 --- /dev/null +++ b/lib/xlib/default.nix @@ -0,0 +1,66 @@ +# Pure host library: no module system involved. +# +# Aggregates the four concerns a host record is built from: +# device.nix identity + capability flags from the device type +# dirs.nix well-known paths, derived from username +# helpers.nix pure helper functions shared by modules +# +# `mkXlib` is called in flake-level code (configurations/default.nix) and +# handed to every module as the `xlib` argument via lib/mkSystem.nix, so +# modules read plain `xlib.*` values instead of `config.xlib.*` and nothing in +# xlib can be overridden per host — the host record is the only place to +# change it. +{ + lib, + ... +}: +let + inherit (import ./device.nix { inherit lib; }) + devices + mkDevice + ; + + # dirs.nix is itself a function of `username`, not an attrset. + mkDirs = import ./dirs.nix; + + helpers = (import ./helpers.nix { inherit lib; }); +in +{ + inherit + devices + helpers + mkDevice + mkDirs + ; + + # Full host record: identity + capability flags + well-known paths + + # shared helpers. + mkXlib = + { + hostname, + type, + username ? "oqyude", + }: + let + device = mkDevice { + inherit + hostname + type + username + ; + }; + in + { + device = { + inherit + hostname + type + username + ; + }; + isDesktop = device.isDesktop; + isHeadless = device.isHeadless; + dirs = mkDirs username; + inherit helpers; + }; +} \ No newline at end of file diff --git a/lib/xlib/device.nix b/lib/xlib/device.nix new file mode 100644 index 0000000..a49b465 --- /dev/null +++ b/lib/xlib/device.nix @@ -0,0 +1,65 @@ +{ + lib, + ... +}: +# Supported device types and the identity record built from one. +# +# Single source of truth for host identity: hostname, type, username and the +# capability flags derived from the type. Replaces the old `lib.types.enum` +# in modules/options.nix and the hand-written type lists in modules/default.nix +# and home/home.nix. +let + devices = { + minimal = { + desktop = false; + headless = false; + }; + primary = { + desktop = true; + headless = false; + }; + secondary = { + desktop = true; + headless = false; + }; + server = { + desktop = false; + headless = true; + }; + vds = { + desktop = false; + headless = true; + }; + wsl = { + desktop = false; + headless = true; + }; + termux = { + desktop = false; + headless = true; + }; + }; +in +{ + inherit devices; + + # Unknown device type fails here, at flake level, with the valid list. + mkDevice = + { + hostname, + type, + username ? "oqyude", + }: + let + capabilities = devices.${type} or (throw "xlib: unknown device type '${type}', expected one of ${lib.concatStringsSep ", " (builtins.attrNames devices)}"); + in + { + inherit + hostname + type + username + ; + isDesktop = capabilities.desktop; + isHeadless = capabilities.headless; + }; +} \ No newline at end of file diff --git a/lib/xlib/dirs.nix b/lib/xlib/dirs.nix new file mode 100644 index 0000000..f5894b0 --- /dev/null +++ b/lib/xlib/dirs.nix @@ -0,0 +1,34 @@ +# Well-known paths. Everything derives from `username`, which is why the +# whole set can be computed outside the module system. +username: +let + user-home = "/home/${username}"; + wsl-home = "/mnt/c/Users/${username}"; + server-home = "${user-home}/External"; + services-mnt-folder = "/mnt/services"; +in +{ + inherit + user-home + wsl-home + server-home + services-mnt-folder + ; + + user-storage = "${user-home}/Storage"; + wsl-storage = "${wsl-home}/Storage"; + server-credentials = "${server-home}/Credentials/server"; + storage = "${server-home}/Storage"; + calibre-library = "${server-home}/Books-Library"; + services-folder = "${server-home}/Services"; + services-nodes-folder = "${services-mnt-folder}/nodes"; + postgresql-folder = "${services-mnt-folder}/postgresql"; + music-library = "${user-home}/Music"; + + archive-drive = "/mnt/archive"; + lamet-drive = "/mnt/lamet"; + mobile-drive = "/mnt/mobile"; + therima-drive = "/mnt/therima"; + vetymae-drive = "/mnt/vetymae"; + soptur-drive = "/mnt/soptur"; +} \ No newline at end of file diff --git a/lib/xlib/helpers.nix b/lib/xlib/helpers.nix new file mode 100644 index 0000000..ee126a3 --- /dev/null +++ b/lib/xlib/helpers.nix @@ -0,0 +1,156 @@ +{ + lib, + ... +}: +# Pure helper functions for module definitions. +# Injected into every module via `xlib.helpers` (see default.nix). +# +# Defined in a `let` because they reference each other (mkTmpDirs uses +# mkTmpfile, mkServiceStorage uses mkTmpDirs + mkSystemdBind). +let + # tmpfiles rule: "type dir mode user group -" + mkTmpfile = + type: dir: mode: user: group: + "${type} ${dir} ${mode} ${user} ${group} -"; + + # several tmpfiles types for the same dir, e.g. ["d" "z"] or ["d" "Z"] + mkTmpDirs = + { + dir, + mode, + user, + group, + types ? [ + "d" + "z" + ], + }: + map (type: mkTmpfile type dir mode user group) types; + + # fileSystems bind mount + mkBindMount = + { + what, + where, + }: + { + "${where}" = { + device = what; + fsType = "none"; + options = [ + "bind" + "nofail" + ]; + }; + }; + + # systemd.mounts bind mount (automount variant) + mkSystemdBind = + { + what, + where, + }: + { + enable = true; + options = "bind,x-systemd.automount,nofail"; + requires = [ "local-fs.target" ]; + type = "none"; + wantedBy = [ "multi-user.target" ]; + inherit what where; + }; + + # Full "service storage" block: services-mnt source dir + /var/lib target, + # tmpfiles d/z + automount bind. Used as: + # storage = xlib.helpers.mkServiceStorage { name = "x"; user = "x"; group = "x"; }; + # systemd = storage.systemd; + mkServiceStorage = + { + name, + user, + group, + mode ? "0755", + target ? "/var/lib/${name}", + base ? "/mnt/services", + }: + let + sourceDir = "${base}/${name}"; + in + { + inherit sourceDir target; + systemd = { + tmpfiles.rules = mkTmpDirs { + dir = sourceDir; + inherit mode user group; + }; + mounts = [ + (mkSystemdBind { + what = sourceDir; + where = target; + }) + ]; + }; + }; + + # ntfs3 mount, e.g. fileSystems = mkNtfsMount { path = ...; uuid = ...; } + mkNtfsMount = + { + path, + uuid, + mask ? "0007", + enable ? null, + }: + { + "${path}" = { + device = "/dev/disk/by-uuid/${uuid}"; + fsType = "ntfs3"; + options = [ + "defaults" + "uid=1000" + "gid=1000" + "fmask=${mask}" + "dmask=${mask}" + "nofail" + ]; + } + // lib.optionalAttrs (enable != null) { inherit enable; }; + }; + + # exfat mount, e.g. fileSystems = mkExfatMount { path = ...; uuid = ...; } + mkExfatMount = + { + path, + uuid ? null, + label ? null, + }: + { + "${path}" = { + device = if uuid != null then "/dev/disk/by-uuid/${uuid}" else "/dev/disk/by-label/${label}"; + fsType = "exfat"; + options = [ + "nofail" + "uid=1000" + "gid=1000" + ]; + }; + }; + + # home-manager out-of-store symlinks: path = source (target name = attr name) + mkSymlinks = + config: paths: + lib.mapAttrs' (sourcePath: targetPath: { + name = targetPath; + value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}"; + }) paths; +in +{ + inherit + mkTmpfile + mkTmpDirs + mkBindMount + mkSystemdBind + mkServiceStorage + mkNtfsMount + mkExfatMount + mkSymlinks + ; +} \ No newline at end of file diff --git a/modules/containers/3x-ui.nix b/modules/containers/3x-ui.nix index 20dc7bd..195a0b9 100644 --- a/modules/containers/3x-ui.nix +++ b/modules/containers/3x-ui.nix @@ -35,35 +35,9 @@ let realityPorts = lib.optional config.host."3x-ui".reality443Forwarding "127.0.0.1:15380:443/tcp"; in { - options.host."3x-ui" = { - # Domain whose LE cert should be mounted into the 3x-ui container at - # /root/cert/fullchain.pem and key.pem. Set null if 3x-ui serves plain - # HTTP and TLS is terminated by an upstream nginx. - certDomain = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "pubray1.zeroq.su"; - description = '' - Domain whose LE cert should be mounted into the 3x-ui - container at /root/cert/fullchain.pem and key.pem. - ''; - }; - # Publish host:15380 → container:443. Only nodes that host an Xray - # REALITY inbound on container:443 need this (so nginx stream can - # forward TLS to Xray via 127.0.0.1:15380 while Xray itself sees - # incoming connections on its configured port 443). Set false on nodes - # that only run the 3x-ui panel. - reality443Forwarding = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - When true, publish host:15380 → container:443 so Xray - inside the container can serve REALITY on its real - configured port 443 (nginx stream forwards 443 → 15380). - ''; - }; - }; - + # `host."3x-ui"` options are declared in modules/options.nix: they are set + # by modules/server and modules/vds, so this module cannot be the only place + # that knows they exist. config = { virtualisation = { podman = { diff --git a/modules/default.nix b/modules/default.nix index a39b866..5df98ef 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -17,6 +17,7 @@ let with inputs; [ ./essentials + ./options.nix ./users.nix home-manager.nixosModules.home-manager # home-manager module @@ -45,6 +46,7 @@ let imports = [ # ./essentials # ./users.nix + ./options.nix (./. + "/${xlib.device.type}") # sops-nix.nixosModules.sops ]; diff --git a/modules/options.nix b/modules/options.nix new file mode 100644 index 0000000..54ef581 --- /dev/null +++ b/modules/options.nix @@ -0,0 +1,41 @@ +{ + lib, + ... +}: +# Cross-module options: declared here, not in the module that reads them. +# +# An option belongs in this file when at least one context *sets* it while +# another module *reads* it — the reader cannot be the only place that knows +# the option exists. `modules/essentials/ssh.nix` does not belong here: it +# declares and reads `host.ssh.enable` itself, within one module. +{ + options.host."3x-ui" = { + # Domain whose Let's Encrypt cert (at /var/lib/acme//) + # gets mounted read-only into the 3x-ui container so the panel + # can terminate TLS itself. Set null if 3x-ui serves plain HTTP + # and TLS is terminated by an upstream nginx. + certDomain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "pubray1.zeroq.su"; + description = '' + Domain whose LE cert should be mounted into the 3x-ui + container at /root/cert/fullchain.pem and key.pem. + ''; + }; + # Publish host:15380 → container:443. Only nodes that host an + # Xray REALITY inbound on container:443 need this (so nginx + # stream can forward TLS to Xray via 127.0.0.1:15380 while Xray + # itself sees incoming connections on its configured port 443). + # Set false on nodes that only run the 3x-ui panel. + reality443Forwarding = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + When true, publish host:15380 → container:443 so Xray + inside the container can serve REALITY on its real + configured port 443 (nginx stream forwards 443 → 15380). + ''; + }; + }; +} \ No newline at end of file