Skip to content

Commit 6d7f6a9

Browse files
committed
lib/types: add types.pathWith
This gives people some flexibility when they need a path type, and prevents a "combinatorial explosion" of various path stops. I've re-implemented our existing `path` and `pathInStore` types using `pathWith`. Our existing `package` type is potentially a candidate for similar treatment, but it's a little quirkier (there's some stuff with `builtins.hasContext` and `toDerivation` that I don't completely understand), and I didn't want to muddy this PR with that. As a happy side effect of this work, we get a new feature: the ability to create a type for paths *not* in the store. This is useful for when a module needs a path to a file, and wants to protect people from accidentally leaking that file into the nix store.
1 parent 2ee1540 commit 6d7f6a9

File tree

4 files changed

+186
-14
lines changed

4 files changed

+186
-14
lines changed

lib/tests/modules.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,42 @@ checkConfigOutput '^38|27$' options.submoduleLine38.declarationPositions.1.line
586586
# nested options work
587587
checkConfigOutput '^34$' options.nested.nestedLine34.declarationPositions.0.line ./declaration-positions.nix
588588

589+
# types.pathWith { inStore = true; }
590+
checkConfigOutput '".*/store/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv"' config.pathInStore.ok1 ./pathWith.nix
591+
checkConfigOutput '".*/store/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15"' config.pathInStore.ok2 ./pathWith.nix
592+
checkConfigOutput '".*/store/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15/bin/bash"' config.pathInStore.ok3 ./pathWith.nix
593+
checkConfigError 'A definition for option .* is not of type .path in the Nix store.. Definition values:\n\s*- In .*: ""' config.pathInStore.bad1 ./pathWith.nix
594+
checkConfigError 'A definition for option .* is not of type .path in the Nix store.. Definition values:\n\s*- In .*: ".*/store"' config.pathInStore.bad2 ./pathWith.nix
595+
checkConfigError 'A definition for option .* is not of type .path in the Nix store.. Definition values:\n\s*- In .*: ".*/store/"' config.pathInStore.bad3 ./pathWith.nix
596+
checkConfigError 'A definition for option .* is not of type .path in the Nix store.. Definition values:\n\s*- In .*: ".*/store/.links"' config.pathInStore.bad4 ./pathWith.nix
597+
checkConfigError 'A definition for option .* is not of type .path in the Nix store.. Definition values:\n\s*- In .*: "/foo/bar"' config.pathInStore.bad5 ./pathWith.nix
598+
599+
# types.pathWith { inStore = false; }
600+
checkConfigOutput '"/foo/bar"' config.pathNotInStore.ok1 ./pathWith.nix
601+
checkConfigOutput '".*/store"' config.pathNotInStore.ok2 ./pathWith.nix
602+
checkConfigOutput '".*/store/"' config.pathNotInStore.ok3 ./pathWith.nix
603+
checkConfigOutput '""' config.pathNotInStore.ok4 ./pathWith.nix
604+
checkConfigOutput '".*/store/.links"' config.pathNotInStore.ok5 ./pathWith.nix
605+
checkConfigError 'A definition for option .* is not of type .path not in the Nix store.. Definition values:\n\s*- In .*: ".*/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv"' config.pathNotInStore.bad1 ./pathWith.nix
606+
checkConfigError 'A definition for option .* is not of type .path not in the Nix store.. Definition values:\n\s*- In .*: ".*/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15"' config.pathNotInStore.bad2 ./pathWith.nix
607+
checkConfigError 'A definition for option .* is not of type .path not in the Nix store.. Definition values:\n\s*- In .*: ".*/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15/bin/bash"' config.pathNotInStore.bad3 ./pathWith.nix
608+
checkConfigError 'A definition for option .* is not of type .path not in the Nix store.. Definition values:\n\s*- In .*: .*/pathWith.nix' config.pathNotInStore.bad4 ./pathWith.nix
609+
610+
# types.pathWith { }
611+
checkConfigOutput '"/this/is/absolute"' config.anyPath.ok1 ./pathWith.nix
612+
checkConfigOutput '"./this/is/relative"' config.anyPath.ok2 ./pathWith.nix
613+
checkConfigError 'A definition for option .anyPath.bad1. is not of type .path.' config.anyPath.bad1 ./pathWith.nix
614+
615+
# types.pathWith { absolute = true; }
616+
checkConfigOutput '"/this/is/absolute"' config.absolutePathNotInStore.ok1 ./pathWith.nix
617+
checkConfigError 'A definition for option .absolutePathNotInStore.bad1. is not of type .absolute path not in the Nix store.' config.absolutePathNotInStore.bad1 ./pathWith.nix
618+
checkConfigError 'A definition for option .absolutePathNotInStore.bad2. is not of type .absolute path not in the Nix store.' config.absolutePathNotInStore.bad2 ./pathWith.nix
619+
620+
# types.pathWith failed type merge
621+
checkConfigError 'The option .conflictingPathOptionType. in .*/pathWith.nix. is already declared in .*/pathWith.nix' config.conflictingPathOptionType ./pathWith.nix
622+
623+
# types.pathWith { inStore = true; absolute = false; }
624+
checkConfigError 'In pathWith, inStore means the path must be absolute' config.impossiblePathOptionType ./pathWith.nix
589625

