about summary refs log tree commit diff
path: root/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs')
-rw-r--r--pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs2128
1 files changed, 2128 insertions, 0 deletions
diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs
new file mode 100644
index 0000000000000..a35b55065c073
--- /dev/null
+++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs
@@ -0,0 +1,2128 @@
+use std::{
+    cell::RefCell,
+    collections::HashMap,
+    io::{BufRead, Read, Write},
+    os::unix::{fs::PermissionsExt, process::CommandExt},
+    path::{Path, PathBuf},
+    rc::Rc,
+    str::FromStr,
+    sync::OnceLock,
+    time::Duration,
+};
+
+use anyhow::{anyhow, bail, Context, Result};
+use dbus::{
+    blocking::{stdintf::org_freedesktop_dbus::Properties, LocalConnection, Proxy},
+    Message,
+};
+use glob::glob;
+use ini::{Ini, ParseOption};
+use log::LevelFilter;
+use nix::{
+    fcntl::{Flock, FlockArg, OFlag},
+    sys::{
+        signal::{self, SigHandler, Signal},
+        stat::Mode,
+    },
+};
+use regex::Regex;
+use syslog::Facility;
+
+mod systemd_manager {
+    #![allow(non_upper_case_globals)]
+    #![allow(non_camel_case_types)]
+    #![allow(non_snake_case)]
+    #![allow(unused)]
+    include!(concat!(env!("OUT_DIR"), "/systemd_manager.rs"));
+}
+
+mod logind_manager {
+    #![allow(non_upper_case_globals)]
+    #![allow(non_camel_case_types)]
+    #![allow(non_snake_case)]
+    #![allow(unused)]
+    include!(concat!(env!("OUT_DIR"), "/logind_manager.rs"));
+}
+
+use crate::systemd_manager::OrgFreedesktopSystemd1Manager;
+use crate::{
+    logind_manager::OrgFreedesktopLogin1Manager,
+    systemd_manager::{
+        OrgFreedesktopSystemd1ManagerJobRemoved, OrgFreedesktopSystemd1ManagerReloading,
+    },
+};
+
+type UnitInfo = HashMap<String, HashMap<String, Vec<String>>>;
+
+const SYSINIT_REACTIVATION_TARGET: &str = "sysinit-reactivation.target";
+
+// To be robust against interruption, record what units need to be started etc. We read these files
+// again every time this program starts to make sure we continue where the old (interrupted) script
+// left off.
+const START_LIST_FILE: &str = "/run/nixos/start-list";
+const RESTART_LIST_FILE: &str = "/run/nixos/restart-list";
+const RELOAD_LIST_FILE: &str = "/run/nixos/reload-list";
+
+// Parse restart/reload requests by the activation script. Activation scripts may write
+// newline-separated units to the restart file and switch-to-configuration will handle them. While
+// `stopIfChanged = true` is ignored, switch-to-configuration will handle `restartIfChanged =
+// false` and `reloadIfChanged = true`. This is the same as specifying a restart trigger in the
+// NixOS module.
+//
+// The reload file asks this program to reload a unit. This is the same as specifying a reload
+// trigger in the NixOS module and can be ignored if the unit is restarted in this activation.
+const RESTART_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/activation-restart-list";
+const RELOAD_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/activation-reload-list";
+const DRY_RESTART_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/dry-activation-restart-list";
+const DRY_RELOAD_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/dry-activation-reload-list";
+
+#[derive(Debug, Clone, PartialEq)]
+enum Action {
+    Switch,
+    Boot,
+    Test,
+    DryActivate,
+}
+
+impl std::str::FromStr for Action {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+        Ok(match s {
+            "switch" => Self::Switch,
+            "boot" => Self::Boot,
+            "test" => Self::Test,
+            "dry-activate" => Self::DryActivate,
+            _ => bail!("invalid action {s}"),
+        })
+    }
+}
+
+impl Into<&'static str> for &Action {
+    fn into(self) -> &'static str {
+        match self {
+            Action::Switch => "switch",
+            Action::Boot => "boot",
+            Action::Test => "test",
+            Action::DryActivate => "dry-activate",
+        }
+    }
+}
+
+// Allow for this switch-to-configuration to remain consistent with the perl implementation.
+// Perl's "die" uses errno to set the exit code: https://perldoc.perl.org/perlvar#%24%21
+fn die() -> ! {
+    std::process::exit(std::io::Error::last_os_error().raw_os_error().unwrap_or(1));
+}
+
+fn parse_os_release() -> Result<HashMap<String, String>> {
+    Ok(std::fs::read_to_string("/etc/os-release")
+        .context("Failed to read /etc/os-release")?
+        .lines()
+        .into_iter()
+        .fold(HashMap::new(), |mut acc, line| {
+            if let Some((k, v)) = line.split_once('=') {
+                acc.insert(k.to_string(), v.to_string());
+            }
+
+            acc
+        }))
+}
+
+fn do_install_bootloader(command: &str, toplevel: &Path) -> Result<()> {
+    let mut cmd_split = command.split_whitespace();
+    let Some(argv0) = cmd_split.next() else {
+        bail!("missing first argument in install bootloader commands");
+    };
+
+    match std::process::Command::new(argv0)
+        .args(cmd_split.collect::<Vec<&str>>())
+        .arg(toplevel)
+        .spawn()
+        .map(|mut child| child.wait())
+    {
+        Ok(Ok(status)) if status.success() => {}
+        _ => {
+            eprintln!("Failed to install bootloader");
+            die();
+        }
+    }
+
+    Ok(())
+}
+
+extern "C" fn handle_sigpipe(_signal: nix::libc::c_int) {}
+
+fn required_env(var: &str) -> anyhow::Result<String> {
+    std::env::var(var).with_context(|| format!("missing required environment variable ${var}"))
+}
+
+#[derive(Debug)]
+struct UnitState {
+    state: String,
+    substate: String,
+}
+
+// Asks the currently running systemd instance via dbus which units are active. Returns a hash
+// where the key is the name of each unit and the value a hash of load, state, substate.
+fn get_active_units<'a>(
+    systemd_manager: &Proxy<'a, &LocalConnection>,
+) -> Result<HashMap<String, UnitState>> {
+    let units = systemd_manager
+        .list_units_by_patterns(Vec::new(), Vec::new())
+        .context("Failed to list systemd units")?;
+
+    Ok(units
+        .into_iter()
+        .filter_map(
+            |(
+                id,
+                _description,
+                _load_state,
+                active_state,
+                sub_state,
+                following,
+                _unit_path,
+                _job_id,
+                _job_type,
+                _job_path,
+            )| {
+                if following == "" && active_state != "inactive" {
+                    Some((id, active_state, sub_state))
+                } else {
+                    None
+                }
+            },
+        )
+        .fold(HashMap::new(), |mut acc, (id, active_state, sub_state)| {
+            acc.insert(
+                id,
+                UnitState {
+                    state: active_state,
+                    substate: sub_state,
+                },
+            );
+
+            acc
+        }))
+}
+
+// This function takes a single ini file that specified systemd configuration like unit
+// configuration and parses it into a HashMap where the keys are the sections of the unit file and
+// the values are HashMaps themselves. These HashMaps have the unit file keys as their keys (left
+// side of =) and an array of all values that were set as their values. If a value is empty (for
+// example `ExecStart=`), then all current definitions are removed.
+//
+// Instead of returning the HashMap, this function takes a mutable reference to a HashMap to return
+// the data in. This allows calling the function multiple times with the same Hashmap to parse
+// override files.
+fn parse_systemd_ini(data: &mut UnitInfo, mut unit_file: impl Read) -> Result<()> {
+    let mut unit_file_content = String::new();
+    _ = unit_file
+        .read_to_string(&mut unit_file_content)
+        .context("Failed to read unit file")?;
+
+    let ini = Ini::load_from_str_opt(
+        &unit_file_content,
+        ParseOption {
+            enabled_quote: true,
+            // Allow for escaped characters that won't get interpreted by the INI parser. These
+            // often show up in systemd unit files device/mount/swap unit names (e.g. dev-disk-by\x2dlabel-root.device).
+            enabled_escape: false,
+        },
+    )
+    .context("Failed parse unit file as INI")?;
+
+    // Copy over all sections
+    for (section, properties) in ini.iter() {
+        let Some(section) = section else {
+            continue;
+        };
+
+        if section == "Install" {
+            // Skip the [Install] section because it has no relevant keys for us
+            continue;
+        }
+
+        let section_map = if let Some(section_map) = data.get_mut(section) {
+            section_map
+        } else {
+            data.insert(section.to_string(), HashMap::new());
+            data.get_mut(section)
+                .ok_or(anyhow!("section name should exist in hashmap"))?
+        };
+
+        for (ini_key, _) in properties {
+            let values = properties.get_all(ini_key);
+            let values = values
+                .into_iter()
+                .map(String::from)
+                .collect::<Vec<String>>();
+
+            // Go over all values
+            let mut new_vals = Vec::new();
+            let mut clear_existing = false;
+
+            for val in values {
+                // If a value is empty, it's an override that tells us to clean the value
+                if val.is_empty() {
+                    new_vals.clear();
+                    clear_existing = true;
+                } else {
+                    new_vals.push(val);
+                }
+            }
+
+            match (section_map.get_mut(ini_key), clear_existing) {
+                (Some(existing_vals), false) => existing_vals.extend(new_vals),
+                _ => _ = section_map.insert(ini_key.to_string(), new_vals),
+            };
+        }
+    }
+
+    Ok(())
+}
+
+// This function takes the path to a systemd configuration file (like a unit configuration) and
+// parses it into a UnitInfo structure.
+//
+// If a directory with the same basename ending in .d exists next to the unit file, it will be
+// assumed to contain override files which will be parsed as well and handled properly.
+fn parse_unit(unit_file: &Path, base_unit_path: &Path) -> Result<UnitInfo> {
+    // Parse the main unit and all overrides
+    let mut unit_data = HashMap::new();
+
+    let base_unit_file = std::fs::File::open(base_unit_path)
+        .with_context(|| format!("Failed to open unit file {}", base_unit_path.display()))?;
+    parse_systemd_ini(&mut unit_data, base_unit_file).with_context(|| {
+        format!(
+            "Failed to parse systemd unit file {}",
+            base_unit_path.display()
+        )
+    })?;
+
+    for entry in
+        glob(&format!("{}.d/*.conf", base_unit_path.display())).context("Invalid glob pattern")?
+    {
+        let Ok(entry) = entry else {
+            continue;
+        };
+
+        let unit_file = std::fs::File::open(&entry)
+            .with_context(|| format!("Failed to open unit file {}", entry.display()))?;
+        parse_systemd_ini(&mut unit_data, unit_file)?;
+    }
+
+    // Handle drop-in template-unit instance overrides
+    if unit_file != base_unit_path {
+        for entry in
+            glob(&format!("{}.d/*.conf", unit_file.display())).context("Invalid glob pattern")?
+        {
+            let Ok(entry) = entry else {
+                continue;
+            };
+
+            let unit_file = std::fs::File::open(&entry)
+                .with_context(|| format!("Failed to open unit file {}", entry.display()))?;
+            parse_systemd_ini(&mut unit_data, unit_file)?;
+        }
+    }
+
+    Ok(unit_data)
+}
+
+// Checks whether a specified boolean in a systemd unit is true or false, with a default that is
+// applied when the value is not set.
+fn parse_systemd_bool(
+    unit_data: Option<&UnitInfo>,
+    section_name: &str,
+    bool_name: &str,
+    default: bool,
+) -> bool {
+    if let Some(Some(Some(Some(b)))) = unit_data.map(|data| {
+        data.get(section_name).map(|section| {
+            section.get(bool_name).map(|vals| {
+                vals.last()
+                    .map(|last| matches!(last.as_str(), "1" | "yes" | "true" | "on"))
+            })
+        })
+    }) {
+        b
+    } else {
+        default
+    }
+}
+
+#[derive(Debug, PartialEq)]
+enum UnitComparison {
+    Equal,
+    UnequalNeedsRestart,
+    UnequalNeedsReload,
+}
+
+// Compare the contents of two unit files and return whether the unit needs to be restarted or
+// reloaded. If the units differ, the service is restarted unless the only difference is
+// `X-Reload-Triggers` in the `Unit` section. If this is the only modification, the unit is
+// reloaded instead of restarted. If the only difference is `Options` in the `[Mount]` section, the
+// unit is reloaded rather than restarted.
+fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison {
+    let mut ret = UnitComparison::Equal;
+
+    let unit_section_ignores = HashMap::from(
+        [
+            "X-Reload-Triggers",
+            "Description",
+            "Documentation",
+            "OnFailure",
+            "OnSuccess",
+            "OnFailureJobMode",
+            "IgnoreOnIsolate",
+            "StopWhenUnneeded",
+            "RefuseManualStart",
+            "RefuseManualStop",
+            "AllowIsolate",
+            "CollectMode",
+            "SourcePath",
+        ]
+        .map(|name| (name, ())),
+    );
+
+    let mut section_cmp = new_unit.keys().fold(HashMap::new(), |mut acc, key| {
+        acc.insert(key.as_str(), ());
+        acc
+    });
+
+    // Iterate over the sections
+    for (section_name, section_val) in current_unit {
+        // Missing section in the new unit?
+        if !section_cmp.contains_key(section_name.as_str()) {
+            // If the [Unit] section was removed, make sure that only keys were in it that are
+            // ignored
+            if section_name == "Unit" {
+                for (ini_key, _ini_val) in section_val {
+                    if !unit_section_ignores.contains_key(ini_key.as_str()) {
+                        return UnitComparison::UnequalNeedsRestart;
+                    }
+                }
+                continue; // check the next section
+            } else {
+                return UnitComparison::UnequalNeedsRestart;
+            }
+        }
+
+        section_cmp.remove(section_name.as_str());
+
+        // Comparison hash for the section contents
+        let mut ini_cmp = new_unit
+            .get(section_name)
+            .map(|section_val| {
+                section_val.keys().fold(HashMap::new(), |mut acc, ini_key| {
+                    acc.insert(ini_key.as_str(), ());
+                    acc
+                })
+            })
+            .unwrap_or_default();
+
+        // Iterate over the keys of the section
+        for (ini_key, current_value) in section_val {
+            ini_cmp.remove(ini_key.as_str());
+            let Some(Some(new_value)) = new_unit
+                .get(section_name)
+                .map(|section| section.get(ini_key))
+            else {
+                // If the key is missing in the new unit, they are different unless the key that is
+                // now missing is one of the ignored keys
+                if section_name == "Unit" && unit_section_ignores.contains_key(ini_key.as_str()) {
+                    continue;
+                }
+                return UnitComparison::UnequalNeedsRestart;
+            };
+
+            // If the contents are different, the units are different
+            if current_value != new_value {
+                if section_name == "Unit" {
+                    if ini_key == "X-Reload-Triggers" {
+                        ret = UnitComparison::UnequalNeedsReload;
+                        continue;
+                    } else if unit_section_ignores.contains_key(ini_key.as_str()) {
+                        continue;
+                    }
+                }
+
+                // If this is a mount unit, check if it was only `Options`
+                if section_name == "Mount" && ini_key == "Options" {
+                    ret = UnitComparison::UnequalNeedsReload;
+                    continue;
+                }
+
+                return UnitComparison::UnequalNeedsRestart;
+            }
+        }
+
+        // A key was introduced that was missing in the previous unit
+        if !ini_cmp.is_empty() {
+            if section_name == "Unit" {
+                for (ini_key, _) in ini_cmp {
+                    if ini_key == "X-Reload-Triggers" {
+                        ret = UnitComparison::UnequalNeedsReload;
+                    } else if unit_section_ignores.contains_key(ini_key) {
+                        continue;
+                    } else {
+                        return UnitComparison::UnequalNeedsRestart;
+                    }
+                }
+            } else {
+                return UnitComparison::UnequalNeedsRestart;
+            }
+        }
+    }
+
+    // A section was introduced that was missing in the previous unit
+    if !section_cmp.is_empty() {
+        if section_cmp.keys().len() == 1 && section_cmp.contains_key("Unit") {
+            if let Some(new_unit_unit) = new_unit.get("Unit") {
+                for (ini_key, _) in new_unit_unit {
+                    if !unit_section_ignores.contains_key(ini_key.as_str()) {
+                        return UnitComparison::UnequalNeedsRestart;
+                    } else if ini_key == "X-Reload-Triggers" {
+                        ret = UnitComparison::UnequalNeedsReload;
+                    }
+                }
+            }
+        } else {
+            return UnitComparison::UnequalNeedsRestart;
+        }
+    }
+
+    ret
+}
+
+// Called when a unit exists in both the old systemd and the new system and the units differ. This
+// figures out of what units are to be stopped, restarted, reloaded, started, and skipped.
+fn handle_modified_unit(
+    toplevel: &Path,
+    unit: &str,
+    base_name: &str,
+    new_unit_file: &Path,
+    new_base_unit_file: &Path,
+    new_unit_info: Option<&UnitInfo>,
+    active_cur: &HashMap<String, UnitState>,
+    units_to_stop: &mut HashMap<String, ()>,
+    units_to_start: &mut HashMap<String, ()>,
+    units_to_reload: &mut HashMap<String, ()>,
+    units_to_restart: &mut HashMap<String, ()>,
+    units_to_skip: &mut HashMap<String, ()>,
+) -> Result<()> {
+    let use_restart_as_stop_and_start = new_unit_info.is_none();
+
+    if matches!(
+        unit,
+        "sysinit.target" | "basic.target" | "multi-user.target" | "graphical.target"
+    ) || unit.ends_with(".unit")
+        || unit.ends_with(".slice")
+    {
+        // Do nothing.  These cannot be restarted directly.
+
+        // Slices and Paths don't have to be restarted since properties (resource limits and
+        // inotify watches) seem to get applied on daemon-reload.
+    } else if unit.ends_with(".mount") {
+        // Just restart the unit. We wouldn't have gotten into this subroutine if only `Options`
+        // was changed, in which case the unit would be reloaded. The only exception is / and /nix
+        // because it's very unlikely we can safely unmount them so we reload them instead. This
+        // means that we may not get all changes into the running system but it's better than
+        // crashing it.
+        if unit == "-.mount" || unit == "nix.mount" {
+            units_to_reload.insert(unit.to_string(), ());
+            record_unit(RELOAD_LIST_FILE, unit);
+        } else {
+            units_to_restart.insert(unit.to_string(), ());
+            record_unit(RESTART_LIST_FILE, unit);
+        }
+    } else if unit.ends_with(".socket") {
+        // FIXME: do something?
+        // Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
+        // Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
+        // More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
+    } else {
+        let fallback = parse_unit(new_unit_file, new_base_unit_file)?;
+        let new_unit_info = if new_unit_info.is_some() {
+            new_unit_info
+        } else {
+            Some(&fallback)
+        };
+
+        if parse_systemd_bool(new_unit_info, "Service", "X-ReloadIfChanged", false)
+            && !units_to_restart.contains_key(unit)
+            && !(if use_restart_as_stop_and_start {
+                units_to_restart.contains_key(unit)
+            } else {
+                units_to_stop.contains_key(unit)
+            })
+        {
+            units_to_reload.insert(unit.to_string(), ());
+            record_unit(RELOAD_LIST_FILE, unit);
+        } else if !parse_systemd_bool(new_unit_info, "Service", "X-RestartIfChanged", true)
+            || parse_systemd_bool(new_unit_info, "Unit", "RefuseManualStop", false)
+            || parse_systemd_bool(new_unit_info, "Unit", "X-OnlyManualStart", false)
+        {
+            units_to_skip.insert(unit.to_string(), ());
+        } else {
+            // It doesn't make sense to stop and start non-services because they can't have
+            // ExecStop=
+            if !parse_systemd_bool(new_unit_info, "Service", "X-StopIfChanged", true)
+                || !unit.ends_with(".service")
+            {
+                // This unit should be restarted instead of stopped and started.
+                units_to_restart.insert(unit.to_string(), ());
+                record_unit(RESTART_LIST_FILE, unit);
+                // Remove from units to reload so we don't restart and reload
+                if units_to_reload.contains_key(unit) {
+                    units_to_reload.remove(unit);
+                    unrecord_unit(RELOAD_LIST_FILE, unit);
+                }
+            } else {
+                // If this unit is socket-activated, then stop the socket unit(s) as well, and
+                // restart the socket(s) instead of the service.
+                let mut socket_activated = false;
+                if unit.ends_with(".service") {
+                    let mut sockets = if let Some(Some(Some(sockets))) = new_unit_info.map(|info| {
+                        info.get("Service")
+                            .map(|service_section| service_section.get("Sockets"))
+                    }) {
+                        sockets
+                            .join(" ")
+                            .split_whitespace()
+                            .into_iter()
+                            .map(String::from)
+                            .collect()
+                    } else {
+                        Vec::new()
+                    };
+
+                    if sockets.is_empty() {
+                        sockets.push(format!("{}.socket", base_name));
+                    }
+
+                    for socket in &sockets {
+                        if active_cur.contains_key(socket) {
+                            // We can now be sure this is a socket-activated unit
+
+                            if use_restart_as_stop_and_start {
+                                units_to_restart.insert(socket.to_string(), ());
+                            } else {
+                                units_to_stop.insert(socket.to_string(), ());
+                            }
+
+                            // Only restart sockets that actually exist in new configuration:
+                            if toplevel.join("etc/systemd/system").join(socket).exists() {
+                                if use_restart_as_stop_and_start {
+                                    units_to_restart.insert(socket.to_string(), ());
+                                    record_unit(RESTART_LIST_FILE, socket);
+                                } else {
+                                    units_to_start.insert(socket.to_string(), ());
+                                    record_unit(START_LIST_FILE, socket);
+                                }
+
+                                socket_activated = true;
+                            }
+
+                            // Remove from units to reload so we don't restart and reload
+                            if units_to_reload.contains_key(unit) {
+                                units_to_reload.remove(unit);
+                                unrecord_unit(RELOAD_LIST_FILE, unit);
+                            }
+                        }
+                    }
+                }
+
+                // If the unit is not socket-activated, record that this unit needs to be started
+                // below. We write this to a file to ensure that the service gets restarted if
+                // we're interrupted.
+                if !socket_activated {
+                    if use_restart_as_stop_and_start {
+                        units_to_restart.insert(unit.to_string(), ());
+                        record_unit(RESTART_LIST_FILE, unit);
+                    } else {
+                        units_to_start.insert(unit.to_string(), ());
+                        record_unit(START_LIST_FILE, unit);
+                    }
+                }
+
+                if use_restart_as_stop_and_start {
+                    units_to_restart.insert(unit.to_string(), ());
+                } else {
+                    units_to_stop.insert(unit.to_string(), ());
+                }
+                // Remove from units to reload so we don't restart and reload
+                if units_to_reload.contains_key(unit) {
+                    units_to_reload.remove(unit);
+                    unrecord_unit(RELOAD_LIST_FILE, unit);
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
+// Writes a unit name into a given file to be more resilient against crashes of the script. Does
+// nothing when the action is dry-activate.
+fn record_unit(p: impl AsRef<Path>, unit: &str) {
+    if ACTION.get() != Some(&Action::DryActivate) {
+        if let Ok(mut f) = std::fs::File::options().append(true).create(true).open(p) {
+            _ = writeln!(&mut f, "{unit}");
+        }
+    }
+}
+
+// The opposite of record_unit, removes a unit name from a file
+fn unrecord_unit(p: impl AsRef<Path>, unit: &str) {
+    if ACTION.get() != Some(&Action::DryActivate) {
+        if let Ok(contents) = std::fs::read_to_string(&p) {
+            if let Ok(mut f) = std::fs::File::options()
+                .write(true)
+                .truncate(true)
+                .create(true)
+                .open(&p)
+            {
+                contents
+                    .lines()
+                    .into_iter()
+                    .filter(|line| line != &unit)
+                    .for_each(|line| _ = writeln!(&mut f, "{line}"))
+            }
+        }
+    }
+}
+
+fn map_from_list_file(p: impl AsRef<Path>) -> HashMap<String, ()> {
+    std::fs::read_to_string(p)
+        .unwrap_or_default()
+        .lines()
+        .filter(|line| !line.is_empty())
+        .into_iter()
+        .fold(HashMap::new(), |mut acc, line| {
+            acc.insert(line.to_string(), ());
+            acc
+        })
+}
+
+#[derive(Debug)]
+struct Filesystem {
+    device: String,
+    fs_type: String,
+    options: String,
+}
+
+#[derive(Debug)]
+#[allow(unused)]
+struct Swap(String);
+
+// Parse a fstab file, given its path. Returns a tuple of filesystems and swaps.
+//
+// Filesystems is a hash of mountpoint and { device, fsType, options } Swaps is a hash of device
+// and { options }
+fn parse_fstab(fstab: impl BufRead) -> (HashMap<String, Filesystem>, HashMap<String, Swap>) {
+    let mut filesystems = HashMap::new();
+    let mut swaps = HashMap::new();
+
+    for line in fstab.lines() {
+        let Ok(line) = line else {
+            break;
+        };
+
+        if line.contains('#') {
+            continue;
+        }
+
+        let mut split = line.split_whitespace();
+        let (Some(device), Some(mountpoint), Some(fs_type), options) = (
+            split.next(),
+            split.next(),
+            split.next(),
+            split.next().unwrap_or_default(),
+        ) else {
+            continue;
+        };
+
+        if fs_type == "swap" {
+            swaps.insert(device.to_string(), Swap(options.to_string()));
+        } else {
+            filesystems.insert(
+                mountpoint.to_string(),
+                Filesystem {
+                    device: device.to_string(),
+                    fs_type: fs_type.to_string(),
+                    options: options.to_string(),
+                },
+            );
+        }
+    }
+
+    (filesystems, swaps)
+}
+
+// Converts a path to the name of a systemd mount unit that would be responsible for mounting this
+// path.
+fn path_to_unit_name(bin_path: &Path, path: &str) -> String {
+    let Ok(output) = std::process::Command::new(bin_path.join("systemd-escape"))
+        .arg("--suffix=mount")
+        .arg("-p")
+        .arg(path)
+        .output()
+    else {
+        eprintln!("Unable to escape {}!", path);
+        die();
+    };
+
+    let Ok(unit) = String::from_utf8(output.stdout) else {
+        eprintln!("Unable to convert systemd-espape output to valid UTF-8");
+        die();
+    };
+
+    unit.trim().to_string()
+}
+
+// Returns a HashMap containing the same contents as the passed in `units`, minus the units in
+// `units_to_filter`.
+fn filter_units(
+    units_to_filter: &HashMap<String, ()>,
+    units: &HashMap<String, ()>,
+) -> HashMap<String, ()> {
+    let mut res = HashMap::new();
+
+    for (unit, _) in units {
+        if !units_to_filter.contains_key(unit) {
+            res.insert(unit.to_string(), ());
+        }
+    }
+
+    res
+}
+
+fn unit_is_active<'a>(conn: &LocalConnection, unit: &str) -> Result<bool> {
+    let unit_object_path = conn
+        .with_proxy(
+            "org.freedesktop.systemd1",
+            "/org/freedesktop/systemd1",
+            Duration::from_millis(5000),
+        )
+        .get_unit(unit)
+        .with_context(|| format!("Failed to get unit {unit}"))?;
+
+    let active_state: String = conn
+        .with_proxy(
+            "org.freedesktop.systemd1",
+            unit_object_path,
+            Duration::from_millis(5000),
+        )
+        .get("org.freedesktop.systemd1.Unit", "ActiveState")
+        .with_context(|| format!("Failed to get ExecMainStatus for {unit}"))?;
+
+    Ok(matches!(active_state.as_str(), "active" | "activating"))
+}
+
+static ACTION: OnceLock<Action> = OnceLock::new();
+
+#[derive(Debug)]
+enum Job {
+    Start,
+    Restart,
+    Reload,
+    Stop,
+}
+
+impl std::fmt::Display for Job {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Job::Start => "start",
+                Job::Restart => "restart",
+                Job::Reload => "reload",
+                Job::Stop => "stop",
+            }
+        )
+    }
+}
+
+fn new_dbus_proxies<'a>(
+    conn: &'a LocalConnection,
+) -> (
+    Proxy<'a, &'a LocalConnection>,
+    Proxy<'a, &'a LocalConnection>,
+) {
+    (
+        conn.with_proxy(
+            "org.freedesktop.systemd1",
+            "/org/freedesktop/systemd1",
+            Duration::from_millis(5000),
+        ),
+        conn.with_proxy(
+            "org.freedesktop.login1",
+            "/org/freedesktop/login1",
+            Duration::from_millis(5000),
+        ),
+    )
+}
+
+fn block_on_jobs(
+    conn: &LocalConnection,
+    submitted_jobs: &Rc<RefCell<HashMap<dbus::Path<'static>, Job>>>,
+) {
+    while !submitted_jobs.borrow().is_empty() {
+        _ = conn.process(Duration::from_millis(500));
+    }
+}
+
+fn remove_file_if_exists(p: impl AsRef<Path>) -> std::io::Result<()> {
+    match std::fs::remove_file(p) {
+        Err(err) if err.kind() != std::io::ErrorKind::NotFound => Err(err),
+        _ => Ok(()),
+    }
+}
+
+/// Performs switch-to-configuration functionality for a single non-root user
+fn do_user_switch(parent_exe: String) -> anyhow::Result<()> {
+    if Path::new(&parent_exe)
+        != Path::new("/proc/self/exe")
+            .canonicalize()
+            .context("Failed to get full path to current executable")?
+            .as_path()
+    {
+        eprintln!(
+            r#"This program is not meant to be called from outside of switch-to-configuration."#
+        );
+        die();
+    }
+
+    let dbus_conn = LocalConnection::new_session().context("Failed to open dbus connection")?;
+    let (systemd, _) = new_dbus_proxies(&dbus_conn);
+
+    let nixos_activation_done = Rc::new(RefCell::new(false));
+    let _nixos_activation_done = nixos_activation_done.clone();
+    let jobs_token = systemd
+        .match_signal(
+            move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
+                  _: &LocalConnection,
+                  _: &Message| {
+                if signal.unit.as_str() == "nixos-activation.service" {
+                    *_nixos_activation_done.borrow_mut() = true;
+                }
+
+                true
+            },
+        )
+        .context("Failed to add signal match for systemd removed jobs")?;
+
+    // The systemd user session seems to not send a Reloaded signal, so we don't have anything to
+    // wait on here.
+    _ = systemd.reexecute();
+
+    systemd
+        .restart_unit("nixos-activation.service", "replace")
+        .context("Failed to restart nixos-activation.service")?;
+
+    while !*nixos_activation_done.borrow() {
+        _ = dbus_conn
+            .process(Duration::from_secs(500))
+            .context("Failed to process dbus messages")?;
+    }
+
+    dbus_conn
+        .remove_match(jobs_token)
+        .context("Failed to remove jobs token")?;
+
+    Ok(())
+}
+
+/// Performs switch-to-configuration functionality for the entire system
+fn do_system_switch() -> anyhow::Result<()> {
+    let out = PathBuf::from(required_env("OUT")?);
+    let toplevel = PathBuf::from(required_env("TOPLEVEL")?);
+    let distro_id = required_env("DISTRO_ID")?;
+    let install_bootloader = required_env("INSTALL_BOOTLOADER")?;
+    let locale_archive = required_env("LOCALE_ARCHIVE")?;
+    let new_systemd = PathBuf::from(required_env("SYSTEMD")?);
+
+    let mut args = std::env::args();
+    let argv0 = args.next().ok_or(anyhow!("no argv[0]"))?;
+
+    let Some(Ok(action)) = args.next().map(|a| Action::from_str(&a)) else {
+        eprintln!(
+            r#"Usage: {} [switch|boot|test|dry-activate]
+switch:       make the configuration the boot default and activate now
+boot:         make the configuration the boot default
+test:         activate the configuration, but don't make it the boot default
+dry-activate: show what would be done if this configuration were activated
+"#,
+            argv0
+                .split(std::path::MAIN_SEPARATOR_STR)
+                .last()
+                .unwrap_or("switch-to-configuration")
+        );
+        std::process::exit(1);
+    };
+
+    let action = ACTION.get_or_init(|| action);
+
+    // The action that is to be performed (like switch, boot, test, dry-activate) Also exposed via
+    // environment variable from now on
+    std::env::set_var("NIXOS_ACTION", Into::<&'static str>::into(action));
+
+    // Expose the locale archive as an environment variable for systemctl and the activation script
+    if !locale_archive.is_empty() {
+        std::env::set_var("LOCALE_ARCHIVE", locale_archive);
+    }
+
+    let current_system_bin = std::path::PathBuf::from("/run/current-system/sw/bin")
+        .canonicalize()
+        .context("/run/current-system/sw/bin is missing")?;
+
+    let os_release = parse_os_release().context("Failed to parse os-release")?;
+
+    let distro_id_re = Regex::new(format!("^\"?{}\"?$", distro_id).as_str())
+        .context("Invalid regex for distro ID")?;
+
+    // This is a NixOS installation if it has /etc/NIXOS or a proper /etc/os-release.
+    if !Path::new("/etc/NIXOS").is_file()
+        && !os_release
+            .get("ID")
+            .map(|id| distro_id_re.is_match(id))
+            .unwrap_or_default()
+    {
+        eprintln!("This is not a NixOS installation!");
+        die();
+    }
+
+    std::fs::create_dir_all("/run/nixos").context("Failed to create /run/nixos directory")?;
+    let perms = std::fs::Permissions::from_mode(0o755);
+    std::fs::set_permissions("/run/nixos", perms)
+        .context("Failed to set permissions on /run/nixos directory")?;
+
+    let Ok(lock) = std::fs::OpenOptions::new()
+        .append(true)
+        .create(true)
+        .open("/run/nixos/switch-to-configuration.lock")
+    else {
+        eprintln!("Could not open lock");
+        die();
+    };
+
+    let Ok(_lock) = Flock::lock(lock, FlockArg::LockExclusive) else {
+        eprintln!("Could not acquire lock");
+        die();
+    };
+
+    if syslog::init(Facility::LOG_USER, LevelFilter::Debug, Some("nixos")).is_err() {
+        bail!("Failed to initialize logger");
+    }
+
+    // Install or update the bootloader.
+    if matches!(action, Action::Switch | Action::Boot) {
+        do_install_bootloader(&install_bootloader, &toplevel)?;
+    }
+
+    // Just in case the new configuration hangs the system, do a sync now.
+    if std::env::var("NIXOS_NO_SYNC")
+        .as_deref()
+        .unwrap_or_default()
+        != "1"
+    {
+        let fd = nix::fcntl::open("/nix/store", OFlag::O_NOCTTY, Mode::S_IROTH)
+            .context("Failed to open /nix/store")?;
+        nix::unistd::syncfs(fd).context("Failed to sync /nix/store")?;
+    }
+
+    if *action == Action::Boot {
+        std::process::exit(0);
+    }
+
+    let current_init_interface_version =
+        std::fs::read_to_string("/run/current-system/init-interface-version").unwrap_or_default();
+
+    let new_init_interface_version =
+        std::fs::read_to_string(toplevel.join("init-interface-version"))
+            .context("File init-interface-version should exist")?;
+
+    // Check if we can activate the new configuration.
+    if current_init_interface_version != new_init_interface_version {
+        eprintln!(
+            r#"Warning: the new NixOS configuration has an ‘init’ that is
+incompatible with the current configuration.  The new configuration
+won't take effect until you reboot the system.
+"#
+        );
+        std::process::exit(100);
+    }
+
+    // Ignore SIGHUP so that we're not killed if we're running on (say) virtual console 1 and we
+    // restart the "tty1" unit.
+    let handler = SigHandler::Handler(handle_sigpipe);
+    unsafe { signal::signal(Signal::SIGPIPE, handler) }.context("Failed to set SIGPIPE handler")?;
+
+    let mut units_to_stop = HashMap::new();
+    let mut units_to_skip = HashMap::new();
+    let mut units_to_filter = HashMap::new(); // units not shown
+
+    let mut units_to_start = map_from_list_file(START_LIST_FILE);
+    let mut units_to_restart = map_from_list_file(RESTART_LIST_FILE);
+    let mut units_to_reload = map_from_list_file(RELOAD_LIST_FILE);
+
+    let dbus_conn = LocalConnection::new_system().context("Failed to open dbus connection")?;
+    let (systemd, logind) = new_dbus_proxies(&dbus_conn);
+
+    let submitted_jobs = Rc::new(RefCell::new(HashMap::new()));
+    let finished_jobs = Rc::new(RefCell::new(HashMap::new()));
+
+    let systemd_reload_status = Rc::new(RefCell::new(false));
+
+    systemd
+        .subscribe()
+        .context("Failed to subscribe to systemd dbus messages")?;
+
+    // Wait for the system to have finished booting.
+    loop {
+        let system_state: String = systemd
+            .get("org.freedesktop.systemd1.Manager", "SystemState")
+            .context("Failed to get system state")?;
+
+        match system_state.as_str() {
+            "running" | "degraded" | "maintenance" => break,
+            _ => {
+                _ = dbus_conn
+                    .process(Duration::from_millis(500))
+                    .context("Failed to process dbus messages")?
+            }
+        }
+    }
+
+    let _systemd_reload_status = systemd_reload_status.clone();
+    let reloading_token = systemd
+        .match_signal(
+            move |signal: OrgFreedesktopSystemd1ManagerReloading,
+                  _: &LocalConnection,
+                  _msg: &Message| {
+                *_systemd_reload_status.borrow_mut() = signal.active;
+
+                true
+            },
+        )
+        .context("Failed to add systemd Reloading match")?;
+
+    let _submitted_jobs = submitted_jobs.clone();
+    let _finished_jobs = finished_jobs.clone();
+    let job_removed_token = systemd
+        .match_signal(
+            move |signal: OrgFreedesktopSystemd1ManagerJobRemoved,
+                  _: &LocalConnection,
+                  _msg: &Message| {
+                if let Some(old) = _submitted_jobs.borrow_mut().remove(&signal.job) {
+                    let mut finished_jobs = _finished_jobs.borrow_mut();
+                    finished_jobs.insert(signal.job, (signal.unit, old, signal.result));
+                }
+
+                true
+            },
+        )
+        .context("Failed to add systemd JobRemoved match")?;
+
+    let current_active_units = get_active_units(&systemd)?;
+
+    let template_unit_re = Regex::new(r"^(.*)@[^\.]*\.(.*)$")
+        .context("Invalid regex for matching systemd template units")?;
+    let unit_name_re = Regex::new(r"^(.*)\.[[:lower:]]*$")
+        .context("Invalid regex for matching systemd unit names")?;
+
+    for (unit, unit_state) in &current_active_units {
+        let current_unit_file = Path::new("/etc/systemd/system").join(&unit);
+        let new_unit_file = toplevel.join("etc/systemd/system").join(&unit);
+
+        let mut base_unit = unit.clone();
+        let mut current_base_unit_file = current_unit_file.clone();
+        let mut new_base_unit_file = new_unit_file.clone();
+
+        // Detect template instances
+        if let Some((Some(template_name), Some(template_instance))) =
+            template_unit_re.captures(&unit).map(|captures| {
+                (
+                    captures.get(1).map(|c| c.as_str()),
+                    captures.get(2).map(|c| c.as_str()),
+                )
+            })
+        {
+            if !current_unit_file.exists() && !new_unit_file.exists() {
+                base_unit = format!("{}@.{}", template_name, template_instance);
+                current_base_unit_file = Path::new("/etc/systemd/system").join(&base_unit);
+                new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
+            }
+        }
+
+        let mut base_name = base_unit.as_str();
+        if let Some(Some(new_base_name)) = unit_name_re
+            .captures(&base_unit)
+            .map(|capture| capture.get(1).map(|first| first.as_str()))
+        {
+            base_name = new_base_name;
+        }
+
+        if current_base_unit_file.exists()
+            && (unit_state.state == "active" || unit_state.state == "activating")
+        {
+            if new_base_unit_file
+                .canonicalize()
+                .map(|full_path| full_path == Path::new("/dev/null"))
+                .unwrap_or(true)
+            {
+                let current_unit_info = parse_unit(&current_unit_file, &current_base_unit_file)?;
+                if parse_systemd_bool(Some(&current_unit_info), "Unit", "X-StopOnRemoval", true) {
+                    _ = units_to_stop.insert(unit.to_string(), ());
+                }
+            } else if unit.ends_with(".target") {
+                let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
+
+                // Cause all active target units to be restarted below. This should start most
+                // changed units we stop here as well as any new dependencies (including new mounts
+                // and swap devices).  FIXME: the suspend target is sometimes active after the
+                // system has resumed, which probably should not be the case.  Just ignore it.
+                if !matches!(
+                    unit.as_str(),
+                    "suspend.target" | "hibernate.target" | "hybrid-sleep.target"
+                ) {
+                    if !(parse_systemd_bool(
+                        Some(&new_unit_info),
+                        "Unit",
+                        "RefuseManualStart",
+                        false,
+                    ) || parse_systemd_bool(
+                        Some(&new_unit_info),
+                        "Unit",
+                        "X-OnlyManualStart",
+                        false,
+                    )) {
+                        units_to_start.insert(unit.to_string(), ());
+                        record_unit(START_LIST_FILE, unit);
+                        // Don't spam the user with target units that always get started.
+                        if std::env::var("STC_DISPLAY_ALL_UNITS").as_deref() != Ok("1") {
+                            units_to_filter.insert(unit.to_string(), ());
+                        }
+                    }
+                }
+
+                // Stop targets that have X-StopOnReconfiguration set. This is necessary to respect
+                // dependency orderings involving targets: if unit X starts after target Y and
+                // target Y starts after unit Z, then if X and Z have both changed, then X should
+                // be restarted after Z.  However, if target Y is in the "active" state, X and Z
+                // will be restarted at the same time because X's dependency on Y is already
+                // satisfied.  Thus, we need to stop Y first. Stopping a target generally has no
+                // effect on other units (unless there is a PartOf dependency), so this is just a
+                // bookkeeping thing to get systemd to do the right thing.
+                if parse_systemd_bool(
+                    Some(&new_unit_info),
+                    "Unit",
+                    "X-StopOnReconfiguration",
+                    false,
+                ) {
+                    units_to_stop.insert(unit.to_string(), ());
+                }
+            } else {
+                let current_unit_info = parse_unit(&current_unit_file, &current_base_unit_file)?;
+                let new_unit_info = parse_unit(&new_unit_file, &new_base_unit_file)?;
+                match compare_units(&current_unit_info, &new_unit_info) {
+                    UnitComparison::UnequalNeedsRestart => {
+                        handle_modified_unit(
+                            &toplevel,
+                            &unit,
+                            base_name,
+                            &new_unit_file,
+                            &new_base_unit_file,
+                            Some(&new_unit_info),
+                            &current_active_units,
+                            &mut units_to_stop,
+                            &mut units_to_start,
+                            &mut units_to_reload,
+                            &mut units_to_restart,
+                            &mut units_to_skip,
+                        )?;
+                    }
+                    UnitComparison::UnequalNeedsReload if !units_to_restart.contains_key(unit) => {
+                        units_to_reload.insert(unit.clone(), ());
+                        record_unit(RELOAD_LIST_FILE, &unit);
+                    }
+                    _ => {}
+                }
+            }
+        }
+    }
+
+    // Compare the previous and new fstab to figure out which filesystems need a remount or need to
+    // be unmounted. New filesystems are mounted automatically by starting local-fs.target.
+    // FIXME: might be nicer if we generated units for all mounts; then we could unify this with
+    // the unit checking code above.
+    let (current_filesystems, current_swaps) = std::fs::read_to_string("/etc/fstab")
+        .map(|fstab| parse_fstab(std::io::Cursor::new(fstab)))
+        .unwrap_or_default();
+    let (new_filesystems, new_swaps) = std::fs::read_to_string(toplevel.join("etc/fstab"))
+        .map(|fstab| parse_fstab(std::io::Cursor::new(fstab)))
+        .unwrap_or_default();
+
+    for (mountpoint, current_filesystem) in current_filesystems {
+        // Use current version of systemctl binary before daemon is reexeced.
+        let unit = path_to_unit_name(&current_system_bin, &mountpoint);
+        if let Some(new_filesystem) = new_filesystems.get(&mountpoint) {
+            if current_filesystem.fs_type != new_filesystem.fs_type
+                || current_filesystem.device != new_filesystem.device
+            {
+                if matches!(mountpoint.as_str(), "/" | "/nix") {
+                    if current_filesystem.options != new_filesystem.options {
+                        // Mount options changes, so remount it.
+                        units_to_reload.insert(unit.to_string(), ());
+                        record_unit(RELOAD_LIST_FILE, &unit)
+                    } else {
+                        // Don't unmount / or /nix if the device changed
+                        units_to_skip.insert(unit, ());
+                    }
+                } else {
+                    // Filesystem type or device changed, so unmount and mount it.
+                    units_to_restart.insert(unit.to_string(), ());
+                    record_unit(RESTART_LIST_FILE, &unit);
+                }
+            } else if current_filesystem.options != new_filesystem.options {
+                // Mount options changes, so remount it.
+                units_to_reload.insert(unit.to_string(), ());
+                record_unit(RELOAD_LIST_FILE, &unit)
+            }
+        } else {
+            // Filesystem entry disappeared, so unmount it.
+            units_to_stop.insert(unit, ());
+        }
+    }
+
+    // Also handles swap devices.
+    for (device, _) in current_swaps {
+        if new_swaps.get(&device).is_none() {
+            // Swap entry disappeared, so turn it off.  Can't use "systemctl stop" here because
+            // systemd has lots of alias units that prevent a stop from actually calling "swapoff".
+            if *action == Action::DryActivate {
+                eprintln!("would stop swap device: {}", &device);
+            } else {
+                eprintln!("stopping swap device: {}", &device);
+                let c_device = std::ffi::CString::new(device.clone())
+                    .context("failed to convert device to cstring")?;
+                if unsafe { nix::libc::swapoff(c_device.as_ptr()) } != 0 {
+                    let err = std::io::Error::last_os_error();
+                    eprintln!("Failed to stop swapping to {device}: {err}");
+                }
+            }
+        }
+        // FIXME: update swap options (i.e. its priority).
+    }
+
+    // Should we have systemd re-exec itself?
+    let current_pid1_path = Path::new("/proc/1/exe")
+        .canonicalize()
+        .unwrap_or_else(|_| PathBuf::from("/unknown"));
+    let current_systemd_system_config = Path::new("/etc/systemd/system.conf")
+        .canonicalize()
+        .unwrap_or_else(|_| PathBuf::from("/unknown"));
+    let Ok(new_pid1_path) = new_systemd.join("lib/systemd/systemd").canonicalize() else {
+        die();
+    };
+    let new_systemd_system_config = toplevel
+        .join("etc/systemd/system.conf")
+        .canonicalize()
+        .unwrap_or_else(|_| PathBuf::from("/unknown"));
+
+    let restart_systemd = current_pid1_path != new_pid1_path
+        || current_systemd_system_config != new_systemd_system_config;
+
+    let units_to_stop_filtered = filter_units(&units_to_filter, &units_to_stop);
+
+    // Show dry-run actions.
+    if *action == Action::DryActivate {
+        if !units_to_stop_filtered.is_empty() {
+            let mut units = units_to_stop_filtered
+                .keys()
+                .into_iter()
+                .map(String::as_str)
+                .collect::<Vec<&str>>();
+            units.sort_by_key(|name| name.to_lowercase());
+            eprintln!("would stop the following units: {}", units.join(", "));
+        }
+
+        if !units_to_skip.is_empty() {
+            let mut units = units_to_skip
+                .keys()
+                .into_iter()
+                .map(String::as_str)
+                .collect::<Vec<&str>>();
+            units.sort_by_key(|name| name.to_lowercase());
+            eprintln!(
+                "would NOT stop the following changed units: {}",
+                units.join(", ")
+            );
+        }
+
+        eprintln!("would activate the configuration...");
+        _ = std::process::Command::new(out.join("dry-activate"))
+            .arg(&out)
+            .spawn()
+            .map(|mut child| child.wait());
+
+        // Handle the activation script requesting the restart or reload of a unit.
+        for unit in std::fs::read_to_string(DRY_RESTART_BY_ACTIVATION_LIST_FILE)
+            .unwrap_or_default()
+            .lines()
+        {
+            let current_unit_file = Path::new("/etc/systemd/system").join(unit);
+            let new_unit_file = toplevel.join("etc/systemd/system").join(unit);
+            let mut base_unit = unit.to_string();
+            let mut new_base_unit_file = new_unit_file.clone();
+
+            // Detect template instances.
+            if let Some((Some(template_name), Some(template_instance))) =
+                template_unit_re.captures(&unit).map(|captures| {
+                    (
+                        captures.get(1).map(|c| c.as_str()),
+                        captures.get(2).map(|c| c.as_str()),
+                    )
+                })
+            {
+                if !current_unit_file.exists() && !new_unit_file.exists() {
+                    base_unit = format!("{}@.{}", template_name, template_instance);
+                    new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
+                }
+            }
+
+            let mut base_name = base_unit.as_str();
+            if let Some(Some(new_base_name)) = unit_name_re
+                .captures(&base_unit)
+                .map(|capture| capture.get(1).map(|first| first.as_str()))
+            {
+                base_name = new_base_name;
+            }
+
+            // Start units if they were not active previously
+            if !current_active_units.contains_key(unit) {
+                units_to_start.insert(unit.to_string(), ());
+                continue;
+            }
+
+            handle_modified_unit(
+                &toplevel,
+                unit,
+                base_name,
+                &new_unit_file,
+                &new_base_unit_file,
+                None,
+                &current_active_units,
+                &mut units_to_stop,
+                &mut units_to_start,
+                &mut units_to_reload,
+                &mut units_to_restart,
+                &mut units_to_skip,
+            )?;
+        }
+
+        remove_file_if_exists(DRY_RESTART_BY_ACTIVATION_LIST_FILE)
+            .with_context(|| format!("Failed to remove {}", DRY_RESTART_BY_ACTIVATION_LIST_FILE))?;
+
+        for unit in std::fs::read_to_string(DRY_RELOAD_BY_ACTIVATION_LIST_FILE)
+            .unwrap_or_default()
+            .lines()
+        {
+            if current_active_units.contains_key(unit)
+                && !units_to_restart.contains_key(unit)
+                && !units_to_stop.contains_key(unit)
+            {
+                units_to_reload.insert(unit.to_string(), ());
+                record_unit(RELOAD_LIST_FILE, unit);
+            }
+        }
+
+        remove_file_if_exists(DRY_RELOAD_BY_ACTIVATION_LIST_FILE)
+            .with_context(|| format!("Failed to remove {}", DRY_RELOAD_BY_ACTIVATION_LIST_FILE))?;
+
+        if restart_systemd {
+            eprintln!("would restart systemd");
+        }
+
+        if !units_to_reload.is_empty() {
+            let mut units = units_to_reload
+                .keys()
+                .into_iter()
+                .map(String::as_str)
+                .collect::<Vec<&str>>();
+            units.sort_by_key(|name| name.to_lowercase());
+            eprintln!("would reload the following units: {}", units.join(", "));
+        }
+
+        if !units_to_restart.is_empty() {
+            let mut units = units_to_restart
+                .keys()
+                .into_iter()
+                .map(String::as_str)
+                .collect::<Vec<&str>>();
+            units.sort_by_key(|name| name.to_lowercase());
+            eprintln!("would restart the following units: {}", units.join(", "));
+        }
+
+        let units_to_start_filtered = filter_units(&units_to_filter, &units_to_start);
+        if !units_to_start_filtered.is_empty() {
+            let mut units = units_to_start_filtered
+                .keys()
+                .into_iter()
+                .map(String::as_str)
+                .collect::<Vec<&str>>();
+            units.sort_by_key(|name| name.to_lowercase());
+            eprintln!("would start the following units: {}", units.join(", "));
+        }
+
+        std::process::exit(0);
+    }
+
+    log::info!("switching to system configuration {}", toplevel.display());
+
+    if !units_to_stop.is_empty() {
+        if !units_to_stop_filtered.is_empty() {
+            let mut units = units_to_stop_filtered
+                .keys()
+                .into_iter()
+                .map(String::as_str)
+                .collect::<Vec<&str>>();
+            units.sort_by_key(|name| name.to_lowercase());
+            eprintln!("stopping the following units: {}", units.join(", "));
+        }
+
+        for unit in units_to_stop.keys() {
+            match systemd.stop_unit(unit, "replace") {
+                Ok(job_path) => {
+                    let mut j = submitted_jobs.borrow_mut();
+                    j.insert(job_path.to_owned(), Job::Stop);
+                }
+                Err(_) => {}
+            };
+        }
+
+        block_on_jobs(&dbus_conn, &submitted_jobs);
+    }
+
+    if !units_to_skip.is_empty() {
+        let mut units = units_to_skip
+            .keys()
+            .into_iter()
+            .map(String::as_str)
+            .collect::<Vec<&str>>();
+        units.sort_by_key(|name| name.to_lowercase());
+        eprintln!(
+            "NOT restarting the following changed units: {}",
+            units.join(", "),
+        );
+    }
+
+    // Wait for all stop jobs to finish
+    block_on_jobs(&dbus_conn, &submitted_jobs);
+
+    let mut exit_code = 0;
+
+    // Activate the new configuration (i.e., update /etc, make accounts, and so on).
+    eprintln!("activating the configuration...");
+    match std::process::Command::new(out.join("activate"))
+        .arg(&out)
+        .spawn()
+        .map(|mut child| child.wait())
+    {
+        Ok(Ok(status)) if status.success() => {}
+        Err(_) => {
+            // allow toplevel to not have an activation script
+        }
+        _ => {
+            eprintln!("Failed to run activate script");
+            exit_code = 2;
+        }
+    }
+
+    // Handle the activation script requesting the restart or reload of a unit.
+    for unit in std::fs::read_to_string(RESTART_BY_ACTIVATION_LIST_FILE)
+        .unwrap_or_default()
+        .lines()
+    {
+        let new_unit_file = toplevel.join("etc/systemd/system").join(unit);
+        let mut base_unit = unit.to_string();
+        let mut new_base_unit_file = new_unit_file.clone();
+
+        // Detect template instances.
+        if let Some((Some(template_name), Some(template_instance))) =
+            template_unit_re.captures(&unit).map(|captures| {
+                (
+                    captures.get(1).map(|c| c.as_str()),
+                    captures.get(2).map(|c| c.as_str()),
+                )
+            })
+        {
+            if !new_unit_file.exists() {
+                base_unit = format!("{}@.{}", template_name, template_instance);
+                new_base_unit_file = toplevel.join("etc/systemd/system").join(&base_unit);
+            }
+        }
+
+        let mut base_name = base_unit.as_str();
+        if let Some(Some(new_base_name)) = unit_name_re
+            .captures(&base_unit)
+            .map(|capture| capture.get(1).map(|first| first.as_str()))
+        {
+            base_name = new_base_name;
+        }
+
+        // Start units if they were not active previously
+        if !current_active_units.contains_key(unit) {
+            units_to_start.insert(unit.to_string(), ());
+            record_unit(START_LIST_FILE, unit);
+            continue;
+        }
+
+        handle_modified_unit(
+            &toplevel,
+            unit,
+            base_name,
+            &new_unit_file,
+            &new_base_unit_file,
+            None,
+            &current_active_units,
+            &mut units_to_stop,
+            &mut units_to_start,
+            &mut units_to_reload,
+            &mut units_to_restart,
+            &mut units_to_skip,
+        )?;
+    }
+
+    // We can remove the file now because it has been propagated to the other restart/reload files
+    remove_file_if_exists(RESTART_BY_ACTIVATION_LIST_FILE)
+        .with_context(|| format!("Failed to remove {}", RESTART_BY_ACTIVATION_LIST_FILE))?;
+
+    for unit in std::fs::read_to_string(RELOAD_BY_ACTIVATION_LIST_FILE)
+        .unwrap_or_default()
+        .lines()
+    {
+        if current_active_units.contains_key(unit)
+            && !units_to_restart.contains_key(unit)
+            && !units_to_stop.contains_key(unit)
+        {
+            units_to_reload.insert(unit.to_string(), ());
+            record_unit(RELOAD_LIST_FILE, unit);
+        }
+    }
+
+    // We can remove the file now because it has been propagated to the other reload file
+    remove_file_if_exists(RELOAD_BY_ACTIVATION_LIST_FILE)
+        .with_context(|| format!("Failed to remove {}", RELOAD_BY_ACTIVATION_LIST_FILE))?;
+
+    // Restart systemd if necessary. Note that this is done using the current version of systemd,
+    // just in case the new one has trouble communicating with the running pid 1.
+    if restart_systemd {
+        eprintln!("restarting systemd...");
+        _ = systemd.reexecute(); // we don't get a dbus reply here
+
+        while !*systemd_reload_status.borrow() {
+            _ = dbus_conn
+                .process(Duration::from_millis(500))
+                .context("Failed to process dbus messages")?;
+        }
+    }
+
+    // Forget about previously failed services.
+    systemd
+        .reset_failed()
+        .context("Failed to reset failed units")?;
+
+    // Make systemd reload its units.
+    _ = systemd.reload(); // we don't get a dbus reply here
+    while !*systemd_reload_status.borrow() {
+        _ = dbus_conn
+            .process(Duration::from_millis(500))
+            .context("Failed to process dbus messages")?;
+    }
+
+    dbus_conn
+        .remove_match(reloading_token)
+        .context("Failed to cleanup systemd Reloading match")?;
+
+    // Reload user units
+    match logind.list_users() {
+        Err(err) => {
+            eprintln!("Unable to list users with logind: {err}");
+            die();
+        }
+        Ok(users) => {
+            for (uid, name, _) in users {
+                eprintln!("reloading user units for {}...", name);
+                let myself = Path::new("/proc/self/exe")
+                    .canonicalize()
+                    .context("Failed to get full path to /proc/self/exe")?;
+
+                std::process::Command::new(&myself)
+                    .uid(uid)
+                    .env("XDG_RUNTIME_DIR", format!("/run/user/{}", uid))
+                    .env("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE", &myself)
+                    .spawn()
+                    .map(|mut child| _ = child.wait())
+                    .with_context(|| format!("Failed to run user activation for {name}"))?;
+            }
+        }
+    }
+
+    // Restart sysinit-reactivation.target. This target only exists to restart services ordered
+    // before sysinit.target. We cannot use X-StopOnReconfiguration to restart sysinit.target
+    // because then ALL services of the system would be restarted since all normal services have a
+    // default dependency on sysinit.target. sysinit-reactivation.target ensures that services
+    // ordered BEFORE sysinit.target get re-started in the correct order. Ordering between these
+    // services is respected.
+    eprintln!("restarting {SYSINIT_REACTIVATION_TARGET}");
+    match systemd.restart_unit(SYSINIT_REACTIVATION_TARGET, "replace") {
+        Ok(job_path) => {
+            let mut jobs = submitted_jobs.borrow_mut();
+            jobs.insert(job_path, Job::Restart);
+        }
+        Err(err) => {
+            eprintln!("Failed to restart {SYSINIT_REACTIVATION_TARGET}: {err}");
+            exit_code = 4;
+        }
+    }
+
+    // Wait for the restart job of sysinit-reactivation.service to finish
+    block_on_jobs(&dbus_conn, &submitted_jobs);
+
+    // Before reloading we need to ensure that the units are still active. They may have been
+    // deactivated because one of their requirements got stopped. If they are inactive but should
+    // have been reloaded, the user probably expects them to be started.
+    if !units_to_reload.is_empty() {
+        for (unit, _) in units_to_reload.clone() {
+            if !unit_is_active(&dbus_conn, &unit)? {
+                // Figure out if we need to start the unit
+                let unit_info = parse_unit(
+                    toplevel.join("etc/systemd/system").join(&unit).as_path(),
+                    toplevel.join("etc/systemd/system").join(&unit).as_path(),
+                )?;
+                if !parse_systemd_bool(Some(&unit_info), "Unit", "RefuseManualStart", false)
+                    || parse_systemd_bool(Some(&unit_info), "Unit", "X-OnlyManualStart", false)
+                {
+                    units_to_start.insert(unit.clone(), ());
+                    record_unit(START_LIST_FILE, &unit);
+                }
+                // Don't reload the unit, reloading would fail
+                units_to_reload.remove(&unit);
+                unrecord_unit(RELOAD_LIST_FILE, &unit);
+            }
+        }
+    }
+
+    // Reload units that need it. This includes remounting changed mount units.
+    if !units_to_reload.is_empty() {
+        let mut units = units_to_reload
+            .keys()
+            .into_iter()
+            .map(String::as_str)
+            .collect::<Vec<&str>>();
+        units.sort_by_key(|name| name.to_lowercase());
+        eprintln!("reloading the following units: {}", units.join(", "));
+
+        for unit in units {
+            match systemd.reload_unit(unit, "replace") {
+                Ok(job_path) => {
+                    submitted_jobs
+                        .borrow_mut()
+                        .insert(job_path.clone(), Job::Reload);
+                }
+                Err(err) => {
+                    eprintln!("Failed to reload {unit}: {err}");
+                    exit_code = 4;
+                }
+            }
+        }
+
+        block_on_jobs(&dbus_conn, &submitted_jobs);
+
+        remove_file_if_exists(RELOAD_LIST_FILE)
+            .with_context(|| format!("Failed to remove {}", RELOAD_LIST_FILE))?;
+    }
+
+    // Restart changed services (those that have to be restarted rather than stopped and started).
+    if !units_to_restart.is_empty() {
+        let mut units = units_to_restart
+            .keys()
+            .into_iter()
+            .map(String::as_str)
+            .collect::<Vec<&str>>();
+        units.sort_by_key(|name| name.to_lowercase());
+        eprintln!("restarting the following units: {}", units.join(", "));
+
+        for unit in units {
+            match systemd.restart_unit(unit, "replace") {
+                Ok(job_path) => {
+                    let mut jobs = submitted_jobs.borrow_mut();
+                    jobs.insert(job_path, Job::Restart);
+                }
+                Err(err) => {
+                    eprintln!("Failed to restart {unit}: {err}");
+                    exit_code = 4;
+                }
+            }
+        }
+
+        block_on_jobs(&dbus_conn, &submitted_jobs);
+
+        remove_file_if_exists(RESTART_LIST_FILE)
+            .with_context(|| format!("Failed to remove {}", RESTART_LIST_FILE))?;
+    }
+
+    // Start all active targets, as well as changed units we stopped above. The latter is necessary
+    // because some may not be dependencies of the targets (i.e., they were manually started).
+    // FIXME: detect units that are symlinks to other units.  We shouldn't start both at the same
+    // time because we'll get a "Failed to add path to set" error from systemd.
+    let units_to_start_filtered = filter_units(&units_to_filter, &units_to_start);
+    if !units_to_start_filtered.is_empty() {
+        let mut units = units_to_start_filtered
+            .keys()
+            .into_iter()
+            .map(String::as_str)
+            .collect::<Vec<&str>>();
+        units.sort_by_key(|name| name.to_lowercase());
+        eprintln!("starting the following units: {}", units.join(", "));
+    }
+
+    for unit in units_to_start.keys() {
+        match systemd.start_unit(unit, "replace") {
+            Ok(job_path) => {
+                let mut jobs = submitted_jobs.borrow_mut();
+                jobs.insert(job_path, Job::Start);
+            }
+            Err(err) => {
+                eprintln!("Failed to start {unit}: {err}");
+                exit_code = 4;
+            }
+        }
+    }
+
+    block_on_jobs(&dbus_conn, &submitted_jobs);
+
+    remove_file_if_exists(START_LIST_FILE)
+        .with_context(|| format!("Failed to remove {}", START_LIST_FILE))?;
+
+    for (unit, job, result) in finished_jobs.borrow().values() {
+        match result.as_str() {
+            "timeout" | "failed" | "dependency" => {
+                eprintln!("Failed to {} {}", job, unit);
+                exit_code = 4;
+            }
+            _ => {}
+        }
+    }
+
+    dbus_conn
+        .remove_match(job_removed_token)
+        .context("Failed to cleanup systemd job match")?;
+
+    // Print failed and new units.
+    let mut failed_units = Vec::new();
+    let mut new_units = Vec::new();
+
+    // NOTE: We want switch-to-configuration to be able to report to the user any units that failed
+    // to start or units that systemd had to restart due to having previously failed. This is
+    // inherently a race condition between how long our program takes to run and how long the unit
+    // in question takes to potentially fail. The amount of time we wait for new messages on the
+    // bus to settle is purely tuned so that this program is compatible with the Perl
+    // implementation.
+    //
+    // Wait for events from systemd to settle. process() will return true if we have received any
+    // messages on the bus.
+    while dbus_conn
+        .process(Duration::from_millis(250))
+        .unwrap_or_default()
+    {}
+
+    let new_active_units = get_active_units(&systemd)?;
+
+    for (unit, unit_state) in new_active_units {
+        if &unit_state.state == "failed" {
+            failed_units.push(unit);
+            continue;
+        }
+
+        if unit_state.substate == "auto-restart" && unit.ends_with(".service") {
+            // A unit in auto-restart substate is a failure *if* it previously failed to start
+            let unit_object_path = systemd
+                .get_unit(&unit)
+                .context("Failed to get unit info for {unit}")?;
+            let exec_main_status: i32 = dbus_conn
+                .with_proxy(
+                    "org.freedesktop.systemd1",
+                    unit_object_path,
+                    Duration::from_millis(5000),
+                )
+                .get("org.freedesktop.systemd1.Service", "ExecMainStatus")
+                .context("Failed to get ExecMainStatus for {unit}")?;
+
+            if exec_main_status != 0 {
+                failed_units.push(unit);
+                continue;
+            }
+        }
+
+        // Ignore scopes since they are not managed by this script but rather created and managed
+        // by third-party services via the systemd dbus API. This only lists units that are not
+        // failed (including ones that are in auto-restart but have not failed previously)
+        if unit_state.state != "failed"
+            && !current_active_units.contains_key(&unit)
+            && !unit.ends_with(".scope")
+        {
+            new_units.push(unit);
+        }
+    }
+
+    if !new_units.is_empty() {
+        new_units.sort_by_key(|name| name.to_lowercase());
+        eprintln!(
+            "the following new units were started: {}",
+            new_units.join(", ")
+        );
+    }
+
+    if !failed_units.is_empty() {
+        failed_units.sort_by_key(|name| name.to_lowercase());
+        eprintln!(
+            "warning: the following units failed: {}",
+            failed_units.join(", ")
+        );
+        _ = std::process::Command::new(new_systemd.join("bin/systemctl"))
+            .arg("status")
+            .arg("--no-pager")
+            .arg("--full")
+            .args(failed_units)
+            .spawn()
+            .map(|mut child| child.wait());
+
+        exit_code = 4;
+    }
+
+    if exit_code == 0 {
+        log::info!(
+            "finished switching to system configuration {}",
+            toplevel.display()
+        );
+    } else {
+        log::error!(
+            "switching to system configuration {} failed (status {})",
+            toplevel.display(),
+            exit_code
+        );
+    }
+
+    std::process::exit(exit_code);
+}
+
+fn main() -> anyhow::Result<()> {
+    match (
+        unsafe { nix::libc::geteuid() },
+        std::env::var("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE").ok(),
+    ) {
+        (0, None) => do_system_switch(),
+        (1..=u32::MAX, None) => bail!("This program does not support being ran outside of the switch-to-configuration environment"),
+        (_, Some(parent_exe)) => do_user_switch(parent_exe),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::collections::HashMap;
+
+    #[test]
+    fn parse_fstab() {
+        {
+            let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(""));
+            assert!(filesystems.is_empty());
+            assert!(swaps.is_empty());
+        }
+
+        {
+            let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
+                r#"\
+invalid
+                    "#,
+            ));
+            assert!(filesystems.is_empty());
+            assert!(swaps.is_empty());
+        }
+
+        {
+            let (filesystems, swaps) = super::parse_fstab(std::io::Cursor::new(
+                r#"\
+# This is a generated file.  Do not edit!
+#
+# To make changes, edit the fileSystems and swapDevices NixOS options
+# in your /etc/nixos/configuration.nix file.
+#
+# <file system> <mount point>   <type>  <options>       <dump>  <pass>
+
+# Filesystems.
+/dev/mapper/root / btrfs x-initrd.mount,compress=zstd,noatime,defaults 0 0
+/dev/disk/by-partlabel/BOOT /boot vfat x-systemd.automount 0 2
+/dev/disk/by-partlabel/home /home ext4 defaults 0 2
+/dev/mapper/usr /nix/.ro-store erofs x-initrd.mount,ro 0 2
+
+
+# Swap devices.
+                    "#,
+            ));
+            assert_eq!(filesystems.len(), 4);
+            assert_eq!(swaps.len(), 0);
+            let home_fs = filesystems.get("/home").unwrap();
+            assert_eq!(home_fs.fs_type, "ext4");
+            assert_eq!(home_fs.device, "/dev/disk/by-partlabel/home");
+            assert_eq!(home_fs.options, "defaults");
+        }
+    }
+
+    #[test]
+    fn filter_units() {
+        assert_eq!(
+            super::filter_units(&HashMap::from([]), &HashMap::from([])),
+            HashMap::from([])
+        );
+
+        assert_eq!(
+            super::filter_units(
+                &HashMap::from([("foo".to_string(), ())]),
+                &HashMap::from([("foo".to_string(), ()), ("bar".to_string(), ())])
+            ),
+            HashMap::from([("bar".to_string(), ())])
+        );
+    }
+
+    #[test]
+    fn compare_units() {
+        {
+            assert!(
+                super::compare_units(&HashMap::from([]), &HashMap::from([]))
+                    == super::UnitComparison::Equal
+            );
+
+            assert!(
+                super::compare_units(
+                    &HashMap::from([("Unit".to_string(), HashMap::from([]))]),
+                    &HashMap::from([])
+                ) == super::UnitComparison::Equal
+            );
+
+            assert!(
+                super::compare_units(
+                    &HashMap::from([(
+                        "Unit".to_string(),
+                        HashMap::from([(
+                            "X-Reload-Triggers".to_string(),
+                            vec!["foobar".to_string()]
+                        )])
+                    )]),
+                    &HashMap::from([])
+                ) == super::UnitComparison::Equal
+            );
+        }
+
+        {
+            assert!(
+                super::compare_units(
+                    &HashMap::from([("foobar".to_string(), HashMap::from([]))]),
+                    &HashMap::from([])
+                ) == super::UnitComparison::UnequalNeedsRestart
+            );
+
+            assert!(
+                super::compare_units(
+                    &HashMap::from([(
+                        "Mount".to_string(),
+                        HashMap::from([("Options".to_string(), vec![])])
+                    )]),
+                    &HashMap::from([(
+                        "Mount".to_string(),
+                        HashMap::from([("Options".to_string(), vec!["ro".to_string()])])
+                    )])
+                ) == super::UnitComparison::UnequalNeedsReload
+            );
+        }
+
+        {
+            assert!(
+                super::compare_units(
+                    &HashMap::from([]),
+                    &HashMap::from([(
+                        "Unit".to_string(),
+                        HashMap::from([(
+                            "X-Reload-Triggers".to_string(),
+                            vec!["foobar".to_string()]
+                        )])
+                    )])
+                ) == super::UnitComparison::UnequalNeedsReload
+            );
+
+            assert!(
+                super::compare_units(
+                    &HashMap::from([(
+                        "Unit".to_string(),
+                        HashMap::from([(
+                            "X-Reload-Triggers".to_string(),
+                            vec!["foobar".to_string()]
+                        )])
+                    )]),
+                    &HashMap::from([(
+                        "Unit".to_string(),
+                        HashMap::from([(
+                            "X-Reload-Triggers".to_string(),
+                            vec!["barfoo".to_string()]
+                        )])
+                    )])
+                ) == super::UnitComparison::UnequalNeedsReload
+            );
+
+            assert!(
+                super::compare_units(
+                    &HashMap::from([(
+                        "Mount".to_string(),
+                        HashMap::from([("Type".to_string(), vec!["ext4".to_string()])])
+                    )]),
+                    &HashMap::from([(
+                        "Mount".to_string(),
+                        HashMap::from([("Type".to_string(), vec!["btrfs".to_string()])])
+                    )])
+                ) == super::UnitComparison::UnequalNeedsRestart
+            );
+        }
+    }
+
+    #[test]
+    fn parse_systemd_ini() {
+        // Ensure we don't attempt to unescape content in unit files.
+        // https://github.com/NixOS/nixpkgs/issues/315602
+        {
+            let mut unit_info = HashMap::new();
+
+            let test_unit = std::io::Cursor::new(
+                r#"[Unit]
+After=dev-disk-by\x2dlabel-root.device
+"#,
+            );
+            super::parse_systemd_ini(&mut unit_info, test_unit).unwrap();
+
+            assert_eq!(
+                unit_info
+                    .get("Unit")
+                    .unwrap()
+                    .get("After")
+                    .unwrap()
+                    .first()
+                    .unwrap(),
+                "dev-disk-by\\x2dlabel-root.device"
+            );
+        }
+    }
+}