diff options
Diffstat (limited to 'install/deps.perl')
-rwxr-xr-x | install/deps.perl | 414 |
1 files changed, 414 insertions, 0 deletions
diff --git a/install/deps.perl b/install/deps.perl new file mode 100755 index 00000000..6563c3ce --- /dev/null +++ b/install/deps.perl @@ -0,0 +1,414 @@ +# Copyright (C) all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> +# Helper script for mass installing/uninstalling with the OS package manager +# TODO: figure out how to handle 3rd-party repo packages for CentOS 7.x +eval 'exec perl -S $0 ${1+"$@"}' # no shebang +if 0; # running under some shell +use v5.12; +my $help = <<EOM; # make sure this fits in 80x24 terminals +usage: $^X $0 [-f PKG_FMT] [--allow-remove] PROFILE [PROFILE_MOD] + + -f PKG_FMT package format (`deb', `pkg', `pkg_add', `pkgin' or `rpm') + --allow-remove allow removing packages (DANGEROUS, non-production use only) + --dry-run | -n show commands that would be run + --yes | -y non-interactive mode / assume yes to package manager + +PROFILE is typically `www-search', `lei', or `nntpd' +Some profile names are intended for developer use only and subject to change. +PROFILE_MOD is only for developers checking dependencies + +OS package installation typically requires administrative privileges +EOM +use Getopt::Long qw(:config gnu_getopt no_ignore_case auto_abbrev); +BEGIN { require './install/os.perl' }; +my $opt = {}; +GetOptions($opt, qw(pkg-fmt|f=s allow-remove dry-run|n yes|y help|h)) + or die $help; +if ($opt->{help}) { print $help; exit } +my $pkg_fmt = $opt->{'pkg-fmt'} // do { + my $fmt = pkg_fmt; + warn "# using detected --pkg-fmt=$fmt on $ID/$VERSION_ID\n"; + $fmt; +}; +@ARGV or die $help; +my @test_essential = qw(Test::Simple); # we actually use Test::More + +# package profiles. Note we specify packages at maximum granularity, +# which is typically deb for most things, but rpm seems to have the +# highest granularity for things in the Perl standard library. +my $profiles = { + # the smallest possible profile for testing + essential => [ qw( + autodie + git + perl + Digest::SHA + ExtUtils::MakeMaker + IO::Compress + Text::ParseWords + URI + ), @test_essential ], + + # Everything else is optional for normal use. Only specify + # the minimum to pull in dependencies here: + optional => [ qw( + Date::Parse + BSD::Resource + DBD::SQLite + Inline::C + Mail::IMAPClient + Net::Server + Parse::RecDescent + Plack + Plack::Test + Plack::Middleware::ReverseProxy + Xapian + curl + highlight.pm + libgit2-dev + libxapian-dev + sqlite3 + xapian-tools + ) ], + # no pkg-config, libsqlite3, libxapian, libz, etc. since + # they'll get pulled in lib*-dev, DBD::SQlite and + # Xapian(.pm) respectively + + # optional developer stuff + devtest => [ qw( + XML::TreePP + w3m + Plack::Test::ExternalServer + ) ], +}; + +# only for distro-agnostic dependencies which are always true: +my $always_deps = { + # we only load DBI explicitly + 'DBD::SQLite' => [ qw(DBI libsqlite3) ], + 'Mail::IMAPClient' => 'Parse::RecDescent', + 'Plack::Middleware::ReverseProxy' => 'Plack', + 'Xapian' => 'libxapian', + 'xapian-tools' => 'libxapian', + 'libxapian-dev' => [ qw(pkg-config libxapian) ], + 'libgit2-dev' => 'pkg-config', +}; + +# bare minimum for v2 +$profiles->{v2essential} = [ @{$profiles->{essential}}, qw(DBD::SQLite) ]; + +# for old v1 installs +$profiles->{'www-v1'} = [ @{$profiles->{essential}}, qw(Plack) ]; +$profiles->{'www-thread'} = [ @{$profiles->{v2essential}}, qw(Plack) ]; + +# common profile for PublicInbox::WWW +$profiles->{'www-search'} = [ @{$profiles->{'www-thread'}}, qw(Xapian) ]; + +# bare mininum for lei +$profiles->{'lei-core'} = [ @{$profiles->{v2essential}}, qw(Xapian) ]; +push @{$profiles->{'lei-core'}}, 'Inline::C' if $^O ne 'linux'; + +# common profile for lei: +$profiles->{lei} = [ @{$profiles->{'lei-core'}}, qw(Mail::IMAPClient curl) ]; + +$profiles->{nntpd} = [ @{$profiles->{v2essential}} ]; +$profiles->{pop3d} = [ @{$profiles->{v2essential}} ]; +$profiles->{'imapd-bare'} = [ @{$profiles->{v2essential}}, + qw(Parse::RecDescent) ]; +$profiles->{imapd} = [ @{$profiles->{'imapd-bare'}}, qw(Xapian) ]; +$profiles->{pop3d} = [ @{$profiles->{v2essential}} ]; +$profiles->{watch} = [ @{$profiles->{v2essential}}, qw(Mail::IMAPClient) ]; +$profiles->{'watch-v1'} = [ @{$profiles->{essential}} ]; +$profiles->{'watch-maildir'} = [ @{$profiles->{v2essential}} ]; + +# package names which can't be mapped automatically and explicit +# dependencies to prevent essential package removal: +my $non_auto = { # git and perl (+autodie) are essential + git => { + pkg => [ qw(curl p5-TimeDate git) ], + rpm => [ qw(curl git) ], + pkg_add => [ qw(curl p5-Time-TimeDate git) ], + }, + perl => { + apk => [ qw(perl perl-utils) ], + pkg => 'perl5', + pkgin => 'perl', + pkg_add => [], # Perl is part of OpenBSD base + }, + # optional stuff: + 'BSD::Resource' => { + apk => [], # not packaged for Alpine 3.19 + }, + 'Date::Parse' => { + apk => 'perl-timedate', + deb => 'libtimedate-perl', + pkg => 'p5-TimeDate', + rpm => 'perl-TimeDate', + pkg_add => 'p5-Time-TimeDate', + }, + 'Inline::C' => { + apk => [ qw(perl-inline-c perl-dev) ], + pkg_add => 'p5-Inline', # tested OpenBSD 7.3 + rpm => 'perl-Inline', # for CentOS 7.x, at least + }, + 'DBD::SQLite' => { deb => 'libdbd-sqlite3-perl' }, + 'Plack::Middleware::ReverseProxy' => { + apk => [], # not packaged for Alpine 3.19.0 + }, + 'Plack::Test' => { + apk => 'perl-plack', + deb => 'libplack-perl', + pkg => 'p5-Plack', + }, + 'Plack::Test::ExternalServer' => { + apk => [], # not packaged for Alpine 3.19.0 + }, + 'Xapian' => { + apk => 'xapian-bindings-perl', + deb => 'libsearch-xapian-perl', + pkg => 'p5-Xapian', + pkg_add => 'xapian-bindings-perl', + rpm => [], # xapian14-bindings-perl in 3rd-party repo + }, + 'highlight.pm' => { + apk => [], + deb => 'libhighlight-perl', + pkg => [], + pkgin => 'p5-highlight', + rpm => [], + }, + + # `libgit2' is the project name (since git has libgit) + 'libgit2-dev' => { + pkg => 'libgit2', + rpm => 'libgit2-devel', + }, + + # some distros have both sqlite 2 and 3, we've only ever used 3 + 'libsqlite3' => { + apk => [], # handled by apk w/ perl-dbd-sqlite + pkg => 'sqlite3', + rpm => [], # `sqlite' is not removable due to yum/systemd + deb => [], # libsqlite3-0, but no need to specify + }, + + # only one version of Xapian distros + 'libxapian' => { # avoid .so version numbers in our deps + deb => [], # libxapian30 atm, but no need to specify + pkg => 'xapian-core', + pkgin => 'xapian', + rpm => 'xapian-core', + }, + 'libxapian-dev' => { + apk => 'xapian-core-dev', + pkg => 'xapian-core', + pkgin => 'xapian', + rpm => 'xapian-core-devel', + }, + 'pkg-config' => { + apk => [], # handled by apk w/ xapian-core-dev + pkg_add => [], # part of the OpenBSD base system + pkg => 'pkgconf', # pkg-config is a symlink to pkgconf + pkgin => 'pkg-config', + }, + 'sqlite3' => { # this is just the executable binary on deb + apk => 'sqlite', + rpm => [], # `sqlite' is not removable due to yum/systemd + }, + + # we call xapian-compact(1) in public-inbox-compact(1) and + # xapian-delve(1) in public-inbox-cindex(1) + 'xapian-tools' => { + apk => 'xapian-core', + pkg => 'xapian-core', + pkgin => 'xapian', + rpm => 'xapian-core', # ??? + }, + + # OS-specific + 'IO::KQueue' => { + apk => [], + deb => [], + rpm => [], + }, +}; + +# standard library stuff that CentOS 7.x (and presumably other RPM) +# split out and can be removed without removing the `perl' RPM: +for (qw(autodie Digest::SHA ExtUtils::MakeMaker IO::Compress Sys::Syslog + Test::Simple Text::ParseWords)) { + # n.b.: Compress::Raw::Zlib is pulled in by IO::Compress + # qw(constant Encode Getopt::Long Exporter Storable Time::HiRes) + # don't need to be here since it's impossible to have `perl' + # on CentOS 7.x without them. + my $rpm = $_; + $rpm =~ s/::/-/g; + $non_auto->{$_} = { + deb => 'perl', # libperl5.XX, but the XX varies + pkg => 'perl5', + pkg_add => [], # perl is in the OpenBSD base system + apk => 'perl', + pkgin => 'perl', + rpm => "perl-$rpm", + }; +} + +# NetBSD and OpenBSD package names are similar to FreeBSD in most cases +if ($pkg_fmt =~ /\A(?:pkg_add|pkgin)\z/) { + for my $name (keys %$non_auto) { + my $fbsd_pkg = $non_auto->{$name}->{pkg}; + $non_auto->{$name}->{$pkg_fmt} //= $fbsd_pkg if $fbsd_pkg; + } +} + +my %inst_check = ( # subs which return true if a package is intalled + apk => sub { system(qw(apk info -q -e), $_[0]) == 0 }, + deb => sub { system("dpkg -s $_[0] >/dev/null 2>&1") == 0 }, + pkg => sub { system(qw(pkg info -q), $_[0]) == 0 }, + pkg_add => sub { system(qw(pkg_info -q -e), "$_[0]->=0") == 0 }, + pkgin => sub { system(qw(pkg_info -q -e), $_[0]) == 0 }, + rpm => sub { system("rpm -qs $_[0] >/dev/null 2>&1") == 0 }, +); + +our $INST_CHECK = $inst_check{$pkg_fmt} || die <<""; +don't know how to check install status for $pkg_fmt + +my (@pkg_install, @pkg_remove, %all); +for my $ary (values %$profiles) { + my @extra; + for my $pkg (@$ary) { + my $deps = $always_deps->{$pkg} // next; + push @extra, list($deps); + } + push @$ary, @extra; + $all{$_} = \@pkg_remove for @$ary; +} +if ($^O =~ /\A(?:free|net|open)bsd\z/) { + $all{'IO::KQueue'} = \@pkg_remove; +} +$profiles->{all} = [ keys %all ]; # pseudo-profile for all packages + +# parse the profile list from the command-line +my @profiles = @ARGV; +while (defined(my $profile = shift @profiles)) { + if ($profile =~ s/-\z//) { + # like apt-get, trailing "-" means remove + profile2dst($profile, \@pkg_remove); + } else { + profile2dst($profile, \@pkg_install); + } +} + +# fill in @pkg_install and @pkg_remove: +while (my ($pkg, $dst_pkg_list) = each %all) { + push @$dst_pkg_list, list(pkg2ospkg($pkg, $pkg_fmt)); +} + +my (%add, %rm); # uniquify lists +@pkg_install = grep { !$add{$_}++ && !$INST_CHECK->($_) } @pkg_install; +@pkg_remove = $opt->{'allow-remove'} ? grep { + !$add{$_} && !$rm{$_}++ && $INST_CHECK->($_) + } @pkg_remove : (); + +(@pkg_remove || @pkg_install) or warn "# no packages to install nor remove\n"; + +# OS-specific cleanups appreciated +if ($pkg_fmt eq 'apk') { + root('apk', 'add', @pkg_install) if @pkg_install; + root('apk', 'del', @pkg_remove) if @pkg_remove; +} elsif ($pkg_fmt eq 'deb') { + my @apt_opt = qw(-o APT::Install-Recommends=false + -o APT::Install-Suggests=false); + push @apt_opt, '-y' if $opt->{yes}; + root('apt-get', @apt_opt, qw(install), + @pkg_install, + # apt-get lets you suffix a package with "-" to + # remove it in an "install" sub-command: + map { "$_-" } @pkg_remove) if (@pkg_remove || @pkg_install); + root('apt-get', @apt_opt, qw(autoremove)) if $opt->{'allow-remove'}; +} elsif ($pkg_fmt eq 'pkg') { # FreeBSD + my @pkg_opt = $opt->{yes} ? qw(-y) : (); + + # don't remove stuff that isn't installed: + root(qw(pkg remove), @pkg_opt, @pkg_remove) if @pkg_remove; + root(qw(pkg install), @pkg_opt, @pkg_install) if @pkg_install; + root(qw(pkg autoremove), @pkg_opt) if $opt->{'allow-remove'}; +} elsif ($pkg_fmt eq 'pkgin') { # NetBSD + my @pkg_opt = $opt->{yes} ? qw(-y) : (); + root(qw(pkgin), @pkg_opt, 'remove', @pkg_remove) if @pkg_remove; + root(qw(pkgin), @pkg_opt, 'install', @pkg_install) if @pkg_install; + root(qw(pkgin), @pkg_opt, 'autoremove') if $opt->{'allow-remove'}; +# TODO: yum / rpm support +} elsif ($pkg_fmt eq 'rpm') { + my @pkg_opt = $opt->{yes} ? qw(-y) : (); + root(qw(yum remove), @pkg_opt, @pkg_remove) if @pkg_remove; + root(qw(yum install), @pkg_opt, @pkg_install) if @pkg_install; +} elsif ($pkg_fmt eq 'pkg_add') { # OpenBSD + my @pkg_opt = $opt->{yes} ? qw(-I) : (); # -I means non-interactive + root(qw(pkg_delete), @pkg_opt, @pkg_remove) if @pkg_remove; + @pkg_install = map { "$_--" } @pkg_install; # disambiguate w3m + root(qw(pkg_add), @pkg_opt, @pkg_install) if @pkg_install; + root(qw(pkg_delete -a), @pkg_opt) if $opt->{'allow-remove'}; +} else { + die "unsupported package format: $pkg_fmt\n"; +} +exit 0; + + +# map a generic package name to an OS package name +sub pkg2ospkg { + my ($pkg, $fmt) = @_; + + # check explicit overrides, first: + if (my $ospkg = $non_auto->{$pkg}->{$fmt}) { + return $ospkg; + } + + # check common Perl module name patterns: + if ($pkg =~ /::/ || $pkg =~ /\A[A-Z]/) { + if ($fmt eq 'apk') { + $pkg =~ s/::/-/g; + return "perl-\L$pkg" + } elsif ($fmt eq 'deb') { + $pkg =~ s/::/-/g; + return "lib\L$pkg-perl"; + } elsif ($fmt eq 'rpm') { + $pkg =~ s/::/-/g; + return "perl-$pkg" + } elsif ($fmt =~ /\Apkg(?:_add|in)?\z/) { + $pkg =~ s/::/-/g; + return "p5-$pkg" + } else { + die "unsupported package format: $fmt for $pkg\n" + } + } + + # use package name as-is (e.g. 'curl' or 'w3m') + $pkg; +} + +# maps a install profile to a package list (@pkg_remove or @pkg_install) +sub profile2dst { + my ($profile, $dst_pkg_list) = @_; + if (my $pkg_list = $profiles->{$profile}) { + $all{$_} = $dst_pkg_list for @$pkg_list; + } elsif ($all{$profile}) { # $profile is just a package name + $all{$profile} = $dst_pkg_list; + } else { + die "unrecognized profile or package: $profile\n"; + } +} + +sub root { + warn "# @_\n"; + return if $opt->{'dry-run'}; + return if system(@_) == 0; + warn "E: command failed: @_\n"; + exit($? >> 8); +} + +# ensure result can be pushed into an array: +sub list { + my ($pkg) = @_; + ref($pkg) eq 'ARRAY' ? @$pkg : $pkg; +} |