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
No known key found for this signature in database
4 changed files with 186 additions and 14 deletions

View file

@ -586,6 +586,42 @@ checkConfigOutput '^38|27$' options.submoduleLine38.declarationPositions.1.line
# nested options work
checkConfigOutput '^34$' options.nested.nestedLine34.declarationPositions.0.line ./declaration-positions.nix
# types.pathWith { inStore = true; }
checkConfigOutput '".*/store/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv"' config.pathInStore.ok1 ./pathWith.nix
checkConfigOutput '".*/store/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15"' config.pathInStore.ok2 ./pathWith.nix
checkConfigOutput '".*/store/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15/bin/bash"' config.pathInStore.ok3 ./pathWith.nix
checkConfigError 'A definition for option .* is not of type .path in the Nix store.. Definition values:\n\s*- In .*: ""' config.pathInStore.bad1 ./pathWith.nix
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
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
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
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
# types.pathWith { inStore = false; }
checkConfigOutput '"/foo/bar"' config.pathNotInStore.ok1 ./pathWith.nix
checkConfigOutput '".*/store"' config.pathNotInStore.ok2 ./pathWith.nix
checkConfigOutput '".*/store/"' config.pathNotInStore.ok3 ./pathWith.nix
checkConfigOutput '""' config.pathNotInStore.ok4 ./pathWith.nix
checkConfigOutput '".*/store/.links"' config.pathNotInStore.ok5 ./pathWith.nix
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
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
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
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
# types.pathWith { }
checkConfigOutput '"/this/is/absolute"' config.anyPath.ok1 ./pathWith.nix
checkConfigOutput '"./this/is/relative"' config.anyPath.ok2 ./pathWith.nix
checkConfigError 'A definition for option .anyPath.bad1. is not of type .path.' config.anyPath.bad1 ./pathWith.nix
# types.pathWith { absolute = true; }
checkConfigOutput '"/this/is/absolute"' config.absolutePathNotInStore.ok1 ./pathWith.nix
checkConfigError 'A definition for option .absolutePathNotInStore.bad1. is not of type .absolute path not in the Nix store.' config.absolutePathNotInStore.bad1 ./pathWith.nix
checkConfigError 'A definition for option .absolutePathNotInStore.bad2. is not of type .absolute path not in the Nix store.' config.absolutePathNotInStore.bad2 ./pathWith.nix
# types.pathWith failed type merge
checkConfigError 'The option .conflictingPathOptionType. in .*/pathWith.nix. is already declared in .*/pathWith.nix' config.conflictingPathOptionType ./pathWith.nix
# types.pathWith { inStore = true; absolute = false; }
checkConfigError 'In pathWith, inStore means the path must be absolute' config.impossiblePathOptionType ./pathWith.nix
cat <<EOF
====== module tests ======

View file

@ -0,0 +1,88 @@
{ lib, ... }:
let
inherit (builtins)
storeDir
;
inherit (lib)
types
mkOption
;
in
{
imports = [
{
options = {
pathInStore = mkOption { type = types.lazyAttrsOf (types.pathWith { inStore = true; }); };
pathNotInStore = mkOption { type = types.lazyAttrsOf (types.pathWith { inStore = false; }); };
anyPath = mkOption { type = types.lazyAttrsOf (types.pathWith { }); };
absolutePathNotInStore = mkOption {
type = types.lazyAttrsOf (
types.pathWith {
inStore = false;
absolute = true;
}
);
};
# This conflicts with `conflictingPathOptionType` below.
conflictingPathOptionType = mkOption { type = types.pathWith { absolute = true; }; };
# This doesn't make sense: the only way to have something be `inStore`
# is to have an absolute path.
impossiblePathOptionType = mkOption {
type = types.pathWith {
inStore = true;
absolute = false;
};
};
};
}
{
options = {
# This should merge cleanly with `pathNotInStore` above.
pathNotInStore = mkOption {
type = types.lazyAttrsOf (
types.pathWith {
inStore = false;
absolute = null;
}
);
};
# This conflicts with `conflictingPathOptionType` above.
conflictingPathOptionType = mkOption { type = types.pathWith { absolute = false; }; };
};
}
];
pathInStore.ok1 = "${storeDir}/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv";
pathInStore.ok2 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15";
pathInStore.ok3 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15/bin/bash";
pathInStore.bad1 = "";
pathInStore.bad2 = "${storeDir}";
pathInStore.bad3 = "${storeDir}/";
pathInStore.bad4 = "${storeDir}/.links"; # technically true, but not reasonable
pathInStore.bad5 = "/foo/bar";
pathNotInStore.ok1 = "/foo/bar";
pathNotInStore.ok2 = "${storeDir}"; # strange, but consistent with `pathInStore` above
pathNotInStore.ok3 = "${storeDir}/"; # also strange, but also consistent
pathNotInStore.ok4 = "";
pathNotInStore.ok5 = "${storeDir}/.links"; # strange, but consistent with `pathInStore` above
pathNotInStore.bad1 = "${storeDir}/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv";
pathNotInStore.bad2 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15";
pathNotInStore.bad3 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15/bin/bash";
pathNotInStore.bad4 = ./pathWith.nix;
anyPath.ok1 = "/this/is/absolute";
anyPath.ok2 = "./this/is/relative";
anyPath.bad1 = 42;
absolutePathNotInStore.ok1 = "/this/is/absolute";
absolutePathNotInStore.bad1 = "./this/is/relative";
absolutePathNotInStore.bad2 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15";
conflictingPathOptionType = "/foo/bar";
impossiblePathOptionType = "/foo/bar";
}