590626
cat <<EOF
591627
====== module tests ======

lib/tests/modules/pathWith.nix

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{ lib, ... }:
2+
let
3+
inherit (builtins)
4+
storeDir
5+
;
6+
inherit (lib)
7+
types
8+
mkOption
9+
;
10+
in
11+
{
12+
imports = [
13+
{
14+
options = {
15+
pathInStore = mkOption { type = types.lazyAttrsOf (types.pathWith { inStore = true; }); };
16+
pathNotInStore = mkOption { type = types.lazyAttrsOf (types.pathWith { inStore = false; }); };
17+
anyPath = mkOption { type = types.lazyAttrsOf (types.pathWith { }); };
18+
absolutePathNotInStore = mkOption {
19+
type = types.lazyAttrsOf (
20+
types.pathWith {
21+
inStore = false;
22+
absolute = true;
23+
}
24+
);
25+
};
26+
27+
# This conflicts with `conflictingPathOptionType` below.
28+
conflictingPathOptionType = mkOption { type = types.pathWith { absolute = true; }; };
29+
30+
# This doesn't make sense: the only way to have something be `inStore`
31+
# is to have an absolute path.
32+
impossiblePathOptionType = mkOption {
33+
type = types.pathWith {
34+
inStore = true;
35+
absolute = false;
36+
};
37+
};
38+
};
39+
}
40+
{
41+
options = {
42+
# This should merge cleanly with `pathNotInStore` above.
43+
pathNotInStore = mkOption {
44+
type = types.lazyAttrsOf (
45+
types.pathWith {
46+
inStore = false;
47+
absolute = null;
48+
}
49+
);
50+
};
51+
52+
# This conflicts with `conflictingPathOptionType` above.
53+
conflictingPathOptionType = mkOption { type = types.pathWith { absolute = false; }; };
54+
};
55+
}
56+
];
57+
58+
pathInStore.ok1 = "${storeDir}/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv";
59+
pathInStore.ok2 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15";
60+
pathInStore.ok3 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15/bin/bash";
61+
pathInStore.bad1 = "";
62+
pathInStore.bad2 = "${storeDir}";
63+
pathInStore.bad3 = "${storeDir}/";
64+
pathInStore.bad4 = "${storeDir}/.links"; # technically true, but not reasonable
65+
pathInStore.bad5 = "/foo/bar";
66+
67+
pathNotInStore.ok1 = "/foo/bar";
68+
pathNotInStore.ok2 = "${storeDir}"; # strange, but consistent with `pathInStore` above
69+
pathNotInStore.ok3 = "${storeDir}/"; # also strange, but also consistent
70+
pathNotInStore.ok4 = "";
71+
pathNotInStore.ok5 = "${storeDir}/.links"; # strange, but consistent with `pathInStore` above
72+
pathNotInStore.bad1 = "${storeDir}/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv";
73+
pathNotInStore.bad2 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15";
74+
pathNotInStore.bad3 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15/bin/bash";
75+
pathNotInStore.bad4 = ./pathWith.nix;
76+
77+
anyPath.ok1 = "/this/is/absolute";
78+
anyPath.ok2 = "./this/is/relative";
79+
anyPath.bad1 = 42;
80+
81+
absolutePathNotInStore.ok1 = "/this/is/absolute";
82+
absolutePathNotInStore.bad1 = "./this/is/relative";
83+
absolutePathNotInStore.bad2 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15";
84+
85+
conflictingPathOptionType = "/foo/bar";
86+
87+
impossiblePathOptionType = "/foo/bar";
88+
}

