Package: debianutils
Version: 4.11.2
Severity: wishlist
Tags: patch
User: helm...@debian.org
Usertags: rebootstrap

We've had a discussion on the expectations on /etc/shells on
d-devel@l.d.o recently. You can find the entry point at:
https://lists.debian.org/ymjtirkqbjdjy...@alf.mars

The present management of /etc/shells using maintainer scripts running
add-shell and remove-shell is suboptimal for a number of reasons.

 * Running maintainer scripts during initial bootstrap is known to be
   difficult due to ordering and other issues. Our present approach is
   to turn it declarative as much as possible.
 * The add-shell and remove-shell tools do not mesh well with DPKG_ROOT.
 * The contents of /etc/shells depend on the order of running maintainer
   scripts.
 * The contents of /etc/shells depend on when a /usr-merge is being
   performed.

As a result of the discussion, we propose that client packages can
replace their add-shell invocations with adding a file
/usr/share/debianutils/shells.d/$PKG containing the shells to be added.
debianutils will be triggered and update /etc/shells when a package
containing this file is installed or removed. As such, the only package
that needs to carry a maintainer script will be debianutils. The
proposed mechanism will not break existing uses, so there is no flag
day.

Please refer to the mailing list discussion for details.

I would like to thank the following people for their notable
contributions:
 * Étienne Mollier
 * Felix C. Stegerman
 * Guillem Jover
 * Johannes Schauer Marin Rodrigues
 * Mattia Rizzolo
 * Sam Hartman

As a result, I am attaching a patch for the proposed solution. While it
does not fully fix all mentioned issues (in particular, reproducibility
and /usr-merge may need improvement), it provides a significant
improvement and makes fixing those other issues simpler.

Given the lack of participation of debianutils maintainers in the
discussion, I intend to NMU this patch post bullseye barring a
maintainer reply. Once debianutils in unstable support triggers, I
intend to file patches converting shell providers to use the new
mechanism.

Helmut
diff --minimal -Nru debianutils-4.11.2/Makefile.am 
debianutils-4.11.2+nmu1/Makefile.am
--- debianutils-4.11.2/Makefile.am      2011-05-18 21:01:01.000000000 +0200
+++ debianutils-4.11.2+nmu1/Makefile.am 2021-06-28 20:02:03.000000000 +0200
@@ -9,9 +9,9 @@
 
 bin_SCRIPTS = which savelog
 
-sbin_SCRIPTS = installkernel add-shell remove-shell
+sbin_SCRIPTS = installkernel add-shell remove-shell update-shells
 
 man_MANS = run-parts.8 \
           installkernel.8 savelog.8 \
           tempfile.1 which.1 add-shell.8 \
-          remove-shell.8 ischroot.1
+          remove-shell.8 update-shells.8 ischroot.1
diff --minimal -Nru debianutils-4.11.2/debian/changelog 
debianutils-4.11.2+nmu1/debian/changelog
--- debianutils-4.11.2/debian/changelog 2020-09-27 19:25:47.000000000 +0200
+++ debianutils-4.11.2+nmu1/debian/changelog    2021-06-28 20:14:17.000000000 
+0200
@@ -1,3 +1,10 @@
+debianutils (4.11.2+nmu1) UNRELEASED; urgency=medium
+
+  * Non-maintainer upload.
+  * Add tool update-shells.  closes: #-1.
+
+ -- Helmut Grohne <hel...@subdivi.de>  Mon, 28 Jun 2021 20:14:17 +0200
+
 debianutils (4.11.2) unstable; urgency=medium
 
   * ischroot: send usage and version to stdout.  closes: #961872.
diff --minimal -Nru debianutils-4.11.2/debian/postinst 
debianutils-4.11.2+nmu1/debian/postinst
--- debianutils-4.11.2/debian/postinst  2020-05-25 14:51:26.000000000 +0200
+++ debianutils-4.11.2+nmu1/debian/postinst     2021-06-28 20:14:17.000000000 
+0200
@@ -6,7 +6,8 @@
 fi
 
 case "$1" in
-    configure)
+    configure|triggered)
+       update-shells --root "${DPKG_ROOT:-}"
     ;;
 
     abort-upgrade|abort-remove|abort-deconfigure)
diff --minimal -Nru debianutils-4.11.2/debian/postrm 
debianutils-4.11.2+nmu1/debian/postrm
--- debianutils-4.11.2/debian/postrm    2020-05-25 14:51:32.000000000 +0200
+++ debianutils-4.11.2+nmu1/debian/postrm       2021-06-28 20:14:17.000000000 
+0200
@@ -3,9 +3,12 @@
 set -e
 
 case "$1" in
