diff options
Diffstat (limited to 'dh_installsystemd')
-rwxr-xr-x | dh_installsystemd | 400 |
1 files changed, 196 insertions, 204 deletions
diff --git a/dh_installsystemd b/dh_installsystemd index 8dd4edc6..bed680b1 100755 --- a/dh_installsystemd +++ b/dh_installsystemd @@ -16,61 +16,55 @@ our $VERSION = DH_BUILTIN_VERSION; =head1 SYNOPSIS -B<dh_installsystemd> [S<I<debhelper options>>] [B<--restart-after-upgrade>] [B<--no-stop-on-upgrade>] [B<--no-enable>] [B<--name=>I<name>] [S<I<unit file> ...>] +B<dh_installsystemd> [S<I<debhelper options>>] [B<--restart-after-upgrade>] [B<--no-stop-on-upgrade>] [B<--no-enable>] [B<--no-start>] [B<--name=>I<name>] [S<I<unit file> ...>] =head1 DESCRIPTION -B<dh_installsystemd> is a debhelper program that is responsible for enabling, -disabling, starting, stopping and restarting systemd unit files. +B<dh_installsystemd> is a debhelper program that is responsible for +installing package maintainer supplied systemd unit files. -In the simple case, it finds all unit files installed by a package (e.g. -bacula-fd.service) and enables them. It is not necessary that the machine -actually runs systemd during package installation time, enabling happens on all -machines in order to be able to switch from sysvinit to systemd and back. +It also finds the service files installed by a package and generates +F<preinst>, F<postinst>, and F<prerm> code blocks for enabling, +disabling, starting, stopping, and restarting the corresponding +systemd services, when the package is installed, updated, or +removed. These snippets are added to the maintainer scripts by +L<dh_installdeb(1)>. -For only generating blocks for specific service files, you need to pass them as -arguments, e.g. B<dh_installsystemd quota.service> and B<dh_installsystemd ---name=quotarpc quotarpc.service>. +L<deb-systemd-helper(1)> is used to enable and disable systemd units, +thus it is not necessary that the machine actually runs systemd during +package installation time, enabling happens on all machines in order +to be able to switch from sysvinit to systemd and back. + +B<dh_installsystemd> operates on all unit files installed by a +package. For only generating blocks for specific unit files, pass them +as arguments, C<dh_installsystemd quota.service>. Specific unit files +can be excluded from processing using the B<-X> common L<debhelper(1)> +option. =head1 FILES =over 4 -=item debian/I<package>.service, debian/I<package>@.service - -If this exists, it is installed into lib/systemd/system/I<package>.service (or -lib/systemd/system/I<package>@.service) in the package build directory. +=item debian/I<package>.mount, + debian/I<package>.path, + debian/I<package>@.path, + debian/I<package>.service, + debian/I<package>@.service, + debian/I<package>.socket, + debian/I<package>@.socket, + debian/I<package>.target, + debian/I<package>@.target, + debian/I<package>.timer, + debian/I<package>@.timer + +If any of those files exists, they are installed into +F<lib/systemd/system/> in the package build directory. =item debian/I<package>.tmpfile -If this exists, it is installed into usr/lib/tmpfiles.d/I<package>.conf in the -package build directory. (The tmpfiles.d mechanism is currently only used -by systemd.) - -=item debian/I<package>.target, debian/I<package>@.target - -If this exists, it is installed into lib/systemd/system/I<package>.target (or -lib/systemd/system/I<package>@.target) in the package build directory. - -=item debian/I<package>.socket, debian/I<package>@.socket - -If this exists, it is installed into lib/systemd/system/I<package>.socket (or -lib/systemd/system/I<package>@.socket) in the package build directory. - -=item debian/I<package>.mount - -If this exists, it is installed into lib/systemd/system/I<package>.mount -in the package build directory. - -=item debian/I<package>.path, debian/I<package>@.path - -If this exists, it is installed into lib/systemd/system/I<package>.path (or -lib/systemd/system/I<package>@.path) in the package build directory. - -=item debian/I<package>.timer, debian/I<package>@.timer - -If this exists, it is installed into lib/systemd/system/I<package>.timer (or -lib/systemd/system/I<package>@.timer) in the package build directory. +If this exists, it is installed into F<usr/lib/tmpfiles.d/> in the +package build directory. Note that the C<tmpfiles.d> mechanism is +currently only used by systemd. =back @@ -88,12 +82,12 @@ should not be started. =item B<--name=>I<name> -Install the service file as I<name.service> instead of the default filename, -which is the I<package.service>. When this parameter is used, +Install the service file as I<name.service> instead of the default +filename I<package.service>. When this parameter is used, B<dh_installsystemd> looks for and installs files named -F<debian/package.name.service> instead of the usual F<debian/package.service>. -Moreover, maintainer scripts are only generated for units that match the given -I<name>. +F<debian/package.name.service> instead of the usual +F<debian/package.service>. Moreover, maintainer scripts are only +generated for units that match the given I<name>. =item B<--restart-after-upgrade> @@ -132,15 +126,13 @@ should not be enabled. =head1 NOTES -Note that this command is not idempotent. L<dh_prep(1)> should be called -between invocations of this command (with the same arguments). Otherwise, it -may cause multiple instances of the same text to be added to maintainer -scripts. +This command is not idempotent. L<dh_prep(1)> should be called between +invocations of this command (with the same arguments). Otherwise, it +may cause multiple instances of the same text to be added to +maintainer scripts. =cut -exit 0 if compat(10); - $dh{RESTART_AFTER_UPGRADE} = 1; init(options => { @@ -150,13 +142,23 @@ init(options => { "no-restart-on-upgrade" => \$dh{R_FLAG}, "no-start" => \$dh{NO_START}, "R|restart-after-upgrade!" => \$dh{RESTART_AFTER_UPGRADE}, - "no-also" => \$dh{NO_ALSO}, + "no-also" => \$dh{NO_ALSO}, # undocumented option }); +sub quote { + # Add single quotes around the argument. + return '\'' . $_[0] . '\''; +} + +sub uniq { + my %seen; + return grep { !$seen{$_}++ } @_; +} + sub contains_install_section { my ($unit_path) = @_; - open(my $fh, '<', $unit_path) or error("Cannot open($unit_path) to check for [Install]: $!"); + open(my $fh, '<', $unit_path) or error("Cannot open($unit_path): $!"); while (my $line = <$fh>) { chomp($line); @@ -166,6 +168,13 @@ sub contains_install_section { return 0; } +sub has_sysv_equivalent { + my ($tmpdir, $unit) = @_; + + $unit =~ s/\.(?:mount|service|socket|target|path)$//g; + return -f "$tmpdir/etc/init.d/$unit"; +} + sub install_unit { my ($package, $script, $pkgsuffix, $path, $installsuffix) = @_; $installsuffix = $installsuffix || $pkgsuffix; @@ -175,26 +184,22 @@ sub install_unit { install_file($unit, "${path}/${script}.${installsuffix}"); } -# Extracts the Also= or Alias= line(s) from a unit file. -# In case this produces horribly wrong results, you can pass --no-also, but -# that should really not be necessary. Please report bugs to -# pkg-systemd-maintainers. +# Extracts the directive values from a unit file. Handles repeated +# directives in the same unit file. Assumes values can only be +# composed of lists of unit names. This is good enough for the 'Also=' +# and 'Alias=' directives handled here. sub extract_key { my ($unit_path, $key) = @_; my @values; - return @values if $dh{NO_ALSO}; - - open(my $fh, '<', $unit_path) or error("Cannot open($unit_path) for extracting the Also= line(s): $!"); + open(my $fh, '<', $unit_path) or error("Cannot open($unit_path): $!"); while (my $line = <$fh>) { chomp($line); - # The keys parsed from the unit file below can only have - # unit names as values. Since unit names can't have - # whitespace in systemd, simply use split and strip any - # leading/trailing quotes. See systemd-escape(1) for - # examples of valid unit names. + # Since unit names can't have whitespace in systemd, simply + # use split and strip any leading/trailing quotes. See + # systemd-escape(1) for examples of valid unit names. if ($line =~ /^\s*$key=(.+)$/i) { for my $value (split(/\s+/, $1)) { $value =~ s/^(["'])(.*)\g1$/$2/; @@ -202,207 +207,194 @@ sub extract_key { } } } + close($fh); return @values; } +sub list_installed_units { + my ($tmpdir, $aliases) = @_; + + my $lib_systemd_system = "$tmpdir/lib/systemd/system"; + my @installed; + + return unless -d $lib_systemd_system; + opendir(my $dh, "$lib_systemd_system") or error("Cannot opendir($lib_systemd_system): $!"); + + foreach my $name (readdir($dh)) { + my $path = "$lib_systemd_system/$name"; + next unless -f $path; + if (-l "$path") { + my $dest = basename(readlink($path)); + $aliases->{$dest} //= [ ]; + push @{$aliases->{$dest}}, $name; + } else { + push @installed, $name; + } + } + + closedir($dh); + return @installed; +} + -# PROMISE: DH NOOP WITHOUT tmp(lib/systemd/system) mount path service socket target tmpfile timer +# PROMISE: DH NOOP WITHOUT tmp(lib/systemd/system) tmp(usr/lib/tmpfiles.d) tmp(etc/tmpfiles.d) mount path service socket target tmpfile timer -my %requested_files = map { basename($_) => 1 } @ARGV; -my %installed_files; +# Install package maintainer supplied unit files foreach my $package (@{$dh{DOPACKAGES}}) { my $tmpdir = tmpdir($package); - my (@installed_units, @start_units, @enable_units, %aliases, @tmpfiles); - # Figure out what filename to install it as. - my $script; - if (defined $dh{NAME}) { - $script=$dh{NAME}; - } - else { - $script=$package; - } + # Intall all unit files in the debian/ directory with names in the + # form $package.(service|target|socket|path|timer|mount|tmpfile) + # and their templated versison when relevant. - for my $service_type (qw(service target socket path timer)) { - install_unit($package, $script, $service_type, "$tmpdir/lib/systemd/system"); - install_unit("${package}@", "${script}@", $service_type, "$tmpdir/lib/systemd/system"); + # This can be modified with the --name option to look for unit + # files with names in the form $package.$name.(service|...) and + # $name.(service|target|socket|path|timer|mount|tmpfile) and their + # templated version when relevant. + my $name = $dh{NAME} // $package; + + for my $type (qw(service target socket path timer)) { + install_unit($package, $name, $type, "$tmpdir/lib/systemd/system"); + install_unit("${package}@", "${name}@", $type, "$tmpdir/lib/systemd/system"); } - install_unit($package, $script, 'mount', "$tmpdir/lib/systemd/system"); - install_unit($package, $script, 'tmpfile', "$tmpdir/usr/lib/tmpfiles.d", 'conf'); + install_unit($package, $name, 'mount', "$tmpdir/lib/systemd/system"); + install_unit($package, $name, 'tmpfile', "$tmpdir/usr/lib/tmpfiles.d", 'conf'); +} + + +# Add postinst code blocks to handle tmpfiles +foreach my $package (@{$dh{DOPACKAGES}}) { + my $tmpdir = tmpdir($package); + my @tmpfiles; + + my @dirs = grep { -d } map { "${tmpdir}/$_" } qw(usr/lib/tmpfiles.d etc/tmpfiles.d); - my $oldcwd = getcwd(); find({ wanted => sub { my $name = $File::Find::name; return unless -f $name; - return unless $name =~ m,^\Q${tmpdir}\E/lib/systemd/system/[^/]+$,; - if (-l) { - my $target = abs_path(readlink()); - $target =~ s,^\Q${oldcwd}\E/,,g; - $aliases{$target} = [ $_ ]; - } else { - push @installed_units, $name; - } - }, + push(@tmpfiles, basename($name)); }, no_chdir => 1, - }, "${tmpdir}/lib/systemd/system") if -d "${tmpdir}/lib/systemd/system"; + }, @dirs) if @dirs; + + if (@tmpfiles) { + autoscript($package, 'postinst', 'postinst-init-tmpfiles', { 'TMPFILES' => join(' ', sort @tmpfiles) }); + } +} + - # Handle either only the unit files which were passed as arguments or - # all unit files that are installed in this package. - my @args; - if (@ARGV > 0) { +# Add postinst, prerm, and postrm code blocks to handle activation, +# deactivation, start and stopping of services when the package is +# installed, upgraded or removed. +foreach my $package (@{$dh{DOPACKAGES}}) { + my $tmpdir = tmpdir($package); + my (@args, @start_units, @enable_units, %aliases); + + my @installed_units = list_installed_units($tmpdir, \%aliases); + + # Handle either only the unit files which were passed as arguments + # or all unit files that are installed in this package. + if (@ARGV) { @args = @ARGV; } elsif ($dh{NAME}) { - # treat --name flag as if the corresponding units were passed in the command line - @args = grep /(^|\/)$dh{NAME}\.(mount|path|service|socket|target|tmpfile)$/, @installed_units; + # Treat --name option as if the corresponding unit names were + # passed in the command line. + @args = grep /(^|\/)$dh{NAME}\.(mount|path|service|socket|target|timer)$/, @installed_units; } else { @args = @installed_units; } - # support excluding units via -X + # Support excluding units via the -X debhelper common option. foreach my $x (@{$dh{EXCLUDE}}) { @args = grep !/(^|\/)$x$/, @args; } - # This hash prevents us from looping forever in the following while loop. - # An actual real-world example of such a loop is systemd’s - # systemd-readahead-drop.service, which contains + # This hash prevents us from looping forever in the following + # while loop. An actual real-world example of such a loop is + # systemd's systemd-readahead-drop.service, which contains # Also=systemd-readahead-collect.service, and that file in turn - # contains Also=systemd-readahead-drop.service, thus forming an endless - # loop. + # contains Also=systemd-readahead-drop.service, thus forming an + # endless loop. my %seen; - # We use while/shift because we push to the list in the body. + + # Must use while and shift because the loop alters the list. while (@args) { - my $name = shift @args; - my $base = basename($name); - - # Try to make the path absolute, so that the user can call - # dh_installsystemd bacula-fd.service - if ($base eq $name) { - # NB: This works because @installed_units contains - # files from precisely one directory. - my ($full) = grep { basename($_) eq $base } @installed_units; - if (defined($full)) { - $name = $full; - } else { - warning(qq|Could not find "$name" in the /lib/systemd/system directory of $package. | . - qq|This could be a typo, or using Also= with a service file from another package. | . - qq|Please check carefully that this message is harmless.|); - } + my $unit = shift @args; + my $path = "${tmpdir}/lib/systemd/system/${unit}"; + + error("Package '$package' does not install unit '$unit'.") unless (-f $path); + + # Skip template service files. Enabling, disabling, starting + # or stopping those services without specifying the instance + # is not useful. + next if ($unit =~ /\@/); + + # Handle all unit files specified via Also= explicitly. This + # is not necessary for enabling, but for disabling, as we + # cannot read the unit file when disabling as it has already + # been deleted. The undocumented --no-also option disables + # handling of units linked via Also=. This option is provided + # only to suport a very specific use case in network-manager. + unless ($dh{NO_ALSO}) { + push @args, $_ for grep { !$seen{$_}++ } extract_key($path, 'Also'); } - $installed_files{$base} = 1 if exists($requested_files{$base}); + # Extract unit aliases. + push @{$aliases{$unit}}, $_ for extract_key($path, 'Alias'); - # Skip template service files like e.g. getty@.service. - # Enabling, disabling, starting or stopping those services - # without specifying the instance (e.g. getty@ttyS0.service) is - # not useful. - if ($name =~ /\@/) { - next; + if (! grep { has_sysv_equivalent($tmpdir, $_) } ($unit, @{$aliases{$unit}})) { + push @start_units, $unit; } - # Handle all unit files specified via Also= explicitly. - # This is not necessary for enabling, but for disabling, as we - # cannot read the unit file when disabling (it was already - # deleted). - my @also = grep { !exists($seen{$_}) } extract_key($name, 'Also'); - $seen{$_} = 1 for @also; - @args = (@args, @also); - - push @{$aliases{$name}}, $_ for extract_key($name, 'Alias'); - my @sysv = grep { - my $base = $_; - $base =~ s/\.(?:mount|service|socket|target|path)$//g; - -f "$tmpdir/etc/init.d/$base" - } ($base, @{$aliases{$name}}); - if (@sysv == 0 && !grep { $_ eq $name } @start_units) { - push @start_units, $name; - } - - if (contains_install_section($name) && !grep { $_ eq $name } @enable_units) { - push @enable_units, $name; + if (contains_install_section($path)) { + push @enable_units, $unit; } } - # Include postinst-init-tmpfiles if the package ships any files - # in /usr/lib/tmpfiles.d or /etc/tmpfiles.d - if (-d $tmpdir) { - my @dirs = grep { -d } map { "${tmpdir}/$_" } qw(usr/lib/tmpfiles.d etc/tmpfiles.d); - find({ - wanted => sub { - my $name = $File::Find::name; - return unless -f $name; - $name =~ s/^\Q$tmpdir\E//g; - push(@tmpfiles, $name); - }, - no_chdir => 1, - }, @dirs) if @dirs; - if (@tmpfiles > 0) { - autoscript($package, 'postinst', 'postinst-init-tmpfiles', { 'TMPFILES' => join(' ', sort @tmpfiles) }); - } - } + @enable_units = map { quote($_) } uniq sort @enable_units; + @start_units = map { quote($_) } uniq sort @start_units; + + my %options = ('snippet-order' => 'service'); if (@enable_units) { - for my $unit (sort @enable_units) { - my $base = q{'} . basename($unit) . q{'}; - if ($dh{NO_ENABLE}) { - autoscript($package, 'postinst', 'postinst-systemd-dont-enable', { 'UNITFILE' => $base }); - } else { - autoscript($package, 'postinst', 'postinst-systemd-enable', { 'UNITFILE' => $base }); - } + for my $unit (@enable_units) { + my $snippet = $dh{NO_ENABLE} ? 'postinst-systemd-dont-enable' : 'postinst-systemd-enable'; + autoscript($package, 'postinst', $snippet, { 'UNITFILE' => $unit }, \%options); } - my $enableunitargs = join(' ', sort map { q{'} . basename($_) . q{'} } @enable_units); - autoscript($package, 'postrm', 'postrm-systemd', {'UNITFILES' => $enableunitargs }); + autoscript($package, 'postrm', 'postrm-systemd', {'UNITFILES' => join(' ', @enable_units) }); } - + if (@start_units) { - # The $package and $sed parameters are always the same. - # This wrapper function makes the following logic easier to read. - my $startunitargs = join(' ', sort map { q{'} . basename($_) . q{'} } @start_units); - my $start_autoscript = sub { - my ($script, $filename) = @_; - autoscript($package, $script, $filename, { 'UNITFILES' => $startunitargs }); - }; + my $units = { 'UNITFILES' => join(' ', @start_units) }; if ($dh{RESTART_AFTER_UPGRADE}) { - my $snippet = "postinst-systemd-restart" . ($dh{NO_START} ? "nostart" : ""); - $start_autoscript->("postinst", $snippet); + my $snippet = $dh{NO_START} ? 'postinst-systemd-restartnostart' : 'postinst-systemd-restart'; + autoscript($package, 'postinst', $snippet, $units, \%options); } elsif (!$dh{NO_START}) { - # We need to stop/start before/after the upgrade. - $start_autoscript->("postinst", "postinst-systemd-start"); + # (stop|start) service (before|after) upgrade + autoscript($package, 'postinst', 'postinst-systemd-start', $units, \%options); } - $start_autoscript->("postrm", "postrm-systemd-reload-only"); + autoscript($package, 'postrm', 'postrm-systemd-reload-only', $units, \%options); if ($dh{R_FLAG} || $dh{RESTART_AFTER_UPGRADE}) { # stop service only on remove - $start_autoscript->("prerm", "prerm-systemd-restart"); + autoscript($package, 'prerm', 'prerm-systemd-restart', $units, \%options); } elsif (!$dh{NO_START}) { # always stop service - $start_autoscript->("prerm", "prerm-systemd"); - } - } -} - -if (%requested_files) { - my $any_missing = 0; - for my $name (sort(keys(%requested_files))) { - if (not exists($installed_files{$name})) { - warning(qq{Requested unit "$name" but it was not found in any package acted on.}); - $any_missing = 1; + autoscript($package, 'prerm', 'prerm-systemd', $units, \%options); } } - error("Could not handle all of the requested services") if $any_missing; } =head1 SEE ALSO -L<debhelper(7)> +L<debhelper(7)>, L<dh_installinit(1)>, L<deb-systemd-helper(1)> =head1 AUTHORS |