Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib.packagesFromDirectoryRecursive: use explicit recursion, support nested scopes #359984

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b377da6
lib.filesystem: remove dead `let`-bindings
nbraud Dec 28, 2024
3eb6c8c
lib/tests: move `packages-from-directory/*` into a subdir
nbraud Nov 28, 2024
03968c4
lib.packagesFromDirectoryRecursive: use explicit recursion, support n…
nbraud Nov 28, 2024
b0df85b
lib.packagesFromDirectoryRecursive: add tests for nested scopes
nbraud Nov 28, 2024
3ae1e97
lib.packagesFromDirectoryRecursive: default to creating new scopes wh…
nbraud Dec 1, 2024
1ca4a58
lib.packagesFromDirectoryRecursive: hoist `processDir` out of the lambda
nbraud Dec 3, 2024
bf5cd41
lib.packagesFromDirectoryRecursive: reject unknown arguments
nbraud Dec 3, 2024
0fe9ad2
lib.packagesFromDirectoryRecursive: Add example use of `recurseIntoDi…
nbraud Dec 28, 2024
6b9379f
fixup! lib.packagesFromDirectoryRecursive: use explicit recursion, su…
nbraud Jan 12, 2025
8e448ee
fixup! lib.packagesFromDirectoryRecursive: default to creating new sc…
nbraud Jan 12, 2025
b3977a7
lib.packagesFromDirectoryRecursive: document that symlinks are ignored
nbraud Jan 12, 2025
91f94d8
fixup! lib.packagesFromDirectoryRecursive: Add example use of `recurs…
nbraud Jan 12, 2025
d742f77
squash! lib.packagesFromDirectoryRecursive: hoist `processDir` out of…
nbraud Jan 12, 2025
22feb4c
lib.packagesFromDirectoryRecursive: drop open recursion from API surface
nbraud Jan 18, 2025
25c495e
fixup! lib.packagesFromDirectoryRecursive: drop open recursion from A…
nbraud Jan 18, 2025
7477abe
fixup! lib.packagesFromDirectoryRecursive: drop open recursion from A…
nbraud Jan 18, 2025
168b2ab
fixup! lib.packagesFromDirectoryRecursive: drop open recursion from A…
nbraud Jan 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 56 additions & 46 deletions lib/filesystem.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ let
toString
;

inherit (lib.attrsets)
mapAttrs'
filterAttrs
;

inherit (lib.filesystem)
pathIsDirectory
pathIsRegularFile
Expand All @@ -26,7 +21,6 @@ let

inherit (lib.strings)
hasSuffix
removeSuffix
;
in

Expand Down Expand Up @@ -296,26 +290,27 @@ in
`callPackage <directory>/package.nix { }` is returned.
- Otherwise, the input directory's contents are listed and transformed into
an attribute set.
- If a file name has the `.nix` extension, it is turned into attribute
- If a regular file's name has the `.nix` extension, it is turned into attribute
where:
- The attribute name is the file name without the `.nix` extension
- The attribute value is `callPackage <file path> { }`
- Other files are ignored.
- Directories are turned into an attribute where:
- The attribute name is the name of the directory
- The attribute value is the result of calling
`packagesFromDirectoryRecursive { ... }` on the directory.

As a result, directories with no `.nix` files (including empty
directories) will be transformed into empty attribute sets.
- Other files are ignored, including symbolic links to directories and to regular `.nix`
files; this is because nixlang code cannot distinguish the type of a link's target.

# Type

```
packagesFromDirectoryRecursive :: {
callPackage :: Path -> {} -> a,
newScope? :: AttrSet -> scope,
directory :: Path,
...
} -> AttrSet
```

Expand All @@ -325,10 +320,14 @@ in
: The function used to convert a Nix file's path into a leaf of the attribute set.
It is typically the `callPackage` function, taken from either `pkgs` or a new scope corresponding to the `directory`.

`newScope`
: If present, this function is used when recursing into a directory, to generate a new scope.
The arguments are updated with the scope's `callPackage` and `newScope` functions, so packages can require
anything in their scope, or in an ancestor of their scope.

`directory`
: The directory to read package files from.


