lib.types: introduce a fileset type (#428293)

This commit is contained in:
Robert Hensing
2025-11-04 13:44:33 +00:00
committed by GitHub
6 changed files with 157 additions and 12 deletions

View File

@@ -101,6 +101,7 @@ let
inherit (import ./internal.nix { inherit lib; }) inherit (import ./internal.nix { inherit lib; })
_coerce _coerce
_coerceResult
_singleton _singleton
_coerceMany _coerceMany
_toSourceFilter _toSourceFilter
@@ -1005,4 +1006,49 @@ in
{ {
submodules = recurseSubmodules; 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;
} }

View File

@@ -165,14 +165,27 @@ rec {
_noEval = throw _noEvalMessage; _noEval = throw _noEvalMessage;
}; };
# Coerce a value to a fileset, erroring when the value cannot be coerced. # Coerce a value to a fileset. Return a set containing the attribute `success`
# The string gives the context for error messages. # indicating whether coercing succeeded, and either `value` when `success ==
# Type: String -> (fileset | Path) -> fileset # true`, or an error `message` when `success == false`. The string gives the
_coerce = # 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: context: value:
if value._type or "" == "fileset" then if value._type or "" == "fileset" then
if value._internalVersion > _currentVersion 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: ${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 file set: ${toString value._internalVersion}
- Internal version of the library: ${toString _currentVersion} - Internal version of the library: ${toString _currentVersion}
@@ -184,27 +197,37 @@ rec {
_currentVersion - value._internalVersion _currentVersion - value._internalVersion
) migrations; ) migrations;
in in
foldl' (value: migration: migration value) value migrationsToApply ok (foldl' (value: migration: migration value) value migrationsToApply)
else else
value ok value
else if !isPath value then else if !isPath value then
if value ? _isLibCleanSourceWith 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. ${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`. 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.'' Note that this only works for sources created from paths.''
else if isStringLike value then 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. ${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.'' Paths represented as strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.''
else 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 else if !pathExists value then
throw '' error ''
${context} (${toString value}) is a path that does not exist. ${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`.'' To create a file set from a path that may not exist, use `lib.fileset.maybeMissing`.''
else 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, # Coerce many values to filesets, erroring when any value cannot be coerced,
# or if the filesystem root of the values doesn't match. # or if the filesystem root of the values doesn't match.

View File

@@ -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 .*: ".*/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 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. # Check boolean option.
checkConfigOutput '^false$' config.enable ./declare-enable.nix checkConfigOutput '^false$' config.enable ./declare-enable.nix
checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*: true' config.enable ./define-enable.nix checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*: true' config.enable ./define-enable.nix

View File

@@ -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; }
];
}

View File

@@ -0,0 +1 @@
Do not remove. This file is used by the tests in `../fileset.nix`.

View File

@@ -66,6 +66,11 @@ let
fixupOptionType fixupOptionType
mergeOptionDecls mergeOptionDecls
; ;
inherit (lib.fileset)
isFileset
unions
empty
;
inAttrPosSuffix = inAttrPosSuffix =
v: name: 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: # A package is a top-level store path (/nix/store/hash-name). This includes:
# - derivations # - derivations
# - more generally, attribute sets with an `outPath` or `__toString` attribute # - more generally, attribute sets with an `outPath` or `__toString` attribute