> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/soriphoono/homelab/llms.txt
> Use this file to discover all available pages before exploring further.

# Dynamic Discovery

> How automatic configuration discovery works under the hood

## Overview

Dynamic discovery is the mechanism that automatically finds and builds configurations without requiring manual imports. Instead of maintaining lists of systems and users, the flake scans directories and constructs configurations on the fly.

## The Problem

Traditional Nix flake configurations require explicit imports:

```nix theme={null}
# Traditional approach ❌
nixosConfigurations = {
  server1 = mkSystem "server1" ./systems/server1;
  server2 = mkSystem "server2" ./systems/server2;
  server3 = mkSystem "server3" ./systems/server3;
  # ... must manually add every system
};
```

**Problems:**

* Adding a new system requires updating the flake
* Easy to forget to add new configurations
* Boilerplate scales linearly with number of systems
* Refactoring is tedious

## The Solution

This homelab uses discovery functions that scan directories and build configurations automatically:

```nix theme={null}
# Discovery approach ✅
nixosConfigurations = lib.mapAttrs mkSystem (lib.discover ./systems);
```

**Benefits:**

* Add a new system → instantly available
* Zero boilerplate maintenance
* Scales to hundreds of systems
* Refactoring is trivial

## Discovery Functions

The discovery logic lives in `lib/default.nix` and provides three main functions.

### `discover` - Configuration Discovery

**Purpose:** Finds directories with `default.nix` and standalone `.nix` files.

**Source code from `lib/default.nix:4-14`:**

```nix theme={null}
discover = dir:
  self.mapAttrs' (name: _: {
    name = self.removeSuffix ".nix" name;
    value = dir + "/${name}";
  }) (
    self.filterAttrs (
      name: type:
        (type == "directory" && builtins.pathExists (dir + "/${name}/default.nix"))
        || (type == "regular" && name != "default.nix" && self.hasSuffix ".nix" name)
    ) (builtins.readDir dir)
  );
```

**How it works:**

1. **Read directory:** `builtins.readDir dir` returns an attribute set of `{ name = type; }`
2. **Filter entries:** Keep only:
   * Directories containing `default.nix`
   * Regular `.nix` files (except `default.nix` itself)
3. **Transform names:** Remove `.nix` suffix from filenames
4. **Build paths:** Create full paths to configurations

**Example:**

Given this directory structure:

```
systems/
├── server/
│   └── default.nix
├── desktop/
│   └── default.nix
└── laptop.nix
```

The `discover` function returns:

```nix theme={null}
{
  server = ./systems/server;
  desktop = ./systems/desktop;
  laptop = ./systems/laptop.nix;
}
```

<Tip>
  This allows both directory-based configs (`server/default.nix`) and single-file configs (`laptop.nix`) to coexist.
</Tip>

### `discoverTests` - Test Discovery

**Purpose:** Specifically designed to find and evaluate test files.

**Source code from `lib/default.nix:17-26`:**

```nix theme={null}
discoverTests = args: dir:
  self.mapAttrs' (name: _: {
    name = self.removeSuffix ".nix" name;
    value = import (dir + "/${name}") (args // {lib = self;});
  }) (
    self.filterAttrs (
      name: type:
        type == "regular" && self.hasSuffix ".nix" name
    ) (builtins.readDir dir)
  );
```

**Differences from `discover`:**

* Only finds regular `.nix` files (no directories)
* Immediately imports and evaluates each file
* Passes `args` to each test with `lib` override

**Usage:**

```nix theme={null}
checks = lib.discoverTests { inherit pkgs; } ./tests;
```

Given:

```
tests/
├── unit-test.nix
└── integration-test.nix
```

Returns:

```nix theme={null}
{
  unit-test = import ./tests/unit-test.nix { pkgs = pkgs; lib = lib; };
  integration-test = import ./tests/integration-test.nix { pkgs = pkgs; lib = lib; };
}
```

### `readMeta` - Metadata Reader

**Purpose:** Reads optional `meta.json` files from configuration directories.

**Source code from `lib/default.nix:29-33`:**

```nix theme={null}
readMeta = path:
  if builtins.pathExists (path + "/meta.json")
  then builtins.fromJSON (builtins.readFile (path + "/meta.json"))
  else {};
```

**How it works:**

* Checks if `meta.json` exists in the given path
* If yes: parses JSON and returns attribute set
* If no: returns empty set `{}`

**Usage in builders:**

From `flake.nix:233-235`:

```nix theme={null}
mkSystem = hostName: path: let
  meta = lib.readMeta path;
  systemArch = meta.system or "x86_64-linux";
```

This allows overriding the target architecture:

```json theme={null}
// systems/raspi/meta.json
{
  "system": "aarch64-linux"
}
```

<Info>
  The `or` operator provides a default value when `system` is not specified in `meta.json`.
</Info>

## Discovery in Practice

### Systems Discovery

**From `flake.nix:348`:**

```nix theme={null}
nixosConfigurations = lib.mapAttrs mkSystem (lib.discover ./systems);
```

**Process:**

1. `lib.discover ./systems` → `{ server = ./systems/server; ... }`
2. `lib.mapAttrs mkSystem` → Calls `mkSystem "server" ./systems/server` for each
3. Result: `{ server = <nixosSystem>; ... }`

