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; })
_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;
}

View File

@@ -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.

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 .*: "/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

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
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