51 Commits

Author SHA1 Message Date
267370d425 Makefile: simplify install command. 2025-01-25 12:46:03 +01:00
f941a14dc4 Update version on man page. 2024-06-19 10:44:57 +02:00
17eb2b9ad1 Bump to v1.1.1 2024-06-16 18:16:11 +02:00
3eb87b5907 Add option for fallback yay cache directory. 2024-05-27 13:58:07 +02:00
c78f9a36d9 Bump version to v1.1 2024-05-25 18:39:51 +02:00
5fd6da816f Add contribution guide in README. 2024-05-25 18:39:07 +02:00
d9d807b754 Correct name of GNU Make in README. 2024-05-25 18:29:27 +02:00
7c5f748791 Remove pacundo binary from gitignore.
No longer exists.
2024-05-25 18:28:38 +02:00
2bda4e6d7c Makefile: Fix install target. 2024-05-25 18:27:18 +02:00
795a06f4be Implement download from ArchLinux Archive. 2024-05-25 18:24:41 +02:00
e9e2737bd4 Silence ls errors.
It is expected behavior that sometimes the local file will not be found.
2024-05-25 18:17:47 +02:00
d4f0f15341 Add uninstall target for Makefile. 2024-05-25 18:01:37 +02:00
962c54b8e0 Remove perl compiling.
This is giving some weird outputs when I try running it after making the
ArchLinux package (see AUR pacundo).
2024-05-25 17:58:51 +02:00
ebdf36d3c0 Update description to include "rollback" keyword.
This is the keyword that I assume most people use when looking for this
sort of a tool. Should be helpful.

Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-22 17:14:30 +02:00
858d44b5ca Bump version to v1.0.1
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-21 15:45:25 +02:00
e18136e43c Makefile: create parent directories if not exists.
Signed-off-by: Nicolas A. Ortega Froysa <nicolas.ortega@babelgroup.com>
2024-05-21 15:41:36 +02:00
d692d4aa59 Makefile: Fix PREFIX handling.
Signed-off-by: Nicolas A. Ortega Froysa <nicolas.ortega@babelgroup.com>
2024-05-21 15:41:36 +02:00
1516ce9ff3 Include section for supported AUR helpers.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-07 18:07:13 +02:00
555ead272e Improve repository check.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-07 18:05:11 +02:00
d616930208 Remove unnecessary sudo.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-07 17:49:20 +02:00
347b49d5ee Find local package when reinstalling removed package.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-02 13:36:07 +02:00
641163a495 Implement dry-run mode.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-02 13:17:02 +02:00
75c663eb1e Update help information.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-02 13:12:34 +02:00
8525b7f4e8 Automatically detect package manager.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-05-02 13:00:27 +02:00
e6d814d438 Add more helpful information to the README.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-04-30 17:21:08 +02:00
a7bae301b3 Add compressed manpage to gitignore.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-04-29 18:54:52 +02:00
b2f5793c21 Add manpage.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-04-29 18:53:57 +02:00
edd942b91a Create script to build and install pacundo.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-04-29 18:29:04 +02:00
b30dea35b8 Implement package management commands.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-04-26 10:00:28 +02:00
d90016fd8c Obtain package files for installation.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-04-26 09:48:45 +02:00
c08a9f4b40 Simplify and clean up strings and output.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-28 11:40:57 +01:00
ec535f860c Add TODO for autodetecting AUR helper.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-28 11:32:38 +01:00
a458f2dc67 Fix filter regex.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-28 11:14:16 +01:00
615f491433 Simplify get_pkgmgr() subroutine.
Currently it only is configured to deal with pacman or yay (as noted in
the comment). Other AUR helpers would need to be configured.

Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-28 10:45:24 +01:00
aa7e2d2fbc Add package manager remove command.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-28 10:42:49 +01:00
f55124759e Don't print stderr output of commands (send to stdout).
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-28 10:37:46 +01:00
3822696640 Use single print statement for help information.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-27 10:39:14 +01:00
b7e98246ad Align assignment operators.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-27 10:33:50 +01:00
2d4ec58a81 Get package manager variables.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-27 10:26:35 +01:00
9e1ba7159f Simplify code.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-13 17:15:31 +01:00
601b4b7b81 Change license copyright year.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-12 09:27:27 +01:00
8a62fc9064 Avoid invalid ranges.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-11 20:09:16 +01:00
e6514c37d3 Simplify syntax.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-11 20:05:40 +01:00
e11cc694f5 Fix selection regex.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-11 20:02:59 +01:00
4cb213d585 Refactor into subroutines.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-09 16:47:25 +01:00
e9eec6cfcc Use local $~
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-07 16:25:03 +01:00
1065595f6d Change grep format.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-07 16:15:28 +01:00
e7b3192a00 Add 'return' statement to subroutines.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-07 16:12:29 +01:00
891427ead0 Use || instead of 'or'.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-03 18:51:44 +01:00
9dc43cc866 Implement interactive mode.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-03 18:34:18 +01:00
86c20031bc Use // operator for args/flags default values.
Signed-off-by: Nicolás Ortega Froysa <nicolas@ortegas.org>
2024-03-01 18:00:25 +01:00
6 changed files with 399 additions and 49 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
pacundo.1.gz