### Droids Discovery

**From `flake.nix:351`:**

```nix theme={null}
nixOnDroidConfigurations = lib.mapAttrs mkDroid (lib.discover ./droids);
```

Identical pattern applied to Android devices.

### Homes Discovery

Home Manager uses custom discovery logic to handle the three naming patterns.

**From `flake.nix:355-374`:**

```nix theme={null}
homeConfigurations = let
  # Read all entries in homes/
  allEntries = builtins.readDir ./homes;
  homeDirs = builtins.attrNames (lib.filterAttrs (_n: v: v == "directory") allEntries);

  # Filter for valid user directories
  # Valid: "alice" or "alice@global"
  # Invalid: "alice@workstation" (system-specific, not standalone)
  validUsers = lib.filter (
    name: (! lib.hasInfix "@" name) || (lib.hasSuffix "@global" name)
  ) homeDirs;

  # Normalize to base username
  # "alice" → "alice"
  # "alice@global" → "alice"
  usernames = lib.unique (map (name: lib.removeSuffix "@global" name) validUsers);
in
  lib.genAttrs usernames mkHome;
```

**Step-by-step example:**

Given:

```
homes/
├── alice/
├── alice@global/
├── alice@workstation/
└── bob/
```

1. `allEntries` = `{ alice = "directory"; alice@global = "directory"; ... }`
2. `homeDirs` = `[ "alice" "alice@global" "alice@workstation" "bob" ]`
3. `validUsers` = `[ "alice" "alice@global" "bob" ]` (removed `alice@workstation`)
4. `usernames` = `[ "alice" "bob" ]` (normalized and deduplicated)
5. Final: `{ alice = <homeConfiguration>; bob = <homeConfiguration>; }`

<Note>
  The `alice@workstation` directory is intentionally excluded from `homeConfigurations` because it's meant to be imported by the NixOS system configuration, not used standalone.
</Note>

### The `mkHome` Builder

**From `flake.nix:166-204`:**

```nix theme={null}
mkHome = username: let
  basePath = ./homes + "/${username}";
  globalPath = ./homes + "/${username}@global";

  # Check which paths exist
  hasBase = builtins.pathExists basePath;
  hasGlobal = builtins.pathExists globalPath;

  # Read metadata with fallback
  meta = if hasBase
    then lib.readMeta basePath
    else if hasGlobal
    then lib.readMeta globalPath
    else {};

  systemArch = meta.system or "x86_64-linux";
  pkgs = pkgsFor.${systemArch};
in
  inputs.home-manager.lib.homeManagerConfiguration {
    inherit pkgs;
    modules =
      homeManagerModules
      ++ lib.optional hasBase (basePath + "/default.nix")
      ++ lib.optional hasGlobal (globalPath + "/default.nix")
      ++ [ /* ... */ ];
  };
```

**Key features:**

* Checks if both `alice/` and `alice@global/` exist
* Combines them into a single configuration
* Uses `lib.optional` to conditionally include modules
* Respects architecture from metadata

## Extending Discovery

### Adding a New System Type

To add discovery for a new configuration type:

1. **Create a builder function:**

```nix theme={null}
mkEnvironment = name: path:
  # ... build logic
```

2. **Apply discovery:**

```nix theme={null}
environmentConfigurations = lib.mapAttrs mkEnvironment (lib.discover ./environments);
```

3. **Export in flake:**

```nix theme={null}
flake = {
  environmentConfigurations = lib.mapAttrs mkEnvironment (lib.discover ./environments);
};
```

### Custom Discovery Functions

You can create specialized discovery for unique patterns:

```nix theme={null}
# Discover only production systems
discoverProd = dir:
  lib.filterAttrs (name: path:
    let meta = lib.readMeta path;
    in (meta.environment or "dev") == "production"
  ) (lib.discover dir);
```

## Benefits

### Reduced Cognitive Load

No need to remember to update imports. Just create a directory and it's automatically available.

### Scalability

The approach handles 1 system as easily as 100 systems:

* Same amount of code
* Same performance characteristics
* Same mental model

### Refactoring Freedom

Renaming is as simple as renaming the directory:

```bash theme={null}
mv systems/old-name systems/new-name
nixos-rebuild switch --flake .#new-name
```

No imports to update.

### Type Safety

Discovery still maintains Nix's evaluation guarantees:

* `nix flake check` validates all discovered configs
* Type errors caught at evaluation time
* No "hidden" configurations

## Inspection

You can see what was discovered:

```bash theme={null}
# List all discovered systems
nix flake show | grep nixosConfigurations

# Inspect a specific discovery
nix eval .#nixosConfigurations --apply builtins.attrNames
# Output: [ "server" "desktop" "laptop" ]
```

## Summary

Dynamic discovery provides:

* **Automation** - Configurations found automatically
* **Simplicity** - No import boilerplate
* **Scalability** - Handles growth effortlessly
* **Flexibility** - Supports multiple patterns
* **Safety** - Still validated by `nix flake check`

The three functions—`discover`, `discoverTests`, and `readMeta`—work together to create a self-organizing configuration system that grows with your infrastructure.
