summaryrefslogtreecommitdiff
path: root/modules/launchd/types.nix
diff options
context:
space:
mode:
authorTyler Miller <tmillr@proton.me>2023-06-29 00:50:28 -0700
committerTyler Miller <tmillr@proton.me>2024-06-09 11:20:15 -0700
commit861af0fc94df9454f4e92d6892f75588763164bb (patch)
tree5641177f735e7cdcfa22d2bb8c7b5628cdb2d58a /modules/launchd/types.nix
parentc0d5b8c54d6828516c97f6be9f2d00c63a363df4 (diff)
fix(launchd): improve `StartCalendarInterval`
Stricter launchd -> StartCalendarInterval type: - Verify that the integers passed to `Minute`, `Hour`, etc. are within range. - When provided, the value for StartCalendarInterval must be a non-empty list of calendar intervals and must not contain duplicates entries (throw an error otherwise). - For increased flexibility and backwards-compatibility, allow an attrset to be passed as well (which will be type-checked and is functionally equivalent to passing a singleton list). Allowing an attrset or list is precisely in-line with what `launchd.plist(5)` accepts for StartCalendarInterval. Migrate `nix.gc.interval` and `nix.optimise.interval` over to use this new type, and update their defaults to run weekly instead of daily. Create `modules/launchd/types.nix` file for easier/modular use of launchd types needed in multiple files. Documentation: - Update and improve wording/documentation of launchd's `StartCalendarInterval`. - Improve wording/documentation of `nix.gc.interval` and `nix.optimise.interval` ("time interval" can be misleading as it's actually a "calendar interval"; e.g. `{ Hour = 3; Minute = 15;}` runs daily, not every 3.25 hours).
Diffstat (limited to 'modules/launchd/types.nix')
-rw-r--r--modules/launchd/types.nix110
1 files changed, 110 insertions, 0 deletions
diff --git a/modules/launchd/types.nix b/modules/launchd/types.nix
new file mode 100644
index 0000000..38d7f20
--- /dev/null
+++ b/modules/launchd/types.nix
@@ -0,0 +1,110 @@
+{ lib, ... }:
+
+let
+ inherit (lib) imap1 types mkOption showOption optionDescriptionPhrase mergeDefinitions;
+ inherit (builtins) map filter length deepSeq throw toString concatLists;
+ inherit (lib.options) showDefs;
+ wildcardText = lib.literalMD "`*`";
+
+ /**
+ A type of list which does not allow duplicate elements. The base/inner
+ list type to use (e.g. `types.listOf` or `types.nonEmptyListOf`) is passed
+ via argument `listType`, which must be the final type and not a function.
+
+ NOTE: The extra check for duplicates is quadratic and strict, so use this
+ type sparingly and only:
+
+ * when needed, and
+ * when the list is expected to be recursively short (e.g. < 10 elements)
+ and shallow (i.e. strict evaluation of the list won't take too long)
+
+ The implementation of this function is similar to that of
+ `types.nonEmptyListOf`.
+ */
+ types'.uniqueList = listType: listType // {
+ description = "unique ${types.optionDescriptionPhrase (class: class == "noun") listType}";
+ substSubModules = m: types'.uniqueList (listType.substSubModules m);
+ # This has been taken from the implementation of `types.listOf`, but has
+ # been modified to throw on duplicates. This check cannot be done in the
+ # `check` fn as this check is deep/strict, and because `check` runs
+ # prior to merging.
+ merge = loc: defs:
+ let
+ # Each element of `dupes` is a list. When there are duplicates,
+ # later lists will be duplicates of earlier lists, so just throw on
+ # the first set of duplicates found so that we don't have duplicate
+ # error msgs.
+ checked = filter (li:
+ if length li > 1
+ then throw "The option `${showOption loc}' contains duplicate entries after merging:\n${showDefs li}"
+ else false) dupes;
+ dupes = map (def: filter (def': def'.value == def.value) merged) merged;
+ merged = filter (x: x ? value) (concatLists (imap1 (n: def:
+ imap1 (m: el:
+ let
+ inherit (def) file;
+ loc' = loc ++ ["[definition ${toString n}-entry ${toString m}]"];
+ in
+ (mergeDefinitions
+ loc'
+ listType.nestedTypes.elemType
+ [{ inherit file; value = el; }]
+ ).optionalValue // {inherit loc' file;}
+ ) def.value
+ ) defs));
+ in
+ deepSeq checked (map (x: x.value) merged);
+ };
+in {
+ StartCalendarInterval = let
+ CalendarIntervalEntry = types.submodule {
+ options = {
+ Minute = mkOption {
+ type = types.nullOr (types.ints.between 0 59);
+ default = null;
+ defaultText = wildcardText;
+ description = ''
+ The minute on which this job will be run.
+ '';
+ };
+
+ Hour = mkOption {
+ type = types.nullOr (types.ints.between 0 23);
+ default = null;
+ defaultText = wildcardText;
+ description = ''
+ The hour on which this job will be run.
+ '';
+ };
+
+ Day = mkOption {
+ type = types.nullOr (types.ints.between 1 31);
+ default = null;
+ defaultText = wildcardText;
+ description = ''
+ The day on which this job will be run.
+ '';
+ };
+
+ Weekday = mkOption {
+ type = types.nullOr (types.ints.between 0 7);
+ default = null;
+ defaultText = wildcardText;
+ description = ''
+ The weekday on which this job will be run (0 and 7 are Sunday).
+ '';
+ };
+
+ Month = mkOption {
+ type = types.nullOr (types.ints.between 1 12);
+ default = null;
+ defaultText = wildcardText;
+ description = ''
+ The month on which this job will be run.
+ '';
+ };
+ };
+ };
+ in
+ types.either CalendarIntervalEntry (types'.uniqueList (types.nonEmptyListOf CalendarIntervalEntry));
+}