0
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-14 14:10:33 +03:00
nixpkgs/lib/filesystem.nix
Rebecca Turner 4a81a5e556
lib.packagesFromDirectoryRecursive: Allow non-"path" directory
As initially designed, `lib.packagesFromDirectoryRecursive` allowed
passing a string for the `directory` argument. This is necessary for
several reasons:

- `outPath` on derivations and Flake inputs is not a path.
- Derivations can be coerced to their `outPath` in string interpolation,
  but that produces strings, not paths.
- `builtins.path`, bizarrely, returns a string instead of a path (not
  that the documentation makes this clear).

If a path is used instead of a string here, then Nix will dutifully copy
the entire directory into a new path in the Nix store (ignored as
WONTFIX by Eelco in https://github.com/NixOS/nix/issues/9428). For
industrial use cases, this can result in an extra 10-15 seconds on every
single eval just to copy files from one spot in the Nix store to another
spot in the Nix store.

In #361424, this was changed so that `directory` must be a path,
breaking these use-cases.

I'm not really sure what happened here -- #361424 has very little
justification for why it exists, only a reference to a previous version
of the PR (#359941), which itself had very little justification given.
The description on #359941 explained that it would "Shrink the
function's code by ~2/3rd 🎉", but 60% of the reduction in size was just
deleting comments (!) and bindings like `directoryEntryIsPackage` that
helped clarify the intent of the implementation. As a result, the new
implementation is (to my eyes) more challenging to read and understand.
I think the whole thing was in service of #392800, which adds a
`newScope` argument in order "to create nested scopes for each
(sub)directory (not just the top-level one) when `newScope` is given."

Nobody noticed this regression until after the commit was merged. After
@phanirithvij pointed out the regression, @nbraud said they would
"shortly prepare a PR to fix this" [1] but did not. Later, they would
explain that they were "quite ill the last month(s)" [2], which explains
why this got forgotten about. @nbraud also requested a review from
@Gabriella439 [3], as she had reviewed the original PR adding
`lib.packagesFromDirectoryRecursive`, but not from me, the original
author of that PR. @Gabriella439 did not review the "refactor" PR, and
no attempt to contact her or myself was made after that initial request.
This behavior is admittedly rather subtle, so I'm not sure either
Gabriella or myself would have noticed the change (especially since the
relevant PR restructures the entire implementation).

While I find this a bit frustrating, I should have added a test for this
use-case in my original PR; if there was a test that relied on passing
paths in as a string, perhaps the authors modifying this code would have
noticed that the implementation was not an accident.

[1]: https://github.com/NixOS/nixpkgs/pull/361424#discussion_r1912407693
[2]: https://github.com/NixOS/nixpkgs/pull/359984#issuecomment-2775768808
[3]: https://github.com/NixOS/nixpkgs/pull/361424#issuecomment-2521308983
2025-07-10 16:13:38 -07:00

498 lines
13 KiB
Nix

/**
Functions for querying information about the filesystem
without copying any files to the Nix store.
*/
{ lib }:
# Tested in lib/tests/filesystem.sh
let
inherit (builtins)
readDir
pathExists
toString
;
inherit (lib.filesystem)
pathIsDirectory
pathIsRegularFile
pathType
packagesFromDirectoryRecursive
;
inherit (lib.strings)
hasSuffix
;
in
{
/**
The type of a path. The path needs to exist and be accessible.
The result is either "directory" for a directory, "regular" for a regular file, "symlink" for a symlink, or "unknown" for anything else.
# Inputs
path
: The path to query
# Type
```
pathType :: Path -> String
```
# Examples
:::{.example}
## `lib.filesystem.pathType` usage example
```nix
pathType /.
=> "directory"
pathType /some/file.nix
=> "regular"
```
:::
*/
pathType =
builtins.readFileType or
# Nix <2.14 compatibility shim
(
path:
if
!pathExists path
# Fail irrecoverably to mimic the historic behavior of this function and
# the new builtins.readFileType
then
abort "lib.filesystem.pathType: Path ${toString path} does not exist."
# The filesystem root is the only path where `dirOf / == /` and
# `baseNameOf /` is not valid. We can detect this and directly return
# "directory", since we know the filesystem root can't be anything else.
else if dirOf path == path then
"directory"
else
(readDir (dirOf path)).${baseNameOf path}
);
/**
Whether a path exists and is a directory.
# Inputs
`path`
: 1\. Function argument
# Type
```
pathIsDirectory :: Path -> Bool
```
# Examples
:::{.example}
## `lib.filesystem.pathIsDirectory` usage example
```nix
pathIsDirectory /.
=> true
pathIsDirectory /this/does/not/exist
=> false
pathIsDirectory /some/file.nix
=> false
```
:::
*/
pathIsDirectory = path: pathExists path && pathType path == "directory";
/**
Whether a path exists and is a regular file, meaning not a symlink or any other special file type.
# Inputs
`path`
: 1\. Function argument
# Type
```
pathIsRegularFile :: Path -> Bool
```
# Examples
:::{.example}
## `lib.filesystem.pathIsRegularFile` usage example
```nix
pathIsRegularFile /.
=> false
pathIsRegularFile /this/does/not/exist
=> false
pathIsRegularFile /some/file.nix
=> true
```
:::
*/
pathIsRegularFile = path: pathExists path && pathType path == "regular";
/**
A map of all haskell packages defined in the given path,
identified by having a cabal file with the same name as the
directory itself.
# Inputs
`root`
: The directory within to search
# Type
```
Path -> Map String Path
```
*/
haskellPathsInDir =
root:
let
# Files in the root
root-files = builtins.attrNames (builtins.readDir root);
# Files with their full paths
root-files-with-paths = map (file: {
name = file;
value = root + "/${file}";
}) root-files;
# Subdirectories of the root with a cabal file.
cabal-subdirs = builtins.filter (
{ name, value }: builtins.pathExists (value + "/${name}.cabal")
) root-files-with-paths;
in
builtins.listToAttrs cabal-subdirs;
/**
Find the first directory containing a file matching 'pattern'
upward from a given 'file'.
Returns 'null' if no directories contain a file matching 'pattern'.
# Inputs
`pattern`
: The pattern to search for
`file`
: The file to start searching upward from
# Type
```
RegExp -> Path -> Nullable { path : Path; matches : [ MatchResults ]; }
```
*/
locateDominatingFile =
pattern: file:
let
go =
path:
let
files = builtins.attrNames (builtins.readDir path);
matches = builtins.filter (match: match != null) (map (builtins.match pattern) files);
in
if builtins.length matches != 0 then
{ inherit path matches; }
else if path == /. then
null
else
go (dirOf path);
parent = dirOf file;
isDir =
let
base = baseNameOf file;
type = (builtins.readDir parent).${base} or null;
in
file == /. || type == "directory";
in
go (if isDir then file else parent);
/**
Given a directory, return a flattened list of all files within it recursively.
# Inputs
`dir`
: The path to recursively list
# Type
```
Path -> [ Path ]
```
*/
listFilesRecursive =
dir:
lib.flatten (
lib.mapAttrsToList (
name: type:
if type == "directory" then
lib.filesystem.listFilesRecursive (dir + "/${name}")
else
dir + "/${name}"
) (builtins.readDir dir)
);
/**
Transform a directory tree containing package files suitable for
`callPackage` into a matching nested attribute set of derivations.
For a directory tree like this:
```
my-packages
a.nix
b.nix
c
my-extra-feature.patch
package.nix
support-definitions.nix
my-namespace
d.nix
e.nix
f
package.nix
```
`packagesFromDirectoryRecursive` will produce an attribute set like this:
```nix
# packagesFromDirectoryRecursive {
# callPackage = pkgs.callPackage;
# directory = ./my-packages;
# }
{
a = pkgs.callPackage ./my-packages/a.nix { };
b = pkgs.callPackage ./my-packages/b.nix { };
c = pkgs.callPackage ./my-packages/c/package.nix { };
my-namespace = {
d = pkgs.callPackage ./my-packages/my-namespace/d.nix { };
e = pkgs.callPackage ./my-packages/my-namespace/e.nix { };
f = pkgs.callPackage ./my-packages/my-namespace/f/package.nix { };
};
}
```
In particular:
- If the input directory contains a `package.nix` file, then
`callPackage <directory>/package.nix { }` is returned.
- Otherwise, the input directory's contents are listed and transformed into
an attribute set.
- 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> { }`
- 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
```
# Inputs
`callPackage`
: 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`
```nix
packagesFromDirectoryRecursive {
inherit (pkgs) callPackage;
directory = ./my-packages;
}
=> { ... }
```
In this case, `callPackage` will only search `pkgs` for a file's input parameters.
In other words, a file cannot refer to another file in the directory in its input parameters.
:::
::::{.example}
## Create a scope for the nix files found in a directory
```nix
packagesFromDirectoryRecursive {
inherit (pkgs) callPackage newScope;
directory = ./my-packages;
}
=> { ... }
```
For example, take the following directory structure:
```
my-packages
a.nix { b }: assert b ? b1; ...
b
b1.nix { a }: ...
b2.nix
```
Here, `b1.nix` can specify `{ a }` as a parameter, which `callPackage` will resolve as expected.
Likewise, `a.nix` receive an attrset corresponding to the contents of the `b` directory.
:::{.note}
`a.nix` cannot directly take as inputs packages defined in a child directory, such as `b1`.
:::
::::
*/
packagesFromDirectoryRecursive =
let
inherit (lib)
concatMapAttrs
id
makeScope
recurseIntoAttrs
removeSuffix
;
# 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 = 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 ''
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
defaultPath = directory + "/package.nix";
in
if pathExists defaultPath then
# if `${directory}/package.nix` exists, call it directly
callPackage defaultPath { }
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;
/**
Append `/default.nix` if the passed path is a directory.
# Type
```
resolveDefaultNix :: (Path | String) -> (Path | String)
```
# Inputs
A single argument which can be a [path](https://nix.dev/manual/nix/stable/language/types#type-path) value or a string containing an absolute path.
# Output
If the input refers to a directory that exists, the output is that same path with `/default.nix` appended.
Furthermore, if the input is a string that ends with `/`, `default.nix` is appended to it.
Otherwise, the input is returned unchanged.
# Examples
:::{.example}
## `lib.filesystem.resolveDefaultNix` usage example
This expression checks whether `a` and `b` refer to the same locally available Nix file path.
```nix
resolveDefaultNix a == resolveDefaultNix b
```
For instance, if `a` is `/some/dir` and `b` is `/some/dir/default.nix`, and `/some/dir/` exists, the expression evaluates to `true`, despite `a` and `b` being different references to the same Nix file.
*/
resolveDefaultNix =
v:
if pathIsDirectory v then
v + "/default.nix"
else if lib.isString v && hasSuffix "/" v then
# A path ending in `/` can only refer to a directory, so we take the hint, even if we can't verify the validity of the path's `/` assertion.
# A `/` is already present, so we don't add another one.
v + "default.nix"
else
v;
}