vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / misc / klipper.nix
blobf0972f8caff49fab5647e1723bc309f1f7746f80
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.klipper;
4   format = pkgs.formats.ini {
5     # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
6     listToValue = l:
7       if builtins.length l == 1 then lib.generators.mkValueStringDefault { } (lib.head l)
8       else lib.concatMapStrings (s: "\n  ${lib.generators.mkValueStringDefault {} s}") l;
9     mkKeyValue = lib.generators.mkKeyValueDefault { } ":";
10   };
13   ##### interface
14   options = {
15     services.klipper = {
16       enable = lib.mkEnableOption "Klipper, the 3D printer firmware";
18       package = lib.mkPackageOption pkgs "klipper" { };
20       logFile = lib.mkOption {
21         type = lib.types.nullOr lib.types.path;
22         default = null;
23         example = "/var/lib/klipper/klipper.log";
24         description = ''
25           Path of the file Klipper should log to.
26           If `null`, it logs to stdout, which is not recommended by upstream.
27         '';
28       };
30       inputTTY = lib.mkOption {
31         type = lib.types.path;
32         default = "/run/klipper/tty";
33         description = "Path of the virtual printer symlink to create.";
34       };
36       apiSocket = lib.mkOption {
37         type = lib.types.nullOr lib.types.path;
38         default = "/run/klipper/api";
39         description = "Path of the API socket to create.";
40       };
42       mutableConfig = lib.mkOption {
43         type = lib.types.bool;
44         default = false;
45         example = true;
46         description = ''
47           Whether to copy the config to a mutable directory instead of using the one directly from the nix store.
48           This will only copy the config if the file at `services.klipper.mutableConfigPath` doesn't exist.
49         '';
50       };
52       mutableConfigFolder = lib.mkOption {
53         type = lib.types.path;
54         default = "/var/lib/klipper";
55         description = "Path to mutable Klipper config file.";
56       };
58       configFile = lib.mkOption {
59         type = lib.types.nullOr lib.types.path;
60         default = null;
61         description = ''
62           Path to default Klipper config.
63         '';
64       };
66       octoprintIntegration = lib.mkOption {
67         type = lib.types.bool;
68         default = false;
69         description = "Allows Octoprint to control Klipper.";
70       };
72       user = lib.mkOption {
73         type = lib.types.nullOr lib.types.str;
74         default = null;
75         description = ''
76           User account under which Klipper runs.
78           If null is specified (default), a temporary user will be created by systemd.
79         '';
80       };
82       group = lib.mkOption {
83         type = lib.types.nullOr lib.types.str;
84         default = null;
85         description = ''
86           Group account under which Klipper runs.
88           If null is specified (default), a temporary user will be created by systemd.
89         '';
90       };
92       settings = lib.mkOption {
93         type = lib.types.nullOr format.type;
94         default = null;
95         description = ''
96           Configuration for Klipper. See the [documentation](https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides)
97           for supported values.
98         '';
99       };
101       firmwares = lib.mkOption {
102         description = "Firmwares klipper should manage";
103         default = { };
104         type = with lib.types; attrsOf
105           (submodule {
106             options = {
107               enable = lib.mkEnableOption ''
108                 building of firmware for manual flashing
109               '';
110               enableKlipperFlash = lib.mkEnableOption ''
111                 flashings scripts for firmware. This will add `klipper-flash-$mcu` scripts to your environment which can be called to flash the firmware.
112                 Please check the configs at [klipper](https://github.com/Klipper3d/klipper/tree/master/config) whether your board supports flashing via `make flash`
113               '';
114               serial = lib.mkOption {
115                 type = lib.types.nullOr path;
116                 description = "Path to serial port this printer is connected to. Leave `null` to derive it from `service.klipper.settings`.";
117               };
118               configFile = lib.mkOption {
119                 type = path;
120                 description = "Path to firmware config which is generated using `klipper-genconf`";
121               };
122             };
123           });
124       };
125     };
126   };
128   ##### implementation
129   config = lib.mkIf cfg.enable {
130     assertions = [
131       {
132         assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
133         message = "Option services.klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it.";
134       }
135       {
136         assertion = cfg.user != null -> cfg.group != null;
137         message = "Option services.klipper.group is not set when services.klipper.user is specified.";
138       }
139       {
140         assertion = cfg.settings != null -> lib.foldl (a: b: a && b) true (lib.mapAttrsToList (mcu: _: mcu != null -> (lib.hasAttrByPath [ "${mcu}" "serial" ] cfg.settings)) cfg.firmwares);
141         message = "Option services.klipper.settings.$mcu.serial must be set when settings.klipper.firmware.$mcu is specified";
142       }
143       {
144         assertion = (cfg.configFile != null) != (cfg.settings != null);
145         message = "You need to either specify services.klipper.settings or services.klipper.configFile.";
146       }
147     ];
149     environment.etc = lib.mkIf (!cfg.mutableConfig) {
150       "klipper.cfg".source = if cfg.settings != null then format.generate "klipper.cfg" cfg.settings else cfg.configFile;
151     };
153     services.klipper = lib.mkIf cfg.octoprintIntegration {
154       user = config.services.octoprint.user;
155       group = config.services.octoprint.group;
156     };
158     systemd.services.klipper =
159       let
160         klippyArgs = "--input-tty=${cfg.inputTTY}"
161           + lib.optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}"
162           + lib.optionalString (cfg.logFile != null) " --logfile=${cfg.logFile}"
163         ;
164         printerConfigPath =
165           if cfg.mutableConfig
166           then cfg.mutableConfigFolder + "/printer.cfg"
167           else "/etc/klipper.cfg";
168         printerConfigFile =
169           if cfg.settings != null
170           then format.generate "klipper.cfg" cfg.settings
171           else cfg.configFile;
172       in
173       {
174         description = "Klipper 3D Printer Firmware";
175         wantedBy = [ "multi-user.target" ];
176         after = [ "network.target" ];
177         preStart = ''
178           mkdir -p ${cfg.mutableConfigFolder}
179           ${lib.optionalString (cfg.mutableConfig) ''
180             [ -e ${printerConfigPath} ] || {
181               cp ${printerConfigFile} ${printerConfigPath}
182               chmod +w ${printerConfigPath}
183             }
184           ''}
185           mkdir -p ${cfg.mutableConfigFolder}/gcodes
186         '';
188         serviceConfig = {
189           ExecStart = "${cfg.package}/bin/klippy ${klippyArgs} ${printerConfigPath}";
190           RuntimeDirectory = "klipper";
191           StateDirectory = "klipper";
192           SupplementaryGroups = [ "dialout" ];
193           WorkingDirectory = "${cfg.package}/lib";
194           OOMScoreAdjust = "-999";
195           CPUSchedulingPolicy = "rr";
196           CPUSchedulingPriority = 99;
197           IOSchedulingClass = "realtime";
198           IOSchedulingPriority = 0;
199           UMask = "0002";
200         } // (if cfg.user != null then {
201           Group = cfg.group;
202           User = cfg.user;
203         } else {
204           DynamicUser = true;
205           User = "klipper";
206         });
207       };
209     environment.systemPackages =
210       with pkgs;
211       let
212         default = a: b: if a != null then a else b;
213         firmwares = lib.filterAttrs (n: v: v != null) (lib.mapAttrs
214           (mcu: { enable, enableKlipperFlash, configFile, serial }:
215             if enable then
216               pkgs.klipper-firmware.override
217                 {
218                   mcu = lib.strings.sanitizeDerivationName mcu;
219                   firmwareConfig = configFile;
220                 } else null)
221           cfg.firmwares);
222         firmwareFlasher = lib.mapAttrsToList
223           (mcu: firmware: pkgs.klipper-flash.override {
224             mcu = lib.strings.sanitizeDerivationName mcu;
225             klipper-firmware = firmware;
226             flashDevice = default cfg.firmwares."${mcu}".serial cfg.settings."${mcu}".serial;
227             firmwareConfig = cfg.firmwares."${mcu}".configFile;
228           })
229           (lib.filterAttrs (mcu: firmware: cfg.firmwares."${mcu}".enableKlipperFlash) firmwares);
230       in
231       [ klipper-genconf ] ++ firmwareFlasher ++ lib.attrValues firmwares;
232   };
233   meta.maintainers = [
234     lib.maintainers.cab404
235   ];