# Examples
:::{.example}
## Basic use of `lib.packagesFromDirectoryRecursive`
Expand All @@ -348,12 +347,10 @@ in
::::{.example}
## Create a scope for the nix files found in a directory
```nix
lib.makeScope pkgs.newScope (
self: packagesFromDirectoryRecursive {
inherit (self) callPackage;
directory = ./my-packages;
}
)
packagesFromDirectoryRecursive {
inherit (pkgs) callPackage newScope;
directory = ./my-packages;
}
=> { ... }
```

Expand All @@ -372,46 +369,59 @@ in
:::{.note}
`a.nix` cannot directly take as inputs packages defined in a child directory, such as `b1`.
:::

:::{.warning}
As of now, `lib.packagesFromDirectoryRecursive` cannot create nested scopes for sub-directories.

In particular, files under `b/` can only require (as inputs) other files under `my-packages`,
but not to those in the same directory, nor those in a parent directory; e.g, `b2.nix` cannot directly
require `b1`.
:::
::::
*/
packagesFromDirectoryRecursive =
let
inherit (lib) concatMapAttrs id makeScope recurseIntoAttrs removeSuffix;
inherit (lib.path) append;

# Generate an attrset corresponding to a given directory.
# This function is outside `packagesFromDirectoryRecursive`'s lambda expression,
# to prevent accidentally using its parameters.
processDir = { callPackage, directory, ... }@args:
concatMapAttrs (name: type:
# for each directory entry
let path = append directory name; in
if type == "directory" then {
# recurse into directories
"${name}" = packagesFromDirectoryRecursive (args // {
directory = path;
});
} else if type == "regular" && hasSuffix ".nix" name then {
# call .nix files
"${removeSuffix ".nix" name}" = callPackage path {};
} else if type == "regular" then {
# ignore non-nix files
} else throw ''
nbraud marked this conversation as resolved.
Show resolved Hide resolved
lib.filesystem.packagesFromDirectoryRecursive: Unsupported file type ${type} at path ${toString path}
''
) (builtins.readDir directory);
in
{
callPackage,
newScope ? throw "lib.packagesFromDirectoryRecursive: newScope wasn't passed in args",
directory,
...
}:
}@args:
let
inherit (lib) concatMapAttrs removeSuffix;
inherit (lib.path) append;
defaultPath = append directory "package.nix";
in
if pathExists defaultPath then
# if `${directory}/package.nix` exists, call it directly
callPackage defaultPath {}
else concatMapAttrs (name: type:
# otherwise, for each directory entry
let path = append directory name; in
if type == "directory" then {
# recurse into directories
"${name}" = packagesFromDirectoryRecursive {
inherit callPackage;
directory = path;
};
} else if type == "regular" && hasSuffix ".nix" name then {
# call .nix files
"${removeSuffix ".nix" name}" = callPackage path {};
} else if type == "regular" then {
# ignore non-nix files
} else throw ''
lib.filesystem.packagesFromDirectoryRecursive: Unsupported file type ${type} at path ${toString path}
''
) (builtins.readDir directory);
else if args ? newScope then
# Create a new scope and mark it `recurseForDerivations`.
# This lets the packages refer to each other.
# See:
# [lib.makeScope](https://nixos.org/manual/nixpkgs/unstable/#function-library-lib.customisation.makeScope) and
# [lib.recurseIntoAttrs](https://nixos.org/manual/nixpkgs/unstable/#function-library-lib.customisation.makeScope)
recurseIntoAttrs (makeScope newScope (self:
# generate the attrset representing the directory, using the new scope's `callPackage` and `newScope`
processDir (args // {
inherit (self) callPackage newScope;
})
))
else
processDir args
Comment on lines +418 to +425
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably makes sense to move recurseIntoAttrs into processDir to apply it unconditionally.

