diff options
-rw-r--r-- | autoscripts/postinst-systemd-user-dont-enable | 15 | ||||
-rw-r--r-- | autoscripts/postinst-systemd-user-enable | 15 | ||||
-rw-r--r-- | autoscripts/postrm-systemd-user | 12 | ||||
-rw-r--r-- | debhelper.pod | 5 | ||||
-rw-r--r-- | debian/changelog | 11 | ||||
-rwxr-xr-x | dh | 19 | ||||
-rwxr-xr-x | dh_installsystemduser | 270 | ||||
-rw-r--r-- | t/dh_installsystemduser/debian/baz.user.service | 8 | ||||
-rw-r--r-- | t/dh_installsystemduser/debian/changelog | 5 | ||||
-rw-r--r-- | t/dh_installsystemduser/debian/control | 10 | ||||
-rw-r--r-- | t/dh_installsystemduser/debian/foo.user.service | 8 | ||||
-rw-r--r-- | t/dh_installsystemduser/dh_installsystemduser.t | 75 |
12 files changed, 444 insertions, 9 deletions
diff --git a/autoscripts/postinst-systemd-user-dont-enable b/autoscripts/postinst-systemd-user-dont-enable new file mode 100644 index 00000000..0837257f --- /dev/null +++ b/autoscripts/postinst-systemd-user-dont-enable @@ -0,0 +1,15 @@ +if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then + if deb-systemd-helper --user debian-installed #UNITFILE# ; then + # This will only remove masks created by d-s-h on package removal. + deb-systemd-helper --user unmask #UNITFILE# >/dev/null || true + + if deb-systemd-helper --quiet --user was-enabled #UNITFILE# ; then + # Create new symlinks, if any. + deb-systemd-helper --user enable #UNITFILE# >/dev/null || true + fi + fi + + # Update the statefile to add new symlinks (if any), which need to be cleaned + # up on purge. Also remove old symlinks. + deb-systemd-helper --user update-state #UNITFILE# >/dev/null || true +fi diff --git a/autoscripts/postinst-systemd-user-enable b/autoscripts/postinst-systemd-user-enable new file mode 100644 index 00000000..b16f61fb --- /dev/null +++ b/autoscripts/postinst-systemd-user-enable @@ -0,0 +1,15 @@ +if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then + # This will only remove masks created by d-s-h on package removal. + deb-systemd-helper --user unmask #UNITFILE# >/dev/null || true + + # was-enabled defaults to true, so new installations run enable. + if deb-systemd-helper --quiet --user was-enabled #UNITFILE# ; then + # Enables the unit on first installation, creates new + # symlinks on upgrades if the unit file has changed. + deb-systemd-helper --user enable #UNITFILE# >/dev/null || true + else + # Update the statefile to add new symlinks (if any), which need to be + # cleaned up on purge. Also remove old symlinks. + deb-systemd-helper --user update-state #UNITFILE# >/dev/null || true + fi +fi diff --git a/autoscripts/postrm-systemd-user b/autoscripts/postrm-systemd-user new file mode 100644 index 00000000..bf48d1a3 --- /dev/null +++ b/autoscripts/postrm-systemd-user @@ -0,0 +1,12 @@ +if [ "$1" = "remove" ]; then + if [ -x "/usr/bin/deb-systemd-helper" ] ; then + deb-systemd-helper --user mask #UNITFILES# >/dev/null || true + fi +fi + +if [ "$1" = "purge" ]; then + if [ -x "/usr/bin/deb-systemd-helper" ] ; then + deb-systemd-helper --user purge #UNITFILES# >/dev/null || true + deb-systemd-helper --user unmask #UNITFILES# >/dev/null || true + fi +fi diff --git a/debhelper.pod b/debhelper.pod index 177d2f8f..c758a72b 100644 --- a/debhelper.pod +++ b/debhelper.pod @@ -915,6 +915,11 @@ packages and not only during the building process. Please set B<DH_GOLANG_EXCLUDES_ALL> to false to revert to the previous behaviour. See B<Debian::Debhelper::Buildsystem::golang(3pm)> for details and examples. +=item - + +B<dh_installsystemduser> is now included in the B<dh> standard +sequence by default. + =back =back diff --git a/debian/changelog b/debian/changelog index 6b3e3605..f25e531c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,14 @@ +debhelper (11.5) UNRELEASED; urgency=medium + + [ Daniele Nicolodi ] + * dh_installsystemduser: New helper responsible for istalling package + maintainer supplied systemd user instance units and to produce + postinst and postrm maintiner scripts code blocks to appropriately + enable, mask and disable units when the package is installed, + upgraded, or removed. (Closes: #764678) + + -- Daniele Nicolodi <daniele@grinta.net> Sun, 14 Oct 2018 10:13:17 -0600 + debhelper (11.4.1) unstable; urgency=medium [ Niels Thykier ] @@ -115,10 +115,10 @@ easy way to do with is by adding an override target for that command. #!/usr/bin/make -f %: dh $@ - + override_dh_strip: dh_strip -Xfoo - + override_dh_auto_configure: dh_auto_configure -- --with-foo --disable-bar @@ -199,7 +199,7 @@ want it to run, by defining empty override targets for each command. #!/usr/bin/make -f %: dh $@ - + # Commands not to run: override_dh_auto_test override_dh_compress override_dh_fixperms: @@ -210,7 +210,7 @@ These will be skipped when running build-arch and binary-arch sequences. #!/usr/bin/make -f %: dh $@ - + override_dh_auto_build-indep: $(MAKE) -C docs @@ -272,7 +272,7 @@ option to ensure they only work on architecture dependent packages. =head1 DEPRECATED OPTIONS -The following options are deprecated. It's much +The following options are deprecated. It's much better to use override targets instead. They are B<not> available in compat 10. @@ -308,7 +308,7 @@ my @ARGV_orig=@ARGV; my (@addons, @addon_requests); inhibit_log(); - + init(options => { "until=s" => \$dh{UNTIL}, "after=s" => \$dh{AFTER}, @@ -325,7 +325,7 @@ init(options => { "l" => \&list_addons, "list" => \&list_addons, }, - # Disable complaints about unknown options; they are passed on to + # Disable complaints about unknown options; they are passed on to # the debhelper commands. ignore_unknown_options => 1, # Bundling does not work well since there are unknown options. @@ -405,6 +405,7 @@ my @i = (qw{ dh_installinit }, (!compat(10) ? qw(dh_installsystemd) : qw()), + (compat(12) ? qw(dh_installsystemduser) : qw()), qw{ dh_installmenu @@ -499,7 +500,7 @@ sub remove_command { foreach my $sequence (keys %sequences) { $sequences{$sequence}=[grep { $_ ne $command } @{$sequences{$sequence}}]; } - + } sub add_command { my ($command, $sequence) = @_; @@ -864,7 +865,7 @@ foreach my $i (0..$stoppoint) { close($fd) or error("close($stamp_file) failed: $!"); next; } - + # Check for override targets in debian/rules, and run instead of # the usual command. (The non-arch-specific override is tried first, # for simplest semantics; mixing it with arch-specific overrides diff --git a/dh_installsystemduser b/dh_installsystemduser new file mode 100755 index 00000000..bc1d0368 --- /dev/null +++ b/dh_installsystemduser @@ -0,0 +1,270 @@ +#!/usr/bin/perl -w + +=head1 NAME + +dh_installsystemduser - install systemd unit files + +=cut + +use strict; +use warnings; +use Debian::Debhelper::Dh_Lib; + +our $VERSION = DH_BUILTIN_VERSION; + +=head1 SYNOPSIS + +B<dh_installsystemduser> [S<I<debhelper options>>] [B<--no-enable>] [B<--name=>I<name>] [S<I<unit file> ...>] + +=head1 DESCRIPTION + +B<dh_installsystemduser> finds the systemd user instance service files +installed by a package and generates F<postinst>, and F<prerm> code +blocks for enabling and disabling the corresponding systemd user +instance services, when the package is installed, updated, or +removed. These snippets are added to the maintainer scripts by +L<dh_installdeb(1)>. + +L<deb-systemd-helper(1)> is used to enable and disable the systemd +units, thus it is not necessary that the machine actually runs systemd +during package installation time, enabling happens on all machines. + +B<dh_installsystemduser> operates on all user instance unit files +installed by a package. For only generating blocks for specific unit +files, pass them as arguments. 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>.user.path, + debian/I<package>@.user.path, + debian/I<package>.user.service, + debian/I<package>@.user.service, + debian/I<package>.user.socket, + debian/I<package>@.user.socket, + debian/I<package>.user.target, + debian/I<package>@.user.target, + debian/I<package>.user.timer, + debian/I<package>@.user.timer + +If any of those files exists, they are installed into +F<usr/lib/systemd/user/> in the package build directory removing the +F<.user> file name part. + +=back + +=head1 OPTIONS + +=over 4 + +=item B<--name=>I<name> + +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.user.service> instead of the usual +F<debian/package.user.service>. Moreover, maintainer scripts are only +generated for units that match the given I<name>. + +=item B<--no-enable> + +Disable the service(s) on purge, but do not enable them on install. + +=back + +=head1 NOTES + +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 + +# PROMISE: DH NOOP WITHOUT tmp(usr/lib/systemd/user) user.service + +init(options => { + "no-enable" => \$dh{NO_ENABLE}, +}); + +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]: $!"); + + while (my $line = <$fh>) { + chomp($line); + return 1 if $line =~ /^\s*\[Install\]$/i; + } + close($fh); + return 0; +} + +sub install_user_unit { + my ($package, $name, $suffix) = @_; + + my $tmpdir = tmpdir($package); + my $path = "$tmpdir/usr/lib/systemd/user"; + my $unit = pkgfile($package, "user.$suffix"); + return if $unit eq ''; + + install_dir($path); + install_file($unit, "$path/$name.$suffix"); +} + +# 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; + + open(my $fh, '<', $unit_path) or error("Cannot open($unit_path): $!"); + + while (my $line = <$fh>) { + chomp($line); + + # 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/; + push @values, $value; + } + } + } + + close($fh); + return @values; +} + +sub list_installed_user_units { + my ($tmpdir, $aliases) = @_; + + my $lib_systemd_user = "$tmpdir/usr/lib/systemd/user"; + my @installed; + + return unless -d $lib_systemd_user; + opendir(my $dh, $lib_systemd_user) or error("Cannot opendir($lib_systemd_user): $!"); + + foreach my $name (readdir($dh)) { + my $path = "$lib_systemd_user/$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; +} + +# Install package maintainer provided unit files. +foreach my $package (@{$dh{DOPACKAGES}}) { + my $tmpdir = tmpdir($package); + + # unit file name + my $name = $dh{NAME} // $package; + + for my $type (qw(service target socket path timer)) { + install_user_unit($package, $name, $type); + install_user_unit("${package}@", "${name}@", $type); + } +} + +# Generate postinst and prerm code blocks to enable and disable units +foreach my $package (@{$dh{DOPACKAGES}}) { + my (@args, @enable_units, %aliases); + + my $tmpdir = tmpdir($package); + my @units = list_installed_user_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}\.(service|target|socket|path|timer)$/, @units; + } + else { + @args = @units; + } + + # 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 + # Also=systemd-readahead-collect.service, and that file in turn + # contains Also=systemd-readahead-drop.service, thus forming an + # endless loop. + my %seen; + + # Must use while and shift because the loop alters the list. + while (@args) { + my $name = shift @args; + my $path = "${tmpdir}/usr/lib/systemd/user/${name}"; + + error("User unit file \"$name\" not found in package \"$package\".") if ! -f $path; + + # Skip template service files. Enabling or disabling those + # services without specifying the instance is not useful. + next if ($name =~ /\@/); + + # 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. + push @args, $_ for grep { !$seen{$_}++ } extract_key($path, 'Also'); + + push @enable_units, $name if contains_install_section($path); + } + + @enable_units = map { quote($_) } sort uniq @enable_units; + + if (@enable_units) { + # The generated maintainer script code blocks use the --user + # option that was added to deb-systemd-helper in version 1.52. + addsubstvar($package, 'misc:Depends', 'init-system-helpers', ">= 1.52"); + + my $postinst = $dh{NO_ENABLE} ? 'postinst-systemd-user-dont-enable' : 'postinst-systemd-user-enable'; + foreach my $unit (@enable_units) { + autoscript($package, 'postinst', $postinst, { 'UNITFILE' => $unit }); + } + autoscript($package, 'postrm', 'postrm-systemd-user', { 'UNITFILES' => join(' ', @enable_units) }); + } +} + +=head1 SEE ALSO + +L<debhelper(7)>, L<dh_installsystemd(1)>, L<deb-systemd-helper(1)> + +=head1 AUTHORS + +pkg-systemd-maintainers@lists.alioth.debian.org + +=cut diff --git a/t/dh_installsystemduser/debian/baz.user.service b/t/dh_installsystemduser/debian/baz.user.service new file mode 100644 index 00000000..3af041de --- /dev/null +++ b/t/dh_installsystemduser/debian/baz.user.service @@ -0,0 +1,8 @@ +[Unit] +Description=Baz User Unit + +[Service] +ExecStart=/bin/true + +[Install] +WantedBy=default.target diff --git a/t/dh_installsystemduser/debian/changelog b/t/dh_installsystemduser/debian/changelog new file mode 100644 index 00000000..5850f0e2 --- /dev/null +++ b/t/dh_installsystemduser/debian/changelog @@ -0,0 +1,5 @@ +foo (1.0-1) unstable; urgency=low + + * Initial release. (Closes: #XXXXXX) + + -- Test <testing@nowhere> Mon, 11 Jul 2016 18:10:59 +0200 diff --git a/t/dh_installsystemduser/debian/control b/t/dh_installsystemduser/debian/control new file mode 100644 index 00000000..adccbc63 --- /dev/null +++ b/t/dh_installsystemduser/debian/control @@ -0,0 +1,10 @@ +Source: foo +Section: misc +Priority: optional +Maintainer: Test <testing@exampe.org> +Standards-Version: 3.9.8 + +Package: foo +Architecture: all +Description: package foo + Package foo diff --git a/t/dh_installsystemduser/debian/foo.user.service b/t/dh_installsystemduser/debian/foo.user.service new file mode 100644 index 00000000..7ef597d4 --- /dev/null +++ b/t/dh_installsystemduser/debian/foo.user.service @@ -0,0 +1,8 @@ +[Unit] +Description=Foo User Unit + +[Service] +ExecStart=/bin/true + +[Install] +WantedBy=default.target diff --git a/t/dh_installsystemduser/dh_installsystemduser.t b/t/dh_installsystemduser/dh_installsystemduser.t new file mode 100644 index 00000000..894eeb7a --- /dev/null +++ b/t/dh_installsystemduser/dh_installsystemduser.t @@ -0,0 +1,75 @@ +#!/usr/bin/perl +use strict; +use Test::More; +use File::Path qw(make_path); +use File::Basename qw(dirname); +use lib dirname(dirname(__FILE__)); +use Test::DH; +use Debian::Debhelper::Dh_Lib qw(!dirname); + +plan(tests => 1); + +our @TEST_DH_EXTRA_TEMPLATE_FILES = (qw( + debian/changelog + debian/control + debian/foo.user.service + debian/baz.user.service +)); + + +sub read_script { + my ($package, $name) = @_; + my @lines; + + foreach my $script (find_script($package, $name)) { + open(my $fh, '<', $script) or die("open($script): $!"); + push @lines, $_ for <$fh>; + close($fh); + } + + return @lines; +} + +sub _unit_check_user_enabled { + my ($package, $unit, $enabled) = @_; + my $verb = $enabled ? "is" : "isnt"; + my $matches; + + my @postinst = read_script($package, 'postinst'); + # Match exactly one tab character. The "dont-enable" script has + # an "enable" line for re-enabling the service if the admin had it + # enabled, but we do not want to include that in our count. + $matches = grep { m{^\tif deb-systemd-helper( --\w+)* --user was-enabled.*'\Q$unit'} } @postinst; + is($matches, $enabled, "$unit $verb enabled"); + + my @postrm = read_script($package, 'postrm'); + $matches = grep { m{deb-systemd-helper( --\w+)* --user mask.*'\Q$unit'} } @postrm; + is($matches, $enabled, "$unit $verb masked"); +} + +sub is_enabled { _unit_check_user_enabled(@_, 1); } +sub isnt_enabled { _unit_check_user_enabled(@_, 0); } + +each_compat_subtest { + make_path('debian/foo/usr/lib/systemd/user/'); + install_file('debian/foo.user.service', 'debian/foo/usr/lib/systemd/user/bar.service'); + ok(run_dh_tool('dh_installsystemduser')); + ok(-e 'debian/foo/usr/lib/systemd/user/foo.service'); + is_enabled('foo', 'foo.service'); + is_enabled('foo', 'bar.service'); + ok(run_dh_tool('dh_clean')); + + ok(run_dh_tool('dh_installsystemduser')); + ok(-e 'debian/foo/usr/lib/systemd/user/foo.service'); + ok(! -e 'debian/foo/usr/lib/systemd/user/baz.service'); + is_enabled('foo', 'foo.service'); + isnt_enabled('foo', 'baz.service'); + ok(run_dh_tool('dh_clean')); + + ok(run_dh_tool('dh_installsystemduser', '--name', 'baz')); + ok(! -e 'debian/foo/usr/lib/systemd/user/foo.service'); + ok(-e 'debian/foo/usr/lib/systemd/user/baz.service'); + isnt_enabled('foo', 'foo.service'); + is_enabled('foo', 'baz.service'); + ok(run_dh_tool('dh_clean')); +}; |