lib/types.nix

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -566,21 +566,48 @@ rec {
566566
})
567567
(x: (x._type or null) == "pkgs");
568568

569-
path = mkOptionType {
570-
name = "path";
571-
descriptionClass = "noun";
572-
check = x: isStringLike x && builtins.substring 0 1 (toString x) == "/";
573-
merge = mergeEqualOption;
569+
path = pathWith {
570+
absolute = true;
574571
};
575572

576-
pathInStore = mkOptionType {
577-
name = "pathInStore";
578-
description = "path in the Nix store";
579-
descriptionClass = "noun";
580-
check = x: isStringLike x && builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null;
581-
merge = mergeEqualOption;
573+
pathInStore = pathWith {
574+
inStore = true;
582575
};
583576

577+
pathWith = {
578+
inStore ? null,
579+
absolute ? null,
580+
}:
581+
throwIf (inStore != null && absolute != null && inStore && !absolute) "In pathWith, inStore means the path must be absolute" mkOptionType {
582+
name = "pathWith";
583+
description = (
584+
(if absolute == null then "" else (if absolute then "absolute " else "relative ")) +
585+
"path" +
586+
(if inStore == null then "" else (if inStore then " in the Nix store" else " not in the Nix store"))
587+
);
588+
descriptionClass = "noun";
589+
590+
merge = mergeEqualOption;
591+
functor = defaultFunctor "pathWith" // {
592+
type = pathWith;
593+
payload = {inherit inStore absolute; };
594+
binOp = lhs: rhs: if lhs == rhs then lhs else null;
595+
};
596+
597+
check = x:
598+
let
599+
isInStore = builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null;
600+
isAbsolute = builtins.substring 0 1 (toString x) == "/";
601+
isExpectedType = (
602+
if inStore == null || inStore then
603+
isStringLike x
604+
else
605+
isString x # Do not allow a true path, which could be copied to the store later on.
606+
);
607+
in
608+
isExpectedType && (inStore == null || inStore == isInStore) && (absolute == null || absolute == isAbsolute);
609+
};
610+
584611
listOf = elemType: mkOptionType rec {
585612
name = "listOf";
586613
description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";

nixos/doc/manual/development/option-types.section.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,36 @@ merging is handled.
2323

2424
`types.path`
2525

26-
: A filesystem path is anything that starts with a slash when
27-
coerced to a string. Even if derivations can be considered as
28-
paths, the more specific `types.package` should be preferred.
26+
: A filesystem path that starts with a slash. Even if derivations can be
27+
considered as paths, the more specific `types.package` should be preferred.
2928

3029
`types.pathInStore`
3130

3231
: A path that is contained in the Nix store. This can be a top-level store
3332
path like `pkgs.hello` or a descendant like `"${pkgs.hello}/bin/hello"`.
3433

34+
`types.pathWith` { *`inStore`* ? `null`, *`absolute`* ? `null` }
35+
36+
: A filesystem path. Either a string or something that can be coerced
37+
to a string.
38+
39+
**Parameters**
40+
41+
`inStore` (`Boolean` or `null`, default `null`)
42+
: Whether the path must be in the store (`true`), must not be in the store
43+
(`false`), or it doesn't matter (`null`)
44+
45+
`absolute` (`Boolean` or `null`, default `null`)
46+
: Whether the path must be absolute (`true`), must not be absolute
47+
(`false`), or it doesn't matter (`null`)
48+
49+
**Behavior**
50+
- `pathWith { inStore = true; }` is equivalent to `pathInStore`
51+
- `pathWith { absolute = true; }` is equivalent to `path`
52+
- `pathWith { inStore = false; absolute = true; }` requires an absolute
53+
path that is not in the store. Useful for password files that shouldn't be
54+
leaked into the store.
55+
3556
`types.package`
3657

3758
: A top-level store path. This can be an attribute set pointing

0 commit comments

Comments
 (0)