v2writable: done: force synchronous awaitpid
[public-inbox.git] / install / deps.perl
blob6563c3cef0939f7adcde62ab783e6855cad4d138
1 # Copyright (C) all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 # Helper script for mass installing/uninstalling with the OS package manager
4 # TODO: figure out how to handle 3rd-party repo packages for CentOS 7.x
5 eval 'exec perl -S $0 ${1+"$@"}' # no shebang
6 if 0; # running under some shell
7 use v5.12;
8 my $help = <<EOM; # make sure this fits in 80x24 terminals
9 usage: $^X $0 [-f PKG_FMT] [--allow-remove] PROFILE [PROFILE_MOD]
11 -f PKG_FMT package format (`deb', `pkg', `pkg_add', `pkgin' or `rpm')
12 --allow-remove allow removing packages (DANGEROUS, non-production use only)
13 --dry-run | -n show commands that would be run
14 --yes | -y non-interactive mode / assume yes to package manager
16 PROFILE is typically `www-search', `lei', or `nntpd'
17 Some profile names are intended for developer use only and subject to change.
18 PROFILE_MOD is only for developers checking dependencies
20 OS package installation typically requires administrative privileges
21 EOM
22 use Getopt::Long qw(:config gnu_getopt no_ignore_case auto_abbrev);
23 BEGIN { require './install/os.perl' };
24 my $opt = {};
25 GetOptions($opt, qw(pkg-fmt|f=s allow-remove dry-run|n yes|y help|h))
26 or die $help;
27 if ($opt->{help}) { print $help; exit }
28 my $pkg_fmt = $opt->{'pkg-fmt'} // do {
29 my $fmt = pkg_fmt;
30 warn "# using detected --pkg-fmt=$fmt on $ID/$VERSION_ID\n";
31 $fmt;
33 @ARGV or die $help;
34 my @test_essential = qw(Test::Simple); # we actually use Test::More
36 # package profiles. Note we specify packages at maximum granularity,
37 # which is typically deb for most things, but rpm seems to have the
38 # highest granularity for things in the Perl standard library.
39 my $profiles = {
40 # the smallest possible profile for testing
41 essential => [ qw(
42 autodie
43 git
44 perl
45 Digest::SHA
46 ExtUtils::MakeMaker
47 IO::Compress
48 Text::ParseWords
49 URI
50 ), @test_essential ],
52 # Everything else is optional for normal use. Only specify
53 # the minimum to pull in dependencies here:
54 optional => [ qw(
55 Date::Parse
56 BSD::Resource
57 DBD::SQLite
58 Inline::C
59 Mail::IMAPClient
60 Net::Server
61 Parse::RecDescent
62 Plack
63 Plack::Test
64 Plack::Middleware::ReverseProxy
65 Xapian
66 curl
67 highlight.pm
68 libgit2-dev
69 libxapian-dev
70 sqlite3
71 xapian-tools
72 ) ],
73 # no pkg-config, libsqlite3, libxapian, libz, etc. since
74 # they'll get pulled in lib*-dev, DBD::SQlite and
75 # Xapian(.pm) respectively
77 # optional developer stuff
78 devtest => [ qw(
79 XML::TreePP
80 w3m
81 Plack::Test::ExternalServer
82 ) ],
85 # only for distro-agnostic dependencies which are always true:
86 my $always_deps = {
87 # we only load DBI explicitly
88 'DBD::SQLite' => [ qw(DBI libsqlite3) ],
89 'Mail::IMAPClient' => 'Parse::RecDescent',
90 'Plack::Middleware::ReverseProxy' => 'Plack',
91 'Xapian' => 'libxapian',
92 'xapian-tools' => 'libxapian',
93 'libxapian-dev' => [ qw(pkg-config libxapian) ],
94 'libgit2-dev' => 'pkg-config',
97 # bare minimum for v2
98 $profiles->{v2essential} = [ @{$profiles->{essential}}, qw(DBD::SQLite) ];
100 # for old v1 installs
101 $profiles->{'www-v1'} = [ @{$profiles->{essential}}, qw(Plack) ];
102 $profiles->{'www-thread'} = [ @{$profiles->{v2essential}}, qw(Plack) ];
104 # common profile for PublicInbox::WWW
105 $profiles->{'www-search'} = [ @{$profiles->{'www-thread'}}, qw(Xapian) ];
107 # bare mininum for lei
108 $profiles->{'lei-core'} = [ @{$profiles->{v2essential}}, qw(Xapian) ];
109 push @{$profiles->{'lei-core'}}, 'Inline::C' if $^O ne 'linux';
111 # common profile for lei:
112 $profiles->{lei} = [ @{$profiles->{'lei-core'}}, qw(Mail::IMAPClient curl) ];
114 $profiles->{nntpd} = [ @{$profiles->{v2essential}} ];
115 $profiles->{pop3d} = [ @{$profiles->{v2essential}} ];
116 $profiles->{'imapd-bare'} = [ @{$profiles->{v2essential}},
117 qw(Parse::RecDescent) ];
118 $profiles->{imapd} = [ @{$profiles->{'imapd-bare'}}, qw(Xapian) ];
119 $profiles->{pop3d} = [ @{$profiles->{v2essential}} ];
120 $profiles->{watch} = [ @{$profiles->{v2essential}}, qw(Mail::IMAPClient) ];
121 $profiles->{'watch-v1'} = [ @{$profiles->{essential}} ];
122 $profiles->{'watch-maildir'} = [ @{$profiles->{v2essential}} ];
124 # package names which can't be mapped automatically and explicit
125 # dependencies to prevent essential package removal:
126 my $non_auto = { # git and perl (+autodie) are essential
127 git => {
128 pkg => [ qw(curl p5-TimeDate git) ],
129 rpm => [ qw(curl git) ],
130 pkg_add => [ qw(curl p5-Time-TimeDate git) ],
132 perl => {
133 apk => [ qw(perl perl-utils) ],
134 pkg => 'perl5',
135 pkgin => 'perl',
136 pkg_add => [], # Perl is part of OpenBSD base
138 # optional stuff:
139 'BSD::Resource' => {
140 apk => [], # not packaged for Alpine 3.19
142 'Date::Parse' => {
143 apk => 'perl-timedate',
144 deb => 'libtimedate-perl',
145 pkg => 'p5-TimeDate',
146 rpm => 'perl-TimeDate',
147 pkg_add => 'p5-Time-TimeDate',
149 'Inline::C' => {
150 apk => [ qw(perl-inline-c perl-dev) ],
151 pkg_add => 'p5-Inline', # tested OpenBSD 7.3
152 rpm => 'perl-Inline', # for CentOS 7.x, at least
154 'DBD::SQLite' => { deb => 'libdbd-sqlite3-perl' },
155 'Plack::Middleware::ReverseProxy' => {
156 apk => [], # not packaged for Alpine 3.19.0
158 'Plack::Test' => {
159 apk => 'perl-plack',
160 deb => 'libplack-perl',
161 pkg => 'p5-Plack',
163 'Plack::Test::ExternalServer' => {
164 apk => [], # not packaged for Alpine 3.19.0
166 'Xapian' => {
167 apk => 'xapian-bindings-perl',
168 deb => 'libsearch-xapian-perl',
169 pkg => 'p5-Xapian',
170 pkg_add => 'xapian-bindings-perl',
171 rpm => [], # xapian14-bindings-perl in 3rd-party repo
173 'highlight.pm' => {
174 apk => [],
175 deb => 'libhighlight-perl',
176 pkg => [],
177 pkgin => 'p5-highlight',
178 rpm => [],
181 # `libgit2' is the project name (since git has libgit)
182 'libgit2-dev' => {
183 pkg => 'libgit2',
184 rpm => 'libgit2-devel',
187 # some distros have both sqlite 2 and 3, we've only ever used 3
188 'libsqlite3' => {
189 apk => [], # handled by apk w/ perl-dbd-sqlite
190 pkg => 'sqlite3',
191 rpm => [], # `sqlite' is not removable due to yum/systemd
192 deb => [], # libsqlite3-0, but no need to specify
195 # only one version of Xapian distros
196 'libxapian' => { # avoid .so version numbers in our deps
197 deb => [], # libxapian30 atm, but no need to specify
198 pkg => 'xapian-core',
199 pkgin => 'xapian',
200 rpm => 'xapian-core',
202 'libxapian-dev' => {
203 apk => 'xapian-core-dev',
204 pkg => 'xapian-core',
205 pkgin => 'xapian',
206 rpm => 'xapian-core-devel',
208 'pkg-config' => {
209 apk => [], # handled by apk w/ xapian-core-dev
210 pkg_add => [], # part of the OpenBSD base system
211 pkg => 'pkgconf', # pkg-config is a symlink to pkgconf
212 pkgin => 'pkg-config',
214 'sqlite3' => { # this is just the executable binary on deb
215 apk => 'sqlite',
216 rpm => [], # `sqlite' is not removable due to yum/systemd
219 # we call xapian-compact(1) in public-inbox-compact(1) and
220 # xapian-delve(1) in public-inbox-cindex(1)
221 'xapian-tools' => {
222 apk => 'xapian-core',
223 pkg => 'xapian-core',
224 pkgin => 'xapian',
225 rpm => 'xapian-core', # ???
228 # OS-specific
229 'IO::KQueue' => {
230 apk => [],
231 deb => [],
232 rpm => [],
236 # standard library stuff that CentOS 7.x (and presumably other RPM)
237 # split out and can be removed without removing the `perl' RPM:
238 for (qw(autodie Digest::SHA ExtUtils::MakeMaker IO::Compress Sys::Syslog
239 Test::Simple Text::ParseWords)) {
240 # n.b.: Compress::Raw::Zlib is pulled in by IO::Compress
241 # qw(constant Encode Getopt::Long Exporter Storable Time::HiRes)
242 # don't need to be here since it's impossible to have `perl'
243 # on CentOS 7.x without them.
244 my $rpm = $_;
245 $rpm =~ s/::/-/g;
246 $non_auto->{$_} = {
247 deb => 'perl', # libperl5.XX, but the XX varies
248 pkg => 'perl5',
249 pkg_add => [], # perl is in the OpenBSD base system
250 apk => 'perl',
251 pkgin => 'perl',
252 rpm => "perl-$rpm",
256 # NetBSD and OpenBSD package names are similar to FreeBSD in most cases
257 if ($pkg_fmt =~ /\A(?:pkg_add|pkgin)\z/) {
258 for my $name (keys %$non_auto) {
259 my $fbsd_pkg = $non_auto->{$name}->{pkg};
260 $non_auto->{$name}->{$pkg_fmt} //= $fbsd_pkg if $fbsd_pkg;
264 my %inst_check = ( # subs which return true if a package is intalled
265 apk => sub { system(qw(apk info -q -e), $_[0]) == 0 },
266 deb => sub { system("dpkg -s $_[0] >/dev/null 2>&1") == 0 },
267 pkg => sub { system(qw(pkg info -q), $_[0]) == 0 },
268 pkg_add => sub { system(qw(pkg_info -q -e), "$_[0]->=0") == 0 },
269 pkgin => sub { system(qw(pkg_info -q -e), $_[0]) == 0 },
270 rpm => sub { system("rpm -qs $_[0] >/dev/null 2>&1") == 0 },
273 our $INST_CHECK = $inst_check{$pkg_fmt} || die <<"";
274 don't know how to check install status for $pkg_fmt
276 my (@pkg_install, @pkg_remove, %all);
277 for my $ary (values %$profiles) {
278 my @extra;
279 for my $pkg (@$ary) {
280 my $deps = $always_deps->{$pkg} // next;
281 push @extra, list($deps);
283 push @$ary, @extra;
284 $all{$_} = \@pkg_remove for @$ary;
286 if ($^O =~ /\A(?:free|net|open)bsd\z/) {
287 $all{'IO::KQueue'} = \@pkg_remove;
289 $profiles->{all} = [ keys %all ]; # pseudo-profile for all packages
291 # parse the profile list from the command-line
292 my @profiles = @ARGV;
293 while (defined(my $profile = shift @profiles)) {
294 if ($profile =~ s/-\z//) {
295 # like apt-get, trailing "-" means remove
296 profile2dst($profile, \@pkg_remove);
297 } else {
298 profile2dst($profile, \@pkg_install);
302 # fill in @pkg_install and @pkg_remove:
303 while (my ($pkg, $dst_pkg_list) = each %all) {
304 push @$dst_pkg_list, list(pkg2ospkg($pkg, $pkg_fmt));
307 my (%add, %rm); # uniquify lists
308 @pkg_install = grep { !$add{$_}++ && !$INST_CHECK->($_) } @pkg_install;
309 @pkg_remove = $opt->{'allow-remove'} ? grep {
310 !$add{$_} && !$rm{$_}++ && $INST_CHECK->($_)
311 } @pkg_remove : ();
313 (@pkg_remove || @pkg_install) or warn "# no packages to install nor remove\n";
315 # OS-specific cleanups appreciated
316 if ($pkg_fmt eq 'apk') {
317 root('apk', 'add', @pkg_install) if @pkg_install;
318 root('apk', 'del', @pkg_remove) if @pkg_remove;
319 } elsif ($pkg_fmt eq 'deb') {
320 my @apt_opt = qw(-o APT::Install-Recommends=false
321 -o APT::Install-Suggests=false);
322 push @apt_opt, '-y' if $opt->{yes};
323 root('apt-get', @apt_opt, qw(install),
324 @pkg_install,
325 # apt-get lets you suffix a package with "-" to
326 # remove it in an "install" sub-command:
327 map { "$_-" } @pkg_remove) if (@pkg_remove || @pkg_install);
328 root('apt-get', @apt_opt, qw(autoremove)) if $opt->{'allow-remove'};
329 } elsif ($pkg_fmt eq 'pkg') { # FreeBSD
330 my @pkg_opt = $opt->{yes} ? qw(-y) : ();
332 # don't remove stuff that isn't installed:
333 root(qw(pkg remove), @pkg_opt, @pkg_remove) if @pkg_remove;
334 root(qw(pkg install), @pkg_opt, @pkg_install) if @pkg_install;
335 root(qw(pkg autoremove), @pkg_opt) if $opt->{'allow-remove'};
336 } elsif ($pkg_fmt eq 'pkgin') { # NetBSD
337 my @pkg_opt = $opt->{yes} ? qw(-y) : ();
338 root(qw(pkgin), @pkg_opt, 'remove', @pkg_remove) if @pkg_remove;
339 root(qw(pkgin), @pkg_opt, 'install', @pkg_install) if @pkg_install;
340 root(qw(pkgin), @pkg_opt, 'autoremove') if $opt->{'allow-remove'};
341 # TODO: yum / rpm support
342 } elsif ($pkg_fmt eq 'rpm') {
343 my @pkg_opt = $opt->{yes} ? qw(-y) : ();
344 root(qw(yum remove), @pkg_opt, @pkg_remove) if @pkg_remove;
345 root(qw(yum install), @pkg_opt, @pkg_install) if @pkg_install;
346 } elsif ($pkg_fmt eq 'pkg_add') { # OpenBSD
347 my @pkg_opt = $opt->{yes} ? qw(-I) : (); # -I means non-interactive
348 root(qw(pkg_delete), @pkg_opt, @pkg_remove) if @pkg_remove;
349 @pkg_install = map { "$_--" } @pkg_install; # disambiguate w3m
350 root(qw(pkg_add), @pkg_opt, @pkg_install) if @pkg_install;
351 root(qw(pkg_delete -a), @pkg_opt) if $opt->{'allow-remove'};
352 } else {
353 die "unsupported package format: $pkg_fmt\n";
355 exit 0;
358 # map a generic package name to an OS package name
359 sub pkg2ospkg {
360 my ($pkg, $fmt) = @_;
362 # check explicit overrides, first:
363 if (my $ospkg = $non_auto->{$pkg}->{$fmt}) {
364 return $ospkg;
367 # check common Perl module name patterns:
368 if ($pkg =~ /::/ || $pkg =~ /\A[A-Z]/) {
369 if ($fmt eq 'apk') {
370 $pkg =~ s/::/-/g;
371 return "perl-\L$pkg"
372 } elsif ($fmt eq 'deb') {
373 $pkg =~ s/::/-/g;
374 return "lib\L$pkg-perl";
375 } elsif ($fmt eq 'rpm') {
376 $pkg =~ s/::/-/g;
377 return "perl-$pkg"
378 } elsif ($fmt =~ /\Apkg(?:_add|in)?\z/) {
379 $pkg =~ s/::/-/g;
380 return "p5-$pkg"
381 } else {
382 die "unsupported package format: $fmt for $pkg\n"
386 # use package name as-is (e.g. 'curl' or 'w3m')
387 $pkg;
390 # maps a install profile to a package list (@pkg_remove or @pkg_install)
391 sub profile2dst {
392 my ($profile, $dst_pkg_list) = @_;
393 if (my $pkg_list = $profiles->{$profile}) {
394 $all{$_} = $dst_pkg_list for @$pkg_list;
395 } elsif ($all{$profile}) { # $profile is just a package name
396 $all{$profile} = $dst_pkg_list;
397 } else {
398 die "unrecognized profile or package: $profile\n";
402 sub root {
403 warn "# @_\n";
404 return if $opt->{'dry-run'};
405 return if system(@_) == 0;
406 warn "E: command failed: @_\n";
407 exit($? >> 8);
410 # ensure result can be pushed into an array:
411 sub list {
412 my ($pkg) = @_;
413 ref($pkg) eq 'ARRAY' ? @$pkg : $pkg;