Merge pull request #11484 from oxij/nixos-toposort-filesystems

lib: add toposort, nixos: use toposort for fileSystems to properly support bind and move mounts

Changed files
+133 -35
lib
nixos
lib
modules
+80
lib/lists.nix
···
reverseList = xs:
let l = length xs; in genList (n: elemAt xs (l - n - 1)) l;
+
/* Depth-First Search (DFS) for lists `list != []`.
+
+
`before a b == true` means that `b` depends on `a` (there's an
+
edge from `b` to `a`).
+
+
Examples:
+
+
listDfs true hasPrefix [ "/home/user" "other" "/" "/home" ]
+
== { minimal = "/"; # minimal element
+
visited = [ "/home/user" ]; # seen elements (in reverse order)
+
rest = [ "/home" "other" ]; # everything else
+
}
+
+
listDfs true hasPrefix [ "/home/user" "other" "/" "/home" "/" ]
+
== { cycle = "/"; # cycle encountered at this element
+
loops = [ "/" ]; # and continues to these elements
+
visited = [ "/" "/home/user" ]; # elements leading to the cycle (in reverse order)
+
rest = [ "/home" "other" ]; # everything else
+
+
*/
+
+
listDfs = stopOnCycles: before: list:
+
let
+
dfs' = us: visited: rest:
+
let
+
c = filter (x: before x us) visited;
+
b = partition (x: before x us) rest;
+
in if stopOnCycles && (length c > 0)
+
then { cycle = us; loops = c; inherit visited rest; }
+
else if length b.right == 0
+
then # nothing is before us
+
{ minimal = us; inherit visited rest; }
+
else # grab the first one before us and continue
+
dfs' (head b.right)
+
([ us ] ++ visited)
+
(tail b.right ++ b.wrong);
+
in dfs' (head list) [] (tail list);
+
+
/* Sort a list based on a partial ordering using DFS. This
+
implementation is O(N^2), if your ordering is linear, use `sort`
+
instead.
+
+
`before a b == true` means that `b` should be after `a`
+
in the result.
+
+
Examples:
+
+
toposort hasPrefix [ "/home/user" "other" "/" "/home" ]
+
== { result = [ "/" "/home" "/home/user" "other" ]; }
+
+
toposort hasPrefix [ "/home/user" "other" "/" "/home" "/" ]
+
== { cycle = [ "/home/user" "/" "/" ]; # path leading to a cycle
+
loops = [ "/" ]; } # loops back to these elements
+
+
toposort hasPrefix [ "other" "/home/user" "/home" "/" ]
+
== { result = [ "other" "/" "/home" "/home/user" ]; }
+
+
toposort (a: b: a < b) [ 3 2 1 ] == { result = [ 1 2 3 ]; }
+
+
*/
+
+
toposort = before: list:
+
let
+
dfsthis = listDfs true before list;
+
toporest = toposort before (dfsthis.visited ++ dfsthis.rest);
+
in
+
if length list < 2
+
then # finish
+
{ result = list; }
+
else if dfsthis ? "cycle"
+
then # there's a cycle, starting from the current vertex, return it
+
{ cycle = reverseList ([ dfsthis.cycle ] ++ dfsthis.visited);
+
inherit (dfsthis) loops; }
+
else if toporest ? "cycle"
+
then # there's a cycle somewhere else in the graph, return it
+
toporest
+
# Slow, but short. Can be made a bit faster with an explicit stack.
+
else # there are no cycles
+
{ result = [ dfsthis.minimal ] ++ toporest.result; };
+
/* Sort a list based on a comparator function which compares two
elements and returns true if the first argument is strictly below
the second argument. The returned list is sorted in an increasing
+9
nixos/lib/utils.nix
···
rec {
+
# Check whenever fileSystem is needed for boot
+
fsNeededForBoot = fs: fs.neededForBoot
+
|| elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
+
+
# Check whenever `b` depends on `a` as a fileSystem
+
# FIXME: it's incorrect to simply use hasPrefix here: "/dev/a" is not a parent of "/dev/ab"
+
fsBefore = a: b: ((any (x: elem x [ "bind" "move" ]) b.options) && (a.mountPoint == b.device))
+
|| (hasPrefix a.mountPoint b.mountPoint);
+
# Escape a path according to the systemd rules, e.g. /dev/xyzzy
# becomes dev-xyzzy. FIXME: slow.
escapeSystemdPath = s:
+1 -1
nixos/modules/security/grsecurity.nix
···
(fs: (fs.neededForBoot
|| elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ])
&& fs.fsType == "zfs")
-
(attrValues config.fileSystems) != [];
+
config.system.build.fileSystems != [];
# Ascertain whether NixOS container support is required
containerSupportRequired =
+9 -19
nixos/modules/system/boot/stage-1.nix
···
# the modules necessary to mount the root file system, then calls the
# init in the root file system to start the second boot stage.
-
{ config, lib, pkgs, ... }:
+
{ config, lib, utils, pkgs, ... }:
with lib;
···
};
+
# The initrd only has to mount `/` or any FS marked as necessary for
+
# booting (such as the FS containing `/nix/store`, or an FS needed for
+
# mounting `/`, like `/` on a loopback).
+
fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
+
+
# Some additional utilities needed in stage 1, like mount, lvm, fsck
# etc. We don't want to bring in all of those packages, so we just
# copy what we need. Instead of using statically linked binaries,
···
ln -sf kmod $out/bin/modprobe
# Copy resize2fs if needed.
-
${optionalString (any (fs: fs.autoResize) (attrValues config.fileSystems)) ''
+
${optionalString (any (fs: fs.autoResize) fileSystems) ''
# We need mke2fs in the initrd.
copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs
''}
···
${config.boot.initrd.extraUtilsCommandsTest}
''; # */
-
-
-
# The initrd only has to mount / or any FS marked as necessary for
-
# booting (such as the FS containing /nix/store, or an FS needed for
-
# mounting /, like / on a loopback).
-
#
-
# We need to guarantee that / is the first filesystem in the list so
-
# that if and when lustrateRoot is invoked, nothing else is mounted
-
fileSystems = let
-
filterNeeded = filter
-
(fs: fs.mountPoint != "/" && (fs.neededForBoot || elem fs.mountPoint [ "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]));
-
filterRoot = filter
-
(fs: fs.mountPoint == "/");
-
allFileSystems = attrValues config.fileSystems;
-
in (filterRoot allFileSystems) ++ (filterNeeded allFileSystems);
udevRules = pkgs.stdenv.mkDerivation {
···
};
config = mkIf (!config.boot.isContainer) {
-
assertions = [
-
{ assertion = any (fs: fs.mountPoint == "/") (attrValues config.fileSystems);
+
{ assertion = any (fs: fs.mountPoint == "/") fileSystems;
message = "The ‘fileSystems’ option does not specify your root file system.";
}
{ assertion = let inherit (config.boot) resumeDevice; in
+1 -1
nixos/modules/tasks/encrypted-devices.nix
···
with lib;
let
-
fileSystems = attrValues config.fileSystems ++ config.swapDevices;
+
fileSystems = config.system.build.fileSystems ++ config.swapDevices;
encDevs = filter (dev: dev.encrypted.enable) fileSystems;
keyedEncDevs = filter (dev: dev.encrypted.keyFile != null) encDevs;
keylessEncDevs = filter (dev: dev.encrypted.keyFile == null) encDevs;
+30 -9
nixos/modules/tasks/filesystems.nix
···
let
-
fileSystems = attrValues config.fileSystems;
+
fileSystems' = toposort fsBefore (attrValues config.fileSystems);
+
+
fileSystems = if fileSystems' ? "result"
+
then # use topologically sorted fileSystems everywhere
+
fileSystems'.result
+
else # the assertion below will catch this,
+
# but we fall back to the original order
+
# anyway so that other modules could check
+
# their assertions too
+
(attrValues config.fileSystems);
prioOption = prio: optionalString (prio != null) " pri=${toString prio}";
···
config = {
+
assertions = let
+
ls = sep: concatMapStringsSep sep (x: x.mountPoint);
+
in [
+
{ assertion = ! (fileSystems' ? "cycle");
+
message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
+
}
+
];
+
+
# Export for use in other modules
+
system.build.fileSystems = fileSystems;
+
boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
# Add the mount helpers to the system path so that `mount' can find them.
···
# in your /etc/nixos/configuration.nix file.
# Filesystems.
-
${flip concatMapStrings fileSystems (fs:
+
${concatMapStrings (fs:
(if fs.device != null then fs.device
else if fs.label != null then "/dev/disk/by-label/${fs.label}"
else throw "No device specified for mount point ‘${fs.mountPoint}’.")
···
+ " " + (if skipCheck fs then "0" else
if fs.mountPoint == "/" then "1" else "2")
+ "\n"
-
)}
+
) fileSystems}
# Swap devices.
${flip concatMapStrings config.swapDevices (sw:
···
formatDevice = fs:
let
-
mountPoint' = escapeSystemdPath fs.mountPoint;
-
device' = escapeSystemdPath fs.device;
+
mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
+
device' = escapeSystemdPath fs.device;
+
device'' = "${device}.device";
in nameValuePair "mkfs-${device'}"
{ description = "Initialisation of Filesystem ${fs.device}";
-
wantedBy = [ "${mountPoint'}.mount" ];
-
before = [ "${mountPoint'}.mount" "systemd-fsck@${device'}.service" ];
-
requires = [ "${device'}.device" ];
-
after = [ "${device'}.device" ];
+
wantedBy = [ mountPoint' ];
+
before = [ mountPoint' "systemd-fsck@${device'}.service" ];
+
requires = [ device'' ];
+
after = [ device'' ];
path = [ pkgs.utillinux ] ++ config.system.fsPackages;
script =
''
+3 -5
nixos/modules/tasks/filesystems/zfs.nix
···
fsToPool = fs: datasetToPool fs.device;
-
zfsFilesystems = filter (x: x.fsType == "zfs") (attrValues config.fileSystems);
-
-
isRoot = fs: fs.neededForBoot || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
+
zfsFilesystems = filter (x: x.fsType == "zfs") config.system.build.fileSystems;
allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools);
-
rootPools = unique (map fsToPool (filter isRoot zfsFilesystems));
+
rootPools = unique (map fsToPool (filter fsNeededForBoot zfsFilesystems));
dataPools = unique (filter (pool: !(elem pool rootPools)) allPools);
···
systemd.services = let
getPoolFilesystems = pool:
-
filter (x: x.fsType == "zfs" && (fsToPool x) == pool) (attrValues config.fileSystems);
+
filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems;
getPoolMounts = pool:
let