11use std:: env;
22use std:: fs;
3+ use std:: os:: unix:: fs:: symlink;
34use std:: path:: { Path , PathBuf } ;
45
56use color_eyre:: eyre:: { bail, Context } ;
@@ -11,7 +12,7 @@ use crate::commands::Command;
1112use crate :: generations;
1213use crate :: installable:: Installable ;
1314use crate :: interface:: OsSubcommand :: { self } ;
14- use crate :: interface:: { self , OsGenerationsArgs , OsRebuildArgs , OsReplArgs } ;
15+ use crate :: interface:: { self , OsGenerationsArgs , OsRebuildArgs , OsReplArgs , OsRollbackArgs } ;
1516use crate :: update:: update;
1617use crate :: util:: get_hostname;
1718
@@ -35,6 +36,7 @@ impl interface::OsArgs {
3536 }
3637 OsSubcommand :: Repl ( args) => args. run ( ) ,
3738 OsSubcommand :: Info ( args) => args. info ( ) ,
39+ OsSubcommand :: Rollback ( args) => args. rollback ( ) ,
3840 }
3941 }
4042}
@@ -187,6 +189,255 @@ impl OsRebuildArgs {
187189 }
188190}
189191
192+ impl OsRollbackArgs {
193+ fn rollback ( & self ) -> Result < ( ) > {
194+ let elevate = if self . bypass_root_check {
195+ warn ! ( "Bypassing root check, now running nix as root" ) ;
196+ false
197+ } else {
198+ if nix:: unistd:: Uid :: effective ( ) . is_root ( ) {
199+ bail ! ( "Don't run nh os as root. I will call sudo internally as needed" ) ;
200+ }
201+ true
202+ } ;
203+
204+ // Find previous generation or specific generation
205+ let target_generation = if let Some ( gen_number) = self . to {
206+ find_generation_by_number ( gen_number) ?
207+ } else {
208+ find_previous_generation ( ) ?
209+ } ;
210+
211+ info ! ( "Rolling back to generation {}" , target_generation. number) ;
212+
213+ // Construct path to the generation
214+ let profile_dir = Path :: new ( SYSTEM_PROFILE )
215+ . parent ( )
216+ . unwrap_or ( Path :: new ( "/nix/var/nix/profiles" ) ) ;
217+ let generation_link = profile_dir. join ( format ! ( "system-{}-link" , target_generation. number) ) ;
218+
219+ // Handle specialisations
220+ let current_specialisation = fs:: read_to_string ( SPEC_LOCATION ) . ok ( ) ;
221+
222+ let target_specialisation = if self . no_specialisation {
223+ None
224+ } else {
225+ self . specialisation . clone ( ) . or ( current_specialisation)
226+ } ;
227+
228+ debug ! ( "target_specialisation: {target_specialisation:?}" ) ;
229+
230+ // Compare changes between current and target generation
231+ Command :: new ( "nvd" )
232+ . arg ( "diff" )
233+ . arg ( CURRENT_PROFILE )
234+ . arg ( & generation_link)
235+ . message ( "Comparing changes" )
236+ . run ( ) ?;
237+
238+ if self . dry {
239+ info ! (
240+ "Dry run: would roll back to generation {}" ,
241+ target_generation. number
242+ ) ;
243+ return Ok ( ( ) ) ;
244+ }
245+
246+ if self . ask {
247+ info ! ( "Roll back to generation {}?" , target_generation. number) ;
248+ let confirmation = dialoguer:: Confirm :: new ( ) . default ( false ) . interact ( ) ?;
249+
250+ if !confirmation {
251+ bail ! ( "User rejected the rollback" ) ;
252+ }
253+ }
254+
255+ // Get current generation number for potential rollback
256+ let current_gen_number = match get_current_generation_number ( ) {
257+ Ok ( num) => num,
258+ Err ( e) => {
259+ warn ! ( "Failed to get current generation number: {}" , e) ;
260+ 0
261+ }
262+ } ;
263+
264+ // Set the system profile
265+ info ! ( "Setting system profile..." ) ;
266+
267+ // Instead of direct symlink operations, use a command with proper elevation
268+ Command :: new ( "ln" )
269+ . arg ( "-sfn" ) // force, symbolic link
270+ . arg ( & generation_link)
271+ . arg ( SYSTEM_PROFILE )
272+ . elevate ( elevate)
273+ . message ( "Setting system profile" )
274+ . run ( ) ?;
275+
276+ // Set up rollback protection flag
277+ let mut rollback_profile = false ;
278+
279+ // Determine the correct profile to use with specialisations
280+ let final_profile = match & target_specialisation {
281+ None => generation_link,
282+ Some ( spec) => {
283+ let spec_path = generation_link. join ( "specialisation" ) . join ( spec) ;
284+ if !spec_path. exists ( ) {
285+ warn ! (
286+ "Specialisation '{}' does not exist in generation {}" ,
287+ spec, target_generation. number
288+ ) ;
289+ warn ! ( "Using base configuration without specialisations" ) ;
290+ generation_link
291+ } else {
292+ spec_path
293+ }
294+ }
295+ } ;
296+
297+ // Activate the configuration
298+ info ! ( "Activating..." ) ;
299+
300+ let switch_to_configuration = final_profile. join ( "bin" ) . join ( "switch-to-configuration" ) ;
301+
302+ match Command :: new ( & switch_to_configuration)
303+ . arg ( "switch" )
304+ . elevate ( elevate)
305+ . run ( )
306+ {
307+ Ok ( _) => {
308+ info ! (
309+ "Successfully rolled back to generation {}" ,
310+ target_generation. number
311+ ) ;
312+ }
313+ Err ( e) => {
314+ rollback_profile = true ;
315+
316+ // If activation fails, rollback the profile
317+ if rollback_profile && current_gen_number > 0 {
318+ let current_gen_link =
319+ profile_dir. join ( format ! ( "system-{}-link" , current_gen_number) ) ;
320+
321+ Command :: new ( "ln" )
322+ . arg ( "-sfn" ) // Force, symbolic link
323+ . arg ( & current_gen_link)
324+ . arg ( SYSTEM_PROFILE )
325+ . elevate ( elevate)
326+ . message ( "Rolling back system profile" )
327+ . run ( ) ?;
328+ }
329+
330+ return Err ( e) . context ( "Failed to activate configuration" ) ;
331+ }
332+ }
333+
334+ Ok ( ( ) )
335+ }
336+ }
337+
338+ fn find_previous_generation ( ) -> Result < generations:: GenerationInfo > {
339+ let profile_path = PathBuf :: from ( SYSTEM_PROFILE ) ;
340+
341+ let mut generations: Vec < generations:: GenerationInfo > = fs:: read_dir (
342+ profile_path
343+ . parent ( )
344+ . unwrap_or ( Path :: new ( "/nix/var/nix/profiles" ) ) ,
345+ ) ?
346+ . filter_map ( |entry| {
347+ entry. ok ( ) . and_then ( |e| {
348+ let path = e. path ( ) ;
349+ if let Some ( filename) = path. file_name ( ) {
350+ if let Some ( name) = filename. to_str ( ) {
351+ if name. starts_with ( "system-" ) && name. ends_with ( "-link" ) {
352+ return generations:: describe ( & path, & profile_path) ;
353+ }
354+ }
355+ }
356+ None
357+ } )
358+ } )
359+ . collect ( ) ;
360+
361+ if generations. is_empty ( ) {
362+ bail ! ( "No generations found" ) ;
363+ }
364+
365+ generations. sort_by ( |a, b| {
366+ a. number
367+ . parse :: < u64 > ( )
368+ . unwrap_or ( 0 )
369+ . cmp ( & b. number . parse :: < u64 > ( ) . unwrap_or ( 0 ) )
370+ } ) ;
371+
372+ let current_idx = generations
373+ . iter ( )
374+ . position ( |g| g. current )
375+ . ok_or_else ( || eyre ! ( "Current generation not found" ) ) ?;
376+
377+ if current_idx == 0 {
378+ bail ! ( "No generation older than the current one exists" ) ;
379+ }
380+
381+ Ok ( generations[ current_idx - 1 ] . clone ( ) )
382+ }
383+
384+ fn find_generation_by_number ( number : u64 ) -> Result < generations:: GenerationInfo > {
385+ let profile_path = PathBuf :: from ( SYSTEM_PROFILE ) ;
386+
387+ let generations: Vec < generations:: GenerationInfo > = fs:: read_dir (
388+ profile_path
389+ . parent ( )
390+ . unwrap_or ( Path :: new ( "/nix/var/nix/profiles" ) ) ,
391+ ) ?
392+ . filter_map ( |entry| {
393+ entry. ok ( ) . and_then ( |e| {
394+ let path = e. path ( ) ;
395+ if let Some ( filename) = path. file_name ( ) {
396+ if let Some ( name) = filename. to_str ( ) {
397+ if name. starts_with ( "system-" ) && name. ends_with ( "-link" ) {
398+ return generations:: describe ( & path, & profile_path) ;
399+ }
400+ }
401+ }
402+ None
403+ } )
404+ } )
405+ . filter ( |gen| gen. number == number. to_string ( ) )
406+ . collect ( ) ;
407+
408+ if generations. is_empty ( ) {
409+ bail ! ( "Generation {} not found" , number) ;
410+ }
411+
412+ Ok ( generations[ 0 ] . clone ( ) )
413+ }
414+
415+ fn get_current_generation_number ( ) -> Result < u64 > {
416+ let profile_path = PathBuf :: from ( SYSTEM_PROFILE ) ;
417+
418+ let generations: Vec < generations:: GenerationInfo > = fs:: read_dir (
419+ profile_path
420+ . parent ( )
421+ . unwrap_or ( Path :: new ( "/nix/var/nix/profiles" ) ) ,
422+ ) ?
423+ . filter_map ( |entry| {
424+ entry
425+ . ok ( )
426+ . and_then ( |e| generations:: describe ( & e. path ( ) , & profile_path) )
427+ } )
428+ . collect ( ) ;
429+
430+ let current_gen = generations
431+ . iter ( )
432+ . find ( |g| g. current )
433+ . ok_or_else ( || eyre ! ( "Current generation not found" ) ) ?;
434+
435+ current_gen
436+ . number
437+ . parse :: < u64 > ( )
438+ . map_err ( |_| eyre ! ( "Invalid generation number" ) )
439+ }
440+
190441pub fn toplevel_for < S : AsRef < str > > ( hostname : S , installable : Installable ) -> Installable {
191442 let mut res = installable;
192443 let hostname = hostname. as_ref ( ) . to_owned ( ) ;
0 commit comments