Merge branch 'refactor/snowflake' into v0.2.6-alpha

This commit is contained in:
do butterflies cry? 2026-03-17 20:42:44 +10:00
commit e7fdbf416c
Signed by: cry
GPG key ID: F68745A836CA0412
30 changed files with 958 additions and 324 deletions

53
flake.lock generated
View file

@ -9,11 +9,11 @@
"utils": "utils"
},
"locked": {
"lastModified": 1766051518,
"narHash": "sha256-znKOwPXQnt3o7lDb3hdf19oDo0BLP4MfBOYiWkEHoik=",
"lastModified": 1770019181,
"narHash": "sha256-hwsYgDnby50JNVpTRYlF3UR/Rrpt01OrxVuryF40CFY=",
"owner": "serokell",
"repo": "deploy-rs",
"rev": "d5eff7f948535b9c723d60cd8239f8f11ddc90fa",
"rev": "77c906c0ba56aabdbc72041bf9111b565cdd6171",
"type": "github"
},
"original": {
@ -68,11 +68,11 @@
"spectrum": "spectrum"
},
"locked": {
"lastModified": 1771365290,
"narHash": "sha256-1XJOslVyF7yzf6yd/yl1VjGLywsbtwmQh3X1LuJcLI4=",
"lastModified": 1773018425,
"narHash": "sha256-fpgZBmZpKoEXEowBK/6m8g9FcOLWQ4UxhXHqCw2CpSM=",
"owner": "microvm-nix",
"repo": "microvm.nix",
"rev": "789c90b164b55b4379e7a94af8b9c01489024c18",
"rev": "25ebda3c558e923720c965832dc9a04f559a055c",
"type": "github"
},
"original": {
@ -129,11 +129,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1768323494,
"narHash": "sha256-yBXJLE6WCtrGo7LKiB6NOt6nisBEEkguC/lq/rP3zRQ=",
"lastModified": 1773375660,
"narHash": "sha256-SEzUWw2Rf5Ki3bcM26nSKgbeoqi2uYy8IHVBqOKjX3w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2c3e5ec5df46d3aeee2a1da0bfedd74e21f4bf3a",
"rev": "3e20095fe3c6cbb1ddcef89b26969a69a1570776",
"type": "github"
},
"original": {
@ -166,11 +166,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1770975056,
"narHash": "sha256-ZXTz/P3zUbbM6lNXzt91u8EwfNqhXpYMu8+wvFZqQHE=",
"lastModified": 1773738366,
"narHash": "sha256-oH22HyNHEdCoCQo734sQCHUr6C0jmGQJMZ13dsgEHkk=",
"owner": "cry128",
"repo": "nt",
"rev": "f42dcdd49a7921a7f433512e83d5f93696632412",
"rev": "f32c3a726a3d608d30aaaa1df2301c1eaf5ef8f4",
"type": "github"
},
"original": {
@ -185,17 +185,38 @@
"microvm": "microvm",
"nixpkgs": "nixpkgs",
"nt": "nt",
"sops-nix": "sops-nix",
"systems": "systems_3"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773096132,
"narHash": "sha256-M3zEnq9OElB7zqc+mjgPlByPm1O5t2fbUrH3t/Hm5Ag=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "d1ff3b1034d5bab5d7d8086a7803c5a5968cd784",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"spectrum": {
"flake": false,
"locked": {
"lastModified": 1759482047,
"narHash": "sha256-H1wiXRQHxxPyMMlP39ce3ROKCwI5/tUn36P8x6dFiiQ=",
"lastModified": 1772189877,
"narHash": "sha256-i1p90Rgssb//aNiTDFq46ZG/fk3LmyRLChtp/9lddyA=",
"ref": "refs/heads/main",
"rev": "c5d5786d3dc938af0b279c542d1e43bce381b4b9",
"revCount": 996,
"rev": "fe39e122d898f66e89ffa17d4f4209989ccb5358",
"revCount": 1255,
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
},

View file

@ -44,7 +44,7 @@
nt,
...
} @ inputs:
import ./cerulean
import ./nix
{
inherit inputs self nt;
inherit (nt) mix;

View file

@ -15,22 +15,28 @@
mix,
inputs,
...
} @ args:
mix.newMixture args (mixture: {
submods.public = [
./snow
];
} @ args: let
mixArgs =
args
// {
inherit (inputs.nixpkgs) lib;
};
in
mix.newMixture mixArgs (mixture: {
submods.public = [
./snow
];
version = "0.2.6-alpha";
version = "0.2.6-alpha";
overlays = [
# build deploy-rs as a package not from the flake input,
# hence we can rely on a nixpkg binary cache.
inputs.deploy-rs.overlays.default
];
overlays = [
# build deploy-rs as a package not from the flake input,
# hence we can rely on a nixpkg binary cache.
inputs.deploy-rs.overlays.default
];
nixosModules = rec {
default = cerulean;
cerulean = ./nixos;
};
})
nixosModules = rec {
default = cerulean;
cerulean = ./nixos;
};
})

