| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | #!/usr/bin/env perl | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Copyright (C) 2024 Ortega Froysa, Nicolás <nicolas@ortegas.org> All rights reserved. | 
					
						
							|  |  |  | # Author: Ortega Froysa, Nicolás <nicolas@ortegas.org> | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # This software is provided 'as-is', without any express or implied | 
					
						
							|  |  |  | # warranty. In no event will the authors be held liable for any damages | 
					
						
							|  |  |  | # arising from the use of this software. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Permission is granted to anyone to use this software for any purpose, | 
					
						
							|  |  |  | # including commercial applications, and to alter it and redistribute it | 
					
						
							|  |  |  | # freely, subject to the following restrictions: | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # 1. The origin of this software must not be misrepresented; you must not | 
					
						
							|  |  |  | #    claim that you wrote the original software. If you use this software | 
					
						
							|  |  |  | #    in a product, an acknowledgment in the product documentation would be | 
					
						
							|  |  |  | #    appreciated but is not required. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # 2. Altered source versions must be plainly marked as such, and must not be | 
					
						
							|  |  |  | #    misrepresented as being the original software. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # 3. This notice may not be removed or altered from any source | 
					
						
							|  |  |  | #    distribution. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | use strict; | 
					
						
							|  |  |  | use warnings; | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | use feature qw(signatures); | 
					
						
							| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | use Getopt::Std; | 
					
						
							| 
									
										
										
										
											2024-02-24 13:57:09 +01:00
										 |  |  | use File::ReadBackwards; | 
					
						
							| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-28 11:40:57 +01:00
										 |  |  | my $VERSION   = '1.0'; | 
					
						
							|  |  |  | my $PROG_NAME = 'pacundo'; | 
					
						
							| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | sub print_version() { | 
					
						
							| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | 	print("$PROG_NAME v$VERSION\n"); | 
					
						
							| 
									
										
										
										
											2024-03-07 16:12:29 +01:00
										 |  |  | 	return; | 
					
						
							| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | sub print_help() { | 
					
						
							| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | 	&print_version(); | 
					
						
							| 
									
										
										
										
											2024-03-27 10:39:14 +01:00
										 |  |  | 	print("A time machine to return your ArchLinux machine back to a working state. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | USAGE: | 
					
						
							| 
									
										
										
										
											2024-02-24 13:57:09 +01:00
										 |  |  | 	$PROG_NAME [-i|-r] [-t <num>] [-d] | 
					
						
							| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | 	$PROG_NAME -h | 
					
						
							|  |  |  | 	$PROG_NAME -v | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | OPTIONS: | 
					
						
							| 
									
										
										
										
											2024-02-24 13:57:09 +01:00
										 |  |  | 	-i         Enter interactive mode to select packages to downgrade [default behavior] | 
					
						
							|  |  |  | 	-r         Automatically downgrade all packages from last upgrade | 
					
						
							|  |  |  | 	-t <num>   Specify the number of transactions to include for undoing selection [default 1] | 
					
						
							|  |  |  | 	-d         Dry run, i.e. don't actually do anything | 
					
						
							|  |  |  | 	-h         Show this help information | 
					
						
							|  |  |  | 	-v         Print program version\n"); | 
					
						
							| 
									
										
										
										
											2024-03-07 16:12:29 +01:00
										 |  |  | 	return; | 
					
						
							| 
									
										
										
										
											2024-02-22 18:58:30 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | sub read_txs($num_txs = 1) { | 
					
						
							|  |  |  | 	my $found_txs = 0; | 
					
						
							|  |  |  | 	my $in_tx = 0; | 
					
						
							|  |  |  | 	my @undo_txs; | 
					
						
							| 
									
										
										
										
											2024-03-28 11:40:57 +01:00
										 |  |  | 	my $pacman_log = File::ReadBackwards->new('/var/log/pacman.log') or | 
					
						
							|  |  |  | 		die("Failed to load pacman log file.\n$!\n"); | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	while ($found_txs < $num_txs && defined(my $line = $pacman_log->readline)) { | 
					
						
							|  |  |  | 		unless ($in_tx) { | 
					
						
							|  |  |  | 			# Remeber that we're reading this in reverse order | 
					
						
							|  |  |  | 			if ($line =~ /\[ALPM\] transaction completed/) { | 
					
						
							|  |  |  | 				$in_tx = 1; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} elsif ($line =~ /\[ALPM\] transaction started/) { | 
					
						
							| 
									
										
										
										
											2024-02-24 13:57:09 +01:00
										 |  |  | 			$found_txs++; | 
					
						
							|  |  |  | 			$in_tx = 0; | 
					
						
							|  |  |  | 		} elsif ($line =~ /\[ALPM\] (upgraded|downgraded)/) { | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | 			my ($action, $pkg_name, $oldver, $newver) = | 
					
						
							|  |  |  | 				$line =~ /\[ALPM\] (upgraded|downgraded) ([^\s]+) \((.*) -> (.*)\)/; | 
					
						
							| 
									
										
										
										
											2024-02-24 14:15:09 +01:00
										 |  |  | 			push(@undo_txs, | 
					
						
							|  |  |  | 				{ | 
					
						
							| 
									
										
										
										
											2024-03-28 11:40:57 +01:00
										 |  |  | 					action   => $action, | 
					
						
							|  |  |  | 					pkg_name => $pkg_name, | 
					
						
							|  |  |  | 					oldver   => $oldver, | 
					
						
							|  |  |  | 					newver   => $newver, | 
					
						
							| 
									
										
										
										
											2024-02-24 14:15:09 +01:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			); | 
					
						
							| 
									
										
										
										
											2024-02-24 13:57:09 +01:00
										 |  |  | 		} elsif ($line =~ /\[ALPM\] (installed|removed)/) { | 
					
						
							| 
									
										
										
										
											2024-02-24 14:15:09 +01:00
										 |  |  | 			my ($action, $pkg_name) = $line =~ /\[ALPM\] (installed|removed) ([^\s]+)/; | 
					
						
							|  |  |  | 			push(@undo_txs, | 
					
						
							|  |  |  | 				{ | 
					
						
							| 
									
										
										
										
											2024-03-28 11:40:57 +01:00
										 |  |  | 					action   => $action, | 
					
						
							|  |  |  | 					pkg_name => $pkg_name, | 
					
						
							| 
									
										
										
										
											2024-02-24 14:15:09 +01:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			); | 
					
						
							| 
									
										
										
										
											2024-02-24 13:57:09 +01:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | 	return @undo_txs; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | sub select_txs(@undo_txs) { | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 	print("Last changes:\n"); | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 	my $n = 1; | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 	foreach my $tx (@undo_txs) { | 
					
						
							| 
									
										
										
										
											2024-03-07 16:25:03 +01:00
										 |  |  | 		format UPGRFORMAT = | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  |  @||  @<<  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<  @<<<<<<<<<<< -> @<<<<<<<<<<<<< | 
					
						
							|  |  |  | $n, $tx->{action}, $tx->{pkg_name}, $tx->{oldver}, $tx->{newver} | 
					
						
							|  |  |  | . | 
					
						
							| 
									
										
										
										
											2024-03-07 16:25:03 +01:00
										 |  |  | 		format INSTFORMAT = | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  |  @||  @<<  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | 
					
						
							|  |  |  | $n, $tx->{action}, $tx->{pkg_name} | 
					
						
							|  |  |  | . | 
					
						
							| 
									
										
										
										
											2024-03-07 16:25:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		local $~ = ($tx->{action} =~ /(upgraded|downgraded)/) ? "UPGRFORMAT" : "INSTFORMAT"; | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 		write(); | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 		$n++; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-28 11:40:57 +01:00
										 |  |  | 	print("Select transactions to undo (e.g. '1 2 3', '1-3')\n> "); | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 	my @sel = split(' ', <STDIN>); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-13 17:15:31 +01:00
										 |  |  | 	foreach my $i (grep({/^[0-9]+-[0-9]+$/} @sel)) { | 
					
						
							|  |  |  | 		my ($start, $end) = $i =~ /^([0-9]+)-([0-9]+)$/; | 
					
						
							|  |  |  | 		die("Invalid range: $start-$end\n") if ($start >= $end); | 
					
						
							|  |  |  | 		push(@sel, ($start..$end)); | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-28 11:14:16 +01:00
										 |  |  | 	@sel = sort grep({!/[0-9]+-[0-9]+/} @sel); | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	my @sel_undo; | 
					
						
							| 
									
										
										
										
											2024-03-13 17:15:31 +01:00
										 |  |  | 	push(@sel_undo, $undo_txs[$_-1]) foreach (@sel); | 
					
						
							| 
									
										
										
										
											2024-03-03 18:34:18 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | 	return @sel_undo; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-28 10:45:24 +01:00
										 |  |  | # NOTE: Currently this subroutine only works for pacman and yay. You'll have to | 
					
						
							|  |  |  | # add options for additional AUR helpers. | 
					
						
							| 
									
										
										
										
											2024-03-27 10:26:35 +01:00
										 |  |  | sub get_pkgmgr() { | 
					
						
							| 
									
										
										
										
											2024-03-28 11:32:38 +01:00
										 |  |  | 	# TODO: autodetect AUR helper | 
					
						
							| 
									
										
										
										
											2024-03-27 10:26:35 +01:00
										 |  |  | 	my $mgr = $ENV{DEFAULT_PKGMGR} // 'pacman'; | 
					
						
							| 
									
										
										
										
											2024-04-26 10:00:28 +02:00
										 |  |  | 	my $sudo = ''; | 
					
						
							|  |  |  | 	my $user = $ENV{LOGNAME} || $ENV{USER}; | 
					
						
							| 
									
										
										
										
											2024-03-27 10:26:35 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-26 10:00:28 +02:00
										 |  |  | 	my $mgr_bin = `which $mgr 2>&1`; | 
					
						
							| 
									
										
										
										
											2024-03-27 10:26:35 +01:00
										 |  |  | 	if ($? != 0) { | 
					
						
							|  |  |  | 		print(STDERR "Failed to find pacman executable. Are you using an ArchLinux system?\n"); | 
					
						
							|  |  |  | 		exit 1; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-04-26 10:00:28 +02:00
										 |  |  | 	chomp($mgr_bin); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if ($mgr eq 'pacman' && $user ne 'root') { | 
					
						
							|  |  |  | 		$sudo = 'sudo'; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-03-27 10:26:35 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	my %pkgmgr = ( | 
					
						
							|  |  |  | 		name           => $mgr, | 
					
						
							|  |  |  | 		bin            => $mgr_bin, | 
					
						
							| 
									
										
										
										
											2024-04-26 10:00:28 +02:00
										 |  |  | 		search         => "$sudo $mgr_bin -Ss", | 
					
						
							|  |  |  | 		install_remote => "$sudo $mgr_bin -S", | 
					
						
							|  |  |  | 		install_local  => "$sudo $mgr_bin -U", | 
					
						
							|  |  |  | 		remove         => "$sudo $mgr_bin -R", | 
					
						
							| 
									
										
										
										
											2024-03-27 10:26:35 +01:00
										 |  |  | 	); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return \%pkgmgr; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-26 09:45:37 +02:00
										 |  |  | sub find_local_pkg($pkgmgr, $pkg_name, $pkg_ver) { | 
					
						
							|  |  |  | 	my $pkg_file = ''; | 
					
						
							|  |  |  | 	my $aur_dir = "$ENV{'XDG_CACHE_HOME'}/yay/$pkg_name"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if ($pkgmgr->{name} eq 'yay' && -d $aur_dir) { | 
					
						
							|  |  |  | 		$pkg_file = `ls $aur_dir/$pkg_name-$pkg_ver-*.pkg.tar.zst | tail -n1`; | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		$pkg_file = `ls /var/cache/pacman/pkg/$pkg_name-$pkg_ver-*.pkg.tar.zst | tail -n1`; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-26 10:00:28 +02:00
										 |  |  | 	chomp($pkg_file); | 
					
						
							| 
									
										
										
										
											2024-04-26 09:45:37 +02:00
										 |  |  | 	return $pkg_file; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | getopts("irt:dvh", \my %opts); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if ($opts{'v'}) { | 
					
						
							|  |  |  | 	&print_version(); | 
					
						
							|  |  |  | 	exit 0; | 
					
						
							|  |  |  | } elsif ($opts{'h'}) { | 
					
						
							|  |  |  | 	&print_help(); | 
					
						
							|  |  |  | 	exit 0; | 
					
						
							|  |  |  | } elsif ($opts{'r'} && $opts{'i'}) { | 
					
						
							|  |  |  | 	print("Improper usage. -r and -i cannot be used at the same time.\n"); | 
					
						
							|  |  |  | 	print("Use -h for help information.\n"); | 
					
						
							|  |  |  | 	exit 1; | 
					
						
							|  |  |  | } elsif ($opts{'t'} && !($opts{'t'} =~ /[0-9]+/)) { | 
					
						
							|  |  |  | 	print("The argument for -t must be a positive integer.\n"); | 
					
						
							|  |  |  | 	exit 1; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-27 10:33:50 +01:00
										 |  |  | my $r_flag  = $opts{'r'} // 0; | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | my $dry_run = $opts{'d'} // 0; | 
					
						
							|  |  |  | my $num_txs = $opts{'t'} // 1; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-27 10:26:35 +01:00
										 |  |  | my $pkgmgr = &get_pkgmgr(); | 
					
						
							| 
									
										
										
										
											2024-03-09 16:47:25 +01:00
										 |  |  | my @undo_txs = &read_txs($num_txs); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Interactive mode | 
					
						
							| 
									
										
										
										
											2024-03-11 20:05:40 +01:00
										 |  |  | @undo_txs = &select_txs(@undo_txs) unless ($r_flag); | 
					
						
							| 
									
										
										
										
											2024-04-26 09:45:37 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | my $remove_pkgs = "";          # executed via -R | 
					
						
							|  |  |  | my $remote_install_pkgs = "";  # executed via -S | 
					
						
							|  |  |  | my $local_install_pkgs = "";   # executed via -U | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | foreach my $tx (@undo_txs) { | 
					
						
							|  |  |  | 	if ($tx->{action} eq 'installed') { | 
					
						
							|  |  |  | 		$remove_pkgs .= "$tx->{pkg_name} "; | 
					
						
							|  |  |  | 	} elsif ($tx->{action} eq 'removed') { | 
					
						
							|  |  |  | 		# TODO: install local package if available | 
					
						
							|  |  |  | 		$remote_install_pkgs .= "$tx->{pkg_name} "; | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		my $pkg_file = &find_local_pkg($pkgmgr, $tx->{pkg_name}, $tx->{oldver}); | 
					
						
							|  |  |  | 		if ($pkg_file eq '') { | 
					
						
							|  |  |  | 			$remote_install_pkgs .= "$tx->{pkg_name} "; | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			$local_install_pkgs .= "$pkg_file "; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-04-26 10:00:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | system("$pkgmgr->{remove} $remove_pkgs") if ($remove_pkgs ne ''); | 
					
						
							|  |  |  | system("$pkgmgr->{install_remote} $remote_install_pkgs") if ($remote_install_pkgs ne ''); | 
					
						
							|  |  |  | system("$pkgmgr->{install_local} $local_install_pkgs") if ($local_install_pkgs ne ''); |