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.
This commit is contained in:
Jeremy Fleischman
2025-01-12 12:21:10 -08:00
parent 2ee154072b
commit 6d7f6a92cc
4 changed files with 186 additions and 14 deletions

View File

@@ -566,21 +566,48 @@ rec {
})
(x: (x._type or null) == "pkgs");
path = mkOptionType {
name = "path";
descriptionClass = "noun";
check = x: isStringLike x && builtins.substring 0 1 (toString x) == "/";
merge = mergeEqualOption;
path = pathWith {
absolute = true;
};
pathInStore = mkOptionType {
name = "pathInStore";
description = "path in the Nix store";
descriptionClass = "noun";
check = x: isStringLike x && builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null;
merge = mergeEqualOption;
pathInStore = pathWith {
inStore = true;
};
pathWith = {
inStore ? null,
absolute ? null,
}:
throwIf (inStore != null && absolute != null && inStore && !absolute) "In pathWith, inStore means the path must be absolute" mkOptionType {
name = "pathWith";
description = (
(if absolute == null then "" else (if absolute then "absolute " else "relative ")) +
"path" +
(if inStore == null then "" else (if inStore then " in the Nix store" else " not in the Nix store"))
);
descriptionClass = "noun";
merge = mergeEqualOption;
functor = defaultFunctor "pathWith" // {
type = pathWith;
payload = {inherit inStore absolute; };
binOp = lhs: rhs: if lhs == rhs then lhs else null;
};
check = x:
let
isInStore = builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null;
isAbsolute = builtins.substring 0 1 (toString x) == "/";
isExpectedType = (
if inStore == null || inStore then
isStringLike x
else
isString x # Do not allow a true path, which could be copied to the store later on.
);
in
isExpectedType && (inStore == null || inStore == isInStore) && (absolute == null || absolute == isAbsolute);
};
listOf = elemType: mkOptionType rec {
name = "listOf";
description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";