+    purge)
+       rm -f "${DPKG_ROOT:-}/etc/shells" "${DPKG_ROOT:-}/var/lib/shells.state"
+    ;;
     remove|disappear)
         ;;
-    upgrade|failed-upgrade|purge|abort-install|abort-upgrade)
+    upgrade|failed-upgrade|abort-install|abort-upgrade)
         ;;
     *)
         echo "postrm called with unknown argument \`$1'" >&2
diff --minimal -Nru debianutils-4.11.2/debian/rules 
debianutils-4.11.2+nmu1/debian/rules
--- debianutils-4.11.2/debian/rules     2020-05-25 14:54:02.000000000 +0200
+++ debianutils-4.11.2+nmu1/debian/rules        2021-06-28 20:14:17.000000000 
+0200
@@ -56,6 +56,7 @@
                       debian/tmp/sbin \
                       debian/tmp/usr/bin \
                       debian/tmp/usr/sbin \
+                      debian/tmp/usr/share/debianutils/shells.d \
                       debian/tmp/usr/share/man/man1 \
                       debian/tmp/usr/share/man/man8 \
                       debian/tmp/usr/share/doc/$(package) \
@@ -93,6 +94,7 @@
        $(INSTALL_FILE) debian/copyright debian/tmp/usr/share/doc/$(package)
        $(INSTALL_SCRIPT) debian/postinst debian/tmp/DEBIAN/
        $(INSTALL_SCRIPT) debian/postrm debian/tmp/DEBIAN/
+       $(INSTALL_FILE) debian/triggers debian/tmp/DEBIAN/
 
        cd debian/tmp && find * -type f ! -regex '^DEBIAN/.*' -print0 | 
LC_ALL=C sort -z | xargs -r0 md5sum > DEBIAN/md5sums
 
