Skip to content

Instantly share code, notes, and snippets.

@sini
Last active June 25, 2026 22:14
Show Gist options
  • Select an option

  • Save sini/fb09063f2b304a416fa1f6bcb0600099 to your computer and use it in GitHub Desktop.

Select an option

Save sini/fb09063f2b304a416fa1f6bcb0600099 to your computer and use it in GitHub Desktop.
Den में Per-Aspect Settings — शुरुआती लोगों के लिए आसान हिंदी गाइड (Hindi beginner's guide to the host/aspect settings pattern in Den)

Den में Per-Aspect Settings — शुरुआती लोगों के लिए आसान हिंदी गाइड (Hindi beginner's guide to the host/aspect settings pattern in Den)

Den में Per-Aspect Settings — शुरुआती लोगों के लिए आसान गाइड

नमस्ते! 👋 यह गाइड आपको एक बहुत उपयोगी pattern सिखाएगी जो Den नाम के एक Nix framework में इस्तेमाल होती है। चिंता मत कीजिए अगर आप Nix में नए हैं — हम हर चीज़ को धीरे-धीरे, आसान भाषा में, उदाहरणों के साथ समझाएँगे।

किसके लिए है यह गाइड? उन लोगों के लिए जो Computer Science पढ़ रहे हैं और Nix/NixOS के बारे में थोड़ा-बहुत जानते हैं, पर अभी expert नहीं हैं। अगर कुछ शब्द नए लगें, तो घबराइए मत — सबसे नीचे एक शब्दावली (glossary) दी गई है जहाँ हर technical शब्द का मतलब समझाया गया है।

एक छोटा सा नोट code के बारे में: इस गाइड में सारा code (program) अंग्रेज़ी में ही रहेगा, क्योंकि programming की भाषा अंग्रेज़ी में लिखी जाती है। लेकिन code के अंदर के # comments (टिप्पणियाँ) हमने हिंदी में लिख दी हैं ताकि आपको समझने में आसानी हो।


भाग 0 — पहले कुछ बुनियादी बातें

अगर आप ये चीज़ें पहले से जानते हैं, तो इस भाग को छोड़ सकते हैं। पर अगर नहीं जानते, तो इन्हें पढ़ लेना आगे बहुत काम आएगा।

Nix और NixOS क्या है?

सोचिए आप अपने कंप्यूटर को एक खास तरीके से set up करना चाहते हैं — कौन-कौन से programs install हों, कौन सी settings हों, वगैरह। आम तौर पर आप एक-एक करके सब manually करते हैं, और बाद में याद रखना मुश्किल हो जाता है कि आपने क्या-क्या बदला था।

Nix एक ऐसा tool है जिसमें आप अपने पूरे system को एक text file में लिख देते हैं — जैसे एक recipe (विधि)। फिर Nix उस recipe को पढ़कर ठीक वैसा ही system बना देता है। इसका फायदा: आपकी पूरी setup एक file में, हमेशा एक जैसी, और दूसरों के साथ share करने लायक।

इस तरह की approach को declarative कहते हैं — यानी आप बताते हैं कि "मुझे क्या चाहिए" (result), न कि "इसे कैसे step-by-step करना है"। NixOS एक पूरा Linux operating system है जो इसी Nix के ऊपर बना है।

Attribute set (attrset) क्या है?

Nix में डेटा रखने का सबसे आम तरीका है attribute set, जिसे छोटे में attrset कहते हैं। अगर आपने Python की dictionary या JavaScript का object देखा है, तो यह वैसा ही है — एक नाम (key) के सामने एक value:

{
  name = "cortex";
  cores = 16;
  enabled = true;
}

attrsets एक-दूसरे के अंदर भी रह सकते हैं (nested), और तब हम dot (.) से अंदर वाली value तक पहुँचते हैं। ये दोनों लिखावटें एक ही चीज़ हैं:

# तरीका 1: nested
{ disk = { device = "/dev/sda"; }; }

# तरीका 2: dotted (छोटा रूप) — बिल्कुल वही मतलब
{ disk.device = "/dev/sda"; }

Module, option और type क्या है?

NixOS एक चीज़ का इस्तेमाल करता है जिसे module system कहते हैं। इसे ऐसे समझिए:

  • एक option एक "खाली जगह" (slot) है जिसे कोई value से भरा जा सकता है — जैसे एक form में एक खाना। हर option का एक type होता है — यानी उसमें किस तरह की value डल सकती है (number, true/false, text, list, वगैरह)। हम option को lib.mkOption से बनाते हैं।
  • एक module बस एक attrset है जिसमें या तो नई options बनाई जाती हैं (options = {...}) या मौजूद options में values डाली जाती हैं (config = {...})।

एक छोटा उदाहरण:

{
  options = {
    # "workers" नाम की एक option बना रहे हैं, जिसमें सिर्फ़ number डल सकता है,
    # और अगर कोई value न दे तो default 4 होगा।
    workers = lib.mkOption {
      type = lib.types.int;       # type = पूर्णांक (integer)
      default = 4;                # default value
      description = "कितने worker चलाने हैं";
    };
  };
}

module system का सबसे बढ़िया फायदा: अगर आप गलत type की value डालेंगे (जैसे number की जगह text), तो आपको build के समय ही error मिल जाएगा, program चलाने से पहले। यानी गलतियाँ जल्दी पकड़ी जाती हैं।

Den और "aspect" क्या है?

Den एक framework है जो बड़ी-बड़ी NixOS setups को manage करना आसान बनाता है — खासकर जब आपके पास कई कंप्यूटर हों (एक "fleet")।

Den में सबसे ज़रूरी idea है aspect। एक aspect एक छोटा, दोबारा इस्तेमाल होने वाला टुकड़ा है। इसे pizza के topping की तरह सोचिए 🍕 — हर pizza (कंप्यूटर) पर आप अलग-अलग toppings (aspects) डाल सकते हैं:

  • एक aspect "gaming" set up करता है,
  • एक aspect ZFS नाम की disk layout लगाता है,
  • एक aspect एक खास kernel चुनता है।

जब आप एक कंप्यूटर बनाते हैं, तो आप बस उसमें ज़रूरी aspects को includes कर देते हैं, और वे सब अपने-आप जुड़ जाते हैं।

"Host" क्या है?

Den में एक host का मतलब है एक कंप्यूटर/मशीन — जैसे आपका laptop, या एक server। हर host अपने ज़रूरी aspects चुनता है।

अब हम तैयार हैं! आगे की पूरी गाइड इन्हीं ideas पर टिकी है।


असली समस्या क्या है?

aspects को कई कंप्यूटरों में share किया जाता है। ज़्यादातर हिस्सा हर जगह एक जैसा रहता है। लेकिन कुछ values हर मशीन पर अलग होती हैं। उदाहरण:

  • ZFS disk वाले aspect को disk का पता (device id) चाहिए — हर मशीन पर अलग।
  • kernel वाले aspect को CPU optimization चाहिए — किसी पर zen4, किसी पर server
  • BGP (एक networking चीज़) वाले aspect को इस मशीन का AS number चाहिए।

अब सवाल: ये अलग-अलग values aspect में कैसे डालें, बिना aspect को हर मशीन के लिए copy किए?

गलत तरीके (इन्हें मत कीजिए):

  • value को सीधे aspect में hard-code कर देना → तब वो aspect दोबारा इस्तेमाल नहीं हो सकता।
  • हर aspect के लिए host में एक अलग option बना देना → host की settings बेतहाशा बढ़ जाएँगी।
  • value को किसी global जगह से पढ़ना → typing और साफ़-सफ़ाई दोनों खो जाती हैं।

सही तरीका — "settings" pattern: हर aspect अपने लिए कुछ typed "knobs" (घुंडियाँ, यानी adjustable values) declare करता है। फिर host उन knobs को भरता है। बस इतना ही। आगे हम इसी को सीखेंगे।


चार मुख्य हिस्से (पूरी तस्वीर)

यह pattern 4 हिस्सों में काम करती है। पहले पूरी तस्वीर देख लीजिए — घबराइए मत, नीचे हर हिस्से को अलग से समझाएँगे:

┌─ 1. Aspect अपनी settings DECLARE करता है ──────────────────────────────┐
│   den.aspects.core.system.linux-kernel = {                             │
│     settings = { optimization = lib.mkOption { ... }; };               │
│   };                                                                    │
└────────────────────────────────────────────────────────────────────────┘
                              │  (अपने-आप खोज लिया जाता है)
                              ▼
┌─ 2. Host schema typed namespace खुद-ब-खुद GENERATE करता है ────────────┐
│   आपका host schema den.aspects को scan करता है और एक typed option      │
│   बना देता है:                                                          │
│     host.settings.core.system.linux-kernel.optimization                 │
└────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─ 3. Host value SET करता है ────────────────────────────────────────────┐
│   den.hosts.x86_64-linux.cortex.settings = {                           │
│     core.system.linux-kernel.optimization = "zen4";                    │
│   };                                                                    │
└────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─ 4. Aspect उस value को CONSUME (इस्तेमाल) करता है ──────────────────────┐
│   nixos = { host, pkgs, ... }:                                         │
│     let cfg = host.settings.core.system.linux-kernel; in { ... };      │
└────────────────────────────────────────────────────────────────────────┘

सबसे ज़रूरी बात समझिए: हिस्सा 1 और 4 aspect के अंदर रहते हैं, हिस्सा 3 host के अंदर, और हिस्सा 2 अपने-आप होता है। यानी aspect में नई setting जोड़ने पर आपको host schema को छूना नहीं पड़ता। हिस्सा 2 (generator) आप ज़िंदगी में सिर्फ़ एक बार लिखते हैं।


भाग 1 — आसान वाला तरीका (module-system pattern)

एक पूरा उदाहरण, शुरू से अंत तक

सबसे छोटा aspect जो यह पूरी pattern दिखाता है, वह है एक kernel चुनने वाला aspect। यहाँ declare करना (हिस्सा 1) और इस्तेमाल करना (हिस्सा 4), दोनों एक ही file में हैं:

{ lib, ... }:
{
  den.aspects.core.system.linux-kernel = {
    # (1) DECLARE: दो typed knobs, दोनों के default दिए हुए हैं।
    settings = {
      channel = lib.mkOption {
        type = lib.types.enum [ "lts" "latest" ];   # सिर्फ़ इन दो में से एक
        default = "latest";
        description = "CachyOS kernel का release channel";
      };
      optimization = lib.mkOption {
        type = lib.types.enum [ "server" "zen4" "x86_64-v4" ];
        default = "server";
        description = "CachyOS kernel किस CPU के लिए optimize हो";
      };
    };

    # (4) CONSUME: यह module `host` को argument के रूप में पाता है, और अपनी ही
    #     settings को host.settings.<अपना रास्ता> से वापस पढ़ लेता है।
    nixos =
      { host, pkgs, ... }:
      let
        # cfg = एक छोटा नाम, ताकि बार-बार पूरा रास्ता न लिखना पड़े
        cfg = host.settings.core.system.linux-kernel;
        kernelName =
          if cfg.optimization == "server" then
            "linuxPackages-cachyos-server-lto"
          else
            "linuxPackages-cachyos-${cfg.channel}-lto-${cfg.optimization}";
      in
      {
        boot.kernelPackages = pkgs.cachyosKernels.${kernelName};
      };
  };
}

और एक host सिर्फ़ उस एक knob को भरता है जिसकी उसे परवाह है। channel को वो छूता ही नहीं, तो वह अपने-आप अपने default ("latest") पर रहेगा:

den.hosts.x86_64-linux.cortex.settings = {
  core.system.linux-kernel.optimization = "zen4";
};

बस! यही पूरी कहानी है। host को aspect का type import नहीं करना पड़ा, कोई special argument नहीं बताना पड़ा, कुछ भी जोड़ना नहीं पड़ा। उसने बस settings.core.system.linux-kernel.optimization लिखा, और value उसी रास्ते से aspect के अंदर पहुँच गई। यह "जादू" हिस्सा 2 की वजह से होता है — जो हम आगे देखेंगे।

Step 1 — aspect पर settings declare करना

अपने aspect में एक settings attribute जोड़िए — includes, nixos, और homeManager के बगल में। इसके अंदर बस lib.mkOption declarations होती हैं:

den.aspects.disk.zfs-disk-single = {
  includes = [ den.aspects.disk.zfs-disk-single.root ];

  settings = {
    device_id = lib.mkOption {
      type = lib.types.str;   # str = text (string)
      description = "ZFS pool के लिए disk का रास्ता (जैसे /dev/disk/by-id/nvme-...)";
    };
  };

  nixos = { config, host, ... }: {
    # ... यहाँ host.settings.disk.zfs-disk-single.device_id इस्तेमाल होता है
  };
};

दो ज़रूरी नियम:

  • settings में सिर्फ़ option declarations रखिए, config नहीं। यानी यहाँ सिर्फ़ mkOption लिखिए, value-assignment नहीं। (अगर आपको सच में कोई default config भी डालनी हो, तो नीचे "module-shaped" तरीका देखिए।)
  • default न देने का मतलब है setting "ज़रूरी (required)" है। ऊपर device_id का कोई default नहीं है, इसलिए जो भी host zfs-disk-single को include करेगा, उसे यह value देनी ही पड़ेगी, वरना build fail हो जाएगा। यह अच्छी बात है — type system आपको ज़बरदस्ती याद दिला देता है कि "भई, disk कौन सी है यह तो बताओ।"

Required vs. Default — फ़र्क समझिए

लिखावट मतलब
mkOption { type = ...; } (default नहीं) ज़रूरी। host को देना पड़ेगा; न दिया तो error।
mkOption { type = ...; default = x; } वैकल्पिक। host कुछ न कहे तो अपने-आप x इस्तेमाल होगा।

सलाह: जिस value का कोई अच्छा "सबके लिए एक जैसा" default हो सकता है, उसे default दे दीजिए। "required" सिर्फ़ उन्हीं चीज़ों के लिए रखिए जो वाकई हर मशीन पर अलग होती हैं (जैसे disk id, AS number)।

"Module-shaped" तरीका (थोड़ा advanced — पहली बार में छोड़ सकते हैं)

आम तौर पर settings बस options का एक attrset होता है। पर अगर ज़रूरत पड़े, तो आप इसे एक पूरा module भी बना सकते हैं, जिसमें imports / config / options अलग-अलग लिखे हों — तब aspect न सिर्फ़ options declare कर सकता है, बल्कि कुछ default config भी डाल सकता है:

settings = {
  options = {
    replicas = lib.mkOption { type = lib.types.int; default = 1; };
  };
  config = {
    # `default =` से ज़्यादा complex default यहाँ दे सकते हैं
    replicas = lib.mkDefault 3;
  };
  imports = [ ./extra-settings-module.nix ];
};

generator (हिस्सा 2) सादे attrset को अपने-आप इसी shape में बदल देता है, इसलिए यह लंबा रूप तभी इस्तेमाल कीजिए जब आपको config या imports चाहिए। चूँकि settings को module system चलाता है, इसमें lib.mkDefault / lib.mkForce जैसी priorities भी काम करती हैं (इनके बारे में आगे "किसकी जीत होती है" वाले हिस्से में पढ़िए)।

Step 2 — Generator (यह आप सिर्फ़ एक बार लिखते हैं)

यह वही "जादुई" हिस्सा है जिसकी वजह से ऊपर सब अपने-आप जुड़ता है। यह आपके host schema का एक टुकड़ा है जो "जिन-जिन aspects ने settings declare की हैं" उन सबको इकट्ठा करके एक typed host.settings tree बना देता है। आप इसे एक बार लिखते हैं, फिर हमेशा के लिए हर aspect की settings अपने-आप दिखने लगती हैं।

पहले थोड़ी background: Den में entity schema कैसा दिखता है?

अगर आप Den में नए हैं, तो generator समझने से पहले बस इतना जान लीजिए।

Den हर तरह की चीज़ (entity) — host, environment, user, group — को एक schema से बनाता है, जो den.schema.<नाम> के नीचे declare होता है। यहाँ सबसे ज़रूरी key है den.schema.<नाम>.imports: यह modules की एक list है, और इन modules की options ही उस entity की options बन जाती हैं। एक पूरा, सबसे छोटा उदाहरण देखिए — यह group entity है (थोड़ा छोटा करके दिखाया है), ताकि आपको ढाँचा (skeleton) साफ़ दिखे:

{ lib, ... }:
let
  inherit (lib) mkOption types;
in
{
  den.schema.group.imports = [
    (_: {
      options = {
        gid = mkOption {
          type = types.nullOr types.int;   # या तो number, या कुछ नहीं (null)
          default = null;
          description = "POSIX group ID";
        };
        # ... और भी options ...
      };
    })
  ];
}

बस इतना ही ढाँचा है: एक file जो खुद एक module है, और उसके अंदर den.schema.<नाम>.imports की एक list। Den में हर entity — host भी — ठीक इसी तरह बनती है; host schema बस इसी का बड़ा रूप है।

इस file के arguments (function के inputs) आपके औज़ार हैं:

  • lib — Nix की standard library (mkOption, types, वगैरह इसी से आते हैं)।
  • inputs — आपके flake के inputs (जैसे validators के लिए gen-algebra लाना)।
  • den — सबसे ज़रूरी! यह पूरा framework registry है। den.aspects में सारे aspects का tree है; den.classes में output classes (nixos, homeManager...) हैं; den.quirks में extras। Generator den.aspects को पढ़कर settings खोजता है, और den.classes / den.quirks को देखकर समझता है कि कौन सी keys aspect नहीं हैं (बल्कि framework की अंदरूनी चीज़ें हैं)।

तो host के लिए plan यह है: file के let हिस्से में den.aspects से एक settingsType बनाओ, फिर उसे den.schema.host.imports के अंदर एक और option (settings) की तरह जोड़ दो। अगले दो हिस्से यही दिखाते हैं।

पहले एक ज़रूरी setup step (इसे मत छोड़िए!)

नीचे वाला generator एक helper skipKey से तय करता है कि कौन सी keys असली child aspect हैं और कौन सी framework की अंदरूनी चीज़ें (जिन्हें छोड़ देना है)। यह तभी सही चलेगा जब आप Den को बता दें कि settings एक खास key है, कोई आम aspect नहीं। यह बस एक बार करना होता है, किसी भी module में (जैसे defaults.nix में):

den.reservedKeys = [ "settings" ];

अगर आप यह लाइन भूल गए, तो जब आप किसी host की settings देखेंगे तो ऐसा डरावना error आएगा:

error: stack overflow; max-call-depth exceeded
       at .../host.nix: ... hasSettingsDeep ...

आसान भाषा में वजह: इस लाइन के बिना generator को पता नहीं चलता कि settings खास है, तो वह आपकी settings के "अंदर झाँकने" लगता है — जैसे वह aspects के अंदर झाँकता है। वह हर mkOption में घुसता है, फिर option के type में, और Nix में type अपने-आप को ही loop में point करता है — इसलिए program गोल-गोल घूमता रहता है और जगह ख़त्म हो जाती है (stack overflow)। den.reservedKeys = [ "settings" ]; जोड़ने से generator settings block को छोड़ देता है, और error ग़ायब हो जाता है। तो: सबसे पहले यह एक लाइन ज़रूर जोड़िए।

Generator खुद (the code)

settingsType क्या करता है, एक लाइन में:

aspect tree में जिस-जिस जगह .settings मिले, उसी रास्ते पर host.settings के नीचे एक typed option बना दो। अगर कोई जगह खुद settings भी रखती है और उसके बच्चे भी settings रखते हैं, तो दोनों को जोड़ दो। framework की अंदरूनी keys को छोड़ दो।

यह रहा पूरा code, हिंदी comments के साथ। nix-config में यह modules/den/schema/host.nix में रहता है। (पहली बार में हर लाइन समझने की ज़रूरत नहीं — नीचे हमने तीनों helpers को आसान भाषा में समझाया है।)

settingsType =
  let
    # वे keys जो aspect के बच्चे नहीं हैं: structural keys (includes, nixos...),
    # साथ ही आपके framework की class names और quirk/extension keys।
    # इन तीन स्रोतों को अपने framework के हिसाब से बदलिए।
    inherit (den.lib.aspects.fx.keyClassification) structuralKeysSet;
    classKeys = den.classes or { };
    quirkKeys = den.quirks or { };
    skipKey = k: structuralKeysSet ? ${k} || classKeys ? ${k} || quirkKeys ? ${k};

    # settings या तो सादा options-attrset हो सकती है ({ foo = mkOption {...}; }),
    # या module-shaped ({ imports; config; options; })। दोनों को एक जैसा बना देते हैं।
    reshapeSettings =
      raw:
      let
        # अलग-अलग नाम जान-बूझकर — नीचे "सावधानियाँ" में statix वाली बात देखिए।
        imports' = raw.imports or [ ];
        config' = raw.config or { };
      in
      {
        imports = imports';
        config = config';
        options = removeAttrs raw [ "imports" "config" ];
      };

    # सच (true) अगर इस जगह या इसके नीचे कहीं भी settings declare हुई हैं।
    hasSettingsDeep =
      node:
      builtins.isAttrs node
      && (
        (node ? settings)
        || lib.any (k: !(skipKey k) && hasSettingsDeep (node.${k} or null)) (builtins.attrNames node)
      );

    # aspect-tree की एक जगह (node) के लिए submodule बनाओ, tree की नक़ल करते हुए।
    # इस जगह की अपनी settings + इसके बच्चों की settings, दोनों को जोड़ो।
    nodeModule =
      node:
      let
        ownSettings =
          if node ? settings then
            reshapeSettings node.settings
          else
            { imports = [ ]; config = { }; options = { }; };

        settingChildren = lib.filterAttrs (
          k: v: !(skipKey k) && builtins.isAttrs v && hasSettingsDeep v
        ) node;

        childOptions = lib.mapAttrs (
          name: child:
          mkOption {
            type = types.submodule (nodeModule child);   # यहाँ recursion (खुद को दोबारा बुलाना)
            default = { };
            description = "Settings under ${name}";
          }
        ) settingChildren;

        # फिर से अलग नाम — statix को `or` default हटाने से रोकने के लिए।
        ownImports = ownSettings.imports or [ ];
        ownConfig = ownSettings.config or { };
      in
      {
        imports = ownImports;
        config = ownConfig;
        options = (ownSettings.options or { }) // childOptions;
      };
  in
  types.submodule (nodeModule (den.aspects or { }));   # पूरे aspect-tree से शुरू

तीनों helpers का काम आसान शब्दों में:

  • skipKey — यह अकेला हिस्सा है जो हर framework में अलग होता है। यह उन सब keys के लिए "true" लौटाता है जो aspect के बच्चे नहीं हैं (जैसे includes, nixos, homeManager, आपकी class names, वगैरह)। अगर यह गलत हो, तो generator गलती से host.settings.<aspect>.nixos जैसी बेकार options बना देगा।
  • hasSettingsDeep — यह खाली शाखाओं (branches) को छाँट देता है, ताकि सिर्फ़ उन्हीं रास्तों पर options बनें जहाँ सच में कोई settings है।
  • nodeModule — यह असली recursion है (tree में नीचे-नीचे जाकर नक़ल बनाना)। सबसे ज़रूरी बारीकी: एक जगह खुद की settings और बच्चों की settings, दोनों रख सकती है। यह दोनों को जोड़ देता है, ताकि माँ-aspect के knobs और बच्चों के knobs एक ही रास्ते पर साथ रहें।

Recursion क्या है? जब कोई function अपने अंदर खुद को ही बुलाता है, उसे recursion कहते हैं। यहाँ tree में हर शाखा के लिए nodeModule खुद को फिर से बुलाता है — इसीलिए वह पूरे tree को, चाहे कितना भी गहरा हो, संभाल लेता है।

सब जोड़कर — पूरी file का ढाँचा

अब दोनों हिस्सों को एक file में रखते हैं। Generator let में रहता है; और settings option को den.schema.host.imports के अंदर, host की बाक़ी options के बगल में जोड़ देते हैं। यह रहा पूरा host schema, ढाँचे के रूप में (बाक़ी options जैसे channel आम mkOption हैं, जिन्हें आप ज़रूरत अनुसार जोड़ते हैं):

{ lib, inputs, den, self, ... }:        # ← ध्यान दीजिए: arguments में `den` है
let
  inherit (lib) mkOption types;

  # ... और भी helpers: interfaceType, channel definitions, वगैरह ...

  settingsType =
    let
      # skipKey / reshapeSettings / hasSettingsDeep / nodeModule
      # (ऊपर वाला generator यहाँ आता है)
      # ...
    in
    types.submodule (nodeModule (den.aspects or { }));   # ← aspect tree पढ़ता है
in
{
  den.schema.host.isEntity = true;

  den.schema.host.imports = [
    (
      { config, ... }:
      {
        options = {
          channel = mkOption { /* ... */ };
          environment = mkOption { /* ... */ };
          # ... host की बाक़ी options ...

          # यह रहा अपने-आप बना हुआ settings namespace:
          settings =
            mkOption {
              type = settingsType;
              default = { };
              description = "हर aspect की typed settings";
            }
            # settings को entity की पहचान (identity) से बाहर रखो (नीचे सावधानियाँ देखें)।
            // {
              identity = false;
            };
        };

        # config = { ... };   # बाक़ी options के computed defaults, अगर हों
      }
    )
  ];
}

पहली बार पढ़ते समय बस तीन बातें नोट कीजिए:

  • पूरी file एक ही module-function है जिसके arguments में den है — इसी वजह से settingsType, den.aspects को देख पाता है। अगर आपका framework registry को किसी और नाम से देता है, तो वही इस्तेमाल कीजिए।
  • settings host की बहुत सारी options में से बस एक है। बाक़ी schema को इसके होने का पता भी नहीं — aspects और hosts खुद-ब-खुद इसके ज़रिए जुड़ जाते हैं।
  • // { identity = false; } वाला हिस्सा nix-config की अपनी खास बात है (entity identity hashing)। अगर आपके framework में ऐसा कुछ नहीं है, तो इसे छोड़ दीजिए।

बस इतना ही host-side काम है: एक option, जिसके पीछे एक ~80-लाइन का let, सिर्फ़ एक बार लिखा हुआ। इसके बाद जो भी aspect settings declare करेगा, वह बिना किसी और बदलाव के host.settings के नीचे अपने-आप दिखने लगेगा।

Step 3 — host पर values set करना

host की definition में एक settings attribute लिखिए। values को aspect के रास्ते से address कीजिए। याद रखिए, nested और dotted लिखावट एक ही बात है:

den.hosts.x86_64-linux.cortex = {
  channel = "nixpkgs-master";
  environment = "dev";
  # ...
  settings = {
    disk.zfs-disk-single.device_id =
      "/dev/disk/by-id/nvme-Samsung_SSD_990_PRO_4TB_…";

    core.system.linux-kernel.optimization = "zen4";

    core.impermanence = {
      wipeRootOnBoot = true;
      wipeHomeOnBoot = false;
    };
  };
};

हर value typed है, इसलिए अगर रास्ते में typo हो या गलत type की value हो, तो आपको build के समय ही, ठीक उसी option पर इशारा करते हुए error मिल जाएगा — चुपचाप अनदेखा नहीं होगा। जो host सारे defaults से खुश है, वह settings लिखता ही नहीं।

Step 4 — aspect के अंदर values को इस्तेमाल करना

aspect के delivery modules (nixos, homeManager) असल में functions होते हैं जिन्हें पूरा-resolved host एक argument के रूप में मिलता है। अपनी settings को host.settings.<अपना रास्ता> से पढ़िए:

nixos =
  { config, host, ... }:
  let
    disk-device = host.settings.disk.zfs-disk-single.device_id;
  in
  {
    disko.devices.disk.disk0.device = disk-device;
    # ...
  };

पढ़ने में आसानी के लिए दो आदतें:

  • अपनी settings के टुकड़े को cfg नाम दे दीजिए (cfg = host.settings.core.system.linux-kernel;), फिर cfg.optimization लिखिए। यह वही आम तरीका है जो आपको असली NixOS modules में cfg = config.services.foo के रूप में दिखता है।
  • हमेशा उसी रास्ते से पढ़िए जिस पर declare किया था। एक aspect दूसरे aspect की settings भी पढ़ सकता है (सब एक ही host.settings tree है), पर ऐसा कम ही कीजिए — इससे दोनों aspects आपस में जुड़ जाते हैं।

host पूरा host-entity है, इसलिए इसी से आपको host.system, host.environment, host.networking वगैरह भी मिलते हैं। settings तो बस उसकी एक शाखा है।

किसकी जीत होती है? (Layering और priority)

जब एक ही value को कई जगह set किया जाए, तो किसकी चलेगी? दो स्तर हैं:

एक host के अंदरhost.settings एक module है, तो module-system की priority चलती है। कमज़ोर से ताक़तवर:

  1. aspect के mkOption का default = (सबसे कमज़ोर)।
  2. aspect का खुद डाला हुआ config (आम तौर पर lib.mkDefault के साथ)।
  3. host ने settings में सीधे जो value लिखी (यह ऊपर वालों को हराती है)।
  4. कहीं भी lib.mkForce — यह सबको हरा देती है (सबसे ताक़तवर)।

पूरे fleet में — एक बड़ा cascade भी होता है: root → environment → host। यानी एक environment अपने सारे hosts के लिए एक default set कर सकता है, और कोई host चाहे तो उसे बदल सकता है। (इसका advanced रूप भाग 2 में है।)

root  <  environment  <  host        (बाद वाला जीतता है)

सावधानियाँ (ध्यान देने लायक बातें)

  • Declarations अलग, config अलग। सादे settings attrset में सिर्फ़ mkOption होने चाहिए। अगर आप गलती से वहाँ foo = "bar"; (एक assignment) लिख रहे हैं, तो या तो उसे mkOption { default = "bar"; } बनाइए, या module-shaped तरीका इस्तेमाल कीजिए।

  • or-default वाला statix जाल। Generator में हमने जान-बूझकर imports' = raw.imports or [ ] और config' = raw.config or { } को अलग-अलग नामों में रखा है। इन्हें inherit (raw) imports; में मत "सुधारिए" — statix नाम का एक tool उसे ऐसा बदल देता है जिससे or [ ] वाला default हट जाता है, और फिर जैसे ही कोई सादी settings (जिसमें imports key नहीं) आती है, error आ जाता है। (अगर आप statix को formatter के साथ चलाते हैं, तो यह बदलाव save करते समय चुपके से आ सकता है — उस file के लिए उस rule को बंद रखिए।)

  • settings को entity identity से बाहर रखिए। अगर आपका framework entities को उनकी values से hash करता है, तो settings को identity-excluded mark कीजिए, ताकि सिर्फ़ settings में फ़र्क होने पर दो hosts अलग न गिने जाएँ। nix-config में यह option पर // { identity = false; } है।

  • रास्ता बिल्कुल सही होना चाहिए। aspect जिस रास्ते पर host.settings.<रास्ता> पढ़ता है, वही रास्ता होना चाहिए जिस पर उसने settings declare की थी। कोई shortcut/alias नहीं है।

  • ज़रूरी setting छूटने पर साफ़ error आता है। किसी aspect को include करके उसकी required setting न देना एक सीधा error है। यह design है — "disk कौन सी है यह बताना भूल गए" वाली गलती चलने से पहले ही पकड़ी जाती है।

नई setting जोड़ने की checklist

  1. aspect में, उसकी settings में एक नई mkOption जोड़िए (अगर block नहीं है तो बना लीजिए)। जब तक वो वाकई हर मशीन पर अलग न हो, उसे एक default दे दीजिए।
  2. aspect के nixos / homeManager function में host को argument लीजिए और host.settings.<रास्ता>.<key> पढ़िए (उसे cfg नाम दे दीजिए)।
  3. जिस-जिस host को default से अलग value चाहिए, उसमें settings.<रास्ता>.<key> set कीजिए।
  4. उस host को build करके देख लीजिए कि type ठीक है और सारी required values मौजूद हैं।

भाग 2 — Addendum: "first-class" settings (advanced)

नोट: यह हिस्सा थोड़ा advanced है। अगर आप अभी शुरुआत कर रहे हैं, तो भाग 1 अच्छे से समझ लीजिए — यह भाग बाद में पढ़ सकते हैं। यहाँ हम दिखा रहे हैं कि यह pattern आगे किस दिशा में जा रही है।

भाग 1 में हमने यह pattern NixOS module-system के ऊपर "हाथ से" बनाई: settings असल में mkOption थीं, और aspect सीधे host.settings पढ़ता था। यह आज इस्तेमाल हो रहा है और बढ़िया काम करता है।

den-hoag के साथ जो version आ रहा है, वह इसी idea को एक library (gen-aspects) के ज़रिए और भी ताक़तवर बनाता है। मुख्य फ़र्क ये हैं (हर एक को एक-दो लाइन में समझिए):

1. settings अब अपनी "merge strategy" भी बताती हैं

भाग 1 में जब एक ही value कई layers में हो, तो वे कैसे जुड़ती हैं यह type पर निर्भर था। यहाँ हर field साफ़-साफ़ बताती है कि कैसे जुड़ना है — replace (नई value पुरानी की जगह ले ले), append (lists को जोड़ दो), या recursive (attrsets को गहराई से merge करो):

settings = {
  performance.workers     = { default = 4; };                      # replace (default)
  security.allowed-origins = { default = [ ]; merge = "append"; };  # जोड़ते जाओ
  locations               = { default = { }; merge = "recursive"; }; # गहरा merge
};

यह भाग 1 के मुक़ाबले सबसे बड़ी सहूलियत है, क्योंकि merge का व्यवहार अब साफ़ दिखता है, छुपा हुआ नहीं।

2. overrides अब scope के नाम से, entity से अलग

हर host में अलग-अलग value लिखने के बजाय, सारे overrides एक जगह scopeSettings में रहते हैं, और scope के नाम (env:<नाम> / host:<नाम>) से रखे जाते हैं:

config.scopeSettings = {
  "env:prod"        = { nginx.performance.workers = 16; };
  "host:prod-web-1" = { nginx.performance.workers = 32; };
};

इससे "entity क्या है" और "उसकी settings क्या हैं" — ये दोनों अलग हो जाते हैं।

3. cascade एक साफ़, traceable प्रक्रिया है

values को layers में जोड़ने का काम एक असली pipeline करती है (module-merge के भरोसे नहीं): पहले हर aspect की settings-schema निकालो → फिर एक scope graph बनाओ (env, host nodes) → फिर सबसे-खास-से-कम-खास के क्रम में layers इकट्ठा करो → फिर हर field की strategy लगाकर merge करो। उदाहरण:

aspect default:  nginx.performance.workers = 4
env:prod:        nginx.performance.workers = 16
host:prod-web-1: nginx.performance.workers = 32   ← यह जीतता है

4. policy एक आख़िरी, सबसे ताक़तवर layer है

एक policy-engine नियम लगा सकता है जैसे "production वाले hosts पर hardening लगाओ"। ये नियम जो settings देते हैं वे सबसे आख़िरी layer के रूप में जुड़ती हैं — यानी policy, environment और host दोनों को हरा देती है।

5. aspect अब settings को argument के रूप में पाता है (injection)

भाग 1 में aspect host.settings.<पूरा रास्ता> पढ़ता था। यहाँ aspect का code सीधे एक settings argument पाता है, जिसमें पहले से resolved values होती हैं:

nixos = { settings, host, lib, ... }: {
  services.nginx.config = ''
    worker_processes ${toString settings.nginx.performance.workers};
  '';
};

एक construct (injectAspectSettings) हर (host, aspect) जोड़े के लिए cascade की final values को इस code में "inject" कर देता है — साथ में एक contract (जाँच कि value सही shape की है) और provenance (यह value कहाँ से आई)।

6. provenance — "यह value आई कहाँ से?"

system हर field के लिए याद रखता है कि उसकी आख़िरी value किस layer से आई (default / env / host / policy)। यानी आप पूछ सकते हैं "workers 32 क्यों है?" और जवाब मिलेगा "host की वजह से"।

भाग 1 बनाम भाग 2 — एक नज़र में

बात भाग 1 (module system) भाग 2 (gen-aspects, den-hoag)
Declaration mkOption (पूरे NixOS types) schema leaf { default; merge?; }
Merge का व्यवहार type + module priority में छुपा हुआ हर field पर साफ़: replace / append / recursive
Override कहाँ सीधे host पर scopeSettings, scope के नाम से
Cascade module priority + एक side graph scope graph + traverse + traced fold
Policy layer नहीं (या जुगाड़) हाँ, सबसे आख़िरी layer
aspect कैसे पढ़ता है host.settings.<पूरा रास्ता> settings.<leaf> (inject किया हुआ)
Provenance track नहीं होता हर field का (कौन सी layer जीती)

कौन सा कब इस्तेमाल करें?

  • भाग 1 चुनिए जब आपको सबसे सरल चीज़ चाहिए, पूरा NixOS type-checking चाहिए, और module-merge का व्यवहार ठीक लगता हो। यह एक बढ़िया default है और generator सिर्फ़ ~80 लाइन का है।
  • भाग 2 चुनिए जब आपको हर field पर साफ़ merge strategy चाहिए, एक policy layer वाला cascade चाहिए, या यह जानना हो कि "कौन सी value कहाँ से आई" (provenance)। यह वही दिशा है जिधर den-hoag जा रहा है।

दोनों साथ भी चल सकते हैं: मशीन-विशेष values (जैसे disk id) के लिए भाग 1 रखिए, और fleet-भर की feature settings के लिए भाग 2 का cascade अपनाइए।


शब्दावली (Glossary) — हर शब्द का मतलब

शब्द आसान मतलब
Nix एक tool जिससे आप अपने पूरे system को एक text file में "recipe" की तरह लिखते हैं।
NixOS Nix के ऊपर बना एक पूरा Linux operating system।
Declarative "क्या चाहिए" बताना, "कैसे करना है" नहीं। result-केंद्रित।
Attrset attribute set — नाम→value के जोड़े (Python dict / JS object जैसा)।
Module एक attrset जो options बनाता है या उनमें values डालता है।
Option एक "खाली जगह" जिसे value से भरा जाता है; lib.mkOption से बनती है।
Type किसी option में किस तरह की value डल सकती है (int, str, bool, enum...)।
Default अगर कोई value न दे, तो अपने-आप इस्तेमाल होने वाली value।
Required वह setting जिसका default नहीं — host को देनी ही पड़ती है।
Den बड़ी NixOS setups (कई कंप्यूटर) को manage करने का framework।
Aspect छोटा, दोबारा इस्तेमाल होने वाला config टुकड़ा (pizza topping जैसा)।
Host एक कंप्यूटर/मशीन।
Settings aspect के typed "knobs" जिन्हें host भरता है।
Schema किसी entity (host/user...) का ढाँचा — उसमें कौन सी options हैं।
Generator वह code जो aspects की settings से host.settings tree अपने-आप बनाता है।
Recursion जब function अपने अंदर खुद को बुलाता है (tree में नीचे-नीचे जाने के लिए)।
Cascade कई layers (default → env → host) से values का जुड़ना।
Provenance किसी value का "स्रोत" — वह कहाँ से आई।
lib.mkDefault कमज़ोर value (आसानी से override हो जाती है)।
lib.mkForce सबसे ताक़तवर value (सबको हरा देती है)।

बस! अगर यहाँ तक समझ आ गया, तो आप यह pattern अपने खुद के Den config में इस्तेमाल कर सकते हैं। 🎉 शुरुआत भाग 1 से कीजिए — वही 90% मामलों में काफ़ी है। शुभकामनाएँ! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment