Bartosz Stebel | 56ff18c | 2021-08-30 23:28:45 +0200 | [diff] [blame] | 1 | # Copyright (c) 2019 Vincent Ambo |
| 2 | # Copyright (c) 2020-2021 The TVL Authors |
| 3 | # SPDX-License-Identifier: MIT |
| 4 | # |
| 5 | # Provides a function to automatically read a a filesystem structure |
| 6 | # into a Nix attribute set. |
| 7 | # |
| 8 | # Optionally accepts an argument `argsFilter` on import, which is a |
| 9 | # function that receives the current tree location (as a list of |
| 10 | # strings) and the argument set and can arbitrarily modify it. |
| 11 | { argsFilter ? (x: _parts: x) |
| 12 | , ... }: |
| 13 | |
| 14 | let |
| 15 | inherit (builtins) |
| 16 | attrNames |
| 17 | baseNameOf |
| 18 | concatStringsSep |
| 19 | filter |
| 20 | hasAttr |
| 21 | head |
| 22 | isAttrs |
| 23 | length |
| 24 | listToAttrs |
| 25 | map |
| 26 | match |
| 27 | readDir |
| 28 | substring; |
| 29 | |
| 30 | assertMsg = pred: msg: |
| 31 | if pred |
| 32 | then true |
| 33 | else builtins.trace msg false; |
| 34 | |
| 35 | argsWithPath = args: parts: |
| 36 | let meta.locatedAt = parts; |
| 37 | in meta // (if isAttrs args then args else args meta); |
| 38 | |
| 39 | readDirVisible = path: |
| 40 | let |
| 41 | children = readDir path; |
| 42 | isVisible = f: f == ".skip-subtree" || (substring 0 1 f) != "."; |
| 43 | names = filter isVisible (attrNames children); |
| 44 | in listToAttrs (map (name: { |
| 45 | inherit name; |
| 46 | value = children.${name}; |
| 47 | }) names); |
| 48 | |
| 49 | # Create a mark containing the location of this attribute. |
| 50 | marker = parts: { |
| 51 | __readTree = parts; |
| 52 | }; |
| 53 | |
| 54 | # The marker is added to every set that was imported directly by |
| 55 | # readTree. |
| 56 | importWithMark = args: path: parts: |
| 57 | let |
| 58 | importedFile = import path; |
| 59 | pathType = builtins.typeOf importedFile; |
| 60 | imported = |
| 61 | assert assertMsg |
| 62 | (pathType == "lambda") |
| 63 | "readTree: trying to import ${toString path}, but it’s a ${pathType}, you need to make it a function like { depot, pkgs, ... }"; |
| 64 | importedFile (argsFilter (argsWithPath args parts) parts); |
| 65 | in if (isAttrs imported) |
| 66 | then imported // (marker parts) |
| 67 | else imported; |
| 68 | |
| 69 | nixFileName = file: |
| 70 | let res = match "(.*)\\.nix" file; |
| 71 | in if res == null then null else head res; |
| 72 | |
| 73 | readTree = { args, initPath, rootDir, parts }: |
| 74 | let |
| 75 | dir = readDirVisible initPath; |
| 76 | joinChild = c: initPath + ("/" + c); |
| 77 | |
| 78 | self = if rootDir |
| 79 | then { __readTree = []; } |
| 80 | else importWithMark args initPath parts; |
| 81 | |
| 82 | # Import subdirectories of the current one, unless the special |
| 83 | # `.skip-subtree` file exists which makes readTree ignore the |
| 84 | # children. |
| 85 | # |
| 86 | # This file can optionally contain information on why the tree |
| 87 | # should be ignored, but its content is not inspected by |
| 88 | # readTree |
| 89 | filterDir = f: dir."${f}" == "directory"; |
| 90 | children = if hasAttr ".skip-subtree" dir then [] else map (c: { |
| 91 | name = c; |
| 92 | value = readTree { |
| 93 | args = args; |
| 94 | initPath = (joinChild c); |
| 95 | rootDir = false; |
| 96 | parts = (parts ++ [ c ]); |
| 97 | }; |
| 98 | }) (filter filterDir (attrNames dir)); |
| 99 | |
| 100 | # Import Nix files |
| 101 | nixFiles = filter (f: f != null) (map nixFileName (attrNames dir)); |
| 102 | nixChildren = map (c: let p = joinChild (c + ".nix"); in { |
| 103 | name = c; |
| 104 | value = importWithMark args p (parts ++ [ c ]); |
| 105 | }) nixFiles; |
| 106 | in if dir ? "default.nix" |
| 107 | then (if isAttrs self then self // (listToAttrs children) else self) |
| 108 | else (listToAttrs (nixChildren ++ children) // (marker parts)); |
| 109 | |
| 110 | in { |
| 111 | __functor = _: args: initPath: readTree { |
| 112 | inherit args initPath; |
| 113 | rootDir = true; |
| 114 | parts = []; |
| 115 | }; |
| 116 | } |