public-inbox.git  about / heads / tags
an "archives first" approach to mailing lists
blob 6563c3cef0939f7adcde62ab783e6855cad4d138 12782 bytes (raw)
$ git show HEAD:install/deps.perl	# shows this blob on the CLI

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
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;
}

git clone https://public-inbox.org/public-inbox.git
git clone http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/public-inbox.git