View File

@ -1,4 +1,4 @@
Copyright (C) [year] Nicolás A. Ortega Froysa <nicolas@ortegas.org>
Copyright (C) 2024 Nicolás A. Ortega Froysa <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

43
Makefile Normal file
View File

@ -0,0 +1,43 @@
# 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.
ifeq ($(PREFIX),)
PREFIX := /usr/local
endif
pacundo.1.gz: pacundo.1
gzip -c $^ > $@
.PHONY: clean doc install uninstall
clean:
$(RM) pacundo.1.gz
doc: pacundo.1.gz
install: pacundo.1.gz
install -Dm755 pacundo.pl $(PREFIX)/bin/pacundo
install -Dm644 pacundo.1.gz $(PREFIX)/share/man/man1/
uninstall:
$(RM) $(PREFIX)/bin/pacundo
$(RM) $(PREFIX)/share/man/man1/pacundo.1.gz

View File

@ -7,10 +7,67 @@ to boot from a USB depending on just how broken it is).
## Installation
Dependencies:
### Dependencies
- Perl 5
- `File::ReadBackwards` module
- cURL
- GNU Make
You can install these packages with the following command:
```console
# pacman -S perl perl-file-readbackwards curl
```
### Compiling & Installing
The script is compiled and installed using GNU Makefile. Therefore you can use
`make install` to build and install the script and its man-page as expected.
They are installed (by default) to `/usr/local`. To change this to a different
directory simply prepend the `PREFIX=<path>` to your `make install` command.
## Usage
The first concept to understand is that of a transaction. A transaction is
defined in the pacman logs as package operations done during a single use of the
command (or so it seems, at least). If you look at the logs
(`/var/log/pacman.log`) this would be everything between the lines `[ALPM]
transaction started` and `[ALPM] transaction completed`. You can set how many
transactions to list/undo by using the `-t` argument.
There are two modes for undoing pacman transactions:
- Interactive (`-i`, default): will show you a numbered list with all the package
operations of the selected transactions.
- Automatic (`-r`): will automatically undo all package operations of the
selected transactions.
Look at the man-page (`man pacundo`) for more information.
### Supported AUR Helpers
- [yay](https://github.com/Jguer/yay)
## Contributing
If you find any issues, feel free to report them on GitHub or send me an E-Mail
(see my website/profile for the address). I will add these issues to my personal
Gitea page and (unless specified otherwise) mention you as the person who found
the issue.
For patches/pull requests, if you open a PR on GitHub I will likely not merge
directly but instead apply the patches locally (via Git patches, conserving
authorship), push them to my Gitea repository, which will finally be mirrored to
GitHub. However, you can save me a bit of work by just sending me the Git
patches directly (via E-Mail).
If you're looking for a way to contribute, there are a few to-do items within
the code which you can find using `grep`:
```console
# grep -n "TODO" pacundo.pl
```
## License

49
pacundo.1 Normal file
View File

@ -0,0 +1,49 @@
.TH PACUNDO "1" "April 2024" "pacundo 1.1.1" "User Commands"
.SH "NAME"
pacundo - A time machine to roll back your ArchLinux machine to a working state.
.SH "SYNOPSIS"
.B pacundo
[\fI\-i\fR|\fI\-r\fR] [\fI\-t\fR <\fInum\fR>] [\fI\-d\fR]
.B pacundo
\fI\-h\fR
.B pacundo
\fI\-v\fR
.SH "DESCRIPTION"
This program helps to undo the last pacman/yay transactions automatically, which
is especially helpful if the last update broke your system for some reason.
\fBNote:\fR the term \fItransaction\fR refers to an operation done by pacman
(e.g. all packages upgraded by \fIpacman -Syu\fR).
.SH "OPTIONS"
.TP
\fB\-i\fR
Enter interactive mode to select package operations to undo (default behavior)
.TP
\fB\-r\fR
Non-interactively undo entire transactions
.TP
\fB\-t\fR <\fInum\fR>
Specify the number of transactions to include (default: 1)
.TP
\fB\-d\fR
Dry run, i.e. don't actually do anything
.TP
\fB\-h\fR
Show this help information
.TP
\fB\-v\fR
Print program version
.SH "AUTHOR"
Written by Nicolás A. Ortega Froysa.
.SH "COPYRIGHT"
Copyright \(co 2024 Ortega Froysa, Nicolás A. <nicolas@ortegas.org>.
License: ZLib License.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

View File

@ -24,36 +24,232 @@
use strict;
use warnings;
use feature qw(signatures);
use Getopt::Std;
use File::ReadBackwards;
my $VERSION = "1.0";
my $PROG_NAME = "pacundo";
my $VERSION = '1.1.1';
my $PROG_NAME = 'pacundo';
my $r_flag = 0;
my $dry_run = 0;
my $num_txs = 1;
sub print_version {
sub print_version() {
print("$PROG_NAME v$VERSION\n");
return;
}
sub print_help {
sub print_help() {
&print_version();
print("A time machine to return your ArchLinux machine back to a working state.\n");
print("\nUSAGE:
print("A time machine to roll back your ArchLinux machine to a working state.
USAGE:
$PROG_NAME [-i|-r] [-t <num>] [-d]
$PROG_NAME -h
$PROG_NAME -v
OPTIONS:
-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]
-i Enter interactive mode to select package operations to undo (default behavior)
-r Non-interactively undo entire transactions
-t <num> Specify the number of transactions to include (default: 1)
-d Dry run, i.e. don't actually do anything
-h Show this help information
-v Print program version\n");
return;
}
sub read_txs($num_txs = 1) {
my $found_txs = 0;
my $in_tx = 0;
my @undo_txs;
my $pacman_log = File::ReadBackwards->new('/var/log/pacman.log') or
die("Failed to load pacman log file.\n$!\n");
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/) {
$found_txs++;
$in_tx = 0;
} elsif ($line =~ /\[ALPM\] (upgraded|downgraded)/) {
my ($action, $pkg_name, $oldver, $newver) =
$line =~ /\[ALPM\] (upgraded|downgraded) ([^\s]+) \((.*) -> (.*)\)/;
push(@undo_txs,
{
action => $action,
pkg_name => $pkg_name,
oldver => $oldver,
newver => $newver,
}
);
} elsif ($line =~ /\[ALPM\] (installed|removed)/) {
my ($action, $pkg_name) = $line =~ /\[ALPM\] (installed|removed) ([^\s]+)/;
push(@undo_txs,
{
action => $action,
pkg_name => $pkg_name,
}
);
}
}
return @undo_txs;
}
sub select_txs(@undo_txs) {
print("Last changes:\n");
my $n = 1;
foreach my $tx (@undo_txs) {
format UPGRFORMAT =
@|| @<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<< -> @<<<<<<<<<<<<<
$n, $tx->{action}, $tx->{pkg_name}, $tx->{oldver}, $tx->{newver}
.
format INSTFORMAT =
@|| @<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$n, $tx->{action}, $tx->{pkg_name}
.
local $~ = ($tx->{action} =~ /(upgraded|downgraded)/) ? "UPGRFORMAT" : "INSTFORMAT";
write();
$n++;
}
print("Select transactions to undo (e.g. '1 2 3', '1-3')\n> ");
my @sel = split(' ', <STDIN>);
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));
}
@sel = sort grep({!/[0-9]+-[0-9]+/} @sel);
my @sel_undo;
push(@sel_undo, $undo_txs[$_-1]) foreach (@sel);
return @sel_undo;
}
# NOTE: Currently this subroutine only works for pacman and yay. You'll have to
# add options for additional AUR helpers.
sub get_pkgmgr() {
my $mgr = '';
my $mgr_bin;
my @supported_mgrs = (
'yay',
'pacman',
);
my $sudo = '';
my $user = $ENV{LOGNAME} || $ENV{USER};
foreach my $i (@supported_mgrs) {
$mgr_bin = `which $i 2>&1`;
if ($? == 0) {
$mgr = $i;
last;
}
}
if ($mgr eq '') {
print(STDERR "Failed to find pacman executable. Are you using an ArchLinux system?\n");
exit 1;
}
chomp($mgr_bin);
if ($mgr eq 'pacman' && $user ne 'root') {
$sudo = 'sudo';
}
my %pkgmgr = (
name => $mgr,
bin => $mgr_bin,
search => "$mgr_bin -Ss",
info => "$mgr_bin -Si",
install_remote => "$sudo $mgr_bin -S",
install_local => "$sudo $mgr_bin -U",
remove => "$sudo $mgr_bin -R",
);
return \%pkgmgr;
}
sub find_local_pkg($pkgmgr, $pkg_name, $pkg_ver='') {
my $pkg_file = '';
my $pkg_pat;
my $repo = `$pkgmgr->{info} $pkg_name | awk '{ if (\$1 == "Repository") print \$3; }'`;;
chomp($repo);
if ($pkg_ver ne '') {
$pkg_pat = "$pkg_name-$pkg_ver-*.pkg.tar.zst";
} else {
$pkg_pat = "$pkg_name-*.pkg.tar.zst";
}
if ($repo eq 'aur') {
my $aur_dir;
if ($pkgmgr->{name} eq 'yay') {
if ($ENV{'XDG_CACHE_HOME'} ne '') {
$aur_dir = "$ENV{'XDG_CACHE_HOME'}/yay/$pkg_name";
} else {
$aur_dir = "$ENV{'HOME'}/.cache/yay/$pkg_name";
}
} else {
return '';
}
$pkg_file = `ls $aur_dir/$pkg_pat 2> /dev/null | tail -n1`;
} else {
$pkg_file = `ls /var/cache/pacman/pkg/$pkg_pat 2> /dev/null | tail -n1`;
}
chomp($pkg_file);
return $pkg_file;
}
sub find_remote_archive($pkgmgr, $pkg_name, $pkg_ver) {
my $repo = `$pkgmgr->{info} $pkg_name | awk '{ if (\$1 == "Repository") print \$3; }'`;;
chomp($repo);
# TODO: look through git history for version.
if ($repo eq 'aur') {
return '';
}
# TODO: Probably a better way of managing architectures. There should be a
# way of getting the architecture of the package.
my @archs = (`uname -m`, 'any');
my $ala_url = "https://archive.archlinux.org/packages/" .
substr($pkg_name,0,1) . "/" . $pkg_name;
my $pkg_file = '';
foreach my $arch (@archs) {
chomp($arch);
my $filename = "$pkg_name-$pkg_ver-$arch.pkg.tar.zst";
my $pkg_url = "$ala_url/$filename";
my $output_file = "/tmp/$filename";
my $resp = `curl -Lo $output_file -s -w '%{http_code}\n' $pkg_url`;
chomp($resp);
if ($resp eq '200') {
system("curl -Lo $output_file.sig -s $pkg_url.sig");
$pkg_file = $output_file;
last;
} else {
unlink $output_file;
}
}
if ($pkg_file ne '') {
print("Downloaded from archive: $pkg_file\n");
}
return $pkg_file;
}
getopts("irt:dvh", \my %opts);
@ -73,44 +269,48 @@ if ($opts{'v'}) {
exit 1;
}
$r_flag = 1 if ($opts{'r'});
$dry_run = 1 if ($opts{'d'});
$num_txs = $opts{'t'} if ($opts{'t'});
my $r_flag = $opts{'r'} // 0;
my $dry_run = $opts{'d'} // 0;
my $num_txs = $opts{'t'} // 1;
my $pacman_log = File::ReadBackwards->new("/var/log/pacman.log") or
die("Failed to load pacman log file.\n$!");
my $pkgmgr = &get_pkgmgr();
my @undo_txs = &read_txs($num_txs);
my $found_txs = 0;
my $in_tx = 0;
# Interactive mode
@undo_txs = &select_txs(@undo_txs) unless ($r_flag);
my @undo_txs;
my $remove_pkgs = ""; # executed via -R
my $remote_install_pkgs = ""; # executed via -S
my $local_install_pkgs = ""; # executed via -U
while ($found_txs < $num_txs && defined(my $line = $pacman_log->readline)) {
# Remeber that we're reading this in reverse order
if (!$in_tx && $line =~ /\[ALPM\] transaction completed/) {
$in_tx = 1;
} elsif ($in_tx) {
if ($line =~ /\[ALPM\] transaction started/) {
$found_txs++;
$in_tx = 0;
} elsif ($line =~ /\[ALPM\] (upgraded|downgraded)/) {
my ($action, $pkg_name, $oldver, $newver) = $line =~ /\[ALPM\] (upgraded|downgraded) ([^\s]+) \((.*) -> (.*)\)/;
push(@undo_txs,
{
'action' => $action,
'pkg_name' => $pkg_name,
'oldver' => $oldver,
'newver' => $newver,
foreach my $tx (@undo_txs) {
if ($tx->{action} eq 'installed') {
$remove_pkgs .= "$tx->{pkg_name} ";
} elsif ($tx->{action} eq 'removed') {
my $pkg_file = &find_local_pkg($pkgmgr, $tx->{pkg_name});
if ($pkg_file eq '') {
$remote_install_pkgs .= "$tx->{pkg_name} ";
} else {
$local_install_pkgs .= "$pkg_file ";
}
);
} elsif ($line =~ /\[ALPM\] (installed|removed)/) {
my ($action, $pkg_name) = $line =~ /\[ALPM\] (installed|removed) ([^\s]+)/;
push(@undo_txs,
{
'action' => $action,
'pkg_name' => $pkg_name,
}
);
} else {
my $pkg_file = &find_local_pkg($pkgmgr, $tx->{pkg_name}, $tx->{oldver});
$pkg_file = &find_remote_archive($pkgmgr, $tx->{pkg_name}, $tx->{oldver}) if ($pkg_file eq '');
if ($pkg_file ne '') {
$local_install_pkgs .= "$pkg_file ";
} else {
print(STDERR "Unable to find $tx->{pkg_name} $tx->{pkg_ver}");
}
}
}
if ($dry_run) {
print("$pkgmgr->{remove} $remove_pkgs\n") if ($remove_pkgs ne '');
print("$pkgmgr->{install_remote} $remote_install_pkgs\n") if ($remote_install_pkgs ne '');
print("$pkgmgr->{install_local} $local_install_pkgs\n") if ($local_install_pkgs ne '');
} else {
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 '');
}