From 46450f556c593fc0d19319ced0c71460124d707a Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Tue, 31 Mar 2026 23:23:40 +0100 Subject: [PATCH 1/6] switch-to-configuration-ng: Reload when ExecReload changes Instead of triggering a restart when the reload script changes, signal that the unit only needs to be reloaded instead. This is an important optimisation for systemd-nspawn containers where the reload script can house the container's config activation script and we want to explicitly avoid restarting. --- .../sw/switch-to-configuration-ng/src/main.rs | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) 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 index 22b340895f56..aa0d6e6b6012 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -576,6 +576,12 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison } } + // If this is a service unit, check if it was only `ExecReload` + if section_name == "Service" && ini_key == "ExecReload" { + ret = UnitComparison::UnequalNeedsReload; + continue; + } + // If this is a mount unit, check if it was only `Options` if section_name == "Mount" && ini_key == "Options" { ret = UnitComparison::UnequalNeedsReload; @@ -598,6 +604,10 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison return UnitComparison::UnequalNeedsRestart; } } + } else if section_name == "Exec" && ini_cmp.len() == 1 + && ini_cmp.contains_key("ExecReload") { + ret = UnitComparison::UnequalNeedsReload; + continue; } else { return UnitComparison::UnequalNeedsRestart; } @@ -606,7 +616,7 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison // 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 section_cmp.keys().len() == 1 && (section_cmp.contains_key("Unit") || section_cmp.contains_key("Service")) { if let Some(new_unit_unit) = new_unit.get("Unit") { for ini_key in new_unit_unit.keys() { if !unit_section_ignores.contains_key(ini_key.as_str()) { @@ -615,6 +625,14 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison ret = UnitComparison::UnequalNeedsReload; } } + } else if let Some(new_unit_service) = new_unit.get("Service") { + if new_unit_service.len() == 1 && new_unit_service.contains_key("ExecReload") { + ret = UnitComparison::UnequalNeedsReload; + } else { + return UnitComparison::UnequalNeedsRestart; + } + } else { + return UnitComparison::UnequalNeedsRestart; } } else { return UnitComparison::UnequalNeedsRestart; @@ -2623,6 +2641,38 @@ invalid ) == super::UnitComparison::UnequalNeedsReload ); + assert!( + super::compare_units( + &HashMap::from([]), + &HashMap::from([( + "Service".to_string(), + HashMap::from([( + "ExecReload".to_string(), + vec!["foobar".to_string()] + )]) + )]) + ) == super::UnitComparison::UnequalNeedsReload + ); + + assert!( + super::compare_units( + &HashMap::from([( + "Service".to_string(), + HashMap::from([( + "ExecReload".to_string(), + vec!["foobar".to_string()] + )]) + )]), + &HashMap::from([( + "Service".to_string(), + HashMap::from([( + "ExecReload".to_string(), + vec!["barfoo".to_string()] + )]) + )]) + ) == super::UnitComparison::UnequalNeedsReload + ); + assert!( super::compare_units( &HashMap::from([( From bb97db3918f6313f1bfda4c22c4ba8c1754230b8 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Thu, 23 Apr 2026 09:26:51 +0200 Subject: [PATCH 2/6] switch-to-configuration-ng: rustfmt --- .../sw/switch-to-configuration-ng/src/main.rs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 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 index aa0d6e6b6012..348f7cae5d65 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -604,8 +604,10 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison return UnitComparison::UnequalNeedsRestart; } } - } else if section_name == "Exec" && ini_cmp.len() == 1 - && ini_cmp.contains_key("ExecReload") { + } else if section_name == "Exec" + && ini_cmp.len() == 1 + && ini_cmp.contains_key("ExecReload") + { ret = UnitComparison::UnequalNeedsReload; continue; } else { @@ -616,7 +618,9 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison // 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") || section_cmp.contains_key("Service")) { + if section_cmp.keys().len() == 1 + && (section_cmp.contains_key("Unit") || section_cmp.contains_key("Service")) + { if let Some(new_unit_unit) = new_unit.get("Unit") { for ini_key in new_unit_unit.keys() { if !unit_section_ignores.contains_key(ini_key.as_str()) { @@ -2646,10 +2650,7 @@ invalid &HashMap::from([]), &HashMap::from([( "Service".to_string(), - HashMap::from([( - "ExecReload".to_string(), - vec!["foobar".to_string()] - )]) + HashMap::from([("ExecReload".to_string(), vec!["foobar".to_string()])]) )]) ) == super::UnitComparison::UnequalNeedsReload ); @@ -2658,17 +2659,11 @@ invalid super::compare_units( &HashMap::from([( "Service".to_string(), - HashMap::from([( - "ExecReload".to_string(), - vec!["foobar".to_string()] - )]) + HashMap::from([("ExecReload".to_string(), vec!["foobar".to_string()])]) )]), &HashMap::from([( "Service".to_string(), - HashMap::from([( - "ExecReload".to_string(), - vec!["barfoo".to_string()] - )]) + HashMap::from([("ExecReload".to_string(), vec!["barfoo".to_string()])]) )]) ) == super::UnitComparison::UnequalNeedsReload ); From 1086ea49e0d70da5627873753dc1febdd114d41b Mon Sep 17 00:00:00 2001 From: r-vdp Date: Thu, 23 Apr 2026 09:27:05 +0200 Subject: [PATCH 3/6] switch-to-configuration-ng: Fix ExecReload check for added key The branch handling "a key was added to an existing section" compared the section name against "Exec" (the .nspawn section header) instead of "Service", so adding ExecReload= to a [Service] that previously lacked it still triggered a restart instead of a reload. Add a unit test covering this path. --- .../sw/switch-to-configuration-ng/src/main.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 index 348f7cae5d65..fce4abcd997c 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -604,7 +604,7 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison return UnitComparison::UnequalNeedsRestart; } } - } else if section_name == "Exec" + } else if section_name == "Service" && ini_cmp.len() == 1 && ini_cmp.contains_key("ExecReload") { @@ -2668,6 +2668,23 @@ invalid ) == super::UnitComparison::UnequalNeedsReload ); + // ExecReload added to an existing [Service] section + assert!( + super::compare_units( + &HashMap::from([( + "Service".to_string(), + HashMap::from([("ExecStart".to_string(), vec!["x".to_string()])]) + )]), + &HashMap::from([( + "Service".to_string(), + HashMap::from([ + ("ExecStart".to_string(), vec!["x".to_string()]), + ("ExecReload".to_string(), vec!["y".to_string()]), + ]) + )]), + ) == super::UnitComparison::UnequalNeedsReload + ); + assert!( super::compare_units( &HashMap::from([( From ea3ca97b366a1fe188febe18b1e29bcc645e6fb7 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Thu, 23 Apr 2026 09:27:31 +0200 Subject: [PATCH 4/6] switch-to-configuration-ng: Dispatch on new section in compare_units When exactly one section was newly introduced, the code looked up new_unit.get("Unit") first regardless of which section was actually new. If [Service] was the new section but the unit also had an unchanged [Unit] section, the [Service] contents were never inspected and a newly added ExecStart= would not trigger a restart. Dispatch on section_cmp instead and add unit tests for both the restart and reload cases. --- .../sw/switch-to-configuration-ng/src/main.rs | 70 +++++++++++++++---- 1 file changed, 56 insertions(+), 14 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 index fce4abcd997c..175b295a71f8 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -618,22 +618,25 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison // 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") || section_cmp.contains_key("Service")) - { - if let Some(new_unit_unit) = new_unit.get("Unit") { - for ini_key in new_unit_unit.keys() { - if !unit_section_ignores.contains_key(ini_key.as_str()) { - return UnitComparison::UnequalNeedsRestart; - } else if ini_key == "X-Reload-Triggers" { - ret = UnitComparison::UnequalNeedsReload; + if section_cmp.keys().len() == 1 { + // Dispatch on which section is actually new. + if section_cmp.contains_key("Unit") { + if let Some(new_unit_unit) = new_unit.get("Unit") { + for ini_key in new_unit_unit.keys() { + if !unit_section_ignores.contains_key(ini_key.as_str()) { + return UnitComparison::UnequalNeedsRestart; + } else if ini_key == "X-Reload-Triggers" { + ret = UnitComparison::UnequalNeedsReload; + } } } - } else if let Some(new_unit_service) = new_unit.get("Service") { - if new_unit_service.len() == 1 && new_unit_service.contains_key("ExecReload") { - ret = UnitComparison::UnequalNeedsReload; - } else { - return UnitComparison::UnequalNeedsRestart; + } else if section_cmp.contains_key("Service") { + if let Some(new_unit_service) = new_unit.get("Service") { + if new_unit_service.len() == 1 && new_unit_service.contains_key("ExecReload") { + ret = UnitComparison::UnequalNeedsReload; + } else { + return UnitComparison::UnequalNeedsRestart; + } } } else { return UnitComparison::UnequalNeedsRestart; @@ -2668,6 +2671,45 @@ invalid ) == super::UnitComparison::UnequalNeedsReload ); + // New [Service] section while [Unit] already existed: must inspect + // the [Service] section, not the (unchanged) [Unit] one. + assert!( + super::compare_units( + &HashMap::from([( + "Unit".to_string(), + HashMap::from([("Description".to_string(), vec!["x".to_string()])]) + )]), + &HashMap::from([ + ( + "Unit".to_string(), + HashMap::from([("Description".to_string(), vec!["x".to_string()])]) + ), + ( + "Service".to_string(), + HashMap::from([("ExecStart".to_string(), vec!["y".to_string()])]) + ), + ]), + ) == super::UnitComparison::UnequalNeedsRestart + ); + assert!( + super::compare_units( + &HashMap::from([( + "Unit".to_string(), + HashMap::from([("Description".to_string(), vec!["x".to_string()])]) + )]), + &HashMap::from([ + ( + "Unit".to_string(), + HashMap::from([("Description".to_string(), vec!["x".to_string()])]) + ), + ( + "Service".to_string(), + HashMap::from([("ExecReload".to_string(), vec!["y".to_string()])]) + ), + ]), + ) == super::UnitComparison::UnequalNeedsReload + ); + // ExecReload added to an existing [Service] section assert!( super::compare_units( From da43acd6e7fe55f9d929e61fd50a21f78faf8a17 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Thu, 23 Apr 2026 09:28:38 +0200 Subject: [PATCH 5/6] switch-to-configuration-ng: Ignore removal of ExecReload Adding or changing ExecReload now triggers a reload, but removing it still triggered a restart. The running process is unaffected by the removal and the new unit no longer has a reload command to invoke, so treat it like the other ignored [Unit] keys and take no action. Covers both "key removed from existing [Service]" and "[Service] section removed that only contained ExecReload". --- .../sw/switch-to-configuration-ng/src/main.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) 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 index 175b295a71f8..1085e75674ce 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -532,6 +532,13 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison } } continue; // check the next section + } else if section_name == "Service" + && section_val.len() == 1 + && section_val.contains_key("ExecReload") + { + // Dropping ExecReload does not affect the running process and the + // new unit can no longer be reloaded, so there is nothing to do. + continue; } else { return UnitComparison::UnequalNeedsRestart; } @@ -562,6 +569,11 @@ fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison if section_name == "Unit" && unit_section_ignores.contains_key(ini_key.as_str()) { continue; } + // Dropping ExecReload does not affect the running process and the + // new unit can no longer be reloaded, so there is nothing to do. + if section_name == "Service" && ini_key == "ExecReload" { + continue; + } return UnitComparison::UnequalNeedsRestart; }; @@ -2710,6 +2722,33 @@ invalid ) == super::UnitComparison::UnequalNeedsReload ); + // ExecReload removed: running process is unaffected and the new + // unit cannot be reloaded, so no action is needed. + assert!( + super::compare_units( + &HashMap::from([( + "Service".to_string(), + HashMap::from([ + ("ExecStart".to_string(), vec!["x".to_string()]), + ("ExecReload".to_string(), vec!["y".to_string()]), + ]) + )]), + &HashMap::from([( + "Service".to_string(), + HashMap::from([("ExecStart".to_string(), vec!["x".to_string()])]) + )]), + ) == super::UnitComparison::Equal + ); + assert!( + super::compare_units( + &HashMap::from([( + "Service".to_string(), + HashMap::from([("ExecReload".to_string(), vec!["y".to_string()])]) + )]), + &HashMap::from([]), + ) == super::UnitComparison::Equal + ); + // ExecReload added to an existing [Service] section assert!( super::compare_units( From 2f5b053b9e337a3fedf2c99a1d1cc69f0dbf30e7 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Thu, 23 Apr 2026 09:49:51 +0200 Subject: [PATCH 6/6] switch-to-configuration-ng: Document ExecReload reload semantics Update the compare_units doc comment and add a release-notes entry covering the new reload-instead-of-restart behaviour. --- nixos/doc/manual/release-notes/rl-2605.section.md | 2 ++ pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index ed4559794686..a652c7a3657c 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -347,6 +347,8 @@ See . +- `switch-to-configuration` now reloads a service instead of restarting it when the only change to its unit is `ExecReload=`, and takes no action when `ExecReload=` is removed. Previously both cases triggered a restart. + - [`hardware.nvidia.branch`](#opt-hardware.nvidia.branch) was added to select the NVIDIA driver branch; setting [`hardware.nvidia.package`](#opt-hardware.nvidia.package) overrides this. - The NixOS NVIDIA module wiring has been updated to match the new `nvidia-x11` output layout. 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 index 1085e75674ce..f084f27b9ae2 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -489,9 +489,10 @@ enum UnitComparison { // 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. +// `X-Reload-Triggers` in the `[Unit]` section, `Options` in the `[Mount]` section, or `ExecReload` +// in the `[Service]` section, in which case the unit is reloaded rather than restarted. Removing +// `ExecReload` is treated as a no-op since the running process is unaffected and the new unit can +// no longer be reloaded. fn compare_units(current_unit: &UnitInfo, new_unit: &UnitInfo) -> UnitComparison { let mut ret = UnitComparison::Equal;