;
}
32 changes: 30 additions & 2 deletions lib/tests/misc.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2559,7 +2559,7 @@ runTests {
testPackagesFromDirectoryRecursive = {
expr = packagesFromDirectoryRecursive {
callPackage = path: overrides: import path overrides;
directory = ./packages-from-directory;
directory = ./packages-from-directory/plain;
};
expected = {
a = "a";
Expand All @@ -2584,8 +2584,36 @@ runTests {
testPackagesFromDirectoryRecursiveTopLevelPackageNix = {
expr = packagesFromDirectoryRecursive {
callPackage = path: overrides: import path overrides;
directory = ./packages-from-directory/c;
directory = ./packages-from-directory/plain/c;
};
expected = "c";
};

# Check that `packagesFromDirectoryRecursive` can be used to create scopes
# for sub-directories
testPackagesFromDirectoryNestedScopes = let
inherit (lib) makeScope recurseIntoAttrs;
emptyScope = makeScope lib.callPackageWith (_: {});
in {
expr = lib.filterAttrsRecursive (name: value: !lib.elem name [ "callPackage" "newScope" "overrideScope" "packages" ]) (packagesFromDirectoryRecursive {
inherit (emptyScope) callPackage newScope;
directory = ./packages-from-directory/scope;
});
expected = lib.recurseIntoAttrs {
a = "a";
b = "b";
# Note: Other files/directories in `./test-data/c/` are ignored and can be
# used by `package.nix`.
c = "c";
my-namespace = lib.recurseIntoAttrs {
d = "d";
e = "e";
f = "f";
my-sub-namespace = lib.recurseIntoAttrs {
g = "g";
h = "h";
};
};
};
};
}
1 change: 1 addition & 0 deletions lib/tests/packages-from-directory/scope/a.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ }: "a"
3 changes: 3 additions & 0 deletions lib/tests/packages-from-directory/scope/b.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{ a }:
assert a == "a";
"b"
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ }
1 change: 1 addition & 0 deletions lib/tests/packages-from-directory/scope/c/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ }: "c"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ }
5 changes: 5 additions & 0 deletions lib/tests/packages-from-directory/scope/my-namespace/d.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{ a, e }:
# Check we can get parameter from the parent scope(s) as well as the current one
assert a == "a";
assert e == "e";
"d"
3 changes: 3 additions & 0 deletions lib/tests/packages-from-directory/scope/my-namespace/e.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{ d }:
# Check that mutual recursion is possible
"e"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ }: "f"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
a,
d,
h,
}:
# Check we can get parameters from ancestral scopes (e.g. the scope's grandparent)
"g"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ }: "h"
3 changes: 3 additions & 0 deletions nixos/doc/manual/redirects.json
Original file line number Diff line number Diff line change
Expand Up @@ -1892,6 +1892,9 @@
"sec-release-25.05-lib-breaking": [
"release-notes.html#sec-release-25.05-lib-breaking"
],
"sec-release-25.05-lib-additions-improvements": [
"release-notes.html#sec-release-25.05-lib-additions-improvements"
],
"sec-release-24.11": [
"release-notes.html#sec-release-24.11"
],
Expand Down
8 changes: 8 additions & 0 deletions nixos/doc/manual/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,11 @@
- Structure of the `functor` of some types has changed. `functor` is an implementation detail and should not be relied upon. If you did rely on it let us know in this [PR](https://github.com/NixOS/nixpkgs/pull/363565).
- [`lib.types.enum`](https://nixos.org/manual/nixos/unstable/#sec-option-types-basic): Previously the `functor.payload` was the list of enum values directly. Now it is an attribute set containing the values in the `values` attribute.
- [`lib.types.separatedString`](https://nixos.org/manual/nixos/unstable/#sec-option-types-string): Previously the `functor.payload` was the seperator directly. Now it is an attribute set containing the seperator in the `sep` attribute.

- [`lib.packagesFromDirectoryRecursive`] now rejects unknown arguments, and applies [`lib.recurseIntoAttrs`] when recursing into a directory.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current code only applies recurseIntoAttrs if makeScope is passed. That seems like an oversight.

[`lib.packagesFromDirectoryRecursive`]: https://nixos.org/manual/nixpkgs/stable/#function-library-lib.filesystem.packagesFromDirectoryRecursive
[`lib.recurseIntoAttrs`]: https://nixos.org/manual/nixpkgs/stable/#function-library-lib.attrsets.recurseIntoAttrs

### Additions and Improvements {#sec-release-25.05-lib-additions-improvements}

- [`lib.packagesFromDirectoryRecursive`] can now construct nested scopes matching the directory tree passed as input.