Merge pull request 'build-clan: support constructing darwinConfigurations' (#3115) from mac into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3115
This commit is contained in:
Michael Hoang
2025-04-08 07:44:09 +00:00
27 changed files with 347 additions and 69 deletions

21
flake.lock generated
View File

@@ -69,6 +69,26 @@
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1743496612,
"narHash": "sha256-emPWa5lmKbnyuj8c1mSJUkzJNT+iJoU9GMcXwjp2oVM=",
"owner": "LnL7",
"repo": "nix-darwin",
"rev": "73d59580d01e9b9f957ba749f336a272869c42dd",
"type": "github"
},
"original": {
"owner": "LnL7",
"repo": "nix-darwin",
"type": "github"
}
},
"nixos-facter-modules": {
"locked": {
"lastModified": 1743671943,
@@ -102,6 +122,7 @@
"data-mesher": "data-mesher",
"disko": "disko",
"flake-parts": "flake-parts",
"nix-darwin": "nix-darwin",
"nixos-facter-modules": "nixos-facter-modules",
"nixpkgs": "nixpkgs",
"sops-nix": "sops-nix",

View File

@@ -4,6 +4,9 @@
inputs = {
nixpkgs.url = "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz";
nix-darwin.url = "github:LnL7/nix-darwin";
nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";

View File

@@ -34,7 +34,7 @@ in
type = types.submoduleWith {
specialArgs = {
inherit clan-core self;
inherit (inputs) nixpkgs;
inherit (inputs) nixpkgs nix-darwin;
# TODO: inject the inventory interface
# inventoryInterface = {};
};

View File

@@ -4,6 +4,7 @@
{
lib,
nixpkgs,
nix-darwin ? null,
...
}:
{
@@ -54,6 +55,7 @@
inherit
lib
nixpkgs
nix-darwin
clan-core
self
;

View File

@@ -37,6 +37,7 @@ in
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {

View File

@@ -1,6 +1,7 @@
{
lib,
nixpkgs,
nix-darwin ? null,
clan-core,
self,
specialArgs ? { },
@@ -9,7 +10,12 @@
module:
(lib.evalModules {
specialArgs = {
inherit self clan-core nixpkgs;
inherit
self
clan-core
nixpkgs
nix-darwin
;
};
modules = [
./interface.nix

View File

@@ -123,6 +123,15 @@ in
};
# Outputs
darwinConfigurations = lib.mkOption {
# Hide from documentation.
# Exposed at the top-level of the flake, clan.darwinConfigurations should not used by the user.
# Instead, the user should use the `.#darwinConfigurations` attribute of the flake output.
visible = false;
type = types.lazyAttrsOf types.raw;
default = { };
};
nixosConfigurations = lib.mkOption {
# Hide from documentation.
# Exposed at the top-level of the flake, clan.nixosConfigurations should not used by the user.

View File

@@ -3,6 +3,7 @@
config,
clan-core,
nixpkgs,
nix-darwin,
lib,
...
}:
@@ -48,51 +49,99 @@ let
}
);
# TODO: remove default system once we have a hardware-config mechanism
nixosConfiguration =
{
system ? null,
name,
pkgs ? null,
extraConfig ? { },
}:
nixpkgs.lib.nixosSystem {
modules =
let
hwConfig = "${directory}/machines/${name}/hardware-configuration.nix";
diskoConfig = "${directory}/machines/${name}/disko.nix";
in
[
moduleSystemConstructor = {
# TODO: remove default system once we have a hardware-config mechanism
nixos =
{
system ? null,
name,
pkgs ? null,
extraConfig ? { },
}:
nixpkgs.lib.nixosSystem {
modules =
let
hwConfig = "${directory}/machines/${name}/hardware-configuration.nix";
diskoConfig = "${directory}/machines/${name}/disko.nix";
in
[
{
# Autoinclude configuration.nix and hardware-configuration.nix
imports = builtins.filter builtins.pathExists [
"${directory}/machines/${name}/configuration.nix"
hwConfig
diskoConfig
];
}
clan-core.nixosModules.clanCore
extraConfig
(machines.${name} or { })
# Inherit the inventory assertions ?
# { inherit (mergedInventory) assertions; }
{ imports = inventoryClass.machines.${name}.machineImports or [ ]; }
# Import the distribute services
{ imports = config.clanInternals.distributedServices.allMachines.${name} or [ ]; }
(
{
# Settings
clan.core.settings = {
inherit directory;
inherit (config.inventory.meta) name icon;
machine = {
inherit name;
};
};
# Inherited from clan wide settings
# TODO: remove these
networking.hostName = lib.mkDefault name;
# For vars we need to override the system so we run vars
# generators on the machine that runs `clan vars generate`. If a
# users is using the `pkgsForSystem`, we don't set
# nixpkgs.hostPlatform it would conflict with the `nixpkgs.pkgs`
# option.
nixpkgs.hostPlatform = lib.mkIf (system != null && (pkgsForSystem system) != null) (
lib.mkForce system
);
}
// lib.optionalAttrs (pkgs != null) { nixpkgs.pkgs = lib.mkForce pkgs; }
)
];
specialArgs = {
inherit clan-core;
} // specialArgs;
};
darwin =
{
system ? null,
name,
pkgs ? null,
extraConfig ? { },
}:
nix-darwin.lib.darwinSystem {
modules = [
{
# Autoinclude configuration.nix and hardware-configuration.nix
imports = builtins.filter builtins.pathExists [
"${directory}/machines/${name}/configuration.nix"
hwConfig
diskoConfig
];
}
clan-core.nixosModules.clanCore
(
if !lib.hasAttrByPath [ "darwinModules" "clanCore" ] clan-core then
{ }
else
throw "this should import clan-core.darwinModules.clanCore"
)
extraConfig
(machines.${name} or { })
# Inherit the inventory assertions ?
# { inherit (mergedInventory) assertions; }
{ imports = inventoryClass.machines.${name}.machineImports or [ ]; }
# Import the distribute services
{ imports = config.clanInternals.distributedServices.allMachines.${name} or [ ]; }
# TODO: import inventory when it has support for defining `nix-darwin` modules
(
{
# Settings
clan.core.settings = {
inherit directory;
inherit (config.inventory.meta) name icon;
machine = {
inherit name;
};
};
# Inherited from clan wide settings
# TODO: remove these
# TODO: set clan-core settings when clan-core has support for `nix-darwin`
networking.hostName = lib.mkDefault name;
@@ -109,16 +158,24 @@ let
)
];
specialArgs = {
inherit clan-core;
} // specialArgs;
};
specialArgs = {
inherit clan-core;
} // specialArgs;
};
};
allMachines = inventory.machines or { } // machines;
nixosConfigurations = lib.mapAttrs (name: _: nixosConfiguration { inherit name; }) allMachines;
machineClass = lib.mapAttrs (name: _: inventory.machineClass.${name} or "nixos") allMachines;
# This instantiates nixos for each system that we support:
configurations = lib.mapAttrs (
name: _: moduleSystemConstructor.${machineClass.${name}} { inherit name; }
) allMachines;
nixosConfigurations = lib.filterAttrs (name: _: machineClass.${name} == "nixos") configurations;
darwinConfigurations = lib.filterAttrs (name: _: machineClass.${name} == "darwin") configurations;
# This instantiates NixOS for each system that we support:
# configPerSystem = <system>.<machine>.nixosConfiguration
# We need this to build nixos secret generators for each system
configsPerSystem = builtins.listToAttrs (
@@ -127,7 +184,7 @@ let
lib.nameValuePair system (
lib.mapAttrs (
name: _:
nixosConfiguration {
moduleSystemConstructor.${machineClass.${name}} {
inherit name system;
pkgs = pkgsFor.${system};
}
@@ -142,7 +199,7 @@ let
lib.nameValuePair system (
lib.mapAttrs (
name: _: args:
nixosConfiguration (
moduleSystemConstructor.${machineClass.${name}} (
args
// {
inherit name system;
@@ -194,6 +251,7 @@ in
];
inherit nixosConfigurations;
inherit darwinConfigurations;
clanInternals = {
moduleSchemas = clan-core.lib.modules.getModulesSchema config.inventory.modules;
@@ -223,11 +281,17 @@ in
# machine specifics
machines = configsPerSystem;
machinesFunc = configsFuncPerSystem;
all-machines-json = lib.mapAttrs (
system: configs:
nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (
lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs
)
) configsPerSystem;
all-machines-json =
if !lib.hasAttrByPath [ "darwinModules" "clanCore" ] clan-core then
lib.mapAttrs (
system: configs:
nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (
lib.mapAttrs (_: m: m.config.system.clan.deployment.data) (
lib.filterAttrs (_n: v: v.class == "nixos") configs
)
)
) configsPerSystem
else
throw "remove NixOS filter and support nix-darwin as well";
};
}

View File

@@ -14,5 +14,6 @@
topLevel = [
"clanInternals"
"nixosConfigurations"
"darwinConfigurations"
];
}

View File

@@ -76,7 +76,7 @@ in
};
directory = ./.;
imports = [
# What the user needs to specif
# What the user needs to specify
{
directory = ./.;
inventory.meta.name = "test";
@@ -165,10 +165,9 @@ in
};
directory = ./.;
meta.name = "test";
inventory.machines.machine1.meta.name = "machine1";
inventory.machines.machine1 = { };
machines.machine2 = { };
};
in
{
@@ -200,4 +199,55 @@ in
expr = result.nixosConfigurations.machine2.config.networking.hostName;
expected = "dream2nix";
};
test_buildClan_darwin_machines =
let
result = buildClan {
self = {
inputs = { };
};
directory = ./.;
meta.name = "test";
machines.machine1 = { };
machines.machine2 = { };
machines.machine3 = { };
inventory.machineClass.machine2 = "darwin";
inventory.machineClass.machine3 = "nixos";
};
in
{
expr = {
nixos = builtins.attrNames result.nixosConfigurations;
darwin = builtins.attrNames result.darwinConfigurations;
};
expected = {
nixos = [
"machine1"
"machine3"
];
darwin = [ "machine2" ];
};
};
test_buildClan_all_machines_laziness =
let
result = buildClan {
self = {
inputs = { };
};
directory = ./.;
meta.name = "test";
machines.machine1.non_existent_option = throw "eval error";
inventory.machines.machine1.other_non_existent_option = throw "different eval error";
};
in
{
expr = builtins.attrNames result.nixosConfigurations;
expected = [
"machine1"
];
};
}

View File

@@ -2,6 +2,7 @@
lib,
self,
nixpkgs,
nix-darwin ? null,
...
}:
# Produces the
@@ -20,7 +21,7 @@ lib.fix (clanLib: {
clan-core = self;
pkgs = nixpkgs.legacyPackages.x86_64-linux;
};
buildClanModule = clanLib.callLib ./build-clan { inherit nixpkgs; };
buildClanModule = clanLib.callLib ./build-clan { inherit nixpkgs nix-darwin; };
buildClan = clanLib.buildClanModule.buildClanWith { clan-core = self; };
# ------------------------------------

View File

@@ -15,7 +15,7 @@ rec {
];
flake.clanLib = import ./default.nix {
inherit lib inputs self;
inherit (inputs) nixpkgs;
inherit (inputs) nixpkgs nix-darwin;
};
# TODO: remove this legacy alias
flake.lib = flake.clanLib;

View File

@@ -23,6 +23,7 @@ in
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {

View File

@@ -277,6 +277,22 @@ in
)
);
};
machineClass = lib.mkOption {
default = { };
type = types.attrsOf (
types.enum [
"nixos"
"darwin"
]
);
description = ''
The module system that should be used to construct the machine
Set this to `darwin` for macOS machines
'';
};
instances = lib.mkOption {
# Keep as internal until all de-/serialization issues are resolved
visible = false;

View File

@@ -24,6 +24,7 @@ in
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${self}#legacyPackages.${system}.evalTests-distributedServices

View File

@@ -48,6 +48,7 @@ in
export NIX_ABORT_ON_WARN=1
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {

View File

@@ -309,7 +309,13 @@ rec {
option.type.name == "enum"
# return jsonschema property definition for enum
then
exposedModuleInfo // default // example // description // { enum = option.type.functor.payload; }
exposedModuleInfo
// default
// example
// description
// {
enum = option.type.functor.payload.values;
}
# parse listOf submodule
else if
option.type.name == "listOf" && option.type.nestedTypes.elemType.name == "submodule"

View File

@@ -50,6 +50,16 @@
];
description = "A list of enabled kernel modules";
};
# enum
colour = lib.mkOption {
type = lib.types.enum [
"red"
"blue"
"green"
];
default = "red";
description = "The colour of the user";
};
destinations = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (

View File

@@ -46,6 +46,12 @@
},
"description": "Some attributes"
},
"colour": {
"$exportedModuleInfo": { "path": ["colour"] },
"default": "red",
"description": "The colour of the user",
"enum": ["red", "blue", "green"]
},
"services": {
"$exportedModuleInfo": { "path": ["services"] },
"type": "object",

View File

@@ -23,7 +23,8 @@
export NIX_PATH=nixpkgs=${pkgs.path}
${pkgs.nix-unit}/bin/nix-unit \
${./.}/test.nix \
--eval-store $(realpath .)
--eval-store $(realpath .) \
--show-trace
touch $out
'';
};

View File

@@ -1,7 +1,14 @@
{
"_module.args": {
"declarations": ["lib/modules.nix"],
"description": "Additional arguments passed to each module in addition to ones\nlike `lib`, `config`,\nand `pkgs`, `modulesPath`.\n\nThis option is also available to all submodules. Submodules do not\ninherit args from their parent module, nor do they provide args to\ntheir parent module or sibling submodules. The sole exception to\nthis is the argument `name` which is provided by\nparent modules to a submodule and contains the attribute name\nthe submodule is bound to, or a unique generated name if it is\nnot bound to an attribute.\n\nSome arguments are already passed by default, of which the\nfollowing *cannot* be changed with this option:\n- {var}`lib`: The nixpkgs library.\n- {var}`config`: The results of all options after merging the values from all modules together.\n- {var}`options`: The options declared in all modules.\n- {var}`specialArgs`: The `specialArgs` argument passed to `evalModules`.\n- All attributes of {var}`specialArgs`\n\n Whereas option values can generally depend on other option values\n thanks to laziness, this does not apply to `imports`, which\n must be computed statically before anything else.\n\n For this reason, callers of the module system can provide `specialArgs`\n which are available during import resolution.\n\n For NixOS, `specialArgs` includes\n {var}`modulesPath`, which allows you to import\n extra modules from the nixpkgs package tree without having to\n somehow make the module aware of the location of the\n `nixpkgs` or NixOS directories.\n ```\n { modulesPath, ... }: {\n imports = [\n (modulesPath + \"/profiles/minimal.nix\")\n ];\n }\n ```\n\nFor NixOS, the default value for this option includes at least this argument:\n- {var}`pkgs`: The nixpkgs package set according to\n the {option}`nixpkgs.pkgs` option.\n",
"loc": ["_module", "args"],
"readOnly": false,
"type": "lazy attribute set of raw value"
},
"age": {
"declarations": [
"/home/grmpf/synced/projects/clan/clan-core/lib/jsonschema/example-interface.nix"
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",
@@ -12,9 +19,57 @@
"readOnly": false,
"type": "signed integer"
},
"colour": {
"declarations": [
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",
"text": "\"red\""
},
"description": "The colour of the user",
"loc": ["colour"],
"readOnly": false,
"type": "one of \"red\", \"blue\", \"green\""
},
"destinations": {
"declarations": [
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",
"text": "{ }"
},
"description": null,
"loc": ["destinations"],
"readOnly": false,
"type": "attribute set of (submodule)"
},
"destinations.<name>.name": {
"declarations": [
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",
"text": "\"name\""
},
"description": "the name of the backup job",
"loc": ["destinations", "<name>", "name"],
"readOnly": false,
"type": "string matching the pattern ^[a-zA-Z0-9._-]+$"
},
"destinations.<name>.repo": {
"declarations": [
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"description": "the borgbackup repository to backup to",
"loc": ["destinations", "<name>", "repo"],
"readOnly": false,
"type": "string"
},
"isAdmin": {
"declarations": [
"/home/grmpf/synced/projects/clan/clan-core/lib/jsonschema/example-interface.nix"
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",
@@ -27,7 +82,7 @@
},
"kernelModules": {
"declarations": [
"/home/grmpf/synced/projects/clan/clan-core/lib/jsonschema/example-interface.nix"
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",
@@ -40,7 +95,7 @@
},
"name": {
"declarations": [
"/home/grmpf/synced/projects/clan/clan-core/lib/jsonschema/example-interface.nix"
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",
@@ -53,7 +108,7 @@
},
"services": {
"declarations": [
"/home/grmpf/synced/projects/clan/clan-core/lib/jsonschema/example-interface.nix"
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"description": null,
"loc": ["services"],
@@ -62,7 +117,7 @@
},
"services.opt": {
"declarations": [
"/home/grmpf/synced/projects/clan/clan-core/lib/jsonschema/example-interface.nix"
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",
@@ -75,7 +130,7 @@
},
"userIds": {
"declarations": [
"/home/grmpf/synced/projects/clan/clan-core/lib/jsonschema/example-interface.nix"
"/Users/enzime/Work/clan/clan-core/lib/jsonschema/example-interface.nix"
],
"default": {
"_type": "literalExpression",

View File

@@ -4,6 +4,6 @@
slib ? (import ./. { inherit lib; } { }),
}:
{
# parseOption = import ./test_parseOption.nix { inherit lib slib; };
parseOption = import ./test_parseOption.nix { inherit lib slib; };
parseOptions = import ./test_parseOptions.nix { inherit lib slib; };
}

View File

@@ -25,6 +25,7 @@ in
export NIX_ABORT_ON_WARN=1
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {

View File

@@ -23,6 +23,7 @@ in
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
--show-trace \
${inputOverrides} \
--flake ${
self.filter {

View File

@@ -29,6 +29,7 @@ Service = dict[str, Any]
class Inventory(TypedDict):
machineClass: NotRequired[dict[str, Any]]
machines: NotRequired[dict[str, Machine]]
meta: NotRequired[Meta]
modules: NotRequired[dict[str, Any]]

View File

@@ -1,12 +1,13 @@
import importlib
import json
import logging
import re
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from clan_cli.errors import ClanError
from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.facts import public_modules as facts_public_modules
from clan_cli.facts import secret_modules as facts_secret_modules
from clan_cli.flake import Flake
@@ -56,10 +57,22 @@ class Machine:
kwargs.update({"extra": {"command_prefix": self.name}})
log.error(msg, *args, **kwargs)
@property
# `class` is a keyword, `_class` triggers `SLF001` so we use a sunder name
def _class_(self) -> str:
try:
return self.flake.select(
f"clanInternals.inventory.machineClass.{self.name}"
)
except ClanCmdError as e:
if re.search(f"error: attribute '{self.name}' missing", e.cmd.stderr):
return "nixos"
raise
@property
def system(self) -> str:
return self.flake.select(
f"nixosConfigurations.{self.name}.pkgs.hostPlatform.system"
f"{self._class_}Configurations.{self.name}.pkgs.hostPlatform.system"
)
@property

View File

@@ -238,6 +238,8 @@ def update_command(args: argparse.Namespace) -> None:
if len(args.machines) == 0:
ignored_machines = []
for machine in get_all_machines(args.flake, args.option):
if machine._class_ == "darwin":
continue
if machine.deployment.get("requireExplicitUpdate", False):
continue
try:
@@ -265,6 +267,11 @@ def update_command(args: argparse.Namespace) -> None:
machine.override_build_host = args.build_host
machine.host_key_check = HostKeyCheck.from_str(args.host_key_check)
for machine in machines:
if machine._class_ == "darwin":
machine.error("Updating macOS machines is not yet supported")
sys.exit(1)
deploy_machines(machines)
except KeyboardInterrupt:
log.warning("Interrupted by user")