1/**
2 Functions for querying information about the filesystem
3 without copying any files to the Nix store.
4*/
5{ lib }:
6
7# Tested in lib/tests/filesystem.sh
8let
9 inherit (builtins)
10 readDir
11 pathExists
12 toString
13 ;
14
15 inherit (lib.filesystem)
16 pathIsDirectory
17 pathIsRegularFile
18 pathType
19 packagesFromDirectoryRecursive
20 ;
21
22 inherit (lib.strings)
23 hasSuffix
24 ;
25in
26
27{
28
29 /**
30 The type of a path. The path needs to exist and be accessible.
31 The result is either "directory" for a directory, "regular" for a regular file, "symlink" for a symlink, or "unknown" for anything else.
32
33 # Inputs
34
35 path
36
37 : The path to query
38
39 # Type
40
41 ```
42 pathType :: Path -> String
43 ```
44
45 # Examples
46 :::{.example}
47 ## `lib.filesystem.pathType` usage example
48
49 ```nix
50 pathType /.
51 => "directory"
52
53 pathType /some/file.nix
54 => "regular"
55 ```
56
57 :::
58 */
59 pathType =
60 builtins.readFileType or
61 # Nix <2.14 compatibility shim
62 (
63 path:
64 if
65 !pathExists path
66 # Fail irrecoverably to mimic the historic behavior of this function and
67 # the new builtins.readFileType
68 then
69 abort "lib.filesystem.pathType: Path ${toString path} does not exist."
70 # The filesystem root is the only path where `dirOf / == /` and
71 # `baseNameOf /` is not valid. We can detect this and directly return
72 # "directory", since we know the filesystem root can't be anything else.
73 else if dirOf path == path then
74 "directory"
75 else
76 (readDir (dirOf path)).${baseNameOf path}
77 );
78
79 /**
80 Whether a path exists and is a directory.
81
82 # Inputs
83
84 `path`
85
86 : 1\. Function argument
87
88 # Type
89
90 ```
91 pathIsDirectory :: Path -> Bool
92 ```
93
94 # Examples
95 :::{.example}
96 ## `lib.filesystem.pathIsDirectory` usage example
97
98 ```nix
99 pathIsDirectory /.
100 => true
101
102 pathIsDirectory /this/does/not/exist
103 => false
104
105 pathIsDirectory /some/file.nix
106 => false
107 ```
108
109 :::
110 */
111 pathIsDirectory = path: pathExists path && pathType path == "directory";
112
113 /**
114 Whether a path exists and is a regular file, meaning not a symlink or any other special file type.
115
116 # Inputs
117
118 `path`
119
120 : 1\. Function argument
121
122 # Type
123
124 ```
125 pathIsRegularFile :: Path -> Bool
126 ```
127
128 # Examples
129 :::{.example}
130 ## `lib.filesystem.pathIsRegularFile` usage example
131
132 ```nix
133 pathIsRegularFile /.
134 => false
135
136 pathIsRegularFile /this/does/not/exist
137 => false
138
139 pathIsRegularFile /some/file.nix
140 => true
141 ```
142
143 :::
144 */
145 pathIsRegularFile = path: pathExists path && pathType path == "regular";
146
147 /**
148 A map of all haskell packages defined in the given path,
149 identified by having a cabal file with the same name as the
150 directory itself.
151
152 # Inputs
153
154 `root`
155
156 : The directory within to search
157
158 # Type
159
160 ```
161 Path -> Map String Path
162 ```
163 */
164 haskellPathsInDir =
165 root:
166 let
167 # Files in the root
168 root-files = builtins.attrNames (builtins.readDir root);
169 # Files with their full paths
170 root-files-with-paths = map (file: {
171 name = file;
172 value = root + "/${file}";
173 }) root-files;
174 # Subdirectories of the root with a cabal file.
175 cabal-subdirs = builtins.filter (
176 { name, value }: builtins.pathExists (value + "/${name}.cabal")
177 ) root-files-with-paths;
178 in
179 builtins.listToAttrs cabal-subdirs;
180 /**
181 Find the first directory containing a file matching 'pattern'
182 upward from a given 'file'.
183 Returns 'null' if no directories contain a file matching 'pattern'.
184
185 # Inputs
186
187 `pattern`
188
189 : The pattern to search for
190
191 `file`
192
193 : The file to start searching upward from
194
195 # Type
196
197 ```
198 RegExp -> Path -> Nullable { path : Path; matches : [ MatchResults ]; }
199 ```
200 */
201 locateDominatingFile =
202 pattern: file:
203 let
204 go =
205 path:
206 let
207 files = builtins.attrNames (builtins.readDir path);
208 matches = builtins.filter (match: match != null) (map (builtins.match pattern) files);
209 in
210 if builtins.length matches != 0 then
211 { inherit path matches; }
212 else if path == /. then
213 null
214 else
215 go (dirOf path);
216 parent = dirOf file;
217 isDir =
218 let
219 base = baseNameOf file;
220 type = (builtins.readDir parent).${base} or null;
221 in
222 file == /. || type == "directory";
223 in
224 go (if isDir then file else parent);
225
226 /**
227 Given a directory, return a flattened list of all files within it recursively.
228
229 # Inputs
230
231 `dir`
232
233 : The path to recursively list
234
235 # Type
236
237 ```
238 Path -> [ Path ]
239 ```
240 */
241 listFilesRecursive =
242 let
243 # We only flatten at the very end, as flatten is recursive.
244 internalFunc =
245 dir:
246 (lib.mapAttrsToList (
247 name: type: if type == "directory" then internalFunc (dir + "/${name}") else dir + "/${name}"
248 ) (builtins.readDir dir));
249 in
250 dir: lib.flatten (internalFunc dir);
251
252 /**
253 Transform a directory tree containing package files suitable for
254 `callPackage` into a matching nested attribute set of derivations.
255
256 For a directory tree like this:
257
258 ```
259 my-packages
260 ├── a.nix
261 ├── b.nix
262 ├── c
263 │ ├── my-extra-feature.patch
264 │ ├── package.nix
265 │ └── support-definitions.nix
266 └── my-namespace
267 ├── d.nix
268 ├── e.nix
269 └── f
270 └── package.nix
271 ```
272
273 `packagesFromDirectoryRecursive` will produce an attribute set like this:
274
275 ```nix
276 # packagesFromDirectoryRecursive {
277 # callPackage = pkgs.callPackage;
278 # directory = ./my-packages;
279 # }
280 {
281 a = pkgs.callPackage ./my-packages/a.nix { };
282 b = pkgs.callPackage ./my-packages/b.nix { };
283 c = pkgs.callPackage ./my-packages/c/package.nix { };
284 my-namespace = {
285 d = pkgs.callPackage ./my-packages/my-namespace/d.nix { };
286 e = pkgs.callPackage ./my-packages/my-namespace/e.nix { };
287 f = pkgs.callPackage ./my-packages/my-namespace/f/package.nix { };
288 };
289 }
290 ```
291
292 In particular:
293 - If the input directory contains a `package.nix` file, then
294 `callPackage <directory>/package.nix { }` is returned.
295 - Otherwise, the input directory's contents are listed and transformed into
296 an attribute set.
297 - If a regular file's name has the `.nix` extension, it is turned into attribute
298 where:
299 - The attribute name is the file name without the `.nix` extension
300 - The attribute value is `callPackage <file path> { }`
301 - Directories are turned into an attribute where:
302 - The attribute name is the name of the directory
303 - The attribute value is the result of calling
304 `packagesFromDirectoryRecursive { ... }` on the directory.
305
306 As a result, directories with no `.nix` files (including empty
307 directories) will be transformed into empty attribute sets.
308 - Other files are ignored, including symbolic links to directories and to regular `.nix`
309 files; this is because nixlang code cannot distinguish the type of a link's target.
310
311 # Type
312
313 ```
314 packagesFromDirectoryRecursive :: {
315 callPackage :: Path -> {} -> a,
316 newScope? :: AttrSet -> scope,
317 directory :: Path,
318 } -> AttrSet
319 ```
320
321 # Inputs
322
323 `callPackage`
324 : The function used to convert a Nix file's path into a leaf of the attribute set.
325 It is typically the `callPackage` function, taken from either `pkgs` or a new scope corresponding to the `directory`.
326
327 `newScope`
328 : If present, this function is used when recursing into a directory, to generate a new scope.
329 The arguments are updated with the scope's `callPackage` and `newScope` functions, so packages can require
330 anything in their scope, or in an ancestor of their scope.
331
332 `directory`
333 : The directory to read package files from.
334
335 # Examples
336 :::{.example}
337 ## Basic use of `lib.packagesFromDirectoryRecursive`
338
339 ```nix
340 packagesFromDirectoryRecursive {
341 inherit (pkgs) callPackage;
342 directory = ./my-packages;
343 }
344 => { ... }
345 ```
346
347 In this case, `callPackage` will only search `pkgs` for a file's input parameters.
348 In other words, a file cannot refer to another file in the directory in its input parameters.
349 :::
350
351 ::::{.example}
352 ## Create a scope for the nix files found in a directory
353 ```nix
354 packagesFromDirectoryRecursive {
355 inherit (pkgs) callPackage newScope;
356 directory = ./my-packages;
357 }
358 => { ... }
359 ```
360
361 For example, take the following directory structure:
362 ```
363 my-packages
364 ├── a.nix → { b }: assert b ? b1; ...
365 └── b
366 ├── b1.nix → { a }: ...
367 └── b2.nix
368 ```
369
370 Here, `b1.nix` can specify `{ a }` as a parameter, which `callPackage` will resolve as expected.
371 Likewise, `a.nix` receive an attrset corresponding to the contents of the `b` directory.
372
373 :::{.note}
374 `a.nix` cannot directly take as inputs packages defined in a child directory, such as `b1`.
375 :::
376 ::::
377 */
378 packagesFromDirectoryRecursive =
379 let
380 inherit (lib)
381 concatMapAttrs
382 id
383 makeScope
384 recurseIntoAttrs
385 removeSuffix
386 ;
387
388 # Generate an attrset corresponding to a given directory.
389 # This function is outside `packagesFromDirectoryRecursive`'s lambda expression,
390 # to prevent accidentally using its parameters.
391 processDir =
392 { callPackage, directory, ... }@args:
393 concatMapAttrs (
394 name: type:
395 # for each directory entry
396 let
397 path = directory + "/${name}";
398 in
399 if type == "directory" then
400 {
401 # recurse into directories
402 "${name}" = packagesFromDirectoryRecursive (
403 args
404 // {
405 directory = path;
406 }
407 );
408 }
409 else if type == "regular" && hasSuffix ".nix" name then
410 {
411 # call .nix files
412 "${removeSuffix ".nix" name}" = callPackage path { };
413 }
414 else if type == "regular" then
415 {
416 # ignore non-nix files
417 }
418 else
419 throw ''
420 lib.filesystem.packagesFromDirectoryRecursive: Unsupported file type ${type} at path ${toString path}
421 ''
422 ) (builtins.readDir directory);
423 in
424 {
425 callPackage,
426 newScope ? throw "lib.packagesFromDirectoryRecursive: newScope wasn't passed in args",
427 directory,
428 }@args:
429 let
430 defaultPath = directory + "/package.nix";
431 in
432 if pathExists defaultPath then
433 # if `${directory}/package.nix` exists, call it directly
434 callPackage defaultPath { }
435 else if args ? newScope then
436 # Create a new scope and mark it `recurseForDerivations`.
437 # This lets the packages refer to each other.
438 # See:
439 # [lib.makeScope](https://nixos.org/manual/nixpkgs/unstable/#function-library-lib.customisation.makeScope) and
440 # [lib.recurseIntoAttrs](https://nixos.org/manual/nixpkgs/unstable/#function-library-lib.customisation.makeScope)
441 recurseIntoAttrs (
442 makeScope newScope (
443 self:
444 # generate the attrset representing the directory, using the new scope's `callPackage` and `newScope`
445 processDir (
446 args
447 // {
448 inherit (self) callPackage newScope;
449 }
450 )
451 )
452 )
453 else
454 processDir args;
455
456 /**
457 Append `/default.nix` if the passed path is a directory.
458
459 # Type
460
461 ```
462 resolveDefaultNix :: (Path | String) -> (Path | String)
463 ```
464
465 # Inputs
466
467 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.
468
469 # Output
470
471 If the input refers to a directory that exists, the output is that same path with `/default.nix` appended.
472 Furthermore, if the input is a string that ends with `/`, `default.nix` is appended to it.
473 Otherwise, the input is returned unchanged.
474
475 # Examples
476 :::{.example}
477 ## `lib.filesystem.resolveDefaultNix` usage example
478
479 This expression checks whether `a` and `b` refer to the same locally available Nix file path.
480
481 ```nix
482 resolveDefaultNix a == resolveDefaultNix b
483 ```
484
485 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.
486 */
487 resolveDefaultNix =
488 v:
489 if pathIsDirectory v then
490 v + "/default.nix"
491 else if lib.isString v && hasSuffix "/" v then
492 # 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.
493 # A `/` is already present, so we don't add another one.
494 v + "default.nix"
495 else
496 v;
497}