diff --git a/lib/fileset/default.nix b/lib/fileset/default.nix index 3c51c6d4dab4..32bc00a227b3 100644 --- a/lib/fileset/default.nix +++ b/lib/fileset/default.nix @@ -101,6 +101,7 @@ let inherit (import ./internal.nix { inherit lib; }) _coerce + _coerceResult _singleton _coerceMany _toSourceFilter @@ -1005,4 +1006,49 @@ in { submodules = recurseSubmodules; }; + + /** + The empty fileset. It can be useful as a default value or as starting accumulator for a folding operation. + + # Type + + ``` + empty :: FileSet + ``` + */ + empty = _emptyWithoutBase; + + /** + Tests whether a given value is a fileset, or can be used in place of a fileset. + + # Inputs + + `value` + + : The value to test + + # Type + + ``` + isFileset :: Any -> Bool + ``` + + # Examples + :::{.example} + ## `lib.fileset.isFileset` usage example + + ```nix + isFileset ./. + => true + + isFileset (unions [ ]) + => true + + isFileset 1 + => false + ``` + + ::: + */ + isFileset = x: (_coerceResult "" x).success; } diff --git a/lib/fileset/internal.nix b/lib/fileset/internal.nix index 59b8408ae8d6..342b284e1f71 100644 --- a/lib/fileset/internal.nix +++ b/lib/fileset/internal.nix @@ -165,14 +165,27 @@ rec { _noEval = throw _noEvalMessage; }; - # Coerce a value to a fileset, erroring when the value cannot be coerced. - # The string gives the context for error messages. - # Type: String -> (fileset | Path) -> fileset - _coerce = + # Coerce a value to a fileset. Return a set containing the attribute `success` + # indicating whether coercing succeeded, and either `value` when `success == + # true`, or an error `message` when `success == false`. The string gives the + # context for error messages. + # + # Type: String -> (fileset | Path) -> { success :: Bool, value :: fileset } ] -> { success :: Bool, message :: String } + _coerceResult = + let + ok = value: { + success = true; + inherit value; + }; + error = message: { + success = false; + inherit message; + }; + in context: value: if value._type or "" == "fileset" then if value._internalVersion > _currentVersion then - throw '' + error '' ${context} is a file set created from a future version of the file set library with a different internal representation: - Internal version of the file set: ${toString value._internalVersion} - Internal version of the library: ${toString _currentVersion} @@ -184,27 +197,37 @@ rec { _currentVersion - value._internalVersion ) migrations; in - foldl' (value: migration: migration value) value migrationsToApply + ok (foldl' (value: migration: migration value) value migrationsToApply) else - value + ok value else if !isPath value then if value ? _isLibCleanSourceWith then - throw '' + error '' ${context} is a `lib.sources`-based value, but it should be a file set or a path instead. To convert a `lib.sources`-based value to a file set you can use `lib.fileset.fromSource`. Note that this only works for sources created from paths.'' else if isStringLike value then - throw '' + error '' ${context} ("${toString value}") is a string-like value, but it should be a file set or a path instead. Paths represented as strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.'' else - throw ''${context} is of type ${typeOf value}, but it should be a file set or a path instead.'' + error ''${context} is of type ${typeOf value}, but it should be a file set or a path instead.'' else if !pathExists value then - throw '' + error '' ${context} (${toString value}) is a path that does not exist. To create a file set from a path that may not exist, use `lib.fileset.maybeMissing`.'' else - _singleton value; + ok (_singleton value); + + # Coerce a value to a fileset, erroring when the value cannot be coerced. + # The string gives the context for error messages. + # Type: String -> (fileset | Path) -> fileset + _coerce = + context: value: + let + result = _coerceResult context value; + in + if result.success then result.value else throw result.message; # Coerce many values to filesets, erroring when any value cannot be coerced, # or if the filesystem root of the values doesn't match. diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 02f73310dd9b..093196da16c5 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -223,6 +223,17 @@ checkConfigError 'A definition for option .* is not of type .path in the Nix sto checkConfigError 'A definition for option .* is not of type .path in the Nix store.. Definition values:\n\s*- In .*: ".*/store/.links"' config.pathInStore.bad4 ./types.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 ./types.nix +# types.fileset +checkConfigOutput '^0$' config.filesetCardinal.ok1 ./fileset.nix +checkConfigOutput '^1$' config.filesetCardinal.ok2 ./fileset.nix +checkConfigOutput '^1$' config.filesetCardinal.ok3 ./fileset.nix +checkConfigOutput '^1$' config.filesetCardinal.ok4 ./fileset.nix +checkConfigOutput '^0$' config.filesetCardinal.ok5 ./fileset.nix +checkConfigError 'A definition for option .* is not of type .fileset.. Definition values:\n.*' config.filesetCardinal.err1 ./fileset.nix +checkConfigError 'A definition for option .* is not of type .fileset.. Definition values:\n.*' config.filesetCardinal.err2 ./fileset.nix +checkConfigError 'A definition for option .* is not of type .fileset.. Definition values:\n.*' config.filesetCardinal.err3 ./fileset.nix +checkConfigError 'A definition for option .* is not of type .fileset.. Definition values:\n.*' config.filesetCardinal.err4 ./fileset.nix + # Check boolean option. checkConfigOutput '^false$' config.enable ./declare-enable.nix checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*: true' config.enable ./define-enable.nix diff --git a/lib/tests/modules/fileset.nix b/lib/tests/modules/fileset.nix new file mode 100644 index 000000000000..40b1a8ef601f --- /dev/null +++ b/lib/tests/modules/fileset.nix @@ -0,0 +1,50 @@ +{ config, lib, ... }: + +let + inherit (lib) + mkOption + mkIf + types + mapAttrs + length + ; + inherit (lib.fileset) + empty + unions + toList + ; +in + +{ + options = { + fileset = mkOption { type = with types; lazyAttrsOf fileset; }; + + ## The following option is only here as a proxy to test `fileset` that does + ## not work so well with `modules.sh` because it is not JSONable. It exposes + ## the number of elements in the fileset. + filesetCardinal = mkOption { default = mapAttrs (_: fs: length (toList fs)) config.fileset; }; + }; + + config = { + fileset.ok1 = empty; + fileset.ok2 = ./fileset; + fileset.ok3 = unions [ + empty + ./fileset + ]; + # fileset.ok4: see imports below + fileset.ok5 = mkIf false ./fileset; + + fileset.err1 = 1; + fileset.err2 = "foo"; + fileset.err3 = "./."; + fileset.err4 = [ empty ]; + + }; + + imports = [ + { fileset.ok4 = ./fileset; } + { fileset.ok4 = empty; } + { fileset.ok4 = ./fileset; } + ]; +} diff --git a/lib/tests/modules/fileset/keepme b/lib/tests/modules/fileset/keepme new file mode 100644 index 000000000000..27b2fabd2c27 --- /dev/null +++ b/lib/tests/modules/fileset/keepme @@ -0,0 +1 @@ +Do not remove. This file is used by the tests in `../fileset.nix`. diff --git a/lib/types.nix b/lib/types.nix index 9da1d2247bc0..cb41fb5c85aa 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -66,6 +66,11 @@ let fixupOptionType mergeOptionDecls ; + inherit (lib.fileset) + isFileset + unions + empty + ; inAttrPosSuffix = v: name: @@ -618,6 +623,15 @@ let }; }; + fileset = mkOptionType { + name = "fileset"; + description = "fileset"; + descriptionClass = "noun"; + check = isFileset; + merge = loc: defs: unions (map (x: x.value) defs); + emptyValue.value = empty; + }; + # A package is a top-level store path (/nix/store/hash-name). This includes: # - derivations # - more generally, attribute sets with an `outPath` or `__toString` attribute