View file

@ -18,13 +18,13 @@
node,
pkgs,
lib,
_cerulean,
_snow,
...
} @ args: {
imports =
[
_cerulean.inputs.sops-nix.nixosModules.sops
# _cerulean.inputs.microvm.nixosModules.microvm
_snow.inputs.sops-nix.nixosModules.sops
# _snow.inputs.microvm.nixosModules.microvm
# add support for `options.legacyImports`
# ./legacy-imports.nix
@ -36,7 +36,7 @@
(import /${root}/nixpkgs.nix)
]
# homemanager options declarations
++ (lib.optional (_cerulean.homeManager != null) ./home.nix)
++ (lib.optional (_snow.homeManager != null) ./home.nix)
# remote deployment configuration
++ (lib.optional (node.deploy.ssh.host != null) ./remote-deploy);
@ -46,7 +46,7 @@
(with pkgs; [
sops
])
++ (with _cerulean.inputs; [
++ (with _snow.inputs; [
deploy-rs.packages.${system}.default
]);
}

View file

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
{
_cerulean,
_snow,
config,
root,
lib,
@ -30,7 +30,7 @@
;
in {
imports = [
_cerulean.homeManager.nixosModules.default
_snow.homeManager.nixosModules.default
];
options = {
@ -69,7 +69,7 @@ in {
_module.args.username = name;
});
extraSpecialArgs = _cerulean.specialArgs;
extraSpecialArgs = _snow.specialArgs;
sharedModules = [
../home

View file

@ -12,180 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
{
this,
self,
inputs,
systems,
nt,
mix,
...
} @ args: let
inherit
(builtins)
all
attrNames
elem
mapAttrs
warn
;
} @ args:
mix.newMixture (removeAttrs args ["this"]) (mixture: {
submods.public = [
./lib
];
inherit (inputs.nixpkgs) lib;
inherit (nt) findImport;
in
mix.newMixture args (mixture: let
inherit (mixture) mapNodes;
in {
includes.private = [
./lib/nodes.nix
];
inherit findImport;
# snow.flake
flake = flakeInputs: root: let
module = lib.evalModules {
class = "snowflake";
# TODO: abort if inputs contains reserved names
specialArgs =
(flakeInputs
// {
inherit systems root;
inherit (this) snow;
inputs = flakeInputs;
})
|> (x: builtins.removeAttrs x ["self" "nodes"]);
modules = [
./module.nix
({config, ...}: {
_module.args = {
self = config;
nodes = config.nodes.nodes;
};
})
];
};
nodes = module.config.nodes;
in rec {
nixosConfigurations = mapNodes nodes (
{
base,
lib,
name,
node,
groupModules,
...
}: let
homeManager =
if node.homeManager != null
then node.homeManager
else if nodes.homeManager != null
then nodes.homeManager
else
warn ''
[snowflake] Neither `nodes.homeManager` nor `nodes.nodes.${name}.homeManager` were specified!
[snowflake] home-manager will NOT be used! User configuration will be ignored!
''
null;
userArgs = nodes.args // node.args;
ceruleanArgs = {
inherit systems root base nodes node;
inherit (node) system;
inherit (this) snow;
hostname = name;
_cerulean = {
inherit inputs userArgs ceruleanArgs homeManager;
specialArgs = userArgs // ceruleanArgs;
};
};
specialArgs = assert (userArgs
|> attrNames
|> all (argName:
! ceruleanArgs ? argName
|| abort ''
`specialArgs` are like super important to Cerulean my love... </3
But `args.${argName}` is a reserved argument name :(
''));
ceruleanArgs._cerulean.specialArgs;
in
lib.nixosSystem {
inherit (node) system;
inherit specialArgs;
modules =
[
self.nixosModules.default
(findImport /${root}/hosts/${name})
]
++ (groupModules root)
++ node.modules
++ nodes.modules;
}
);
deploy.nodes = mapNodes nodes ({
name,
node,
...
}: let
inherit
(node.deploy)
ssh
user
interactiveSudo
remoteBuild
rollback
autoRollback
magicRollback
activationTimeout
confirmTimeout
;
nixosFor = system: inputs.deploy-rs.lib.${system}.activate.nixos;
in {
hostname =
if ssh.host != null
then ssh.host
else "";
profilesOrder = ["default"]; # profiles priority
profiles.default = {
path = nixosFor node.system nixosConfigurations.${name};
user = user;
sudo = "sudo -u";
interactiveSudo = interactiveSudo;
fastConnection = false;
autoRollback = autoRollback -> rollback;
magicRollback = magicRollback -> rollback;
activationTimeout = activationTimeout;
confirmTimeout = confirmTimeout;
remoteBuild = remoteBuild;
sshUser = ssh.user;
sshOpts =
ssh.opts
++ (
if elem "-p" ssh.opts
then []
else ["-p" (toString ssh.port)]
)
++ (
if elem "-A" ssh.opts
then []
else ["-A"]
);
};
});
checks =
inputs.deploy-rs.lib
|> mapAttrs (system: deployLib:
deployLib.deployChecks deploy);
};
})
includes.public = [
./flake
];
})

View file

@ -0,0 +1,68 @@
{
self,
this,
inputs,
systems,
...
}: let
inherit
(builtins)
attrNames
concatStringsSep
filter
length
warn
;
inherit (inputs.nixpkgs) lib;
in {
# snow.flake
# XXX: TODO: stop taking in root as parameter (maybe take self instead?)
flake = flakeInputs: root: let
snowflake = lib.evalModules {
class = "snowflake";
specialArgs = let
reservedSpecialArgs = {
# inherit (this) snow;
snow = this;
inherit systems root;
inputs = flakeInputs;
_snowFlake = {
inherit self inputs;
};
};
warnIfReserved = let
getReservedNames = names:
reservedSpecialArgs
|> attrNames
|> filter (name: names?${name});
reservedNames =
flakeInputs
|> attrNames
|> getReservedNames;
in
(length reservedNames == 0)
|| warn ''
[snow] Your `flake.nix` declares inputs with reserved names!
[snow] These will be accessible only via `inputs.''${NAME}`
[snow] Please rename the following:
[snow] ${concatStringsSep reservedNames ", "}
''
true;
in
assert warnIfReserved;
flakeInputs // reservedSpecialArgs;
modules = [
./nodes
./modules
./outputs
(this.lib.findImport /${root}/snow)
];
};
in
snowflake.config.outputs;
}

View file

@ -0,0 +1,3 @@
# Snow Module Backend
This source code was tedious so it's just a modified version of the module backend of
[github:hercules-ci/flake-parts](https://github.com/hercules-ci/flake-parts/tree/main/modules).

View file

@ -0,0 +1,69 @@
{
lib,
snow,
...
}: let
inherit
(lib)
mkOption
types
;
inherit
(snow.lib)
mkPerSystemFlakeOutput
;
derivationType =
lib.types.package
// {
check = lib.isDerivation;
};
programType = lib.types.coercedTo derivationType lib.getExe lib.types.str;
appType = lib.types.submodule {
options = {
type = mkOption {
type = lib.types.enum ["app"];
default = "app";
description = ''
A type tag for `apps` consumers.
'';
};
program = mkOption {
type = programType;
description = ''
A path to an executable or a derivation with `meta.mainProgram`.
'';
};
meta = mkOption {
type = types.lazyAttrsOf lib.types.raw;
default = {};
# TODO refer to Nix manual 2.25
description = ''
Metadata information about the app.
Standardized in Nix at <https://github.com/NixOS/nix/pull/11297>.
Note: `nix flake check` is only aware of the `description` attribute in `meta`.
'';
};
};
};
in
mkPerSystemFlakeOutput {
name = "apps";
option = mkOption {
type = types.lazyAttrsOf appType;
default = {};
description = ''
Programs runnable with nix run `<name>`.
'';
example = lib.literalExpression ''
{
default.program = "''${config.packages.hello}/bin/hello";
}
'';
};
file = ./apps.nix;
}

View file

@ -0,0 +1,26 @@
{
lib,
snow,
...
}: let
inherit
(lib)
mkOption
types
;
inherit
(snow.lib)
mkPerSystemFlakeOutput
;
in
mkPerSystemFlakeOutput {
name = "checks";
option = mkOption {
type = types.lazyAttrsOf types.package;
default = {};
description = ''
Derivations to be built by [`nix flake check`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake-check.html).
'';
};
file = ./checks.nix;
}

View file

@ -0,0 +1,15 @@
{...}: {
imports = [
./outputs.nix
./apps.nix
./checks.nix
./devShells.nix
./formatter.nix
./legacyPackages.nix
./nixosConfigurations.nix
./nixosModules.nix
./overlays.nix
./packages.nix
];
}

View file

@ -0,0 +1,35 @@
{
lib,
snow,
...
}: let
inherit
(lib)
mkOption
types
literalExpression
;
inherit
(snow.lib)
mkPerSystemFlakeOutput
;
in
mkPerSystemFlakeOutput {
name = "devShells";
option = mkOption {
type = types.lazyAttrsOf types.package;
default = {};
description = ''
An attribute set of packages to be used as shells.
[`nix develop .#<name>`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-develop.html) will run `devShells.<name>`.
'';
example = literalExpression ''
{
default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ wget bat cargo ];
};
}
'';
};
file = ./devShells.nix;
}

View file

@ -0,0 +1,26 @@
{
lib,
snow,
...
}: let
inherit
(lib)
mkOption
types
;
inherit
(snow.lib)
mkPerSystemFlakeOutput
;
in
mkPerSystemFlakeOutput {
name = "formatter";
option = mkOption {
type = types.nullOr types.package;
default = null;
description = ''
A package used by [`nix fmt`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-fmt.html).
'';
};
file = ./apps.nix;
}

View file

@ -0,0 +1,26 @@
{
lib,
snow,
...
}: let
inherit
(lib)
mkOption
types
;
inherit
(snow.lib)
mkPerSystemFlakeOutput
;
in
mkPerSystemFlakeOutput {
name = "legacyPackages";
option = mkOption {
type = types.lazyAttrsOf types.raw;
default = {};
description = ''
Used for nixpkgs packages, also accessible via `nix build .#<name>` [`nix build .#<name>`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-build.html).
'';
};
file = ./legacyPackages.nix;
}

View file

@ -0,0 +1,35 @@
{lib, ...}: let
inherit
(lib)
mkOption
types
literalExpression
;
in {
options = {
outputs.nixosConfigurations = mkOption {
type = types.lazyAttrsOf types.raw;
default = {};
description = ''
Instantiated NixOS configurations. Used by `nixos-rebuild`.
`nixosConfigurations` is for specific machines. If you want to expose
reusable configurations, add them to [`nixosModules`](#opt-flake.nixosModules)
in the form of modules (no `lib.nixosSystem`), so that you can reference
them in this or another flake's `nixosConfigurations`.
'';
example = literalExpression ''
{
my-machine = inputs.nixpkgs.lib.nixosSystem {
# system is not needed with freshly generated hardware-configuration.nix
# system = "x86_64-linux"; # or set nixpkgs.hostPlatform in a module.
modules = [
./my-machine/nixos-configuration.nix
config.nixosModules.my-module
];
};
}
'';
};
};
}

View file

@ -0,0 +1,29 @@
{
lib,
moduleLocation,
...
}: let
inherit
(lib)
mapAttrs
mkOption
types
;
in {
options = {
outputs.nixosModules = mkOption {
type = types.lazyAttrsOf types.deferredModule;
default = {};
apply = mapAttrs (k: v: {
_class = "nixos";
_file = "${toString moduleLocation}#nixosModules.${k}";
imports = [v];
});
description = ''
NixOS modules.
You may use this for reusable pieces of configuration, service modules, etc.
'';
};
};
}

View file

@ -0,0 +1,46 @@
{
lib,
config,
...
}: let
inherit
(lib)
mkOption
types
;
in {
options = {
outputs = mkOption {
type = types.submoduleWith {
modules = [
{
freeformType =
types.lazyAttrsOf
(types.unique
{
message = ''
No option has been declared for this flake output attribute, so its definitions can't be merged automatically.
Possible solutions:
- Load a module that defines this flake output attribute
- Declare an option for this flake output attribute
- Make sure the output attribute is spelled correctly
- Define the value only once, with a single definition in a single module
'';
}
types.raw);
}
];
};
description = ''
Raw flake output attributes. Any attribute can be set here, but some
attributes are represented by options, to provide appropriate
configuration merging.
'';
};
};
config = {
# ensure a minimal version is set
outputs = {};
};
}

View file

@ -0,0 +1,31 @@
{lib, ...}: let
inherit
(lib)
mkOption
types
;
in {
options = {
outputs.overlays = mkOption {
# uniq -> ordered: https://github.com/NixOS/nixpkgs/issues/147052
# also update description when done
type = types.lazyAttrsOf (types.uniq (types.functionTo (types.functionTo (types.lazyAttrsOf types.unspecified))));
# This eta expansion exists for the sole purpose of making nix flake check happy.
apply = lib.mapAttrs (_k: f: final: prev: f final prev);
default = {};
example = lib.literalExpression ''
{
default = final: prev: {};
}
'';
description = ''
An attribute set of [overlays](https://nixos.org/manual/nixpkgs/stable/#chap-overlays).
Note that the overlays themselves are not mergeable. While overlays
can be composed, the order of composition is significant, but the
module system does not guarantee sufficiently deterministic
definition ordering, across versions and when changing `imports`.
'';
};
};
}

View file

@ -0,0 +1,29 @@
{
lib,
snow,
...
}: let
inherit
(lib)
mkOption
types
;
inherit
(snow.lib)
mkPerSystemFlakeOutput
;
in
mkPerSystemFlakeOutput {
name = "packages";
option = mkOption {
type = types.lazyAttrsOf types.package;
default = {};
description = ''
An attribute set of packages to be built by [`nix build`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-build.html).
`nix build .#<name>` will build `packages.<name>`.
'';
};
file = ./packages.nix;
}

View file

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
{
_snowFlake,
lib,
specialArgs,
...
@ -25,38 +26,20 @@
in
mkOption {
description = ''
Cerulean node declarations.
Snowflake node declarations.
'';
type = types.submoduleWith {
inherit specialArgs;
modules = [
{
imports = [./shared.nix];
options = {
groups = mkOption {
type = types.attrs;
default = {};
example = lib.literalExpression "{ servers = { staging = {}; production = {}; }; }";
description = ''
Hierarchical groups that nodes can be a member of.
'';
};
nodes = mkOption {
type = types.attrsOf (types.submoduleWith {
inherit specialArgs;
modules = [(import ./submodule.nix)];
});
# example = { ... }; # TODO
description = ''
Node (host systems) declarations.
'';
};
};
}
./nodes.nix
];
};
};
config = {
nodes = {
base = _snowFlake.inputs.nixpkgs;
};
};
}

View file

@ -14,16 +14,24 @@
{
lib,
systems,
nodesConfig,
groups,
groupLibs,
...
}: {
imports = [./shared.nix];
options = let
inherit
(lib)
mkOption
types
;
inherit
(groupLibs)
resolveGroupsInheritance
;
flakeRef = types.either types.str types.path;
in {
enabled = lib.mkOption {
type = types.bool;
@ -43,6 +51,65 @@
'';
};
base = lib.mkOption {
# In newer Nix versions, particularly with lazy trees, outPath of
# flakes becomes a Nix-language path object. We deliberately allow this
# to gracefully come through the interface in discussion with @roberth.
#
# See: https://github.com/NixOS/nixpkgs/pull/278522#discussion_r1460292639
type = types.nullOr flakeRef;
default = nodesConfig.base;
defaultText = "nodes.base";
example = lib.literalExpression "inputs.nixpkgs";
description = ''
The path to the nixpkgs source used to build a system. A `base` package set
is required to be set, and can be specified via either:
1. `options.nodes.base` (default `base` used for all systems)
2. `options.nodes.nodes.<name>.base` (takes prescedence over `options.nodes.base`)
This can also be optionally set if the NixOS system is not built with a flake but still uses
pinned sources: set this to the store path for the nixpkgs sources used to build the system,
as may be obtained by `fetchTarball`, for example.
Note: the name of the store path must be "source" due to
<https://github.com/NixOS/nix/issues/7075>.
'';
};
homeManager = mkOption {
type = types.nullOr flakeRef;
default = nodesConfig.homeManager;
defaultText = "nodes.homeManager";
example = lib.literalExpression "inputs.home-manager";
description = ''
The path to the home-manager source. A `homeManager` flake reference
is required to be set for `homes/` to be evaluated, and can be specified via either:
1. `options.nodes.homeManager` (default `homManager` used for all systems)
2. `options.nodes.nodes.<name>.homeManager` (takes prescedence over `options.nodes.homeManager`)
'';
};
modules = mkOption {
type = types.listOf types.raw;
default = [];
example = lib.literalExpression "[ { environment.systemPackages = [ pkgs.git ]; } ]";
description = ''
Shared modules to import; equivalent to the NixOS module system's `extraModules`.
'';
};
args = mkOption {
type = types.attrs;
default = {};
example = lib.literalExpression "{ inherit inputs; }";
description = ''
Shared args to provided for each node; equivalent to the NixOS module system's `specialArgs`.
'';
};
groups = mkOption {
# TODO: write a custom group type that validates better than types.attrs lol
type = types.functionTo (types.listOf types.attrs);
@ -51,6 +118,9 @@
description = ''
A function from the `groups` hierarchy to a list of groups this node inherits from.
'';
# apply = groupsFn:
# groupsFn nodesConfig.groups |> resolveGroupsInheritance;
};
deploy = {
@ -91,7 +161,7 @@
example = false;
description = ''
Whether to enable interactive sudo (password based sudo).
NOT RECOMMENDED. Use one of Cerulean's recommended auth methods instead.
NOT RECOMMENDED. Use one of Snowflake's recommended auth methods instead.
'';
};
@ -164,7 +234,7 @@
user = mkOption {
type = types.str;
default = "cerubld";
default = "snowbld";
example = "custom-user";
description = ''
The user to connect to over ssh during deployment.
@ -183,7 +253,7 @@
publicKeys = mkOption {
type = types.listOf types.str;
default = [];
example = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIeyZuUUmyUYrYaEJwEMvcXqZFYm1NaZab8klOyK6Imr me@puter"];
example = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIeyZuUUmyUYrYaEJwEMvcXqZFYm1NaZab8klOyK6Imr me@myputer"];
description = ''
SSH public keys that will be authorized to the deployment user.
This key is intended solely for deployment, allowing for fine-grained permission control.
@ -201,4 +271,33 @@
};
};
};
# config = let
# throwGotNull = name:
# throw ''
# [snow] `nodes.<name>.${name}` must be set for all nodes! (got: <null>)
# '';
# givenSystem =
# (config.system != null)
# || throwGotNull "system";
# givenBase =
# (config.base != null)
# || throwGotNull "base";
# givenHomeManager =
# (config.homeManager != null)
# || throwGotNull "homeManager";
# givenDeployHost =
# (config.deploy.ssh.host != null)
# || throwGotNull "deploy.ssh.host";
# in
# assert givenSystem
# && givenBase
# && givenHomeManager
# && givenDeployHost; {
# # extend these from the nodes configuration
# inherit (nodesConfig) modules args;
# };
}

View file

@ -11,7 +11,15 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{lib, ...}: let
{
_snowFlake,
snow,
root,
lib,
config,
specialArgs,
...
}: let
inherit
(lib)
mkOption
@ -19,6 +27,11 @@
;
flakeRef = types.either types.str types.path;
groupLibs = import ./groups.nix {
inherit snow root;
inherit (_snowFlake.inputs) nt;
};
in {
options = {
base = lib.mkOption {
@ -49,6 +62,18 @@ in {
'';
};
homeManager = mkOption {
type = types.nullOr flakeRef;
default = null;
example = lib.literalExpression "inputs.home-manager";
description = ''
The path to the home-manager source. A `homeManager` flake reference
is required to be set for `homes/` to be evaluated, and can be specified via either:
1. `options.nodes.homeManager` (default `homManager` used for all systems)
2. `options.nodes.nodes.<name>.homeManager` (takes prescedence over `options.nodes.homeManager`)
'';
};
modules = mkOption {
type = types.listOf types.raw;
default = [];
@ -67,15 +92,27 @@ in {
'';
};
homeManager = mkOption {
type = types.nullOr flakeRef;
default = null;
example = lib.literalExpression "inputs.home-manager";
groups = mkOption {
type = types.attrs;
default = {};
example = lib.literalExpression "{ servers = { staging = {}; production = {}; }; }";
description = ''
The path to the home-manager source. A `homeManager` flake reference
is required to be set for `homes/` to be evaluated, and can be specified via either:
1. `options.nodes.homeManager` (default `homManager` used for all systems)
2. `options.nodes.nodes.<name>.homeManager` (takes prescedence over `options.nodes.homeManager`)
Hierarchical groups that nodes can be a member of.
'';
};
nodes = mkOption {
type = types.attrsOf (types.submoduleWith {
specialArgs =
specialArgs
// {
nodesConfig = config;
inherit groupLibs;
};
modules = [./node.nix];
});
description = ''
Node (host systems) declarations.
'';
};
};

View file

@ -0,0 +1,10 @@
{
config,
_snowFlake,
...
}: {
outputs.checks =
_snowFlake.inputs.deploy-rs.lib
|> builtins.mapAttrs (system: deployLib:
deployLib.deployChecks config.outputs.deploy);
}

View file

@ -0,0 +1,7 @@
{...}: {
imports = [
./checks.nix
./deploy.nix
./nixosConfigurations.nix
];
}

View file

@ -0,0 +1,63 @@
{
_snowFlake,
snow,
config,
...
}: {
outputs.deploy.nodes = snow.lib.mapNodes config.nodes ({
name,
node,
...
}: let
inherit
(node.deploy)
ssh
user
interactiveSudo
remoteBuild
rollback
autoRollback
magicRollback
activationTimeout
confirmTimeout
;
nixosFor = system: _snowFlake.inputs.deploy-rs.lib.${system}.activate.nixos;
in {
hostname =
if ssh.host != null
then ssh.host
else "";
profilesOrder = ["default"]; # profiles priority
profiles.default = {
path = nixosFor node.system config.outputs.nixosConfigurations.${name};
user = user;
sudo = "sudo -u";
interactiveSudo = interactiveSudo;
fastConnection = false;
autoRollback = autoRollback -> rollback;
magicRollback = magicRollback -> rollback;
activationTimeout = activationTimeout;
confirmTimeout = confirmTimeout;
remoteBuild = remoteBuild;
sshUser = ssh.user;
sshOpts =
ssh.opts
++ (
if builtins.elem "-p" ssh.opts
then []
else ["-p" (toString ssh.port)]
)
++ (
if builtins.elem "-A" ssh.opts
then []
else ["-A"]
);
};
});
}

View file

@ -0,0 +1,84 @@
{
_snowFlake,
snow,
config,
systems,
root,
...
}: let
inherit
(builtins)
all
attrNames
warn
;
inherit
(config)
nodes
;
in {
outputs.nixosConfigurations = let
groups = snow.lib.parseGroupDecls root config.nodes.groups;
in
snow.lib.mapNodes nodes (
{
base,
lib,
name,
node,
...
}: let
nodeGroups =
(node.groups groups)
|> snow.lib.resolveGroupsInheritance
|> snow.lib.groupModules;
homeManager =
if node.homeManager != null
then node.homeManager
else if nodes.homeManager != null
then nodes.homeManager
else
warn ''
[snowflake] Neither `nodes.homeManager` nor `nodes.nodes.${name}.homeManager` were specified!
[snowflake] home-manager will NOT be used! User configuration will be ignored!
''
null;
userArgs = nodes.args // node.args;
snowArgs = {
inherit systems snow root base nodes node;
inherit (node) system;
hostname = name;
_snow = {
inherit (_snowFlake) inputs;
inherit userArgs snowArgs homeManager;
specialArgs = userArgs // snowArgs;
};
};
specialArgs = assert (userArgs
|> attrNames
|> all (argName:
! snowArgs ? argName
|| abort ''
`specialArgs` are like super important to Snow my love... </3
But `args.${argName}` is a reserved argument name :(
''));
snowArgs._snow.specialArgs;
in
lib.nixosSystem {
inherit (node) system;
inherit specialArgs;
modules =
[
_snowFlake.self.nixosModules.default
(snow.lib.findImport /${root}/hosts/${name})
]
++ nodeGroups
++ node.modules
++ nodes.modules;
}
);
}

View file

@ -12,12 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
{
root,
snow,
nt,
mix,
...
}: {
imports = [
./nodes
(snow.findImport /${root}/snow)
} @ args:
mix.newMixture args (mixture: {
includes.public = [
./util.nix
./nixpkgs.nix
./nodes.nix
];
}
})

54
nix/snow/lib/nixpkgs.nix Normal file
View file

@ -0,0 +1,54 @@
{
inputs,
lib,
...
}: let
inherit
(lib)
mkOption
types
;
# A best effort, lenient estimate. Please use a recent nixpkgs lib if you
# override it at all.
minVersion = "23.05pre-git";
isNixpkgsValidVersion = let
revInfo =
if inputs.nixpkgs?rev
then " (nixpkgs-lib.rev: ${inputs.nixpkgs.rev})"
else "";
in
(builtins.compareVersions lib.version minVersion >= 0)
|| abort ''
The nixpkgs dependency of snow was overridden but is too old.
The minimum supported version of nixpkgs-lib is ${minVersion},
but the actual version is ${lib.version}${revInfo}.
'';
in
assert isNixpkgsValidVersion; {
# Helper function for defining a per-system option that
# gets transposed by the usual flake system logic to a
# top-level outputs attribute.
mkPerSystemFlakeOutput = {
name,
option,
file,
}: {
_file = file;
options = {
outputs.${name} = mkOption {
type = types.attrsWith {
elemType = option.type;
lazy = true;
placeholder = "system";
};
default = {};
description = ''
See {option}`perSystem.${name}` for description and examples.
'';
};
};
};
}

View file

@ -1,17 +1,8 @@
# Copyright 2025-2026 _cry64 (Emile Clark-Boman)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{nt, ...}: let
{
this,
nt,
...
}: let
inherit
(builtins)
concatLists
@ -23,53 +14,9 @@
typeOf
;
inherit (nt.prim) uniq;
rootGroupName = "all";
parseGroupsDecl = groups: let
validGroup = g:
isAttrs g
|| throw ''
Cerulean Nexus groups must be provided as attribute sets, got "${typeOf g}" instead!
Ensure all the group definitions are attribute sets under your call to `cerulean.mkNexus`.
NOTE: Groups can be accessed via `self.groups.PATH.TO.YOUR.GROUP`
'';
delegate = parent: gName: g: let
result =
(g
// {
_name = gName;
_parent = parent;
})
|> mapAttrs (name: value:
if elem name ["_name" "_parent"]
# ignore metadata fields
then value
else assert validGroup value; (delegate result name value));
in
result;
in
assert validGroup groups;
delegate null rootGroupName groups;
getGroupModules = root: groups:
# ensure root group is always added
groups
# add all inherited groups via _parent
|> map (let
delegate = g:
if g._parent == null
then [g]
else [g] ++ delegate (g._parent);
in
delegate)
# flatten recursion result
|> concatLists
# find import location
|> map (group: nt.findImport /${root}/groups/${group._name})
# filter by uniqueness
|> nt.prim.unique
# ignore missing groups
|> filter pathExists;
in {
mapNodes = nodes: f:
nodes.nodes
@ -82,7 +29,7 @@ in {
then nodes.base
else
abort ''
Cerulean cannot construct nodes node "${name}" without a base package source.
snow cannot construct nodes node "${name}" without a base package source.
Ensure `nodes.nodes.*.base` or `nodes.base` is a flake reference to the github:NixOS/nixpkgs repository.
'';
in
@ -90,7 +37,51 @@ in {
inherit name node base;
inherit (base) lib;
groups = node.groups (parseGroupsDecl nodes.groups);
groupModules = root: getGroupModules root groups;
inherit (node) groups;
});
groupModules = map (group: group._module);
parseGroupDecls = root: groupDecls: let
validGroup = g:
isAttrs g
|| throw ''
Snow node groups must be provided as attribute sets, got "${typeOf g}" instead!
Ensure all the group definitions are attribute sets under your call to `snow.flake`.
'';
delegate = parent: gName: g: let
result =
(g
// {
_name = gName;
_parent = parent;
_module = this.lib.findImport /${root}/groups/${gName};
})
|> mapAttrs (name: value:
if elem name ["_name" "_parent" "_module"]
# ignore metadata fields
then value
else assert validGroup value; (delegate result name value));
in
result;
in
assert validGroup groupDecls;
delegate null rootGroupName groupDecls;
resolveGroupsInheritance = groups:
groups
# add all inherited groups via _parent
|> map (let
delegate = g:
if g._parent == null
then [g]
else [g] ++ delegate (g._parent);
in
delegate)
# flatten recursion result
|> concatLists
# ignore missing groups
|> filter (group: pathExists group._module)
# filter by uniqueness
|> uniq;
}

3
nix/snow/lib/util.nix Normal file
View file

@ -0,0 +1,3 @@
{nt, ...}: {
inherit (nt) findImport;
}