Module Authoring Conventions
Module Authoring Conventions
Section titled “Module Authoring Conventions”This guide covers the standards and best practices for writing NixOS modules in the Ghaf framework. Following these conventions ensures consistency, maintainability, and proper error tracing.
File Structure
Section titled “File Structure”Directory Organization
Section titled “Directory Organization”modules/├── common/ # Modules usable in host or VMs│ ├── my-feature/│ │ ├── default.nix # Main module (imports others)│ │ ├── options.nix # Option definitions (if complex)│ │ └── utils.nix # Helper functions (if needed)│ └── my-simple-feature.nix # Simple single-file module│├── microvm/│ ├── sysvms/│ │ ├── my-vm.nix # VM orchestration (enable, evaluatedConfig)│ │ └── my-vm-base.nix # VM content (what runs inside the VM)│ └── my-vm-features/ # Optional features for a VM│ ├── default.nix│ └── feature-x.nix│├── hardware/│ └── my-hardware/ # Hardware-specific modules│ ├── default.nix│ └── definitions.nix│└── profiles/ └── my-profile.nix # Configuration profilesNaming Conventions
Section titled “Naming Conventions”| Type | Pattern | Example |
|---|---|---|
| Feature module | kebab-case.nix | power-manager.nix |
| VM orchestration | <name>vm.nix | guivm.nix |
| VM base content | <name>vm-base.nix | guivm-base.nix |
| VM features dir | <name>vm-features/ | audiovm-features/ |
| Profile | <name>.nix | laptop-x86.nix |
| Hardware | <arch>-<type>.nix | x86_64-generic.nix |
Required: _file Declaration
Section titled “Required: _file Declaration”Every module MUST have a _file declaration. This enables proper error tracing during evaluation.
# REQUIRED - enables error messages to show source file{ _file = ./my-module.nix;
# Rest of module...}Why _file Matters
Section titled “Why _file Matters”Without _file, error messages show unhelpful paths:
error: ... defined in `/nix/store/abc123-source/modules/microvm/sysvms/guivm-base.nix'With _file, you get the actual source location:
error: ... defined in `modules/microvm/sysvms/guivm-base.nix'Where to Place _file
Section titled “Where to Place _file”Place _file as the first attribute after the opening brace:
{ config, lib, ... }:{ _file = ./my-module.nix; # First attribute
imports = [ ... ];
options = { ... };
config = { ... };}Standard Module Template
Section titled “Standard Module Template”Simple Module
Section titled “Simple Module”# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors## Brief description of what this module does#{ config, lib, pkgs, ...}:let cfg = config.ghaf.services.my-service;in{ _file = ./my-service.nix;
options.ghaf.services.my-service = { enable = lib.mkEnableOption "My Service";
port = lib.mkOption { type = lib.types.port; default = 8080; description = "Port for my service to listen on"; };
package = lib.mkPackageOption pkgs "my-service" { }; };
config = lib.mkIf cfg.enable { systemd.services.my-service = { description = "My Service"; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${cfg.package}/bin/my-service --port ${toString cfg.port}"; }; }; };}VM Base Module
Section titled “VM Base Module”# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors## My VM Base Configuration## This module defines the content that runs inside my-vm.# It uses globalConfig for inherited settings and hostConfig for VM-specific config.#{ config, lib, pkgs, inputs, globalConfig, hostConfig, ...}:{ _file = ./myvm-base.nix;
imports = [ inputs.self.nixosModules.vm-modules inputs.self.nixosModules.givc ];
# Global settings from host ghaf = { profiles.debug.enable = globalConfig.debug.enable; development.ssh.daemon.enable = globalConfig.development.ssh.daemon.enable; logging = { inherit (globalConfig.logging) enable listener; client.enable = globalConfig.logging.enable; }; };
# VM-specific settings networking.hostName = hostConfig.vmName;
# VM's own configuration services.myVmService.enable = true;
system.stateVersion = lib.trivial.release;}VM Orchestration Module
Section titled “VM Orchestration Module”# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors## My VM Orchestration Module## This module manages the my-vm lifecycle on the host.# The actual VM content is defined in myvm-base.nix.#{ config, lib, inputs, ...}:let vmName = "my-vm"; cfg = config.ghaf.virtualization.microvm.myvm;in{ _file = ./myvm.nix;
options.ghaf.virtualization.microvm.myvm = { enable = lib.mkEnableOption "My VM";
evaluatedConfig = lib.mkOption { type = lib.types.nullOr lib.types.unspecified; default = null; description = '' Pre-evaluated NixOS configuration for My VM. Set this via a profile's myvmBase.extendModules. ''; }; };
config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.evaluatedConfig != null; message = '' ghaf.virtualization.microvm.myvm.evaluatedConfig must be set. Use myvmBase.extendModules from a profile. ''; } ];
microvm.vms.${vmName} = { autostart = true; inherit (inputs) nixpkgs; inherit (cfg) evaluatedConfig; }; };}Option Definition Patterns
Section titled “Option Definition Patterns”Enable Options
Section titled “Enable Options”Use mkEnableOption for boolean feature flags:
options.ghaf.services.my-feature = { enable = lib.mkEnableOption "my feature";};This creates an option with:
- Type:
bool - Default:
false - Description: “Whether to enable my feature.”
Package Options
Section titled “Package Options”Use mkPackageOption for package selections:
options.ghaf.services.my-service = { package = lib.mkPackageOption pkgs "my-service" { default = [ "my-service" ]; # Optional: path in pkgs example = "pkgs.my-service-unstable"; };};Type-Safe Options
Section titled “Type-Safe Options”Always specify types:
options.ghaf.services.my-service = { # Port number port = lib.mkOption { type = lib.types.port; default = 8080; description = "Port number"; };
# List of strings allowedHosts = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "localhost" ]; description = "Allowed hostnames"; };
# Enum logLevel = lib.mkOption { type = lib.types.enum [ "debug" "info" "warn" "error" ]; default = "info"; description = "Log level"; };
# Optional with null configFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Custom config file, or null for default"; };
# Attribute set settings = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { }; description = "Additional settings as key-value pairs"; };};Hierarchical Options
Section titled “Hierarchical Options”Organize related options under a common namespace:
# Good: Clear hierarchyoptions.ghaf.services.my-service = { enable = lib.mkEnableOption "my service";
server = { host = lib.mkOption { ... }; port = lib.mkOption { ... }; };
client = { timeout = lib.mkOption { ... }; retries = lib.mkOption { ... }; };};
# Access: config.ghaf.services.my-service.server.portConfig Implementation Patterns
Section titled “Config Implementation Patterns”Conditional Configuration
Section titled “Conditional Configuration”Use mkIf to conditionally apply configuration:
config = lib.mkIf cfg.enable { # Only applied if enable = true systemd.services.my-service = { ... };};Merging Configuration
Section titled “Merging Configuration”Use mkMerge to combine multiple config blocks:
config = lib.mkMerge [ # Always applied { environment.systemPackages = [ pkgs.common-tool ]; }
# Conditionally applied (lib.mkIf cfg.enable { systemd.services.my-service = { ... }; })
# Another condition (lib.mkIf cfg.debug { environment.systemPackages = [ pkgs.debug-tools ]; })];Default vs Force
Section titled “Default vs Force”Use mkDefault for overridable defaults, mkForce to override:
# In base module - can be overriddenconfig.services.openssh.enable = lib.mkDefault true;
# In extension - overrides defaultconfig.services.openssh.enable = lib.mkForce false;Priority Levels
Section titled “Priority Levels”lib.mkOverride 100 value # Same as mkDefaultlib.mkOverride 50 value # Higher priority than mkDefaultlib.mkDefault value # Priority 1000lib.mkForce value # Priority 50Import Patterns
Section titled “Import Patterns”Local Imports
Section titled “Local Imports”{ imports = [ ./submodule-a.nix ./submodule-b.nix ];}Flake Module Imports
Section titled “Flake Module Imports”{ inputs, ... }:{ imports = [ inputs.self.nixosModules.my-module inputs.other-flake.nixosModules.their-module ];}Conditional Imports
Section titled “Conditional Imports”{ config, lib, ... }:{ imports = [ ./always-imported.nix ] ++ lib.optionals config.ghaf.features.myFeature.enable [ ./feature-specific.nix ];}Using globalConfig and hostConfig
Section titled “Using globalConfig and hostConfig”Accessing globalConfig
Section titled “Accessing globalConfig”{ config, lib, globalConfig, ... }:{ config = { # Direct access ghaf.profiles.debug.enable = globalConfig.debug.enable;
# With inherit logging = { inherit (globalConfig.logging) enable listener; };
# With fallback security.audit.enable = globalConfig.security.audit.enable or false; };}Accessing hostConfig
Section titled “Accessing hostConfig”{ config, lib, hostConfig, ... }:{ config = { # VM identity networking.hostName = hostConfig.vmName;
# Network config networking.interfaces.ethint0.ipv4.addresses = [{ address = hostConfig.networking.thisVm.ipv4; prefixLength = 24; }];
# With fallback users.users = hostConfig.users.managed or { }; };}Using Features
Section titled “Using Features”{ config, lib, globalConfig, ... }:{ config = lib.mkIf (lib.ghaf.features.isEnabledFor globalConfig "myFeature" "this-vm") { services.myFeature.enable = true; };}License Headers
Section titled “License Headers”Every file must have SPDX license headers:
# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributorsFor documentation files (MDX):
{/*SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributorsSPDX-License-Identifier: CC-BY-SA-4.0*/}Formatting
Section titled “Formatting”Ghaf uses treefmt with nixfmt-rfc-style. Always format before committing:
nix fmt# Or to check without changing:nix fmt -- --fail-on-changeChecklist for New Modules
Section titled “Checklist for New Modules”- File has SPDX license header
- Module has
_filedeclaration - Options are under
ghaf.*namespace - Options have types and descriptions
- Config uses
mkIffor conditional application - Uses
globalConfig/hostConfiginstead ofconfigHost - No hardcoded values that should be configurable
- Module is added to appropriate
default.niximports - Module is exported in
flake.nixif needed - Code is formatted with
nix fmt -
nix flake checkpasses
Summary
Section titled “Summary”| Convention | Requirement |
|---|---|
_file declaration | Required in every module |
| SPDX headers | Required in every file |
| Option namespace | ghaf.* |
| Enable options | Use mkEnableOption |
| Package options | Use mkPackageOption |
| Conditional config | Use mkIf |
| Host settings | Use globalConfig / hostConfig |
| Formatting | Run nix fmt before commit |