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:
parent
ae778ebdab
commit
af4606c40b
@ -9,6 +9,18 @@ Build the systant CLI binary.
|
|||||||
3. Report the binary size and location
|
3. Report the binary size and location
|
||||||
4. If there are errors, show them clearly and suggest fixes
|
4. If there are errors, show them clearly and suggest fixes
|
||||||
|
|
||||||
|
## Nix Build
|
||||||
|
|
||||||
|
For NixOS deployment, the binary is built by Nix using:
|
||||||
|
```bash
|
||||||
|
nix build .#systant
|
||||||
|
```
|
||||||
|
|
||||||
|
If you update dependencies (bun.lock), update the hash in `nix/package.nix`:
|
||||||
|
```bash
|
||||||
|
nix build .#systant 2>&1 | grep 'got:'
|
||||||
|
```
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
- No TypeScript errors
|
- No TypeScript errors
|
||||||
|
|||||||
47
flake.nix
47
flake.nix
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
description = "Systant";
|
description = "Systant - System monitoring agent with MQTT and Home Assistant integration";
|
||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
@ -11,48 +11,49 @@
|
|||||||
flake-utils,
|
flake-utils,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
{
|
||||||
|
# NixOS module (system-independent)
|
||||||
|
nixosModules.default = import ./nix/nixos-module.nix;
|
||||||
|
|
||||||
|
# Overlay to add systant to pkgs
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
systant = final.callPackage ./nix/package.nix { };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//
|
||||||
flake-utils.lib.eachDefaultSystem (
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config.allowUnfree = true;
|
config.allowUnfree = true;
|
||||||
|
overlays = [ self.overlays.default ];
|
||||||
};
|
};
|
||||||
systant-cli = pkgs.writeShellScriptBin "systant" ''
|
|
||||||
cd "$PROJECT_ROOT" && ./dist/systant "$@"
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
with pkgs;
|
|
||||||
{
|
{
|
||||||
devShell = pkgs.mkShell {
|
packages = {
|
||||||
buildInputs = [
|
systant = pkgs.systant;
|
||||||
|
default = pkgs.systant;
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pkgs.systant}/bin/systant";
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
bashInteractive
|
bashInteractive
|
||||||
glibcLocales
|
glibcLocales
|
||||||
git
|
git
|
||||||
bun
|
bun
|
||||||
inotify-tools
|
inotify-tools
|
||||||
claude-code
|
|
||||||
systant-cli
|
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PROJECT_ROOT=$PWD
|
export PROJECT_ROOT=$PWD
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
packages = {
|
|
||||||
systant-server = pkgs.callPackage ./nix/package.nix {
|
|
||||||
src = ./server;
|
|
||||||
};
|
|
||||||
systant-cli = pkgs.systant-cli;
|
|
||||||
default = systant-cli;
|
|
||||||
};
|
|
||||||
|
|
||||||
apps = {
|
|
||||||
default = {
|
|
||||||
type = "app";
|
|
||||||
program = "${self.packages.${system}.default}/bin/systant";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
123
nix/nixos-module.nix
Normal file
123
nix/nixos-module.nix
Normal 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
nix/package.nix
Normal file
79
nix/package.nix
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user