diff --minimal -Nru debianutils-4.11.2/debian/triggers 
debianutils-4.11.2+nmu1/debian/triggers
--- debianutils-4.11.2/debian/triggers  1970-01-01 01:00:00.000000000 +0100
+++ debianutils-4.11.2+nmu1/debian/triggers     2021-06-28 20:14:17.000000000 
+0200
@@ -0,0 +1 @@
+interest-noawait /usr/share/debianutils/shells.d
diff --minimal -Nru debianutils-4.11.2/update-shells 
debianutils-4.11.2+nmu1/update-shells
--- debianutils-4.11.2/update-shells    1970-01-01 01:00:00.000000000 +0100
+++ debianutils-4.11.2+nmu1/update-shells       2021-06-28 20:14:17.000000000 
+0200
@@ -0,0 +1,144 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright 2021 Helmut Grohne <hel...@subdivi.de>
+
+# A "hashset" is a shell variable containing a sequence of elements separated
+# and surrounded by hash (#) characters. None of the elements may contain a
+# hash character. The character is thus chosen, because it initiates a comment
+# in /etc/shells. All variables ending in _SHELLS in this file are hashsets.
+
+set -e
+
+# Check whether hashset $1 contains element $2.
+hashset_contains() {
+       case "$1" in
+               *"#$2#"*) return 0 ;;
+               *) return 1 ;;
+       esac
+}
+
+log() {
+       if [ "$VERBOSE" = 1 ]; then
+               echo "$*"
+       fi
+}
+
+ROOT=
+VERBOSE=0
+NOACT=0
+
+while [ $# -gt 0 ]; do
+       case "$1" in
+               --help)
+                       cat <<EOF
+usage: $0 [options]
+
+ --no-act    Do not move the actual update into place
+ --verbose   Be more verbose
+ --root DIR  Operate on the given chroot, defaults to /
+EOF
+                       exit 0
+               ;;
+               --no-act)
+                       NOACT=1
+               ;;
+               --root)
+                       shift
+                       if [ "$#" -lt 1 ]; then
+                               echo "missing argument to --root" 1>&2
+                               exit 1
+                       fi
+                       ROOT=$1
+               ;;
+               --verbose)
+                       VERBOSE=1
+               ;;
+               *)
+                       echo "unrecognized option $1" 1>&2
+                       exit 1
+               ;;
+       esac
+       shift
+done
+
+PKG_DIR="$ROOT/usr/share/debianutils/shells.d"
+STATE_FILE="$ROOT/var/lib/shells.state"
+ETC_FILE="$ROOT/etc/shells"
+NEW_ETC_FILE="$ETC_FILE.tmp"
+NEW_STATE_FILE="$STATE_FILE.tmp"
+
+PKG_SHELLS='#'
+LC_COLLATE=C.UTF-8  # glob in reproducible order
+for f in "$PKG_DIR/"*; do
+       [ "$f" = "$PKG_DIR/*" ] && break
+       while IFS='#' read -r line _; do
+               [ -n "$line" ] || continue
+               PKG_SHELLS="$PKG_SHELLS$line#"
+               realshell=$(dpkg-realpath --root "$ROOT" "$line")
+               if [ "$line" != "$realshell" ]; then
+                       PKG_SHELLS="$PKG_SHELLS$realshell#"
+               fi
+       done < "$f"
+done
+
+STATE_SHELLS='#'
+if [ -e "$STATE_FILE" ] ; then
+       while IFS='#' read -r line _; do
+               [ -n "$line" ] && STATE_SHELLS="$STATE_SHELLS$line#"
+       done < "$STATE_FILE"
+fi
+
+cleanup() {
+       rm -f "$NEW_ETC_FILE" "$NEW_STATE_FILE"
+}
+trap cleanup EXIT
+
+: > "$NEW_ETC_FILE"
+ETC_SHELLS='#'
+while IFS= read -r line; do
+       shell=${line%%#*}
+       # copy all comment lines, packaged shells and local additions
+       if [ -z "$shell" ] ||
+                       hashset_contains "$PKG_SHELLS" "$shell" ||
+                       ! hashset_contains "$STATE_SHELLS" "$shell"; then
+               echo "$line" >> "$NEW_ETC_FILE"
+               ETC_SHELLS="$ETC_SHELLS$shell#"
+       else
+               log "removing shell $shell"
+       fi
+done < "$ETC_FILE"
+
+: > "$NEW_STATE_FILE"
+saved_IFS=$IFS
+IFS='#'
+set -f
+# shellcheck disable=SC2086 # word splitting intended, globbing disabled
+set -- ${PKG_SHELLS###}
+set +f
+IFS=$saved_IFS
+for shell; do
+       echo "$shell" >> "$NEW_STATE_FILE"
+       # add shells that are neither already present nor locally removed
+       if ! hashset_contains "$ETC_SHELLS" "$shell" &&
+                       ! hashset_contains "$STATE_SHELLS" "$shell"; then
+               echo "$shell" >> "$NEW_ETC_FILE"
+               log "adding shell $shell"
+       fi
+done
+
+if [ "$NOACT" = 0 ]; then
+       if [ -e "$STATE_FILE" ]; then
+               chmod --reference="$STATE_FILE" "$NEW_STATE_FILE"
+               chown --reference="$STATE_FILE" "$NEW_STATE_FILE"
+       else
+               chmod 0644 "$NEW_STATE_FILE"
+       fi
+       chmod --reference="$ETC_FILE" "$NEW_ETC_FILE"
+       chown --reference="$ETC_FILE" "$NEW_ETC_FILE"
+       sync --data "$NEW_ETC_FILE" "$NEW_STATE_FILE"
+       mv "$NEW_ETC_FILE" "$ETC_FILE"
+       sync "$ETC_FILE"
+       mv "$NEW_STATE_FILE" "$STATE_FILE"
+       sync "$STATE_FILE"
+       trap "" EXIT
+fi
diff --minimal -Nru debianutils-4.11.2/update-shells.8 
debianutils-4.11.2+nmu1/update-shells.8
--- debianutils-4.11.2/update-shells.8  1970-01-01 01:00:00.000000000 +0100
+++ debianutils-4.11.2+nmu1/update-shells.8     2021-06-28 18:17:57.000000000 
+0200
@@ -0,0 +1,31 @@
+.TH UPDATE-SHELLS 8 "28 Jun 2021"
+.SH NAME
+update-shells \- update the list of valid login shells
+.SH SYNOPSIS
+.B  update-shells
+.RI  [ options ]
+.SH DESCRIPTION
+.B update-shells
+locates the shells provided by packages from
+.I /usr/share/debianutils/shells.d
+and updates
+.I /etc/shells
+with newly added or removed shells.
+To track changes made by the administrator, it consults a state file in
+.I /var/lib/shells.state .
+.SH OPTIONS
+.TP
+.B \-\-no\-act
+Do not actually perform the changes to
+.I /etc/shells .
+.TP
+.B \-\-root
+.I ROOT
+
+Operate on a chroot at
+.I ROOT .
+.TP
+.B \-\-verbose
+Print the shells that are being added or removed.
+.SH SEE ALSO
+.BR shells (5)

Reply via email to