Den में Per-Aspect Settings — शुरुआती लोगों के लिए आसान हिंदी गाइड (Hindi beginner's guide to the host/aspect settings pattern in Den)
नमस्ते! 👋 यह गाइड आपको एक बहुत उपयोगी pattern सिखाएगी जो Den नाम के एक Nix framework में इस्तेमाल होती है। चिंता मत कीजिए अगर आप Nix में नए हैं — हम हर चीज़ को धीरे-धीरे, आसान भाषा में, उदाहरणों के साथ समझाएँगे।
किसके लिए है यह गाइड? उन लोगों के लिए जो Computer Science पढ़ रहे हैं और Nix/NixOS के बारे में थोड़ा-बहुत जानते हैं, पर अभी expert नहीं हैं। अगर कुछ शब्द नए लगें, तो घबराइए मत — सबसे नीचे एक शब्दावली (glossary) दी गई है जहाँ हर technical शब्द का मतलब समझाया गया है।
एक छोटा सा नोट code के बारे में: इस गाइड में सारा code (program) अंग्रेज़ी में ही
रहेगा, क्योंकि programming की भाषा अंग्रेज़ी में लिखी जाती है। लेकिन code के अंदर के
# comments (टिप्पणियाँ) हमने हिंदी में लिख दी हैं ताकि आपको समझने में आसानी हो।
अगर आप ये चीज़ें पहले से जानते हैं, तो इस भाग को छोड़ सकते हैं। पर अगर नहीं जानते, तो इन्हें पढ़ लेना आगे बहुत काम आएगा।
सोचिए आप अपने कंप्यूटर को एक खास तरीके से 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 के ऊपर बना है।
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"; }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 एक 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 कर देते हैं, और
वे सब अपने-आप जुड़ जाते हैं।
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) आप ज़िंदगी में सिर्फ़ एक बार लिखते हैं।
सबसे छोटा 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 की वजह से होता है — जो हम आगे देखेंगे।
अपने 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 नहीं है, इसलिए जो भी hostzfs-disk-singleको include करेगा, उसे यह value देनी ही पड़ेगी, वरना build fail हो जाएगा। यह अच्छी बात है — type system आपको ज़बरदस्ती याद दिला देता है कि "भई, disk कौन सी है यह तो बताओ।"
| लिखावट | मतलब |
|---|---|
mkOption { type = ...; } (default नहीं) |
ज़रूरी। host को देना पड़ेगा; न दिया तो error। |
mkOption { type = ...; default = x; } |
वैकल्पिक। host कुछ न कहे तो अपने-आप x इस्तेमाल होगा। |
सलाह: जिस value का कोई अच्छा "सबके लिए एक जैसा" default हो सकता है, उसे default दे दीजिए। "required" सिर्फ़ उन्हीं चीज़ों के लिए रखिए जो वाकई हर मशीन पर अलग होती हैं (जैसे disk id, AS number)।
आम तौर पर 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 भी काम करती हैं (इनके बारे
में आगे "किसकी जीत होती है" वाले हिस्से में पढ़िए)।
यह वही "जादुई" हिस्सा है जिसकी वजह से ऊपर सब अपने-आप जुड़ता है। यह आपके host schema
का एक टुकड़ा है जो "जिन-जिन aspects ने settings declare की हैं" उन सबको इकट्ठा करके एक
typed host.settings tree बना देता है। आप इसे एक बार लिखते हैं, फिर हमेशा के लिए हर
aspect की settings अपने-आप दिखने लगती हैं।
अगर आप 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। Generatorden.aspectsको पढ़कर settings खोजता है, औरden.classes/den.quirksको देखकर समझता है कि कौन सी keys aspect नहीं हैं (बल्कि framework की अंदरूनी चीज़ें हैं)।
तो host के लिए plan यह है: file के let हिस्से में den.aspects से एक settingsType
बनाओ, फिर उसे den.schema.host.imports के अंदर एक और option (settings) की तरह जोड़ दो।
अगले दो हिस्से यही दिखाते हैं।
नीचे वाला 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 ग़ायब हो जाता है। तो: सबसे पहले यह एक लाइन ज़रूर जोड़िए।
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 में रखते हैं। 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 को किसी और नाम से देता है, तो वही इस्तेमाल कीजिए। settingshost की बहुत सारी options में से बस एक है। बाक़ी schema को इसके होने का पता भी नहीं — aspects और hosts खुद-ब-खुद इसके ज़रिए जुड़ जाते हैं।// { identity = false; }वाला हिस्सा nix-config की अपनी खास बात है (entity identity hashing)। अगर आपके framework में ऐसा कुछ नहीं है, तो इसे छोड़ दीजिए।
बस इतना ही host-side काम है: एक option, जिसके पीछे एक ~80-लाइन का let, सिर्फ़ एक बार
लिखा हुआ। इसके बाद जो भी aspect settings declare करेगा, वह बिना किसी और बदलाव के
host.settings के नीचे अपने-आप दिखने लगेगा।
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 लिखता ही नहीं।
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.settingstree है), पर ऐसा कम ही कीजिए — इससे दोनों aspects आपस में जुड़ जाते हैं।
host पूरा host-entity है, इसलिए इसी से आपको host.system, host.environment,
host.networking वगैरह भी मिलते हैं। settings तो बस उसकी एक शाखा है।
जब एक ही value को कई जगह set किया जाए, तो किसकी चलेगी? दो स्तर हैं:
एक host के अंदर — host.settings एक module है, तो module-system की priority चलती है।
कमज़ोर से ताक़तवर:
- aspect के
mkOptionकाdefault =(सबसे कमज़ोर)। - aspect का खुद डाला हुआ
config(आम तौर परlib.mkDefaultके साथ)। - host ने
settingsमें सीधे जो value लिखी (यह ऊपर वालों को हराती है)। - कहीं भी
lib.mkForce— यह सबको हरा देती है (सबसे ताक़तवर)।
पूरे fleet में — एक बड़ा cascade भी होता है: root → environment → host। यानी एक
environment अपने सारे hosts के लिए एक default set कर सकता है, और कोई host चाहे तो उसे
बदल सकता है। (इसका advanced रूप भाग 2 में है।)
root < environment < host (बाद वाला जीतता है)
-
Declarations अलग, config अलग। सादे
settingsattrset में सिर्फ़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 (जिसमेंimportskey नहीं) आती है, 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.<रास्ता>पढ़ता है, वही रास्ता होना चाहिए जिस पर उसनेsettingsdeclare की थी। कोई shortcut/alias नहीं है। -
ज़रूरी setting छूटने पर साफ़ error आता है। किसी aspect को include करके उसकी required setting न देना एक सीधा error है। यह design है — "disk कौन सी है यह बताना भूल गए" वाली गलती चलने से पहले ही पकड़ी जाती है।
- aspect में, उसकी
settingsमें एक नईmkOptionजोड़िए (अगर block नहीं है तो बना लीजिए)। जब तक वो वाकई हर मशीन पर अलग न हो, उसे एकdefaultदे दीजिए। - aspect के
nixos/homeManagerfunction मेंhostको argument लीजिए औरhost.settings.<रास्ता>.<key>पढ़िए (उसेcfgनाम दे दीजिए)। - जिस-जिस host को default से अलग value चाहिए, उसमें
settings.<रास्ता>.<key>set कीजिए। - उस host को build करके देख लीजिए कि type ठीक है और सारी required values मौजूद हैं।
नोट: यह हिस्सा थोड़ा advanced है। अगर आप अभी शुरुआत कर रहे हैं, तो भाग 1 अच्छे से समझ लीजिए — यह भाग बाद में पढ़ सकते हैं। यहाँ हम दिखा रहे हैं कि यह pattern आगे किस दिशा में जा रही है।
भाग 1 में हमने यह pattern NixOS module-system के ऊपर "हाथ से" बनाई: settings असल में
mkOption थीं, और aspect सीधे host.settings पढ़ता था। यह आज इस्तेमाल हो रहा है और
बढ़िया काम करता है।
den-hoag के साथ जो version आ रहा है, वह इसी idea को एक library (gen-aspects) के ज़रिए और भी ताक़तवर बनाता है। मुख्य फ़र्क ये हैं (हर एक को एक-दो लाइन में समझिए):
भाग 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 का व्यवहार अब साफ़ दिखता है, छुपा हुआ नहीं।
हर 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 क्या हैं" — ये दोनों अलग हो जाते हैं।
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 ← यह जीतता है
एक policy-engine नियम लगा सकता है जैसे "production वाले hosts पर hardening लगाओ"। ये नियम जो settings देते हैं वे सबसे आख़िरी layer के रूप में जुड़ती हैं — यानी policy, environment और host दोनों को हरा देती है।
भाग 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 कहाँ से आई)।
system हर field के लिए याद रखता है कि उसकी आख़िरी value किस layer से आई (default / env / host / policy)। यानी आप पूछ सकते हैं "workers 32 क्यों है?" और जवाब मिलेगा "host की वजह से"।
| बात | भाग 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 अपनाइए।
| शब्द | आसान मतलब |
|---|---|
| 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% मामलों में काफ़ी है। शुभकामनाएँ! 🚀