feat: add NixOS module and proper Nix packaging

- nix/package.nix: two-phase build with fixed-output derivation for deps
- nix/nixos-module.nix: systemd service with systant.enable and systant.configFile
- flake.nix: expose nixosModules.default and overlays.default

Usage in NixOS config:
  systant.enable = true;
  systant.configFile = ./systant.toml;

When deps change, update hash: nix build .#systant 2>&1 | grep 'got:'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 20:19:27 -08:00
co-authored by Claude Opus 4.5
parent ae778ebdab
commit af4606c40b
4 changed files with 238 additions and 23 deletions
+123
View File
@@ -0,0 +1,123 @@
{ config, lib, pkgs, ... }:
let
cfg = config.systant;
settingsFormat = pkgs.formats.toml { };
in
{
options.systant = {
enable = lib.mkEnableOption "systant system monitoring agent";
package = lib.mkOption {
type = lib.types.package;
default = pkgs.systant;
defaultText = lib.literalExpression "pkgs.systant";
description = "The systant package to use.";
};
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to the systant configuration file (TOML).
If set, this takes precedence over the settings option.
'';
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
description = ''
Configuration for systant in Nix attribute set form.
Will be converted to TOML. Ignored if configFile is set.
'';
example = lib.literalExpression ''
{
mqtt = {
broker = "mqtt://localhost:1883";
topicPrefix = "systant";
};
entities = {
cpu_usage = {
type = "sensor";
state_command = "awk '/^cpu / {u=$2+$4; t=$2+$4+$5; print int(u*100/t)}' /proc/stat";
unit = "%";
icon = "mdi:cpu-64-bit";
name = "CPU Usage";
};
};
homeassistant = {
discovery = true;
discoveryPrefix = "homeassistant";
};
}
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "systant";
description = "User account under which systant runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "systant";
description = "Group under which systant runs.";
};
};
config = lib.mkIf cfg.enable {
# Create systant user/group if using defaults
users.users.${cfg.user} = lib.mkIf (cfg.user == "systant") {
isSystemUser = true;
group = cfg.group;
description = "Systant service user";
};
users.groups.${cfg.group} = lib.mkIf (cfg.group == "systant") { };
# Generate config file from settings if configFile not provided
environment.etc."systant/config.toml" = lib.mkIf (cfg.configFile == null && cfg.settings != { }) {
source = settingsFormat.generate "systant-config.toml" cfg.settings;
};
systemd.services.systant = {
description = "Systant system monitoring agent";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart =
let
configPath =
if cfg.configFile != null
then cfg.configFile
else "/etc/systant/config.toml";
in
"${cfg.package}/bin/systant run --config ${configPath}";
Restart = "on-failure";
RestartSec = "5s";
# Hardening
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
# Allow reading system metrics
ReadOnlyPaths = [
"/proc"
"/sys"
];
};
};
};
}
+79
View File
@@ -0,0 +1,79 @@
{
lib,
stdenvNoCC,
stdenv,
bun,
cacert,
}:
let
# Fixed-output derivation to fetch npm dependencies
# Update the hash when bun.lock changes by running:
# nix build .#systant 2>&1 | grep 'got:'
deps = stdenvNoCC.mkDerivation {
pname = "systant-deps";
version = "0.1.0";
src = lib.fileset.toSource {
root = ./..;
fileset = lib.fileset.unions [
./../package.json
./../bun.lock
];
};
nativeBuildInputs = [ bun cacert ];
buildPhase = ''
export HOME=$TMPDIR
bun install --frozen-lockfile
'';
installPhase = ''
cp -r node_modules $out
'';
outputHashMode = "recursive";
outputHashAlgo = "sha256";
# To update: nix build .#systant 2>&1 | grep 'got:'
outputHash = "sha256-hQ1ZzOFOHHeaAtyfCXxX6jpqB7poFLwavgMW8yMwaHw=";
};
in
stdenv.mkDerivation {
pname = "systant";
version = "0.1.0";
src = lib.fileset.toSource {
root = ./..;
fileset = lib.fileset.unions [
./../index.ts
./../src
./../package.json
./../tsconfig.json
];
};
nativeBuildInputs = [ bun ];
buildPhase = ''
export HOME=$TMPDIR
cp -r ${deps} node_modules
chmod -R u+w node_modules
bun build index.ts --compile --outfile systant
'';
installPhase = ''
mkdir -p $out/bin
cp systant $out/bin/systant
'';
# Bun's compiled binaries don't like being stripped
dontStrip = true;
meta = with lib; {
description = "System monitoring agent with MQTT and Home Assistant integration";
homepage = "https://git.ryanpandya.com/ryan/systant";
license = licenses.mit;
platforms = platforms.linux;
};
}