Unverified Commit 6d7f6a92 authored by Jeremy Fleischman's avatar Jeremy Fleischman
Browse files

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.
parent 2ee15407
Loading
Loading
Loading
Loading
+36 −0
Original line number Diff line number Diff line
@@ -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 ======
+88 −0
Original line number Diff line number Diff line
{ 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";
}
+38 −11
Original line number Diff line number Diff line
@@ -566,19 +566,46 @@ 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 = pathWith {
      inStore = true;
    };

    pathInStore = mkOptionType {
      name = "pathInStore";
      description = "path in the Nix store";
    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";
      check = x: isStringLike x && builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null;

        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 {
+24 −3
Original line number Diff line number Diff line
@@ -23,15 +23,36 @@ merging is handled.

`types.path`

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

`types.pathInStore`

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

`types.pathWith` { *`inStore`* ? `null`, *`absolute`* ? `null` }

:   A filesystem path. Either a string or something that can be coerced
    to a string.

    **Parameters**

    `inStore` (`Boolean` or `null`, default `null`)
    : Whether the path must be in the store (`true`), must not be in the store
      (`false`), or it doesn't matter (`null`)

    `absolute` (`Boolean` or `null`, default `null`)
    : Whether the path must be absolute (`true`), must not be absolute
      (`false`), or it doesn't matter (`null`)

    **Behavior**
    - `pathWith { inStore = true; }` is equivalent to `pathInStore`
    - `pathWith { absolute = true; }` is equivalent to `path`
    - `pathWith { inStore = false; absolute = true; }` requires an absolute
      path that is not in the store. Useful for password files that shouldn't be
      leaked into the store.

`types.package`

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