mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-07-14 14:10:33 +03:00

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
498 lines
13 KiB
Nix
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;
|
|
}
|