at 24.11-pre 9.3 kB view raw
1# Management of static files in /etc. 2 3{ config, lib, pkgs, ... }: 4 5with lib; 6 7let 8 9 etc' = filter (f: f.enable) (attrValues config.environment.etc); 10 11 etc = pkgs.runCommandLocal "etc" { 12 # This is needed for the systemd module 13 passthru.targets = map (x: x.target) etc'; 14 } /* sh */ '' 15 set -euo pipefail 16 17 makeEtcEntry() { 18 src="$1" 19 target="$2" 20 mode="$3" 21 user="$4" 22 group="$5" 23 24 if [[ "$src" = *'*'* ]]; then 25 # If the source name contains '*', perform globbing. 26 mkdir -p "$out/etc/$target" 27 for fn in $src; do 28 ln -s "$fn" "$out/etc/$target/" 29 done 30 else 31 32 mkdir -p "$out/etc/$(dirname "$target")" 33 if ! [ -e "$out/etc/$target" ]; then 34 ln -s "$src" "$out/etc/$target" 35 else 36 echo "duplicate entry $target -> $src" 37 if [ "$(readlink "$out/etc/$target")" != "$src" ]; then 38 echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src" 39 ret=1 40 41 continue 42 fi 43 fi 44 45 if [ "$mode" != symlink ]; then 46 echo "$mode" > "$out/etc/$target.mode" 47 echo "$user" > "$out/etc/$target.uid" 48 echo "$group" > "$out/etc/$target.gid" 49 fi 50 fi 51 } 52 53 mkdir -p "$out/etc" 54 ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [ 55 "makeEtcEntry" 56 # Force local source paths to be added to the store 57 "${etcEntry.source}" 58 etcEntry.target 59 etcEntry.mode 60 etcEntry.user 61 etcEntry.group 62 ]) etc'} 63 ''; 64 65 etcHardlinks = filter (f: f.mode != "symlink") etc'; 66 67 build-composefs-dump = pkgs.runCommand "build-composefs-dump.py" 68 { 69 buildInputs = [ pkgs.python3 ]; 70 } '' 71 install ${./build-composefs-dump.py} $out 72 patchShebangs --host $out 73 ''; 74 75in 76 77{ 78 79 imports = [ ../build.nix ]; 80 81 ###### interface 82 83 options = { 84 85 system.etc.overlay = { 86 enable = mkOption { 87 type = types.bool; 88 default = false; 89 description = '' 90 Mount `/etc` as an overlayfs instead of generating it via a perl script. 91 92 Note: This is currently experimental. Only enable this option if you're 93 confident that you can recover your system if it breaks. 94 ''; 95 }; 96 97 mutable = mkOption { 98 type = types.bool; 99 default = true; 100 description = '' 101 Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only). 102 103 If this is false, only the immutable lowerdir is mounted. If it is 104 true, a writable upperdir is mounted on top. 105 ''; 106 }; 107 }; 108 109 environment.etc = mkOption { 110 default = {}; 111 example = literalExpression '' 112 { example-configuration-file = 113 { source = "/nix/store/.../etc/dir/file.conf.example"; 114 mode = "0440"; 115 }; 116 "default/useradd".text = "GROUP=100 ..."; 117 } 118 ''; 119 description = '' 120 Set of files that have to be linked in {file}`/etc`. 121 ''; 122 123 type = with types; attrsOf (submodule ( 124 { name, config, options, ... }: 125 { options = { 126 127 enable = mkOption { 128 type = types.bool; 129 default = true; 130 description = '' 131 Whether this /etc file should be generated. This 132 option allows specific /etc files to be disabled. 133 ''; 134 }; 135 136 target = mkOption { 137 type = types.str; 138 description = '' 139 Name of symlink (relative to 140 {file}`/etc`). Defaults to the attribute 141 name. 142 ''; 143 }; 144 145 text = mkOption { 146 default = null; 147 type = types.nullOr types.lines; 148 description = "Text of the file."; 149 }; 150 151 source = mkOption { 152 type = types.path; 153 description = "Path of the source file."; 154 }; 155 156 mode = mkOption { 157 type = types.str; 158 default = "symlink"; 159 example = "0600"; 160 description = '' 161 If set to something else than `symlink`, 162 the file is copied instead of symlinked, with the given 163 file mode. 164 ''; 165 }; 166 167 uid = mkOption { 168 default = 0; 169 type = types.int; 170 description = '' 171 UID of created file. Only takes effect when the file is 172 copied (that is, the mode is not 'symlink'). 173 ''; 174 }; 175 176 gid = mkOption { 177 default = 0; 178 type = types.int; 179 description = '' 180 GID of created file. Only takes effect when the file is 181 copied (that is, the mode is not 'symlink'). 182 ''; 183 }; 184 185 user = mkOption { 186 default = "+${toString config.uid}"; 187 type = types.str; 188 description = '' 189 User name of created file. 190 Only takes effect when the file is copied (that is, the mode is not 'symlink'). 191 Changing this option takes precedence over `uid`. 192 ''; 193 }; 194 195 group = mkOption { 196 default = "+${toString config.gid}"; 197 type = types.str; 198 description = '' 199 Group name of created file. 200 Only takes effect when the file is copied (that is, the mode is not 'symlink'). 201 Changing this option takes precedence over `gid`. 202 ''; 203 }; 204 205 }; 206 207 config = { 208 target = mkDefault name; 209 source = mkIf (config.text != null) ( 210 let name' = "etc-" + lib.replaceStrings ["/"] ["-"] name; 211 in mkDerivedConfig options.text (pkgs.writeText name') 212 ); 213 }; 214 215 })); 216 217 }; 218 219 }; 220 221 222 ###### implementation 223 224 config = { 225 226 system.build.etc = etc; 227 system.build.etcActivationCommands = let 228 etcOverlayOptions = lib.concatStringsSep "," ([ 229 "relatime" 230 "redirect_dir=on" 231 "metacopy=on" 232 ] ++ lib.optionals config.system.etc.overlay.mutable [ 233 "upperdir=/.rw-etc/upper" 234 "workdir=/.rw-etc/work" 235 ]); 236 in if config.system.etc.overlay.enable then '' 237 # This script atomically remounts /etc when switching configuration. On a (re-)boot 238 # this should not run because /etc is mounted via a systemd mount unit 239 # instead. To a large extent this mimics what composefs does. Because 240 # it's relatively simple, however, we avoid the composefs dependency. 241 # Since this script is not idempotent, it should not run when etc hasn't 242 # changed. 243 if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]] && [[ "${config.system.build.etc}/etc" != "$(readlink -f /run/current-system/etc)" ]]; then 244 echo "remounting /etc..." 245 246 tmpMetadataMount=$(mktemp --directory) 247 mount --type erofs ${config.system.build.etcMetadataImage} $tmpMetadataMount 248 249 # Mount the new /etc overlay to a temporary private mount. 250 # This needs the indirection via a private bind mount because you 251 # cannot move shared mounts. 252 tmpEtcMount=$(mktemp --directory) 253 mount --bind --make-private $tmpEtcMount $tmpEtcMount 254 mount --type overlay overlay \ 255 --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \ 256 $tmpEtcMount 257 258 # Move the new temporary /etc mount underneath the current /etc mount. 259 # 260 # This should eventually use util-linux to perform this move beneath, 261 # however, this functionality is not yet in util-linux. See this 262 # tracking issue: https://github.com/util-linux/util-linux/issues/2604 263 ${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc 264 265 # Unmount the top /etc mount to atomically reveal the new mount. 266 umount /etc 267 268 fi 269 '' else '' 270 # Set up the statically computed bits of /etc. 271 echo "setting up /etc..." 272 ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc 273 ''; 274 275 system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } '' 276 set -euo pipefail 277 278 makeEtcEntry() { 279 src="$1" 280 target="$2" 281 282 mkdir -p "$out/$(dirname "$target")" 283 cp "$src" "$out/$target" 284 } 285 286 mkdir -p "$out" 287 ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [ 288 "makeEtcEntry" 289 # Force local source paths to be added to the store 290 "${etcEntry.source}" 291 etcEntry.target 292 ]) etcHardlinks} 293 ''; 294 295 system.build.etcMetadataImage = 296 let 297 etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc'); 298 etcDump = pkgs.runCommand "etc-dump" { } "${build-composefs-dump} ${etcJson} > $out"; 299 in 300 pkgs.runCommand "etc-metadata.erofs" { 301 nativeBuildInputs = [ pkgs.composefs pkgs.erofs-utils ]; 302 } '' 303 mkcomposefs --from-file ${etcDump} $out 304 fsck.erofs $out 305 ''; 306 307 }; 308 309}